package activator // A replica gets its home's key (instance-key plan rulings 26 to 28). A // replica (#162) mirrors its home's data, sealed with the home's key, so // the only key it may be handed is exactly the home's effective key. The // platform never mints one on a replica row (ensureInstanceKeyFor), and // every replica launch passes replicaKeyGate before anything runs: the // flat seed and spawn (seedChild, spawnChildFrom), the nested run, seed // and sidecar roles (prepareAt), the worker (StartWorkerStaged), the // legacy co-spawned sidecar (startSidecarChildFrom) and the claim launch // sources' selection (SelectClaimLaunch, which has no control socket until // O6; the claim's input policy snapshots without the gate, because it // launches nothing). // // - a replica whose env files deliver a key starts only when that key's // digest is its entry in the box's verified list (ReplicaHomeKeys, which // only the pre-roll check writes, read again at every launch) and, when // the key comes from the replica's configuration, the configuration in // the bucket renders that same key now (ConfiguredKey). Where the key // came from is decided by the same read that composed the env the child // gets (instanceEnvLayer, ruling 29), never by reading the files again. // A key that differs from either, or that cannot be checked, refuses // the launch with a log line. The fresh read catches a configuration // change that this box has not synced yet while the home already holds // the new key (ruling 27); // - a replica whose env files deliver no key starts only with a control // socket. There an app on rastrillo's InstanceKey with no key of its // own gets ErrNoInstanceKey and refuses (step 3), and an app that never // asks for a key runs as before. Without a socket (a seed, a worker, a // legacy sidecar, a claim launch before O6, a control bind that // failed) the helper would fall to step 4 and mint its own key over // the home's data, so the agent refuses the launch itself rather than // leave that to the helper. // // Delivering the home's key to a replica that has none is O8's, for a // platform-minted home key only (ruling 28): until then, and always for a // home whose key exists only in configuration, a keyless replica gets // nothing and is gated as above. import ( "context" "errors" "fmt" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/registry" ) // ErrReplicaKey is a replica launch refused by replicaKeyGate. var ErrReplicaKey = errors.New("activator: replica launch refused: its instance key is not its home's") // instanceEnvLayer is instanceEnv returning, from the same read, the layer // its CARLOS_INSTANCE_KEY came from (ruling 29). mergeEnv delivers exactly // the key instancekey.Effective names, so env[Var] and the layer always // describe the same bytes, however the files change afterwards. func (b *ExecBackend) instanceEnvLayer(rt registry.Route) (map[string]string, instancekey.Layer, error) { cfgEnv, ownEnv, err := b.instanceEnvParts(rt) if err != nil { return nil, instancekey.LayerNone, err } _, layer := instancekey.Effective(cfgEnv, ownEnv) return mergeEnv(cfgEnv, ownEnv), layer, nil } // sidecarKeyLayer is the layer of the key in a legacy co-spawned sidecar's // env, its own hand-staged -sidecar.env (one file, never merged with // the instance's layers): LayerOwn when that file holds a key, as the // instance's own .env would. Nothing syncs that file from the bucket, // so a key there is checked against the home's listed digest with no fresh // read, and a keyless one is gated as any launch without a socket is. func sidecarKeyLayer(env map[string]string) instancekey.Layer { if env[instancekey.Var] != "" { return instancekey.LayerOwn } return instancekey.LayerNone } // replicaKeyGate is rulings 26, 27 and 29's check for a launch of rt with // env (the composed env files), layer (where env's key came from, decided // by the same read) and, when haveControl, a bound control socket. It reads // no env file. It is nil for every row that is not a replica. Its errors and // log lines name the host and the reason, never a key or a digest. The // log line is said once an hour per host and reason (logPerFile): a // keyless replica's legacy sidecar is refused at every retry of the // sidecar sweep, and the line is for the operator, who needs it once. A // bucket error is logged as its own argument, apart from the fixed // reason, so a reader can see where the text came from. func (b *ExecBackend) replicaKeyGate(rt registry.Route, env map[string]string, layer instancekey.Layer, haveControl bool) error { if !rt.Replica { return nil } if instanceKeyHooks.replicaGate != nil { instanceKeyHooks.replicaGate() } refuse := func(why string, cause error) error { if cause != nil { logPerFile("replica-gate\x00"+why, rt.Host, fmt.Sprintf("activator: replica %s not started: %s: %v", rt.Host, why, cause)) return fmt.Errorf("%w: %s: %s: %w", ErrReplicaKey, rt.Host, why, cause) } logPerFile("replica-gate\x00"+why, rt.Host, fmt.Sprintf("activator: replica %s not started: %s", rt.Host, why)) return fmt.Errorf("%w: %s: %s", ErrReplicaKey, rt.Host, why) } key := env[instancekey.Var] if key == "" { if haveControl { return nil } return refuse("it has no key of its home's and no control socket, so an app on the instance-key helper would mint its own", nil) } var home string var ok bool if b.ReplicaHomeKeys != nil { home, ok = b.ReplicaHomeKeys(rt.Host) } switch { case !ok: return refuse(instancekey.Var+" cannot be checked against its home's key (no entry for it in /etc/carlos/replica-home-keys; O8)", nil) case instancekey.Digest(key) != home: return refuse(instancekey.Var+" is not its home's key", nil) } switch layer { case instancekey.LayerOwn: return nil // the replica's own file: it changes only by hand case instancekey.LayerConfig: default: return refuse("where its "+instancekey.Var+" came from is unknown", nil) } configured := b.configuredKeyFor(rt) if configured == nil { return refuse("its configured "+instancekey.Var+" cannot be checked against the bucket's configuration", nil) } fresh, err := configured(context.Background()) switch { case err != nil: return refuse("the bucket's configuration could not be read", err) case fresh != instancekey.Digest(key): return refuse("its configured "+instancekey.Var+" is not the one the bucket's configuration now holds: a key change is in flight (ruling 27)", nil) } return nil }