docs(api): write down the gkapi deploy procedure, including setcap - #115
Conversation
Deploying this crate is undocumented tribal knowledge, and one step in it is invisible until it bites: the service runs unprivileged and binds port 80 for the ACME challenge server, which needs CAP_NET_BIND_SERVICE as a file capability. `cp` does not preserve file capabilities, so replacing the binary without re-running setcap makes it bind 443, log "Listening on 0.0.0.0:443", then panic with PermissionDenied and restart-loop. The same trap catches the rollback, which is what makes it genuinely nasty: copying a backup back into place also drops the capability, so the service keeps panicking and it looks like the new build is to blame rather than the copy. `mv` keeps the inode and therefore the capability; `cp` does not. This bit during the deploy of #94 and took the API down for about 100 seconds (16:27:05 to 16:28:45 UTC on 2026-08-16). Writing it down so the next person does not rediscover it in production. Also records what CI does and does not do here: merging a change to rust/api ships nothing, because deploy.yml only publishes the Hugo site. #94 sat merged and undeployed for 18 days for exactly that reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLUFDUS75xXcRZ83yDzq1
Adversarial review of the procedure found two ways the runbook could cause an outage worse than the one it documents. The first is a window where no binary exists. Backing up with `mv` in one ssh call and installing with `cp` in the next leaves /home/gkapi/bin/ghostkey-api absent in between, so anything that restarts the unit in that gap fails with status=203/EXEC and stays down — strictly worse than never starting, since the old process was still serving fine from its open inode. Restructured to stage the new binary at its final directory first, fully prepared and capability verified, then swap with two adjacent renames. That reordering also improves the failure mode of the check itself: the getcap gate now runs while the live binary is completely untouched, so failing it costs nothing. The second is that `getcap` was called without sudo while every neighbouring command had it. It lives in /usr/sbin, which is not on a normal user's PATH, so the doc's one verification gate could fail as "command not found" and be skipped rather than enforced. Also notes that /create-donation has no dry-run mode, so the tier verification loop creates real (uncharged, unattached) PaymentIntents in the live Stripe account, and says to substitute the rollback timestamp rather than pasting the placeholder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLUFDUS75xXcRZ83yDzq1
|
Adversarial review lens run. Two Medium findings, both real, both fixed. It also empirically reproduced the load-bearing claim rather than taking it on trust. Fixed1. A window where no binary exists. The procedure had Restructured to stage → verify → swap: the new binary is copied to 2. Also addressed
Verified independently by the reviewer
All four bash blocks pass Accepted as-isThe reviewer noted that [AI-assisted - Claude] |
Problem
Deploying
gkapiis undocumented, and one step in it is invisible until it bites.The service runs as the unprivileged
gkapiuser and binds port 80 for the HTTP-01 ACME challenge server, which needsCAP_NET_BIND_SERVICEas a file capability on the binary.cpdoes not preserve file capabilities. Replace the binary without re-runningsetcapand it binds 443, logs a healthy-lookingListening on 0.0.0.0:443, then panics withPermissionDeniedon the port 80 bind and systemd restart-loops until it gives up.What makes it genuinely nasty is that the same trap catches the rollback: copying a backup back into place also produces a file with no capability, so the service keeps panicking and it reads as "the new build is broken" rather than "the copy dropped a capability".
mvkeeps the inode and therefore the capability;cpdoes not.This bit while deploying #94 and took the API down for about 100 seconds (16:27:05 to 16:28:45 UTC, 2026-08-16). Diagnosis was only obvious once
getcapwas compared between the backup (which hadcap_net_bind_service=ep, because it was created withmv) and the installed binary (which had nothing).Also recorded
rust/apiships nothing.deploy.ymlbuilds the Hugo site and publishes to GitHub Pages; it never touches the API.rust-api-tests.ymlonly runs fmt/build/test. fix(api): stop concurrent requests minting several Ghost Keys from one donation #94 sat merged and undeployed for 18 days for exactly this reason, and nothing in the repo said so.~gkapi/.cargoexists butbin/is empty), so the binary is built elsewhere and copied. vega and nova are both Ubuntu 24.04 on identical glibc, which is what makes that safe.curl http://gkapi.freenet.org/.well-known/acme-challenge/probeshould return 404. Connection refused means the capability is missing, and certificate renewal will fail at the next attempt even though HTTPS looks perfectly healthy. That is the part that would have turned a 100-second outage into a surprise expiry weeks later.Testing
Documentation only, no code. Every command in it was run against vega during the #94 deploy and the recovery.
Not addressed
The real fix is CI deployment for this crate so the manual procedure stops mattering. That is a bigger change and wants its own discussion, so this just makes the manual path safe and writes down that the gap exists.
[AI-assisted - Claude]