carlos / platform Public

appconfig: CanProvideConfig, can this box provide an instance's config (#213)

The admission predicate: resolve the merged manifest the box would render
for a host and, if it carries sealed secrets, require a parseable key that
opens at least one envelope under the route's binding. Nothing sealed needs
nothing. Any read or parse failure refuses; an error is never 'no sealed
config'. Reasons are a closed set of words so they can be published.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 104e784696ccfe993213b66342028be297d467a7 parent 8d8e4ba
2 files changed, +390 −0
  • internal/appconfig/canprovide.go +122 −0
  • internal/appconfig/canprovide_test.go +268 −0
diff --git a/internal/appconfig/canprovide.go b/internal/appconfig/canprovide.go
new file mode 100644
index 000000000..94b52bc9c
--- /dev/null
+++ b/internal/appconfig/canprovide.go
@@ -0,0 +1,122 @@
+package appconfig
+
+import (
+ "context"
+ "crypto/ecdh"
+ "fmt"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The admission question (amadan #213): can THIS box provide this instance's
+// config? A box that cannot must not take the instance's lease or mint its
+// record, because the instance would start without its secrets. That is what
+// a keyless UK box did to kpmg for 1h54m on 2026-10-01: the config sync loop
+// is gated on CARLOS_SECRETS_KEY, instanceEnv treats a missing
+// <host>.config.env as optional, and the instance booted with nothing.
+//
+// The answer is the same question the sync pass asks, asked before the side
+// effect instead of after: resolve the merged manifest the box would render
+// for this host (default < bound environment < instance overlay), and if it
+// carries sealed secrets, prove this box's key opens them.
+
+// KeyState says what the box found when it read CARLOS_SECRETS_KEY. The
+// private key itself travels separately; this is only why there is none.
+type KeyState int
+
+const (
+ // KeyOK: the key parsed. The *ecdh.PrivateKey argument is non-nil.
+ KeyOK KeyState = iota
+ // KeyAbsent: CARLOS_SECRETS_KEY is unset or blank.
+ KeyAbsent
+ // KeyUnparseable: it is set but is not a P-256 private key.
+ KeyUnparseable
+)
+
+// The closed set of refusal reasons. They are published in the box's status
+// snapshot, so they are words, never text built from a store or crypto error.
+const (
+ ReasonNoKey = "no_key"
+ ReasonKeyUnparseable = "key_unparseable"
+ ReasonCannotOpen = "cannot_open"
+ ReasonStoreError = "store_error"
+)
+
+// ConfigRefusal is CanProvideConfig's refusal. Reason is one of the Reason*
+// words above and is the only part safe to publish; Err is the operator-log
+// detail (never a secret: crypto errors name no plaintext and no envelope)
+// and is what errors.Is/As see through.
+type ConfigRefusal struct {
+ Reason string
+ Err error
+}
+
+func (r *ConfigRefusal) Error() string {
+ if r.Err == nil {
+ return "cannot provide this instance's config: " + r.Reason
+ }
+ return "cannot provide this instance's config: " + r.Reason + ": " + r.Err.Error()
+}
+
+func (r *ConfigRefusal) Unwrap() error { return r.Err }
+
+// CanProvideConfig reports nil when this box can provide the config of the
+// instance at host, and a *ConfigRefusal when it cannot or cannot tell.
+//
+// - Nothing sealed in the merged view (no manifest at all, or only plain
+// vars): nothing needs a key, so nil. A deployment with no sealed config
+// anywhere is unaffected, key or no key.
+// - Something sealed: the box needs a parseable key, and that key must
+// open at least one of the merged view's envelopes under the route's own
+// binding. Sealing records no recipient (an envelope is ephemeral point,
+// nonce, ciphertext), so there is no fingerprint to compare; a GCM-
+// authenticated Open is the only proof, and a stronger one.
+// - Any read or parse failure refuses (ReasonStoreError). An error is never
+// "no sealed config".
+//
+// "At least one" is deliberate. One CARLOS_SECRETS_KEY opens every envelope
+// in a deployment, so one that opens proves the key; a single stale or
+// corrupt secret beside it is the sync pass's to report. Demanding all of
+// them would turn that sync fault into a refusal of every wake on a box
+// that holds the right key and a perfectly good rendered file.
+func CanProvideConfig(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, ks KeyState, acct, app, envName, host string) error {
+ var (
+ m *Manifest
+ err error
+ )
+ if ValidInstanceHost(host) {
+ m, _, err = LoadMergedHost(ctx, s, acct, app, envName, host)
+ } else {
+ // A host that cannot name an overlay key has no overlay (the sweeps
+ // treat it the same way); the app-level layers still apply.
+ m, err = LoadMerged(ctx, s, acct, app, envName)
+ }
+ if err != nil {
+ return &ConfigRefusal{Reason: ReasonStoreError, Err: err}
+ }
+ var sealed []string
+ for _, v := range m.Vars {
+ if v.Kind == "secret" {
+ sealed = append(sealed, v.Envelope)
+ }
+ }
+ if len(sealed) == 0 {
+ return nil
+ }
+ if priv == nil || ks != KeyOK {
+ if ks == KeyUnparseable {
+ return &ConfigRefusal{Reason: ReasonKeyUnparseable}
+ }
+ return &ConfigRefusal{Reason: ReasonNoKey}
+ }
+ bind := Binding{Account: acct, App: app}
+ var lastErr error
+ for _, env := range sealed {
+ if _, err := Open(priv, bind, env); err == nil {
+ return nil
+ } else {
+ lastErr = err
+ }
+ }
+ return &ConfigRefusal{Reason: ReasonCannotOpen, Err: fmt.Errorf("none of %d sealed values opens with this box's key (last: %w)", len(sealed), lastErr)}
+}
diff --git a/internal/appconfig/canprovide_test.go b/internal/appconfig/canprovide_test.go
new file mode 100644
index 000000000..ec8197ceb
--- /dev/null
+++ b/internal/appconfig/canprovide_test.go
@@ -0,0 +1,268 @@
+package appconfig
+
+import (
+ "context"
+ "crypto/ecdh"
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Issue #213: a box takes an instance's lease only if it can provide that
+// instance's config. These tests pin the predicate the activator and the
+// reconcile pass both ask.
+
+type canProvideEnv struct {
+ t *testing.T
+ s release.Store
+ priv *ecdh.PrivateKey
+ pub *ecdh.PublicKey
+ other *ecdh.PrivateKey // a key this deployment never sealed for
+}
+
+func newCanProvideEnv(t *testing.T) *canProvideEnv {
+ t.Helper()
+ priv, err := ecdh.P256().GenerateKey(randReader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ other, err := ecdh.P256().GenerateKey(randReader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return &canProvideEnv{t: t, s: release.NewDirStore(t.TempDir()), priv: priv, pub: priv.PublicKey(), other: other}
+}
+
+func (e *canProvideEnv) put(key string, m Manifest) {
+ e.t.Helper()
+ m.Schema = 1
+ body, err := json.Marshal(m)
+ if err != nil {
+ e.t.Fatal(err)
+ }
+ if _, err := e.s.Put(context.Background(), key, body, ""); err != nil {
+ e.t.Fatal(err)
+ }
+}
+
+func (e *canProvideEnv) secret(plain string) Var {
+ e.t.Helper()
+ env, err := SealUnbound(e.pub, []byte(plain))
+ if err != nil {
+ e.t.Fatal(err)
+ }
+ return Var{Kind: "secret", Envelope: env}
+}
+
+func reasonOf(t *testing.T, err error) string {
+ t.Helper()
+ var r *ConfigRefusal
+ if !errors.As(err, &r) {
+ t.Fatalf("err = %v, want a *ConfigRefusal", err)
+ }
+ return r.Reason
+}
+
+func TestCanProvideConfigNothingSealedNeedsNothing(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ // No manifest anywhere: a dev deployment. No key either.
+ if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com"); err != nil {
+ t.Fatalf("no manifest, no key: %v", err)
+ }
+ // A manifest with plain vars only has no sealed config.
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
+ if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com"); err != nil {
+ t.Fatalf("plain-only manifest, no key: %v", err)
+ }
+ // A manifest that was fully unset exists but is empty.
+ e.put(Key("bab", "other"), Manifest{App: "other", Vars: map[string]Var{}})
+ if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "other", "", "a.example.com"); err != nil {
+ t.Fatalf("empty manifest, no key: %v", err)
+ }
+}
+
+func TestCanProvideConfigSealedNeedsAKeyThatOpensIt(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": e.secret("pw")}})
+
+ if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com"); err != nil {
+ t.Fatalf("right key: %v", err)
+ }
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "a.example.com")); got != ReasonNoKey {
+ t.Errorf("absent key reason = %q, want %q", got, ReasonNoKey)
+ }
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyUnparseable, "bab", "shop", "", "a.example.com")); got != ReasonKeyUnparseable {
+ t.Errorf("unparseable key reason = %q, want %q", got, ReasonKeyUnparseable)
+ }
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.other, KeyOK, "bab", "shop", "", "a.example.com")); got != ReasonCannotOpen {
+ t.Errorf("wrong key reason = %q, want %q", got, ReasonCannotOpen)
+ }
+}
+
+// The instance overlay and the bound environment are part of what the box
+// renders, so a secret only there still needs the key.
+func TestCanProvideConfigLooksAtEveryLayerTheBoxRenders(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
+ e.put(EnvironmentKey("bab", "shop", "staging"), Manifest{App: "shop", Vars: map[string]Var{"S": e.secret("x")}})
+ e.put(InstanceConfigKey("bab", "shop", "cust.example.com"), Manifest{App: "shop", Vars: map[string]Var{"T": e.secret("y")}})
+
+ // Unbound route, no overlay for this host: nothing sealed.
+ if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "plain.example.com"); err != nil {
+ t.Errorf("unbound plain host: %v", err)
+ }
+ // Bound to the environment: sealed.
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "staging", "plain.example.com")); got != ReasonNoKey {
+ t.Errorf("environment secret reason = %q", got)
+ }
+ // The overlay for cust.example.com only.
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "cust.example.com")); got != ReasonNoKey {
+ t.Errorf("overlay secret reason = %q", got)
+ }
+ if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "cust.example.com"); err != nil {
+ t.Errorf("overlay with the key: %v", err)
+ }
+}
+
+// One key opens every envelope in a deployment, so one that opens proves the
+// key. A single stale or corrupt secret next to it is the sync loop's fault
+// to report, not a reason to refuse every wake of a keyed box.
+func TestCanProvideConfigOneOpenableEnvelopeProvesTheKey(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ foreign, err := SealUnbound(e.other.PublicKey(), []byte("z"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{
+ "A": {Kind: "secret", Envelope: foreign},
+ "B": {Kind: "secret", Envelope: "not-base64!!"},
+ "C": e.secret("ok"),
+ }})
+ if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com"); err != nil {
+ t.Fatalf("one good envelope among bad ones: %v", err)
+ }
+ // All bad: no proof.
+ e.put(Key("bab", "bad"), Manifest{App: "bad", Vars: map[string]Var{"A": {Kind: "secret", Envelope: foreign}}})
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "bad", "", "a.example.com")); got != ReasonCannotOpen {
+ t.Errorf("all-foreign reason = %q", got)
+ }
+}
+
+// A v2 envelope is bound to its (account, app): the guard opens it under the
+// route's own binding, as the sync pass does.
+func TestCanProvideConfigOpensBoundEnvelopesUnderTheRoutesBinding(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ env, err := SealBound(e.pub, Binding{Account: "bab", App: "shop"}, []byte("pw"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": {Kind: "secret", Envelope: env}}})
+ if err := CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "shop", "", "a.example.com"); err != nil {
+ t.Fatalf("bound envelope, own app: %v", err)
+ }
+ // The same bytes copied under another app do not open there.
+ e.put(Key("bab", "thief"), Manifest{App: "thief", Vars: map[string]Var{"DB": {Kind: "secret", Envelope: env}}})
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "thief", "", "a.example.com")); got != ReasonCannotOpen {
+ t.Errorf("copied bound envelope reason = %q", got)
+ }
+}
+
+// failingStore fails reads of the keys it is told to, and delegates the rest.
+type failingStore struct {
+ release.Store
+ failGet map[string]error
+}
+
+func (f *failingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if err, ok := f.failGet[key]; ok {
+ return nil, "", err
+ }
+ return f.Store.Get(ctx, key)
+}
+
+// "We could not tell" is never "there is no sealed config": a read error on
+// any layer refuses, with or without a key.
+func TestCanProvideConfigFailsClosedWhenItCannotRead(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ boom := errors.New("s3: 503 slow down")
+ e.put(EnvironmentKey("bab", "shop", "staging"), Manifest{App: "shop", Vars: map[string]Var{}})
+ for name, key := range map[string]string{
+ "default": Key("bab", "shop"),
+ "environment": EnvironmentKey("bab", "shop", "staging"),
+ "overlay": InstanceConfigKey("bab", "shop", "a.example.com"),
+ } {
+ t.Run(name, func(t *testing.T) {
+ fs := &failingStore{Store: e.s, failGet: map[string]error{key: boom}}
+ for _, k := range []struct {
+ priv *ecdh.PrivateKey
+ st KeyState
+ }{{e.priv, KeyOK}, {nil, KeyAbsent}} {
+ err := CanProvideConfig(ctx, fs, k.priv, k.st, "bab", "shop", "staging", "a.example.com")
+ if got := reasonOf(t, err); got != ReasonStoreError {
+ t.Errorf("reason = %q, want %q", got, ReasonStoreError)
+ }
+ if !errors.Is(err, boom) {
+ t.Errorf("err %v does not wrap the store error", err)
+ }
+ }
+ })
+ }
+}
+
+// A manifest the box cannot parse and a bound environment that is gone are
+// both "cannot tell".
+func TestCanProvideConfigFailsClosedOnUnreadableConfig(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ if _, err := e.s.Put(ctx, Key("bab", "garbled"), []byte("{not json"), ""); err != nil {
+ t.Fatal(err)
+ }
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "garbled", "", "a.example.com")); got != ReasonStoreError {
+ t.Errorf("garbled manifest reason = %q", got)
+ }
+ e.put(Key("bab", "bound"), Manifest{App: "bound", Vars: map[string]Var{}})
+ if got := reasonOf(t, CanProvideConfig(ctx, e.s, e.priv, KeyOK, "bab", "bound", "gone", "a.example.com")); got != ReasonStoreError {
+ t.Errorf("missing bound environment reason = %q", got)
+ }
+}
+
+// A host that fails ValidInstanceHost cannot have an overlay (the sweeps read
+// that as "no overlay"); the guard must answer, not error on the host shape.
+func TestCanProvideConfigHostWithNoPossibleOverlay(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}})
+ if err := CanProvideConfig(ctx, e.s, nil, KeyAbsent, "bab", "shop", "", "Mixed.Case.example.com"); err != nil {
+ t.Fatalf("host without a possible overlay: %v", err)
+ }
+}
+
+// Nothing secret leaves in the error: not the envelope, not the plaintext.
+func TestConfigRefusalReasonIsAClosedWord(t *testing.T) {
+ e := newCanProvideEnv(t)
+ ctx := context.Background()
+ v := e.secret("hunter2")
+ e.put(Key("bab", "shop"), Manifest{App: "shop", Vars: map[string]Var{"DB": v}})
+ err := CanProvideConfig(ctx, e.s, e.other, KeyOK, "bab", "shop", "", "a.example.com")
+ var r *ConfigRefusal
+ if !errors.As(err, &r) {
+ t.Fatal(err)
+ }
+ switch r.Reason {
+ case ReasonNoKey, ReasonKeyUnparseable, ReasonCannotOpen, ReasonStoreError:
+ default:
+ t.Errorf("reason %q is not one of the closed words", r.Reason)
+ }
+ if strings.Contains(err.Error(), v.Envelope) || strings.Contains(err.Error(), "hunter2") {
+ t.Errorf("error leaks sealed material: %v", err)
+ }
+}