package main import ( "encoding/json" "os" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // Where root creates and owns a module's directories, and which of the machine's paths reach its container, // are said at the controller's terminal alone (novox/hq issue 339): through a verb, a caller who set `places` // to /etc with an owner of its own would have the next push hand it /etc, and one who set `accesses` to / // would have the machine's root mounted into a container. // throughVerb runs a verb's call the way the serving controller does: the command line argvFor composes, in // a process that carries the verb in its environment (runVerb), through this binary's own dispatch. func throughVerb(t *testing.T, verb string, args map[string]any) error { t.Helper() argv, err := argvFor(verb, args) if err != nil { return err } t.Setenv(verbVar, verb) defer os.Unsetenv(verbVar) return runLine(t, argv) } // atTheTerminal runs a command line the way the operator does at the controller's terminal: no verb. func atTheTerminal(t *testing.T, argv ...string) error { t.Helper() t.Setenv(verbVar, "") os.Unsetenv(verbVar) return runLine(t, argv) } func runLine(t *testing.T, argv []string) error { t.Helper() before := os.Args defer func() { os.Args = before }() os.Args = append([]string{"mesh-controller"}, argv...) return run() } func TestPlacesAndAccessesAreRefusedThroughEveryVerb(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "notes", Version: "1", Accesses: []catalogue.Access{{ID: "media", Path: "/storage/media", Mode: "read"}}, Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"}, // Nothing trusts this file: said, so a verb may change what it asks for (an unmarked one counts as // trusted, and only the terminal could). {"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false, "content": "x = ${setting:x}\n"}}}) if _, err := assign(ctx, open, "laptop", "notes"); err != nil { t.Fatal(err) } layer := func() string { t.Helper() values, _, err := open.inventory.Layer(ctx, "laptop", "notes") if err != nil { t.Fatal(err) } raw, _ := json.Marshal(values) return string(raw) } refused := func(what string, err error) { t.Helper() if err == nil || !strings.Contains(err.Error(), "controller's terminal") || !strings.Contains(err.Error(), "issue 339") { t.Fatalf("%s: %v", what, err) } } // The attack the issue reports, through each verb route: nothing is kept. attacks := []map[string]any{ {"places": map[string]any{"data": map[string]any{"path": "/srv/notes", "owner": "1001:1001"}}, "x": 1}, {"accesses": map[string]any{"media": "/srv/elsewhere"}, "x": 1}, } for _, values := range attacks { raw, _ := json.Marshal(values) refused("settings verb, one machine", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": string(raw)})) refused("settings verb, the whole mesh", throughVerb(t, "settings", map[string]any{"module": "notes", "values": string(raw)})) for _, line := range []string{ "settings set notes '" + string(raw) + "' --node laptop", "settings set --node laptop notes '" + string(raw) + "'", "settings set notes '" + string(raw) + "'", } { if err := throughVerb(t, "command", map[string]any{"command": line}); err == nil { t.Fatalf("the command verb ran %q", line) } } if got := layer(); got != "null" && got != "{}" { t.Fatalf("a refused call kept a layer: %s", got) } } // At the terminal the same placement is taken. if err := atTheTerminal(t, "settings", "set", "notes", `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":0}`, "--node", "laptop"); err != nil { t.Fatalf("places at the terminal: %v", err) } // A verb may change another key and keep the placement as it is. if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":1}`}); err != nil { t.Fatalf("another key through the verb: %v", err) } kept := layer() // But not move it, drop it, or clear the layer that holds it. refused("moved through the verb", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": `{"places":{"data":{"path":"/srv/other","owner":"1001:1001"}},"x":1}`})) refused("dropped through the verb", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": `{"x":1}`, "replace": "true"})) refused("cleared through the verb", throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "clear": "true"})) if err := throughVerb(t, "command", map[string]any{"command": "settings clear notes --node laptop"}); err == nil { t.Fatal("the command verb cleared a layer") } if got := layer(); got != kept { t.Fatalf("a refused call changed the layer: %s, was %s", got, kept) } // Reading through a verb still answers. if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop"}); err != nil { t.Fatalf("reading through the verb: %v", err) } // The machine's own trees are refused from anywhere, the terminal too. for _, path := range []string{"/etc", "/etc/sudoers.d", "/", "/home", "/var/lib", "/var/lib/mesh-host/x", "/srv/../etc"} { err := atTheTerminal(t, "settings", "set", "notes", `{"places":{"data":{"path":"`+path+`","owner":"1001:1001"}},"x":1}`, "--node", "laptop") if err == nil || !strings.Contains(err.Error(), "issue 339") { t.Fatalf("a place at %s at the terminal: %v", path, err) } err = atTheTerminal(t, "settings", "set", "notes", `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"accesses":{"media":"`+path+`"},"x":1}`, "--node", "laptop") if err == nil || !strings.Contains(err.Error(), "issue 339") { t.Fatalf("an access at %s at the terminal: %v", path, err) } } // A line break in any setting is refused where it is kept, through a verb or at the terminal. for _, x := range []string{`"a\nPATH=/tmp"`, `"a\rb"`, `"a\u0000b"`, `["ok","x\ny"]`, `{"k":"x\ny"}`} { err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "replace": "true", "values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}},"x":` + x + `}`}) if err == nil || !strings.Contains(err.Error(), "line break") { t.Fatalf("a line break in %s: %v", x, err) } } if got := layer(); got != kept { t.Fatalf("a refused call changed the layer: %s, was %s", got, kept) } } // What a provider serves is set at the terminal alone (novox/hq issue 339): through the settings verb, a caller // could move a database's port to a listener of its own and collect every consumer's credentials, or point every // login at an issuer of its own. func TestAServedKeyIsRefusedThroughAVerb(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, catalogue.Manifest{Module: "store", Version: "1", Provides: catalogue.FromAnywhere("database"), Serves: map[string]map[string]any{"database": {"port": 5432.0}}, Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/store.conf", "mode": "0644", "trusted": false, "content": "x = ${setting:x}\n"}}}) register(t, open, catalogue.Manifest{Module: "keycloak", Version: "1", Provides: catalogue.FromAnywhere("oidc-client"), Serves: map[string]map[string]any{"oidc-client": {"issuer": "${setting:issuer}"}}, Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/kc.conf", "mode": "0644", "trusted": false, "content": "x = ${setting:x}\n"}}}) register(t, open, catalogue.Manifest{Module: "power", Version: "1", Resources: []map[string]any{{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf", "mode": "0644", "trusted": true, "content": "HandleLidSwitch=${setting:lid}\nx=${setting:x}\n"}}}) if err := atTheTerminal(t, "settings", "set", "keycloak", `{"issuer":"https://id.example/realms/mesh","x":0}`, "--node", "anchor"); err != nil { t.Fatalf("the issuer at the terminal: %v", err) } if err := atTheTerminal(t, "settings", "set", "power", `{"lid":"suspend","x":0}`, "--node", "anchor"); err != nil { t.Fatal(err) } for _, m := range []string{"store", "keycloak", "power"} { if _, err := assign(ctx, open, "anchor", m); err != nil { t.Fatal(err) } } refused := func(what string, err error) { t.Helper() if err == nil || !strings.Contains(err.Error(), "controller's terminal") { t.Fatalf("%s: %v", what, err) } } refused("a served port through the verb", throughVerb(t, "settings", map[string]any{"module": "store", "node": "anchor", "values": `{"port":6543,"x":0}`})) refused("a served port, mesh-wide, through the verb", throughVerb(t, "settings", map[string]any{"module": "store", "values": `{"port":6543}`})) refused("the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak", "node": "anchor", "values": `{"issuer":"https://evil.example/realms/mesh","x":0}`})) refused("clearing the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak", "node": "anchor", "clear": "true"})) if err := throughVerb(t, "settings", map[string]any{"module": "keycloak", "node": "anchor", "values": `{"issuer":"https://id.example/realms/mesh","x":1}`}); err != nil { t.Fatalf("another key through the verb, the issuer kept: %v", err) } refused("a setting a trusted file asks for, through the verb", throughVerb(t, "settings", map[string]any{ "module": "power", "node": "anchor", "values": `{"lid":"ignore","x":0}`})) // And `trusted` is the catalogue's word: it never reaches the machine, whose engine parses strictly. plan := printed(t, func() error { return atTheTerminal(t, "plan", "anchor", "--json") }) if strings.Contains(plan, `"trusted"`) { t.Fatal("the declaration carries the catalogue's `trusted`") } }