Skip to content

docs(remote-ssh): mark the short-lived-cert client path superseded - #217

Merged
robercano merged 3 commits into
mainfrom
docs/remote-ssh-superseded
Aug 5, 2026
Merged

docs(remote-ssh): mark the short-lived-cert client path superseded#217
robercano merged 3 commits into
mainfrom
docs/remote-ssh-superseded

Conversation

@robercano

Copy link
Copy Markdown
Owner

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.md 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 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.md now 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

  • Verify the installed CA is the one Cloudflare signs with. The dashboard-pasted key did not match the account gateway CA (SHA256:UdAX7Pben… vs SHA256:K3EDSsU0olUb…). A cert signed by an untrusted CA fails identically to a client-side fault and sends you chasing the wrong layer.
  • 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. Phase 9 gains: trigger the notification rather than assuming it, check for non-loopback listeners, audit the account's other tunnels, and the claude-rc traparm-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 and systemctl cannot 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

robercano and others added 3 commits August 4, 2026 12:50
`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>
@robercano-ghbot robercano-ghbot added the needs-human Loop escalated: attempt budget exhausted, manual triage required label Aug 5, 2026
@robercano-ghbot

Copy link
Copy Markdown
Collaborator

docs(remote-ssh): mark the short-lived-cert client path superseded (not yet reviewed)

@robercano
robercano merged commit db96baa into main Aug 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human Loop escalated: attempt budget exhausted, manual triage required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants