Files
mesh-controller/internal/catalogue/secrets_into_files.go
T
jschoubben 1314be5282 A file may hold a credential where its content says one belongs
The gap that stopped keycloak and gitea from starting. A granted
credential arrives as a file whose entire content is the password, which
is what a program reading a password file wants — and most programs do
not read one. They read KEY=value, or a JSON document with the token at
an attribute inside it. A module in that position could be handed the
bare value or nothing, and both are useless.

The host has been able to do this all along: content with ${secret:name}
in it, sealed values beside it, substitution on the machine, which is
the only place both halves exist. Nothing filled the values in, so the
hole could be written and never closed and the host refused the file.
That refusal was correct and the feature was unreachable.

A module reaches its own secrets and the credentials it was granted —
both things it wrote in its own manifest — and nothing else. Naming
another module's is refused: two modules on one machine are as separate
as two on different machines, and letting one read the other's
credential by guessing a name would end that to save writing a file.

Filling runs after settings, which is the whole reason it sits where it
does. A setting is how a placeholder gets into a JSON document in the
first place — the desktop client that reads its token from an attribute,
not an environment variable. Before the merge that file's content is
"{}" and asks for nothing.

Tested through Declaration rather than through the helper. Three times
in this repository a test asserted on a helper while the code calling it
was wrong, and each time the injected fault stayed silent. Three faults
injected here — the call removed, the call moved before settings, and
the module boundary widened — each caught by the test meant for it.
2026-09-01 02:28:50 +02:00

140 lines
5.0 KiB
Go

package catalogue
import (
"fmt"
"regexp"
"sort"
)
// A credential and a configuration file meeting.
//
// **The gap this closes.** A granted credential arrives as a file whose entire content is the
// password. That is what a program reading a password file wants — and most programs do not read
// one. They read `KEY=value`, or a JSON document with the password at some path inside it, or a
// YAML file in a home directory. Before this, a module in that position could be handed the bare
// value or nothing, and both are useless.
//
// The mesh cannot compose the document, because it discarded the value (novox/hq ADR 0024). So
// the module supplies the document with a hole in it, the mesh delivers the value sealed beside
// it, and the host — the only thing that ever sees both — puts one into the other on the machine.
//
// The host has always been able to do this. Nothing filled the values in, so the hole could be
// written and never closed, and the host refused the file. That refusal was correct and the
// feature was unreachable.
// placeholder is what a module's file content says where a sealed value belongs: ${secret:name}.
//
// The same expression the host matches, written out again rather than shared. The two
// repositories agree on a wire format, and a format read on both sides is exactly the thing that
// must not be quietly changed on one of them; a test asserts they still agree.
var placeholder = regexp.MustCompile(`\$\{secret:([a-z0-9][a-z0-9-]*)\}`)
// secretsUsed are the names a file's content asks for, in the order they first appear.
func secretsUsed(content string) []string {
var used []string
seen := map[string]bool{}
for _, m := range placeholder.FindAllStringSubmatch(content, -1) {
if !seen[m[1]] {
seen[m[1]] = true
used = append(used, m[1])
}
}
return used
}
// sealedFor is every credential a module may name from inside one of its own files.
//
// **Exactly what it already declared, and nothing else.** A module reaches its own secrets and the
// credentials it was granted for what it requires — both written down in its own manifest. It
// cannot name another module's, which is not an oversight: two modules on one machine are as
// separate as two on different machines, and letting one read the other's credential by guessing a
// name would end that, to save writing a file.
func sealedFor(m Manifest, needs []Needed, with Rendering) (map[string]string, error) {
sealed := map[string]string{}
for name := range m.OwnSecrets {
if value := with.Needed[m.Module][name]; value != "" {
sealed[name] = value
}
}
for _, to := range sortedKeys(m.Secrets) {
if _, taken := sealed[to]; taken {
// A module whose own secret and whose requirement share a name. Refused rather than
// settled by precedence: whichever won, the manifest would read as though the other
// had, and the file would hold the credential for the wrong thing while every check
// passed.
return nil, fmt.Errorf(
"%s has a secret of its own called %q and also requires %q, so a file saying "+
"${secret:%s} could mean either — rename one of them", m.Module, to, to, to)
}
for i := range needs {
if needs[i].Name == to && needs[i].Sealed != "" {
sealed[to] = needs[i].Sealed
}
}
}
return sealed, nil
}
// intoFile gives a file the sealed values its content asks for.
//
// A name the module never declared is refused here rather than on the machine. The host would
// refuse it too — but it would do so having already been handed a declaration, which reads as the
// mesh sending something broken, and the name it could not find is a manifest's typo.
func intoFile(resource map[string]any, sealed map[string]string, module string) error {
if fmt.Sprint(resource["type"]) != "file" {
return nil
}
content, ok := resource["content"].(string)
if !ok {
return nil
}
used := secretsUsed(content)
if len(used) == 0 {
return nil
}
into := map[string]any{}
for _, name := range used {
value := sealed[name]
if value == "" {
return fmt.Errorf(
"%s has a file that says ${secret:%s}, and %s has no secret of its own by that "+
"name and requires nothing called that either. A file may name %s",
module, name, module, namesOr(sealed))
}
into[name] = value
}
resource["secrets"] = into
return nil
}
// namesOr says what a module could have written, because the answer to "that name is wrong" is
// almost always one of two or three right ones.
func namesOr(sealed map[string]string) string {
if len(sealed) == 0 {
return "nothing — it has no secrets of its own and requires nothing that grants one"
}
var names []string
for name := range sealed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
return join(names)
}
func join(names []string) string {
switch len(names) {
case 1:
return names[0]
case 2:
return names[0] + " or " + names[1]
}
out := ""
for i, n := range names[:len(names)-1] {
if i > 0 {
out += ", "
}
out += n
}
return out + " or " + names[len(names)-1]
}