Repository navigation
deploy: images built in CI, run by digest; service key in Secrets Manager - #3
Merged
Merged
Conversation
bootstrap.sh brings up an Amazon Linux 2023 host through SSM: swap, core dumps and sshd off, Docker as on the relay host, and ftw-cloud-deploy, which builds the checked-out commit and starts a new container. The service key is made on the host and copied to tmpfs before Docker starts. deploy/README.md records the running host: ids, how to update, check and tear it down, and the cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reboot test showed sshd listening on 22 again: cloud-init.service wants it, so disabling the unit is not enough. Masking it keeps it off across reboots. The security group already dropped 22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts Manager ADR 0008 asks for images built in CI from a reviewed commit, deployed by digest, and for the service key in Secrets Manager. Until now the host built its own image and kept the key on its disk. - The Image workflow builds linux/arm64 from every commit on main, pushes it to the public ghcr.io/srcfl/ftw-cloud and attests its provenance. A pull request builds and pushes nothing. - The Dockerfile cross-compiles, so CI needs no arm64 emulation, and passes the commit in with -ldflags, since .git stays out of the context. - /source names the commit (X-Source-Revision) and the running image digest (X-Source-Image), as ADR 0008 asks. - ftw-cloud-deploy takes a digest, refuses an image whose commit is not on main, and checks out that commit for compose.yml. It builds nothing and makes no key. - ftw-cloud-key fetches the key from Secrets Manager into tmpfs at boot. The role gets one inline policy: GetSecretValue on that one secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
marked this pull request as ready for review
October 13, 2026 07:00
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…into claude/cloud-ci-image-secret
frahlg
added a commit
that referenced
this pull request
Oct 13, 2026
…a key Like the service key's fetch in #3: at boot the network and the instance's credentials may lag, so a read is tried for a minute. A parameter that does not exist ends it at once, so a host without a key does not hold Docker back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
frahlg
added a commit
that referenced
this pull request
Oct 13, 2026
deploy/ keeps main's version. C4's APNs pieces stay: deploy/apns.sh and apns.md, and the compose volume that mounts /run/ftw-cloud/apns. The -apns-dir line goes, since the flag's default is the same path. server_test.go keeps main's /source test and C3's socket tests; the sockets test covers /v1/box, so main's uplink-only test goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
added a commit
that referenced
this pull request
Oct 13, 2026
* feat(app): phones read their home over Noise at /v1/app
A phone runs Noise IK to the service's Noise key with the home id in the
prologue. The service admits it only through store.For with the key the
handshake proved, so only a phone in the box's signed roster that no
owner has suspended gets in. Message 2 carries the snapshot from memory;
after it each new box envelope the device may read follows on the same
socket, unchanged.
A roster change, a suspension, an expired roster or a purge closes the
sessions it touches at once. While a phone looks, the service asks the
box for one-second updates and renews the ask every 50 s. Limits per
home: 16 sessions, one per device, a queue of 256 per phone and a small
message rate. Owners' suspensions live in the registry and end when the
roster drops the device or its epoch moves.
go/protocol moves to srcfl/ftw claude/cloud-c3-core (7490c64c) for the
app session wire, the Noise initiator and RemoteStatic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: move go/protocol to srcfl/ftw claude/cloud-c3-core c6301a0b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: alerts through APNs, signed by the box (FTW Cloud C4)
A box's signed event now reaches its phones through Apple, wrapped
unchanged; the phone checks the signature, writes its own words and picks
the buttons from the verified kind. The service chooses none of that.
- internal/apns: HTTP/2 to Apple's provider API with an ES256 token from
the team's .p8, renewed every 40 minutes and once on
ExpiredProviderToken. 410, BadDeviceToken and DeviceTokenNotForTopic
mark a token gone. Every alert has the same push type and priority and
no collapse id.
- internal/registry: a push table bound to roster entries (home, device
id, epoch), cascading with the home on purge and expiry, and expiring
30 days after the box's last roster.
- internal/push: registration (only devices whose effective scope reads
events), roster pruning, fan-out with an hourly cap per home, and the
dead man's switch: box.unreachable after ten minutes away, held back for
an hour after bye{why:"update"}, never for a revoke.
- uplink: OnConnect (as C3 has it), OnEvent, OnRoster and OnGone hooks.
- ftwcloudd: -apns-dir; without a key nothing is sent. Development builds
get -dev-push-listen to register a token by hand before the app session
can.
- deploy: apns.sh reads the key from Parameter Store into tmpfs at boot;
the host role can already read it. apns.md is the owner's steps.
Pins go/protocol at srcfl/ftw 8cca4203.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(dev): capture alerts to files in development builds
-dev-apns-capture <dir> writes each alert as JSON instead of sending it to
Apple, so an end-to-end test can hand it to a simulator with xcrun simctl
push before an APNs key exists. Release builds refuse the flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(app): a host without a Noise key still serves boxes
/v1/app answers 501 until a Noise key is installed, so deploying this
before the deploy files learn the second secret cannot stop the service.
The deploy files stay as they are: #3 owns them, and the
Noise key goes in beside the service key once it lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(push): say when a box goes away and when it is out of reach
Neither line names the home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: the registry also keeps owners' suspensions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* deploy(apns): retry while the network comes up, stop at once without a key
Like the service key's fetch in #3: at boot the network and the instance's
credentials may lag, so a read is tried for a minute. A parameter that does
not exist ends it at once, so a host without a key does not hold Docker back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(dev): send a raw payload through APNs in development builds
POST /dev/push/raw on the loopback dev listener sends a payload unchanged
to a home's registered phones, so the end-to-end test can put a forged
alert through Apple's sandbox and watch the phone's extension refuse it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: phones register for alerts over their app session
push.register and push.unregister arrive on a phone's Noise session and
go to the push service with the roster entry and scope store.For gave
that session, never anything from the message. A registration the device
may not make, such as a viewer's, registers nothing and the session goes
on; one the service cannot read closes it as a protocol error. Without a
push service both are ignored.
A device an owner has suspended keeps its tokens but gets no alerts,
box.unreachable included, until the suspension ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows #2, now on
main. The branch mergesmainin rather than rebasing onto it; the diff againstmainholds only this PR's changes.ADR 0008 asks for two things the host lacks: images built in CI from a reviewed commit and deployed by digest, and the service key in Secrets Manager. This PR adds both.
Images from CI
.github/workflows/image.ymlbuildslinux/arm64from every commit onmainand pushes it toghcr.io/srcfl/ftw-cloud:<commit>. It records the build's provenance withactions/attest-build-provenance, sogh attestation verify oci://ghcr.io/srcfl/ftw-cloud@sha256:… -R srcfl/ftw-cloudshows which commit and workflow made a digest. A pull request builds the image and pushes nothing.Dockerfilecross-compiles (--platform=$BUILDPLATFORM), so the runner emulates nothing, and setsinternal/server.Revisionwith-ldflags, because.gitstays out of the build context./sourcenow names the commit (X-Source-Revision) and the running image digest (X-Source-Image, fromFTW_CLOUD_IMAGEincompose.yml). ADR 0008 lists "the running digest published at /source" as a control.ftw-cloud-deploy sha256:<digest>pulls the image, refuses it unless its revision label names a commit onmain, checks/opt/ftw-cloudout at that commit socompose.ymlmatches the image, and starts a new container. It builds nothing and makes no key. With no argument, it runs the digest incompose.envagain.The service key in Secrets Manager
ftw-cloud/service-key(eu-north-1,aws/secretsmanager).ftw-cloud-key.serviceruns/usr/local/sbin/ftw-cloud-keybefore Docker starts. It fetches the key into/run/ftw-cloud/service.key(tmpfs,65532,0400) and retries for a minute while the network comes up. The key is no longer on disk.ftw-cloud-hostgets one inline policy,deploy/host-policy.json:secretsmanager:GetSecretValueon that one secret.Checked
go vet ./... && go test -race ./...,go test -tags ftwclouddev ./...andscripts/check-imports.shpass. A new test,TestSourceNamesRevisionAndImage, covers the two headers.shellcheckpasses onbootstrap.shand on the two scripts it writes.linux/arm64and, as a cross-compile check, forlinux/amd64. In the arm64 image the revision label was set,/healthzsaidok, and/sourcereturned the archive with both headers.On the host
Fredrik said yes to the IAM policy and the host changes on 13 Oct 2026. Done that day:
ftw-cloud/service-key(arn:aws:secretsmanager:eu-north-1:976075258475:secret:ftw-cloud/service-key-4WTn0k, tagProject=ftw-cloud) with no value.ftw-cloud-hostthe inline policyftw-cloud-service-keywithGetSecretValueand, for the move only,PutSecretValueon that ARN.put-secret-value --secret-string file:///var/lib/ftw-cloud/service.key, then read it back into tmpfs. The public key from the disk and from the secret are bothBG6EvBxFzSHPVsQOES6PYaKFo5vHQmte9lJ6D5OBIT9JgcBrJAOxAucdqKXxS76pToQ7orI1Ivsno0OtxbMuliQ. Nothing printed the key.deploy/host-policy.json.simulate-principal-policy:GetSecretValueallowed,PutSecretValueandListSecretsdenied.bootstrap.shon the host. It installedftw-cloud-key, the new units and the newftw-cloud-deploy. The host still has no CI image, so it kept runningftw-cloud:d8d32752ff2d.ftw-cloud-keyfetched the key at 06:58:16, the container started at 06:58:20 and logged the sameservice_key, andcurl https://cloud.ftw.energy/healthzsaidokat 06:58:24./run/ftw-cloud/service.keyis65532:65532 0400./var/lib/ftw-cloud/service.keyand the directory.Still to do, once this is on
main:Imagerun onmainpushes the first image. Fredrik makes the packageghcr.io/srcfl/ftw-cloudpublic once (package settings, Change visibility).ftw-cloud-deploy sha256:<digest>, then check/sourceheaders and/healthz, reboot once more, and prune the old build cache (docker builder prune -af).🤖 Generated with Claude Code