# Build box boot doors

audience: AI coding agents first. Human console procedure lives in
`console-recovery-card.md` — do not merge the two.

Every build box MUST come back from a cold boot with three independent doors. A host that
comes up with fewer is broken even if it answers.

| door | port | owner | survives |
| --- | --- | --- | --- |
| sshd, wildcard bind | 2222 | `ssh.service` + `buildbox-sshd-access` | tailscaled down |
| rescue sshd | 2223 | `buildbox-rescue-sshd.socket` (PID 1 binds) | `/etc/ssh` broken, `ssh.service` dead |
| Tailscale SSH | 22 on tailnet addr | `tailscaled` | any sshd config change |

## Invariants

1. **sshd MUST bind wildcards only.** `0.0.0.0` and `::`. A bind that cannot fail cannot
   lose a boot race.
2. **The rescue door MUST read no file under `/etc/ssh`.** Config `/etc/buildbox-rescue-door/sshd_config`,
   own host key in the same directory.
3. **The rescue socket MUST set `BindIPv6Only=ipv6-only`.** `net.ipv6.bindv6only=0` here, so
   a default `[::]` socket is dual-stack and its bind fails EADDRINUSE behind `0.0.0.0` —
   the unit then fails whole and the door is absent.
4. **The rescue unit MUST NOT declare `RuntimeDirectory=`.** `/run/sshd` belongs to
   `ssh.service`; declaring it makes systemd delete the directory the primary sshd needs.
   Recreate per connection via `ExecStartPre=/usr/bin/install -d /run/sshd`.
5. **The rescue config MUST keep `UsePAM yes`.** Without PAM sshd runs its own shadow check
   and refuses every locked-password account — the door arms and turns everyone away.
6. **Tailscale SSH MUST be converged by a timer, not once.** It lives in tailscaled prefs,
   not a file this repo installs; re-registering a node silently drops it.
   `buildbox-tailscale-ssh.timer`, `OnBootSec=120s` + every 15min.
7. **No door may be ordered behind another daemon.** No `After=`/`Wants=tailscaled.service`
   on `ssh.service`.

## Never reintroduce

```
# DO NOT — this is the change that took debian2 and debian3 off the network
# /etc/ssh/sshd_config.d/10-tailscale.conf
ListenAddress 100.101.104.41
```

`ssh.service` starts before `tailscaled` creates `tailscale0`; the bind fails with
`Cannot assign requested address`; sshd exits 255; Debian's packaged unit lists 255 in
`RestartPreventExitStatus`, so nothing restarts it. Harmless while the box stays up.
Detonates at the next cold boot.

Also never: `ssh.socket` unmasked (its `ListenStream` overrides the drop-in silently), or
`ssh.service.d/10-tailscale-ordering.conf` (sorts before `51-bind-retry.conf`, so its
`After=` still delays every sshd start).

## Returning host

A host that was down MUST be reconverged before it is trusted — its on-disk config is
whatever bricked it.

```
modules/buildbox/bin/buildbox harden <host>
modules/buildbox/bin/buildbox audit  <host>
```

`audit` item `recovery_doors` is the gate: 2223 on both wildcards, `RunSSH=true`,
`buildbox-tailscale-ssh.timer` active. On a freshly enrolled box this item reads DRIFT until
the timer converges Tailscale SSH at `OnBootSec=120s` — one honestly-missing door, not a
false positive.

Reboot survival is not inferred from a converge. Prove it: boot id before, `systemctl reboot`,
boot id after, then reconnect on 2222 **and** 2223.
