region HA 2b: host.env allowlist drops CARLOS_DNSIMPLE_TOKEN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+35
−7
internal/hostenv/hostenv.go+8 −6internal/hostenv/hostenv_test.go+26 −0internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv+1 −1
diff --git a/internal/hostenv/hostenv.go b/internal/hostenv/hostenv.go| index f73919fca..6be2a2fc6 100644 |
| --- a/internal/hostenv/hostenv.go |
| +++ b/internal/hostenv/hostenv.go |
| @@ -51,11 +51,14 @@ var Required = []string{ |
| // carry, from the agent's config surface (README's host.env list and the |
| // variables the agent reads). Together with Required they are the |
| // allowlist: any other name, under either scope, is refused by name, |
| -// never by value, by write (Assemble) and by seed. Keys that would choose |
| -// a box's authority from the store alone are deliberately not on it: a |
| -// fleet's or tunnel's identity, an address override, a pool, the data, |
| -// opt and deployment directories, the relay's settings and any token or |
| -// credential. CARLOS_REGION and the group marker, which do select, are |
| +// never by value, by write (Assemble) and by seed. The list leaves out |
| +// the keys that select a box's authority (a fleet's or tunnel's identity, |
| +// an address override, a pool, the data, opt and deployment directories, |
| +// the relay's settings) and every token or API credential, except the |
| +// deployment's own required secrets, CARLOS_SECRETS_KEY and |
| +// CARLOS_RELEASE_PUBKEY. An endpoint override such as CARLOS_S3_ENDPOINT |
| +// stays, because it selects no box authority and only operators can write |
| +// the parameter paths. CARLOS_REGION and the group marker, which do select, are |
| // cross-checked against the signed roster where the agent uses them |
| // (internal/host's mode selection). Adding a variable is a code change, |
| // reviewed. |
| @@ -68,7 +71,6 @@ var Optional = []string{ |
| "CARLOS_ALERT_WEBHOOK", |
| "CARLOS_AWS_REGION", |
| "CARLOS_BACKHAUL_MAX_INFLIGHT", |
| - "CARLOS_DNSIMPLE_TOKEN", |
| "CARLOS_DNS_PROVIDER", |
| "CARLOS_EDGE_GZIP", |
| "CARLOS_HIBERNATE_IDLE", |
diff --git a/internal/hostenv/hostenv_test.go b/internal/hostenv/hostenv_test.go| index a552a0e33..fc539990d 100644 |
| --- a/internal/hostenv/hostenv_test.go |
| +++ b/internal/hostenv/hostenv_test.go |
| @@ -363,3 +363,29 @@ func withHostEnvRoot(t *testing.T) string { |
| t.Cleanup(func() { hostEnvRoot = was }) |
| return root |
| } |
| + |
| +// A provider token is not on the allowlist: Assemble and Seed refuse it by |
| +// name, and the refusal never carries its value. |
| +func TestDNSProviderTokenIsRefusedByNameNotValue(t *testing.T) { |
| + const name, secret = "CARLOS_DNSIMPLE_TOKEN", "tok-9f8e7d6c" |
| + if Allowed(name) { |
| + t.Fatalf("%s is on the allowlist", name) |
| + } |
| + check := func(what string, err error) { |
| + t.Helper() |
| + if !errors.Is(err, ErrValue) || !strings.Contains(err.Error(), name) || strings.Contains(err.Error(), secret) { |
| + t.Fatalf("%s: %v", what, err) |
| + } |
| + } |
| + env := today() |
| + env[name] = secret |
| + shared, regional := Split(env) |
| + _, err := Assemble(shared, regional) |
| + check("Assemble", err) |
| + p := &fakeParams{vals: map[string]string{}} |
| + _, err = Seed(context.Background(), p, "flagship", "ie", env) |
| + check("Seed", err) |
| + if len(p.sets) != 0 { |
| + t.Fatalf("Seed wrote %v", p.sets) |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv| index bf5f2e1db..7bc13951e 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv |
| @@ -1,7 +1,7 @@ |
| # Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b. |
| # Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands. |
| @coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d |
| -@coverage internal/hostenv a3f3274a86cdf8194afb871301c1ba3289dfbdc9b703dd547178273df151cdaa |
| +@coverage internal/hostenv 45d9d4b90a5d441020f5ba4573821ca92701246c80b32d6f1823665fb0f65208 |
| internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899 |
| internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c |
| internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1 |