Tailnet Transport
By default the CLI reaches a host at the public address in its hosts.toml entry. A host that sets prefer_tailnet = true is reached at its tailscale_ip instead — by SSH, scp, rsync and Ansible, together.
[[hosts]]name = "auberge"address = "203.0.113.10"tailscale_ip = "100.64.0.1" # the factprefer_tailnet = true # the policyThe two fields are separate on purpose. Caching an address never implies routing over it: vieille-auberge holds 100.64.0.4 and must stay on the public route, because it is the rollback surface. auberge host detect-tailscale-ip writes the address and never touches the policy.
Enabling it
Section titled “Enabling it”auberge host detect-tailscale-ip auberge # caches tailscale_ipauberge host edit auberge # answer yes to "Route over the tailnet address"auberge host list shows a ROUTE column — public or tailnet — beside each host. The ADDRESS column keeps showing the declared public address either way.
[!IMPORTANT]
prefer_tailnetwithout a cachedtailscale_ipis refused at every write, including a hand-editedhosts.toml. A host joins the tailnet duringinfrastructure.yml, so a fresh host has no address to route to and the run that enrolls it necessarily goes over the public one.
No fallback
Section titled “No fallback”If the tailnet address does not answer, the command fails. It does not warn and retry over the public address.
That is deliberate. auberge-backup.service runs backup sync … --quiet, so a warning in the nightly path is invisible, and a silent route change is the failure this feature exists to prevent — a hand-added ProxyJump stanza once rerouted every automated path for ten days without anything reporting it.
A tailscale_ip that has gone stale gets no freshness check; it fails and names its own fix:
host auberge is unreachable over ssh at 100.64.0.1:22Check the SSH key and network connectivityThis is auberge's tailnet address. If it is stale, refresh it with`auberge --via public host detect-tailscale-ip auberge`; `--via public` routes onecommand over the public address.--via: overriding the route for one command
Section titled “--via: overriding the route for one command”auberge --via public backup sync --host auberge # this run over the public addressauberge --via tailnet deploy --host ruche # prove a route before declaring it--via is global and applies to every transport the command uses, Ansible’s ansible_host included — a flag that moved SSH but left Ansible behind would be worse than none.
| Behaviour | Why |
|---|---|
Does not change ~/.ssh/config.d/auberge.conf |
That file outlives the command and is regenerated on every roster write; a per-run override baked into it would leave ssh <name> on a route nobody declared |
| Exits non-zero if the command routed nowhere | A flag believed to have moved the route and did not is the failure mode this feature guards against. The command itself still runs |
--via tailnet needs every roster host to have a tailscale_ip |
The whole roster is resolved when the Inventory is built. --via public, the recovery direction, never fails this way |
Recovery
Section titled “Recovery”Headscale runs on auberge, so the tailnet route to the backup target depends on a service that target hosts. This is bounded rather than fixed: no node key on this tailnet expires, so a running tailscaled keeps its WireGuard peers through a Headscale outage. Losing the route needs Headscale down and a tailscaled restart in the same window.
When that happens, --via public is the way out — see Cross-Host Migration.
What still uses the public address
Section titled “What still uses the public address”Enabling prefer_tailnet moves connections only. These keep reading the declared public address, and must:
| Consumer | Reads | Why |
|---|---|---|
auberge dns set-all |
A record value | A CGNAT address in public DNS resolves for nobody |
auberge deploy public DNS check |
Expected A record | Same record, verified |
fail2ban ignoreip |
Peer allowlist | Both addresses are listed, so --via public recovery cannot be banned |
See ADR-0074.