carlos / platform Public

region HA 2b: host.env allowlist drops CARLOS_DNSIMPLE_TOKEN

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6bb22021deeaab9ac4082fbfccb4848abc5272e1 parent d7e4725
3 files changed, +35 −7
  • internal/hostenv/hostenv.go +8 −6
  • internal/hostenv/hostenv_test.go +26 −0
  • internal/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