Files
infrastructure/docs/09-TAILSCALE-VPN.md
T
Kaloyan DanchevandClaude Opus 5 27f5a7d5af docs: Tailscale SSH ACL set to accept
Tailnet SSH policy changed from the default action:"check" to accept. The
effective policy is now a single rule covering all tailnet nodes, with no
expiry and port forwarding allowed, so the per-device ~12h grants and the
browser re-auth prompt are gone.

Verified unattended with BatchMode=yes: a plain command, a -L forward
returning HTTP 302, and the Herdr fallback leg.

Records the trade-off: check acted as a second factor, so a compromised
tailnet device now reaches root on Unraid without a further step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-03 23:03:47 +02:00

308 lines
11 KiB
Markdown

# MikroTik Tailscale Container Setup
**Last Updated:** 2026-02-06
---
## Overview
Tailscale VPN running as a container on MikroTik hAP ax³, providing remote access to the home network via the Tailscale mesh network.
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ MikroTik hAP ax³ (192.168.10.1) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Container Network (172.17.0.0/24) │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ AdGuard Home │ │ Tailscale │ │ │
│ │ │ 172.17.0.2 │ │ 172.17.0.3 │ │ │
│ │ │ veth-adguard │ │ veth-tailscale │ │ │
│ │ └─────────────────────┘ └─────────────────────────┘ │ │
│ │ │ │ │ │
│ │ └───────────┬───────────────┘ │ │
│ │ │ │ │
│ │ Gateway: 172.17.0.1 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ NAT Masquerade │
│ │ │
│ WAN (ether1) │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Tailscale Network │
│ 100.x.x.x mesh │
│ │
│ Home Router IP: │
│ 100.74.219.35 │
└─────────────────────┘
```
---
## Container Details
| Setting | Value |
|---------|-------|
| Image | tailscale/tailscale:latest |
| Interface | veth-tailscale |
| Container IP | 172.17.0.3/24 |
| Gateway | 172.17.0.1 |
| Tailscale IP | 100.74.219.35 |
| Root dir | usb1/tailscale/root |
| State mount | usb1/tailscale → /var/lib/tailscale |
| DNS | 8.8.8.8 |
| Start on boot | Yes |
| Networking mode | Userspace (TS_USERSPACE=true) |
---
## Environment Variables
| Variable | Value | Purpose |
|----------|-------|---------|
| TS_AUTHKEY | tskey-auth-... | One-time auth key (used during setup) |
| TS_STATE_DIR | /var/lib/tailscale | Persistent state directory |
| TS_USERSPACE | true | Required for MikroTik containers (no /dev/net/tun) |
---
## Network Configuration
### veth Interface
```routeros
/interface veth add address=172.17.0.3/24 gateway=172.17.0.1 name=veth-tailscale
```
### Gateway IP on Interface
```routeros
/ip address add address=172.17.0.1/24 interface=veth-tailscale comment="Tailscale container gateway"
```
### NAT Masquerade for Internet Access
```routeros
/ip firewall nat add chain=srcnat action=masquerade src-address=172.17.0.0/24 out-interface-list=WAN comment="Container network NAT"
```
### Firewall Forward Rules
```routeros
/ip firewall filter add chain=forward action=accept dst-address=172.17.0.0/24 comment="[Container] Forward to container network"
/ip firewall filter add chain=forward action=accept src-address=172.17.0.0/24 comment="[Container] Forward from container network"
```
---
## Container Setup Commands
### Create Mounts
```routeros
/container mounts add list=ts-state src=usb1/tailscale dst=/var/lib/tailscale
```
### Create Environment Variables
```routeros
/container envs add list=ts-env key=TS_STATE_DIR value=/var/lib/tailscale
/container envs add list=ts-env key=TS_USERSPACE value=true
/container envs add list=ts-env key=TS_AUTHKEY value=<your-auth-key>
```
### Create Container
```routeros
/container add remote-image=tailscale/tailscale:latest interface=veth-tailscale \
root-dir=usb1/tailscale/root mountlists=ts-state envlists=ts-env \
dns=8.8.8.8 start-on-boot=yes logging=yes
```
---
## Management
### Check Container Status
```routeros
/container print
```
### View Logs
```routeros
/log print where topics~"container" and message~"tailscale"
```
### Start/Stop Container
```routeros
/container start [find name~"tailscale"]
/container stop [find name~"tailscale"]
```
---
## Troubleshooting
### Container Won't Start (Exit Status 1)
**Cause:** Missing /dev/net/tun device (default for Tailscale)
**Solution:** Enable userspace networking mode:
```routeros
/container envs add list=ts-env key=TS_USERSPACE value=true
```
### Can't Reach Internet from Container
**Cause:** Missing NAT masquerade or gateway IP
**Solution:**
1. Verify gateway IP on veth interface:
```routeros
/ip address print where interface=veth-tailscale
```
2. Verify NAT masquerade rule:
```routeros
/ip firewall nat print where src-address=172.17.0.0/24
```
### Container Not Connecting to Tailscale
1. Check DNS resolution works (logs should show no timeout)
2. Verify auth key is valid and not expired
3. Check firewall isn't blocking outbound HTTPS
---
## Tailscale Network Devices
| Tailscale IP | Device | Status |
|--------------|--------|--------|
| 100.74.219.35 | MikroTik hAP ax³ (container) | Online |
| 100.100.208.69 | xtrm-unraid (Unraid plugin, offers exit node) | Online |
| 100.100.208.70 | xtrm-unraid (old node, deleted 2026-11-03) | Removed |
| 100.112.103.7 | hapax3 (old native install) | Offline |
| 100.75.93.123 | mikrotik-tailscale (previous container) | Offline |
---
## Important Notes
1. **Userspace Networking Required:** MikroTik containers don't have /dev/net/tun access, so TS_USERSPACE=true is mandatory
2. **Auth Key:** After initial authentication, the key is no longer needed - state is persisted in the mount
3. **Container Network:** Both AdGuard and Tailscale share the 172.17.0.0/24 network but have separate veth interfaces
4. **Accept Routes:** If subnet routing is needed, add TS_EXTRA_ARGS="--accept-routes" to environment
---
## Unraid Tailscale Plugin Node — 2026-11-03
The Unraid server runs its own Tailscale node via the **Tailscale plugin** (not a
container), separate from the MikroTik container documented above. It had been
logged out since roughly May 2026 and was re-enrolled on 2026-11-03.
| Item | Value |
|------|-------|
| Tailscale IP | `100.100.208.69` |
| MagicDNS name | `xtrm-unraid.tailf789e1.ts.net` |
| Daemon | `/usr/local/sbin/tailscaled` 1.96.2, `-tun tailscale1` |
| State dir | `/boot/config/plugins/tailscale/state` (survives reboot) |
| Previous IP | `100.100.208.70` — stale node deleted, so a new IP was issued |
### Prefs set
| Pref | Value | Reason |
|------|-------|--------|
| `--advertise-routes` | none | The old `192.168.31.0/24` advertisement was dropped. That subnet is only the macvlan shim created in `/boot/config/go` for a Pi-hole at `192.168.31.4`. Pi-hole was replaced by AdGuard: there is no such container any more and the address is silent, so nothing reachable lives there. |
| LAN subnet routing | not advertised | The MikroTik container is primary for `192.168.10.0/24`, `.20`, `.25`, `.30`. A second advertiser for a subnet Unraid itself sits on would create a routing conflict. |
| `--advertise-exit-node` | on | Redundant with the hAP today, kept as a fallback on the 24/7 box. **Approved in the admin console 2026-11-03** — shows as `offers exit node` and appears in `tailscale exit-node list`. |
| `--accept-routes` | **false** | Critical. Unraid is *on* `192.168.10.0/24`; accepting the hAP's route for its own subnet would loop. This is why `tailscale status` reports the health warning "Some peers are advertising routes but --accept-routes is false" — expected here, not a fault. |
| `--accept-dns` | false | Keeps local AdGuard resolution intact (Unraid AdGuard is config master). |
| `--ssh` | on | Tailscale SSH. Needed — see the sshd caveat below. |
### SSH over Tailscale — the sshd caveat
`/etc/ssh/sshd_config` on Unraid binds explicitly and has **no wildcard**:
```
Port 422
ListenAddress 192.168.10.20 # br0
ListenAddress 192.168.31.250 # br0-shim
```
So **sshd does not listen on `100.100.208.69`**. Port 422 on the Tailscale IP is
closed and stays closed unless a `ListenAddress` is added — which needs a go
script, because Unraid regenerates this file per interface at every boot.
Access on the Tailscale IP is served by **Tailscale SSH on port 22**, handled by
`tailscaled` itself and authenticated by tailnet identity rather than by an SSH
key:
```bash
# direct, independent of the MikroTik subnet router
ssh root@100.100.208.69
ssh root@xtrm-unraid.tailf789e1.ts.net
# port forward — verified 2026-11-03 (Unraid web UI returned HTTP 302)
ssh -N -L 18089:127.0.0.1:80 root@100.100.208.69
```
The tailnet SSH ACL was switched from the default `action: "check"` to
`accept` on 2026-11-03, so this needs no browser re-auth and works unattended.
Before the change the policy minted ~12 h grants per device and prompted when
they lapsed. The effective policy is now a single rule: accept for all tailnet
nodes, with local and remote port forwarding and agent forwarding allowed.
The trade-off: `check` acted as a second factor, so a compromised tailnet device
now reaches root on Unraid without a further step. Acceptable here because every
node in the tailnet is the owner's own hardware.
### The other path — via the MikroTik subnet router
Independent of the Unraid node and still working:
```bash
ssh -i ~/.ssh/id_ed25519_unraid -p 422 -N -L 18088:127.0.0.1:80 root@192.168.10.20
```
Forwarding works here only because Unraid's stock sshd config denies it globally
and re-enables it for root alone:
```
AllowTcpForwarding no # limetech - disallow forwarding but see below
...
Match Group root
AllowTcpForwarding yes # limetech - permit only root SSH tunneling
```
Any non-root account added later will not be able to open tunnels.
### Exit node
Approved 2026-11-03. The tailnet now has two exit nodes:
| IP | Node |
|----|------|
| `100.74.219.35` | `mikrotik-tailscale` (primary) |
| `100.100.208.69` | `xtrm-unraid` (fallback, 24/7 box) |
Select one with `tailscale set --exit-node=xtrm-unraid`. Not exercised in anger
yet — note that without `--exit-node-allow-lan-access` an exit node cuts the
client's own LAN access, which will break an SSH session that is itself routed
over Tailscale.