carlos / platform Public

activator: a replica row's worker is gated as its seed is, and the nested test isolates the home-key digest check (L21)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 37d652df6305574d5d771855797257244c9ae087 parent 366067d
3 files changed, +98 −6
  • internal/activator/backend_exec.go +19 −1
  • internal/activator/replica_instancekey_linux_test.go +13 −5
  • internal/activator/replica_instancekey_test.go +66 −0
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index 8d953eb9b..7e04b249b 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -2157,6 +2157,10 @@ func (b *ExecBackend) StartWorker(ctx context.Context, rt registry.Route, extra
// Between resolving that identity and the exec it runs ctx's before-exec hook
// (runBeforeExec), through which the activator publishes the build first; a
// hook error means nothing runs (ruling R19 item 3).
+//
+// A worker on a replica row passes replicaKeyGate as a seed does, with no
+// control socket, before that hook: its home's key, verified, or nothing
+// runs (instance-key plan ruling 26). A worker never mints a key.
func (b *ExecBackend) StartWorkerStaged(ctx context.Context, rt registry.Route, extra map[string]string, onto *release.StagedIdentity) (Proc, release.StagedIdentity, error) {
var argv []string
if err := json.Unmarshal([]byte(rt.Args), &argv); err != nil || len(argv) == 0 {
@@ -2179,13 +2183,27 @@ func (b *ExecBackend) StartWorkerStaged(ctx context.Context, rt registry.Route,
id = *onto
}
- fileEnv, err := b.instanceEnv(rt)
+ // One read gives the env and the layer its CARLOS_INSTANCE_KEY came
+ // from, for the replica gate below (ruling 29). A worker never mints
+ // (ruling 12): the key is whatever the env files already deliver.
+ fileEnv, keyLayer, err := b.instanceEnvLayer(rt)
if err != nil {
return nil, release.StagedIdentity{}, err
}
for k, v := range extra {
fileEnv = withVar(fileEnv, k, v)
}
+ // A worker has no control socket, so a replica row's worker is gated as
+ // its seed is (ruling 26): its home's key, verified, or refused. extra is
+ // agent-issued and has no business carrying a key; one that did would
+ // not be the key the layer describes, so it is refused as of unknown
+ // origin.
+ if _, ok := extra[instancekey.Var]; ok {
+ keyLayer = instancekey.LayerNone
+ }
+ if err := b.replicaKeyGate(rt, fileEnv, keyLayer, false); err != nil {
+ return nil, release.StagedIdentity{}, err
+ }
// The identity is resolved and nothing has run: the activator publishes
// the build now, before the exec, and a failure means no child (ruling
diff --git a/internal/activator/replica_instancekey_linux_test.go b/internal/activator/replica_instancekey_linux_test.go
index 34c079cc4..44929d652 100644
--- a/internal/activator/replica_instancekey_linux_test.go
+++ b/internal/activator/replica_instancekey_linux_test.go
@@ -58,23 +58,31 @@ func TestKeylessReplicaWithAControlSocketLeavesTheHelperToRefuse(t *testing.T) {
// Ruling 26 on the nested layout: the seed role has no control socket, so
// a keyless replica's seed is refused at prepare; a run role handed a key
// that is not the home's is refused too; one handed the home's key is
-// prepared with it.
+// prepared with it. The bucket's fresh read renders the case's own
+// configured key, so the "another key" refusal is the home-key digest
+// check's alone, not the fresh read's.
func TestNestedReplicaPrepareIsGated(t *testing.T) {
homeKey := strings.Repeat("H", 43) + "="
for _, tc := range []struct {
name, config string
+ bucket string // the key the bucket's configuration renders now
role instancectl.Role
refused bool
}{
- {"a keyless seed", "", instancectl.RoleSeed, true},
- {"a run role with another key", InstanceKeyVar + "=configured-B\n", instancectl.RoleRun, true},
- {"a run role with the home's key", InstanceKeyVar + "=" + homeKey + "\n", instancectl.RoleRun, false},
+ {"a keyless seed", "", "", instancectl.RoleSeed, true},
+ {"a run role with another key", InstanceKeyVar + "=configured-B\n", "configured-B", instancectl.RoleRun, true},
+ {"a run role with the home's key", InstanceKeyVar + "=" + homeKey + "\n", homeKey, instancectl.RoleRun, false},
} {
t.Run(tc.name, func(t *testing.T) {
b, rt := nestedBackend(t, &fakeControl{inv: "inv1"}, &fakeUnit{})
rt.Replica = true
b.ReplicaHomeKeys = func(string) (string, bool) { return instancekey.Digest(homeKey), true }
- b.ReplicaConfigKey = func(context.Context, registry.Route) (string, error) { return instancekey.Digest(homeKey), nil }
+ b.ReplicaConfigKey = func(context.Context, registry.Route) (string, error) {
+ if tc.bucket == "" {
+ return "", nil
+ }
+ return instancekey.Digest(tc.bucket), nil
+ }
dir := provision.InstanceDir(b.DataDir, rt.Host)
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
diff --git a/internal/activator/replica_instancekey_test.go b/internal/activator/replica_instancekey_test.go
index 0091b8ee7..4e39304c9 100644
--- a/internal/activator/replica_instancekey_test.go
+++ b/internal/activator/replica_instancekey_test.go
@@ -266,3 +266,69 @@ func TestReplicaSeedIsGated(t *testing.T) {
})
}
}
+
+// A worker (StartWorkerStaged) has no control socket, so a replica row's
+// worker is gated as its seed is (ruling 26), on the layer from the same
+// read: it runs only with its home's key, verified, and is refused with a
+// key that is not the home's or with none at all. A worker never mints
+// (ruling 12), and a worker on a row that is not a replica is unaffected.
+func TestReplicaWorkerIsGatedLikeASeed(t *testing.T) {
+ tok := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("c", 64) + "\n"
+ homeKey := strings.Repeat("H", 43) + "="
+ for _, tc := range []struct {
+ name string
+ replica bool
+ config string // <host>.config.env ("" = none)
+ bucket string // the key the bucket's configuration renders now
+ want string // the helper's outcome; "" = refused, no worker ran
+ // extraKey hands the home's key through extra as well as the env
+ // files: a key whose origin the read cannot vouch for.
+ extraKey bool
+ }{
+ {"a replica worker with the home's key runs", true, InstanceKeyVar + "=" + homeKey + "\n", homeKey, "key " + instancekey.Digest(homeKey), false},
+ // The bucket renders configured-B too, so only the home-key digest
+ // check can refuse this one.
+ {"a replica worker with a different configured key is refused", true, InstanceKeyVar + "=configured-B\n", "configured-B", "", false},
+ {"a keyless replica worker is refused", true, "", "", "", false},
+ {"a worker on a row that is not a replica is unaffected", false, InstanceKeyVar + "=configured-B\n", "", "key " + instancekey.Digest("configured-B"), false},
+ {"a replica worker handed a key through extra is refused", true, InstanceKeyVar + "=" + homeKey + "\n", homeKey, "", true},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ b := testExecBackend(t)
+ rt := registry.Route{Host: "shop-au.example.com", Kind: "sidecar", Args: `["sidecar","run"]`, Replica: tc.replica}
+ own := filepath.Join(b.DataDir, rt.Host+".env")
+ if err := os.WriteFile(own, []byte(tok), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if tc.config != "" {
+ if err := os.WriteFile(filepath.Join(b.DataDir, rt.Host+".config.env"), []byte(tc.config), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if tc.replica {
+ b.ReplicaHomeKeys = func(h string) (string, bool) { return instancekey.Digest(homeKey), h == rt.Host }
+ b.ReplicaConfigKey = freshConfig(replicaCase{bucket: tc.bucket})
+ }
+ out := stageHelperApp(t, b, rt.Host, "v1")
+ extra := map[string]string{"CARLOS_SIDECAR_NAME": "bot"}
+ if tc.extraKey {
+ extra[InstanceKeyVar] = homeKey
+ }
+ proc, _, err := b.StartWorkerStaged(context.Background(), rt, extra, nil)
+ if proc != nil {
+ defer proc.Stop(context.Background())
+ }
+ got := outcomeOf(out)
+ if tc.want == "" {
+ if !errors.Is(err, ErrReplicaKey) || proc != nil || got != "" {
+ t.Fatalf("StartWorkerStaged = %v (proc %v), outcome %q; want ErrReplicaKey and no worker", err, proc != nil, got)
+ }
+ } else if err != nil || got != tc.want {
+ t.Fatalf("StartWorkerStaged = %v, outcome %q; want %q", err, got, tc.want)
+ }
+ if raw, _ := os.ReadFile(own); string(raw) != tok {
+ t.Fatal("a worker launch changed its own env file: a worker never mints")
+ }
+ })
+ }
+}