ADR-0031: Tailscale for administrative access¶
- Status: Accepted
- Date: 2026-08-18
- Deciders: El, Lachlan
- Supersedes: ADR-0021 §5 (in part) and §6
Context¶
ADR-0021 §6 deferred Tailscale, reasoning that at two operators a non-standard SSH port plus fail2ban plus key-only authentication was a sufficient control, and that adopting a mesh network was work that had not yet earned its place.
That reasoning rested on an assumption which turns out to be false: that Tailscale would be new work. Octopodia already runs it across existing infrastructure, and both operators use it daily. The marginal cost of adopting it on ABOS hosts is small, while the deferral leaves an SSH port exposed to the internet on every host, permanently, being scanned continuously.
Retrofitting it later is worse than adopting it now, because the firewall posture and the hardening sequence differ. Changing them after a host is live means re-running hardening against different assumptions on a box carrying client data.
Decision¶
Administrative access is over Tailscale. The public SSH port is closed.
Firewall¶
| Interface | Open |
|---|---|
| Public | 80 and 443 only. 80 exists solely for the ACME challenge and the redirect to HTTPS |
tailscale0 |
SSH |
There is no public SSH port. The non-standard port from ADR-0021 §2 still applies on the Tailscale interface, as noise reduction rather than as a control.
Hardening becomes three phases, not two¶
The no-lockout principle from ADR-0021 is unchanged and now matters more, because the failure mode is losing the mesh rather than mistyping a port.
- Phase 1. Harden SSH, create admin users and install keys, open the new port, leave port 22 open. Public SSH still reachable.
- Phase 2. Install Tailscale, join the tailnet, verify both admins can reach the host over its Tailscale address. Public SSH still reachable throughout.
- Phase 3. Close public SSH entirely. SSH is now reachable only over
tailscale0.
Each phase is verified before the next. Nothing is closed until its replacement is proven.
Node key expiry is disabled on servers¶
Tailscale node keys expire by default. An expired key silently removes the host from the tailnet, and with public SSH closed the only remaining path is the provider's web console. Key expiry must be disabled for every ABOS host, set at the point the machine joins.
This is the single most likely way to lose administrative access under this decision, and it fails months after the change that caused it.
Machines join with a tagged, pre-authorised auth key¶
Hosts join using an auth key carrying tag:server, stored in Ansible Vault under the core vault ID. It is a machine credential (ADR-0027 §7). Tagged nodes are owned by the tailnet rather than by an individual's account, so a host does not disappear when a person's account changes.
Tailnet ACLs restrict SSH to tag:server to the operators.
Tailscale SSH is not used¶
Tailscale can replace sshd authentication with tailnet identity. We keep ordinary sshd with public keys, and use Tailscale purely as the network path. One authentication mechanism, and the keys remain the thing that grants access, so revocation has a single meaning.
Recovery¶
The provider's web console depends on neither SSH nor Tailscale. It is the recovery path if the mesh is unreachable, and it must be located before hardening begins, not during.
Consequences¶
The most-attacked surface on a public host disappears. There is no SSH port to scan, no brute force to mitigate, and fail2ban's SSH jail becomes near-redundant (kept, because it costs nothing).
The cost is a dependency: reaching a host now requires Tailscale to be working. That is acceptable because the web console is an independent fallback, which is the same reasoning that makes the phased hardening safe.
Adding a device now has two steps rather than one: a public key in all.yml, and joining the tailnet. At one admin machine per person this is rare.
ansible_host becomes a Tailscale address, while dns_host_ip remains the public IP. These were already separate fields (ADR-0024), which is what makes this change small.
The three-phase sequence is longer than two. It is also the reason this can be adopted on a live host without risk.
Alternatives considered¶
Continue with a public port plus fail2ban (ADR-0021 §6) is rejected. It was reasonable when adoption was new work. It no longer is, and the deferral's own trigger conditions were met sooner than expected.
Tailscale SSH is rejected: it introduces a second authentication mechanism alongside public keys, so revoking access would mean remembering both.
A VPN or bastion host is rejected as more infrastructure to run and secure, for a capability already in use.
Cloudflare Tunnel for SSH is rejected: it works, but it puts administrative access behind the same provider already fronting the public surface, and Tailscale is already in use.