docs(remote-ssh): mark the short-lived-cert client path superseded - #217
Merged
Conversation
`meta skuid != <agent> accept` never matches kernel-generated packets -- ICMPv6 MLD/ND and DHCP renewal carry no owning socket -- so they fell through to the drop rule. Observed live: MLD listener reports (TYPE=131) from the box's own link-local address were dropped and alarmed on. Restructure to jump on a positive UID match, so only packets the agent actually owns are filtered. Also from the same run: drop link-local multicast WITHOUT the log prefix (mDNS/MLD is discovery noise, and an alarm that cries wolf gets ignored); add the v6 set and the lo/established rules the sketch omitted; and record that statsig.anthropic.com has no A record -- Claude Code's telemetry goes to api.statsig.com / statsigapi.net / events.statsigapi.net / featureassets.org. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Working files from a real provisioning run on bare-metal Ubuntu 26.04, so the next box does not rebuild them from the sketches in HARDENING.md: nftables egress fence, egress alarm, auditd rules, divergence tripwire, and the user-unit-safe systemd drop-in. The README records the reasoning, which is the part that cost time: - The fence restricts protocol/port, not destination. IP-set allowlisting was built, tested and abandoned -- GitHub and Google LBs rotate faster than a DNS refresh can track, and it dropped real work. HARDENING.md already lists GitHub as a sanctioned exfil channel, so an allowlist that must include GitHub could never close that path anyway. - Jump on a positive skuid match; `skuid != <uid> accept` never matches kernel-generated packets and silently filters the host's own stack. - The alarm runs as root, keys its cooldown on destination AND port, and deliberately avoids `set -e`/`pipefail` -- an address with no PTR made getent fail and killed the follower, losing events silently. - ufw is left alone; nftables.service stays disabled so it cannot flush it. - The systemd drop-in is the user-unit-safe subset; the full block crash-loops a --user unit with 218/CAPABILITIES. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cloudflare retired the per-application SSH CA the runbook depends on. On a current account, Access -> Service auth -> SSH -> Generate certificate is disabled and `cloudflared access ssh-gen` returns "Bad request, please create CA for application" -- the client never gets a certificate, so no server-side change can fix it. Verified with the correct account CA trusted, ruling out a CA mismatch first. Steps 1-3 of the runbook are unaffected and still correct: loopback-only hardened sshd, the dedicated outbound-only tunnel, the Access application. Documents the successor and its real cost: Access for Infrastructure requires the WARP client on every device in Traffic + DNS mode, uses a private network route rather than a public hostname ingress, and offers no browser-rendered SSH terminal. Also records the self-managed-keys alternative and why it is a worse trade -- it needs WARP too while reintroducing long-lived authorized_keys, reversing the "nothing long-lived to steal" property the design exists for. Two field notes worth keeping: - Verify the installed CA is the one Cloudflare signs with. On a live run the dashboard-pasted key did not match the account gateway CA (SHA256:UdAX7Pben vs SHA256:K3EDSsU0olUb), and a cert signed by an untrusted CA fails exactly like a client-side fault. - If sshd is loopback-only, register the target as 127.0.0.1 so cloudflared dials its own loopback and the property survives; the LAN-IP fallback needs a second ListenAddress and exposes sshd to the LAN. provision.md Phase 8 rewritten accordingly, plus: run cloudflared as a system unit (the admin account often has Linger=no, and remote access must not depend on a login session), watch for an orphaned DNS record on the chosen hostname, and audit the account's other tunnels as standing ingress paths. Phase 9 additions: trigger the notification rather than assuming it, check for non-loopback listeners, and the claude-rc trap -- arm-loop.sh passes --spawn but the server still prompts "Enable Remote Control? (y/n)" inside tmux, so the unit reports active while parked on a prompt. Also: state the unattended posture out loud before signing off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
|
docs(remote-ssh): mark the short-lived-cert client path superseded (not yet reviewed) |
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.
Third and last batch from the bare-metal provisioning run. Follows #212 and #213.
What's wrong today
Cloudflare retired the per-application SSH CA that
REMOTE_SSH_RUNBOOK.mddepends on. On a current account, Access → Service auth → SSH → Generate certificate is disabled, andcloudflared access ssh-genreturnsBad request, please create CA for application. The client never obtains a certificate, so nothing server-side can fix it — verified with the correct account CA trusted, which rules out a CA mismatch as the cause.Steps 1–3 are unaffected and were used as written: loopback-only hardened
sshd, the dedicated outbound-only tunnel, the Access application. Only the CA source (step 4) and the client config (step 5) are dead.The successor, and its real cost
Access for Infrastructure requires the WARP client on every device in Traffic + DNS mode, uses a private network route instead of the public hostname ingress, and offers no browser-rendered SSH terminal. That's a device-level posture change rather than a config detail, so
provision.mdnow tells the operator to get a deliberate decision — and to offer stopping with the server side complete as a legitimate outcome.The self-managed-keys alternative is documented and recommended against: it needs WARP too, while reintroducing long-lived
authorized_keys— reversing the "nothing long-lived to steal" property that motivates the design.Two field notes
SHA256:UdAX7Pben…vsSHA256:K3EDSsU0olUb…). A cert signed by an untrusted CA fails identically to a client-side fault and sends you chasing the wrong layer.sshdis loopback-only, register the target as127.0.0.1socloudflareddials its own loopback and the property survives. The LAN-IP fallback needs a secondListenAddressand exposessshdto the LAN.provision.md
Phase 8 rewritten. Phase 9 gains: trigger the notification rather than assuming it, check for non-loopback listeners, audit the account's other tunnels, and the
claude-rctrap —arm-loop.shpasses--spawn, but the server still promptsEnable Remote Control? (y/n)inside tmux, so the unit reportsactivewhile parked on a prompt andsystemctlcannot tell. That's the fourth instance this run of a layer that looked healthy and did nothing, so Phase 9 now also asks the operator to state the unattended posture out loud before signing off.🤖 Generated with Claude Code