Repository navigation
Conversation
A second process on the same host, as another user, behind a unix socket whose peer uid it checks. It makes an assistant's X25519 agent key when an authorization starts and seals it at rest: one AES-256 data key per agent key from AWS KMS (a file key in development), with an encryption context that names the purpose and the key id. A key stays pending until the owner approves, and opens no session until then; an unapproved key goes after an hour. It unseals a key only to open one Noise IK session to one box, with the box session's prologue set by ftwkeysd itself, and wipes the secret once message 1 is written. It keeps the transport keys and seals and opens each frame for ftwcloudd, which carries them as tun. Forget deletes the key and ends its sessions. Errors on the socket are a few fixed words, and nothing logs a key. Deploy files, not run: a compose service under the `agents` profile on the host's network (the one container that reaches the instance role with an IMDS hop limit of 1), the KMS key policy (root may administer, only the host role may use, only with the purpose), the role's inline policy, and a host script for the socket directory. The guide's first invariant changes as decided on 15 October 2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2e7c496a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| sed -i '/^FTW_CLOUD_KMS_KEY=/d; /^COMPOSE_PROFILES=/d' "$env" | ||
| printf 'FTW_CLOUD_KMS_KEY=%s\nCOMPOSE_PROFILES=agents\n' "$key_arn" >> "$env" |
There was a problem hiding this comment.
Preserve the key-service settings during deployment
In the documented activation flow, keys.sh writes these settings and then step 5 runs ftw-cloud-deploy, but deploy/bootstrap.sh:145 replaces the entire environment file with only FTW_CLOUD_IMAGE. Consequently COMPOSE_PROFILES=agents and FTW_CLOUD_KMS_KEY disappear immediately before the service restart, so ftwkeysd is not started. The deploy helper must preserve these entries rather than overwrite them.
Useful? React with 👍 / 👎.
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
| if len(s.sessions) >= MaxSessions { | ||
| return "", nil, ErrBusy | ||
| } | ||
| s.sessions[sess] = &session{kid: kid, init: init, used: s.now()} |
There was a problem hiding this comment.
Serialize session creation with key revocation
When an Open is blocked in unseal—notably during the KMS call—a concurrent Forget can delete the key, inspect the session map, and return before this code inserts the new session. The revoked key then leaves a usable session behind after Forget has promised to end all of its sessions. Session insertion must be coordinated with revocation or revalidate that the key still exists while holding the relevant lock.
Useful? React with 👍 / 👎.
| if n, _ := countDir(filepath.Join(s.dir, "pending")); n >= MaxPending { | ||
| return "", nil, ErrBusy | ||
| } |
There was a problem hiding this comment.
Reserve pending-key capacity atomically
Under concurrent new requests, every goroutine can count the same below-limit directory before any of them writes its record, so all proceed to call KMS and create files. A burst of simultaneous authorization requests can therefore exceed MaxPending by an arbitrary amount, defeating the stated resource bound and allowing unbounded KMS work; the check and reservation need synchronization.
Useful? React with 👍 / 👎.
The agent contract (srcfl/ftw#1642) has FTW Cloud sign each grant offer the consent page hands the owner's app, with a key the app pins. It lives in the keys part, apart from the agent keys: one P-256 key, made at the first start, sealed like an agent key under its own context, kept in memory. It signs only the deterministic CBOR ["ftw.offer.v1", offer]. VerifyOffer is the app's check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract's signed offer is {o, kid, sig}, and Core and the apps pin
offer keys by kid. The kid is the first 16 characters of the base64url
SHA-256 of the key's SEC1 point. ftwkeysd logs kid and key when it makes
the offer key, and `ftwkeysd offerkey` prints them from the same state
directory; deploy/keys.md adds the pinning step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r key in KMS, a role of its own - signOffer decodes the GrantOffer strictly and signs only version 1, the agent role, at most OfferTTLMs ahead, and a key that is a pending kid's. The pin of the agent contract (e2559632) moves here from #19 for it. - The offer key is an asymmetric KMS key (ECC_NIST_P256, SIGN_VERIFY) that only the keys role may sign with. A local offer key and the file sealer exist only in a development build (-tags ftwclouddev). - ftwkeysd leaves the host network for a bridge of its own and uses the ftw-cloud-keys role, which may use the two KMS keys and nothing else. The host renews its credentials into tmpfs every 15 minutes. - Key policies: kms:CreateAlias instead of kms:Create*, an explicit deny on kms:CreateGrant, and keys.md says plainly that PutKeyPolicy lets an admin take use. - ftwcloudd runs as 65530, not distroless's shared nonroot. ftw-cloud-own gives its files to the user its image runs as at every start, and ftw-cloud-deploy keeps the lines keys.sh adds to compose.env. - Pin the offer key from ftwkeysd on the host through SSM, not from the public endpoint. - The socket refuses a connection that is not a unix socket's, and a message 2 that does not split ends its session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#19, #20, #21) The MCP stack as one change: ftwkeysd, the one part that can open an agent's session (#18); OAuth for assistants with the owner's approval at home (#19); reads at /mcp (#20); control within the owner's grant, and requests that wait for the owner (#21). Reviewed in two passes; merged with main after #17 (scale), go/protocol pinned to srcfl/ftw master f024251. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First of four stacked PRs that bring MCP into FTW Cloud, as decided on 15 October 2026: keys (this), OAuth and consent, MCP read, MCP control. Each is based on the one before. It pins the agent contract as merged: srcfl/ftw#1642 at bf5bf50.
What this adds
ftwkeysd, the keys part: the one process that can open an agent's Noise session to a box.SO_PEERCREDon Linux,LOCAL_PEERCREDon macOS).GenerateDataKey), and the secret is sealed with it by AES-GCM. The encryption context names the purpose (ftw-agent-key) and the key id, so a wrapped data key opens only for its own key. A local file key replaces KMS only in a development build (-tags ftwclouddev); a production build has no-file-keyand nofilekey.keep, which comes when the owner approves. An unapproved key goes after an hour.ftwkeysdsets the box session's prologue itself, so it opensftw.session.v1sessions and nothing else. The secret is wiped once message 1 is written. The transport keys stay inftwkeysd, which seals and opens each frame.ftwclouddcarries the frames astunand never holds an agent's static or session keys.forgetdeletes a key and ends its sessions at once.ECC_NIST_P256,SIGN_VERIFY).ftwkeysdsigns withkms:Signon the digest and checks the answer against the public half; the private half never leaves KMS. A production build will not start without-offer-kms-key. A local P-256 key in the state directory exists only in a development build.offer.signnames a kid and decodes the offer strictly, as the box does (no unknown or repeated keys). It signs only version 1, the agent role,expno more than ten minutes ahead, and akeythat is the public half of a keyftwkeysdmade and that waits for approval. Softwclouddcannot get an owner to approve a key thatftwclouddholds, or offer any other role.VerifyOfferis the check the app makes with the key it pins, and what it signs opens withprotocol.OpenOffer.Deploy files (not run; the owner deploys)
deploy/keys.mdcovers the shape, the security argument and the steps.ftwkeysdservice under theagentsprofile, as uid 65533 inftwcloudd's group, on a bridge network of its own, with its own volume.ftwclouddmounts only the socket's directory.ftwkeysdreaches KMS and nothing else. With the instance's IMDS hop limit at 1, no container gets the instance role, which reads the service key and the parameters.ftwkeysduses a role of its own,ftw-cloud-keys, whose policy allows the two KMS keys and nothing more. The host assumes it every 15 minutes and leaves hour-long credentials in tmpfs, readable only byftwkeysd's user.ftwclouddneeds no AWS credentials, and links no AWS SDK (check-imports.shchecks that).ftw-cloud-keysmay use each key: the agent key only with the purpose, the offer key only to sign digests with ECDSA_SHA_256 and read its public half. The account root may administer them (kms:CreateAlias, notkms:Create*) but not use them. An explicit deny onkms:CreateGrantstops a grant from opening either key beside its policy.keys.mdsays plainly that whoever may put a key's policy can write themselves in, and that the change shows in CloudTrail.keys-role-trust.jsonandkeys-role-policy.json, for the role;keys-host-policy.json, which lets the host assume it;keys.sh, which makes the socket's directory at every boot, installs the timer that renews the credentials, and turns the profile on.ftwkeysd offerkeythrough SSM, not from the public/.well-known/ftw-offer-key, withaws kms get-public-keyas a cross-check.ftwclouddruns as 65530, its own uid, not distroless's sharednonroot(65532).ftw-cloud-own, run before each start, gives the state volume and the secrets in tmpfs to the user the configured image runs as, so deploying this image, or going back to an older one, finds its files its own.ftw-cloud-deploynow keeps the lineskeys.shadds tocompose.env; before, it overwrote them. Step 0 ofkeys.md: re-runbootstrap.shbefore deploying.ftwkeysdfor the host.The guide's first invariant changes to "forwards ciphertext it cannot read; acts on a home only as an agent its owner approved, within limits the box enforces". A second invariant says agent keys live only in
ftwkeysd.What the tests prove (
internal/keys)ftwkeysdrestarts, kept keys survive, and sessions do not.-race.protocol.OpenOffer. It refuses, each by name: an empty or non-CBOR offer, an unknown field, another pending key's kid, another key in the offer, a made-up key, no kid, the support or owner role, another version, an expired offer, an offer good for too long, and a key the owner already approved.cmd/ftwkeysd). It has no-file-keyand nofilekey, and refuses to start without-kms-keyor-offer-kms-key. The keys role's credentials come from the host's file and expire when it says.Not in this PR
deploy/keys.md.🤖 Generated with Claude Code