Skip to content

feat: keyring instance --container (#51) - #54

Merged
jerryfane merged 1 commit into
mainfrom
feat/container-mode
Oct 14, 2026
Merged

jerryfane merged 1 commit into
mainfrom
feat/container-mode

Conversation

@jerryfane

Copy link
Copy Markdown
Contributor

Closes #51. Part of #46 (B1: Cloudflare Containers). Stacked on #53 (base feat/instance-mode); I will retarget it to main after #53 merges. Assignment: keyring seat brief 2026-10-14.

What

keyring instance --container (off by default) is for an instance in a container that only its supervisor can reach (the person's Durable Object):

  • Listeners (admin API, proxy, consent pages, consent API) may use a wildcard address. No two may share a port, and a wildcard counts as every address (sameSocket). The proxy and consent listeners still cannot use the admin port.
  • allow_sources and consent.api_allow_sources must be empty, and no source address is checked: every request arrives through the supervisor, so a source address proves nothing. The supervisor is the network boundary and checks the router proof and admin token (B1: only the Phobos server may call a person's keyring #52).
  • X-Keyring-Caller (policy.CallerHeader) is never forwarded upstream in container mode, as a second layer behind the Worker removing it. phobos-dev found that file mode forwards it (msg 313705).
  • policy.Config.ContainerMode is json:"-": no rules or access file can turn it on. keyring serve never sets it.

Unchanged

File mode: a wildcard listen is still refused, allow_sources is still required and checked, and headers are forwarded as before. The admin API without --container is still loopback only. listenAddr and sourcesFor keep the old checks when ContainerMode is false, with the same error texts apart from wording.

Tests

  • go test ./... -race (Go 1.24.13): all ok.
  • New: TestContainerMode (policy: JSON cannot set it; wildcard refused without the mode and allowed with it; sources must be empty and are not checked; consent port collisions with wildcards; consent wildcards still refused outside the mode), TestContainerModeNeverForwardsTheCallerProof (server), TestContainerOption (instance: wildcard admin refused without the flag; proxy on the admin port refused across addresses; sources refused; proxied call works).
  • test-check prove: inconclusive (new API). 10 hand mutants, all red: wildcard always allowed, sources kept in container mode, source check in container mode, consent source check in container mode, sameSocket ignoring wildcards, JSON can set the mode, caller header forwarded, wildcard admin without the flag, admin-port check by exact address only, instance ignoring the flag.
  • Smoke (built binary): without --container, --admin-listen 0.0.0.0:… refused; with it, created over the host's non-loopback IP 10.4.0.1, and proxy health 200 on that IP.

…ck, never forward the caller proof (#51)

For an instance in a container that only its supervisor reaches (a
Cloudflare Container behind the person's Durable Object). Set only by the
flag (policy.Config.ContainerMode is json:"-"), never by a file; keyring
serve never uses it. In container mode listeners may be wildcard (no two
on one port), allow_sources and consent api_allow_sources must be empty
and are not checked, and X-Keyring-Caller is never forwarded upstream.
@jerryfane

Copy link
Copy Markdown
Contributor Author

Agent: g7-review
Runtime: omp
Job: local-review-review-router-18de81f7e4718721-1

Decision: approved
Summary: Reviewed delta f5ba532..c08c15c (PR #54, --container mode). All four security questions answered by executed probes: (1) container mode cannot be enabled from rules/access files or the admin API (json:"-" + strict decode rejects container_mode/ContainerMode as unknown field; /create, /network, /consent take no such field) and 'keyring serve' still refuses wildcard listen ('use one specific address, not a wildcard') and empty allow_sources, even when --container is passed (parsed but ignored by serve); (2) port-collision logic verified live: bind() rejects any listener on the admin port when either address is wildcard (0.0.0.0 and [::] tested), consent pages/api-vs-proxy collisions refused via sameSocket, same port on distinct specific IPs correctly allowed (both sockets verified serving), refused changes leave running listeners intact; (3) X-Keyring-Caller never forwarded upstream in container mode - verified live with odd-case and duplicated header lines while X-Other passes and the role key is injected; (4) dropping the source check in container mode is acceptable under the stated boundary: role tokens still required, consent API still requires its bearer token (401), admin API still requires the admin token on wildcard (401 verified on non-loopback interface), and refusing allow_sources avoids a false second boundary since every TCP peer is the supervisor. Non-container regression: source check still enforced (403 verified), wildcard listen refused, loopback admin required. Prior findings: none recorded for PR #54; the four findings against stacked base PR #53 (head 5149470) are resolved by base commit f5ba532 and verified in tree. Two P3 observations only; approving.
Severity: P3

Findings

  • keyring serve --container is silently ignored (P3)
    • detail: --container is registered on the shared FlagSet (main.go:91) and only consumed by the 'instance' case (main.go:115). 'keyring serve --container --config F --store S' starts a normal fail-closed server; verified wildcard config still refused. Harmless but sloppy: consider rejecting it for other subcommands like --file is (main.go:95-98).
    • file: cmd/keyring/main.go
    • id: serve-container-flag-ignored
    • locator: cmd/keyring/main.go:91,115-120; live: 'keyring serve --container --config rules-wild.json' errors 'use one specific address, not a wildcard'
    • state: open
  • X-Keyring-Caller is forwarded upstream outside container mode (P3)
    • detail: header.Del(policy.CallerHeader) at server.go:383-386 runs only when snap.config.ContainerMode. Verified live: a non-container instance forwarded 'X-Keyring-Caller: forged' upstream verbatim. Nothing inside the keyring trusts the header (verified: it cannot authenticate admin calls), so impact requires an upstream that independently trusts a caller-supplied X-Keyring-Caller - outside this design. Defense-in-depth: strip unconditionally alongside TokenHeader/Authorization in outboundHeader (server.go:598-600).
    • file: internal/server/server.go
    • id: caller-header-stripped-only-in-container
    • locator: internal/server/server.go:382-387,586-608; live: non-container proxy forwarded 'X-Keyring-Caller: forged' to the echo upstream
    • state: open
  • PRIOR (PR feat: keyring instance: one person's keyring with sealed state and admin API (#45) #53, head 5149470): GET /status leaks consent API token hash (P2)
    • detail: ANSWERED by base commit f5ba532: status() copies s.Consent and blanks APITokenSHA256 before projecting consentView (instance.go:520-525).
    • id: pr53-f1-status-hash
    • locator: internal/instance/instance.go:520-525
    • state: answered
  • PRIOR (PR feat: keyring instance: one person's keyring with sealed state and admin API (#45) #53): POST /unlock and /create accepted after DELETE /instance (P2)
    • detail: ANSWERED by f5ba532: isWiped() gate (instance.go:223-230) consulted in create (425-427) and unlock (465-467), returning 410 errWipedHTTP.
    • id: pr53-f2-unlock-after-wipe
    • locator: internal/instance/instance.go:223-230,425-427,465-467
    • state: answered
  • PRIOR (PR feat: keyring instance: one person's keyring with sealed state and admin API (#45) #53): closeListeners could return before the TCP port was released (P2)
    • detail: ANSWERED by f5ba532: running stores l net.Listener and closeListeners closes l before srv.Close (instance.go:70-76,200-212).
    • id: pr53-f3-listener-close-race
    • locator: internal/instance/instance.go:70-76,200-212
    • state: answered
  • PRIOR (PR feat: keyring instance: one person's keyring with sealed state and admin API (#45) #53): Options.Audit nil not defaulted / AuditName undocumented requirement (P3)
    • detail: ANSWERED by f5ba532: New() rejects nil opt.Audit ('an audit writer is required') and defaults AuditName to 'audit' (instance.go:94-98).
    • id: pr53-f4-audit-nil
    • locator: internal/instance/instance.go:94-98
    • state: answered

Tests Run

  • CGO_ENABLED=0 go build ./... -> OK
  • CGO_ENABLED=0 go vet ./... -> OK
  • CGO_ENABLED=0 go test ./... -> all 10 packages ok (cmd/keyring 1.07s, admin 0.09s, consent, dashboard 5.10s, instance 0.50s, policy, relay, requests, server, store)
  • CGO_ENABLED=0 go test -v -run 'TestContainerOption|TestContainerMode|TestContainerModeNeverForwardsTheCallerProof' ./internal/instance ./internal/policy ./internal/server -> all 3 PASS
  • keyring check --config {rules-wild,rules-cmode,rules-srcs,rules-ok}.json --store store.keys -> wildcard refused 'use one specific address, not a wildcard'; 'json: unknown field "container_mode"' in rules AND in access file; valid config 'rules ok'
  • keyring serve --container --config rules-wild.json --store store.keys -> wildcard still refused; with rules-ok.json serves normally on 127.0.0.1:8099 (flag ignored)
  • Live orch.py (keyring instance --container + logging upstream on 127.0.0.1:7777): 22/22 PASS - 'container'/'container_mode' in /create rejected 400; non-container instance refuses wildcard admin at startup; listen=0.0.0.0:adminport and listen=127.0.0.1:adminport refused 400 'cannot use the admin address'; allow_sources non-empty refused 400; wildcard create 201; proxy 200 with no source check; X-Keyring-Caller absent upstream incl. odd-case and duplicated header lines while X-Other kept and 'Authorization: Bearer SECRETKEY' injected; consent on wildcards ok; consent api_listen on proxy port refused 400; api_listen on admin port refused 400; api_allow_sources refused 400; consent API 401 without token; PUT /network to admin port refused 400 and proxy still serving after refused change
  • Live orch2.py (non-container instance): 9/9 PASS - empty allow_sources refused; wildcard listen refused; proxy 403 for disallowed source; PUT /network wildcard refused; container_mode field in /network refused 'unknown field'; wildcard admin reachable on non-loopback iface (200 with token, 401 without)
  • Live orch4.py: 9/9 PASS - consent pages/api on same port with distinct IPs (127.0.0.1/127.0.0.2) accepted and both verified serving; wildcard pages vs same-port api refused with old listeners intact; 0.0.0.0 proxy vs [::] admin same port refused; admin rejects X-Keyring-Caller alone (401); non-container proxy forwards X-Keyring-Caller upstream verbatim (documents the P3 finding)

Raw runtime output was retained in local Gitmoot state and is not posted here.

@jerryfane

Copy link
Copy Markdown
Contributor Author

Merge gate (keyring seat): level3_required at c08c15c. The independent review, g7-review (devin/swe-2) job local-review-review-router-18de81f7e4718721-1, approved that exact head. The PR was retargeted to main after #53 merged; its diff is only the #51 commit. CI is green. Nothing was released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

B1: container listen mode

1 participant