rootaws: an inherited disabled shared-file sentinel is not an override
A root child of a scrubbed root parent inherits AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE set to /dev/null/carlos-root-aws. The child's scrub counted them as overrides and logged the ALERT on stderr. A guard-armed box's updater reads the candidate's version through the guard with combined output, so it refused every router update (found by platform-d8's deployment harness at G4). The candidate must carry the fix, since the refusing updater is the old binary. Census: envelope-only overlay claim-rootaws-sentinel, ruled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+57
−1
internal/provision/plan14_root_reads_test.go+7 −0internal/provision/testdata/plan14-root-reads-claim-rootaws-sentinel.tsv+2 −0internal/rootaws/environment.go+10 −1internal/rootaws/environment_test.go+38 −0
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 9ff2cb52b..dc02805f8 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -972,6 +972,13 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| t.Fatal(err) |
| } |
| seal = "393e7f456d8f02b2eee0c683880e5f7bf696b6ee0c87f20e5c113434065bec29" |
| + // Census overlay claim-rootaws-sentinel (rootaws's scrub treats its own |
| + // inherited disabled-file sentinels as no override, so a guard-armed |
| + // box's updater can read a candidate's version), loaded last. Envelope |
| + // only, ruled by platform-1e. The seal stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-rootaws-sentinel.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-claim-rootaws-sentinel.tsv b/internal/provision/testdata/plan14-root-reads-claim-rootaws-sentinel.tsv| new file mode 100644 |
| index 000000000..8c8032e9b |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-claim-rootaws-sentinel.tsv |
| @@ -0,0 +1,2 @@ |
| +# Census overlay claim-rootaws-sentinel (a scrubbed root parent's own disabled shared-file sentinels are not an override in a root child, so the child's scrub stays silent; found by platform-d8's deployment harness at G4: a guard-armed box refused every router update because the candidate's version printed the ALERT into the guarded updater's combined output), loaded LAST, after claim-empty-cohort-fix2. RULED (platform-1e, 2026-10-20): accepted. Envelope only: ScrubEnvironment has no classified root-read unit; no row changes and the seal stands. |
| +@coverage internal/rootaws ad641674b84d17a9f6a7b1acb4cf88b68cc2a882812a28c8f622d4fd9616b470 |
diff --git a/internal/rootaws/environment.go b/internal/rootaws/environment.go| index 343ea793b..6023800b3 100644 |
| --- a/internal/rootaws/environment.go |
| +++ b/internal/rootaws/environment.go |
| @@ -57,7 +57,7 @@ func scrubEnvironment(euid int) { |
| } |
| } |
| for _, key := range keys { |
| - if _, set := os.LookupEnv(key); set { |
| + if value, set := os.LookupEnv(key); set && !inheritedSentinel(key, value) { |
| changed = true |
| } |
| _ = os.Unsetenv(key) |
| @@ -77,6 +77,15 @@ func scrubEnvironment(euid int) { |
| } |
| } |
| +// inheritedSentinel reports a shared-file key that already holds the value |
| +// this scrub sets: a root child of a scrubbed root parent inherits it, and |
| +// that is not an override. Alerting on it would put the alert on the child's |
| +// stderr, where a guarded updater reading `version` with combined output |
| +// refuses the candidate. |
| +func inheritedSentinel(key, value string) bool { |
| + return (key == "AWS_CONFIG_FILE" || key == "AWS_SHARED_CREDENTIALS_FILE") && value == disabledSharedFile |
| +} |
| + |
| // imdsHosts are the IPv4 and IPv6 instance metadata addresses. |
| var imdsHosts = []string{"169.254.169.254", "fd00:ec2::254"} |
diff --git a/internal/rootaws/environment_test.go b/internal/rootaws/environment_test.go| index 3d4dc9774..5bf35f28b 100644 |
| --- a/internal/rootaws/environment_test.go |
| +++ b/internal/rootaws/environment_test.go |
| @@ -149,3 +149,41 @@ func TestScrubEnvironmentKeepsProxyButBypassesIMDS(t *testing.T) { |
| t.Fatal("non-root NO_PROXY changed") |
| } |
| } |
| + |
| +// A root child of a root process inherits the parent's own disabled shared |
| +// files. That is not an override: it must stay silent, or a guarded updater |
| +// that reads a candidate's `version` with its output combined refuses the |
| +// candidate (an armed box could then take no router update at all). |
| +func TestScrubEnvironmentInheritedSentinelIsNotAnOverride(t *testing.T) { |
| + for _, key := range rootScrubbedEnvironment { |
| + t.Setenv(key, "") |
| + os.Unsetenv(key) |
| + } |
| + t.Setenv("AWS_CONFIG_FILE", disabledSharedFile) |
| + t.Setenv("AWS_SHARED_CREDENTIALS_FILE", disabledSharedFile) |
| + var logs bytes.Buffer |
| + old := log.Writer() |
| + log.SetOutput(&logs) |
| + defer log.SetOutput(old) |
| + scrubEnvironment(0) |
| + if logs.Len() != 0 { |
| + t.Fatalf("alert for the inherited sentinel: %q", logs.String()) |
| + } |
| + if os.Getenv("AWS_CONFIG_FILE") != disabledSharedFile || os.Getenv("AWS_SHARED_CREDENTIALS_FILE") != disabledSharedFile { |
| + t.Fatal("shared files not disabled for root") |
| + } |
| + // Any other value for either key is still an override. |
| + for _, key := range []string{"AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE"} { |
| + logs.Reset() |
| + t.Setenv("AWS_CONFIG_FILE", disabledSharedFile) |
| + t.Setenv("AWS_SHARED_CREDENTIALS_FILE", disabledSharedFile) |
| + t.Setenv(key, disabledSharedFile+"x") |
| + scrubEnvironment(0) |
| + if strings.Count(logs.String(), "ALERT:") != 1 { |
| + t.Fatalf("%s set to another file did not alert: %q", key, logs.String()) |
| + } |
| + if os.Getenv(key) != disabledSharedFile { |
| + t.Fatalf("%s not reset to the sentinel", key) |
| + } |
| + } |
| +} |