Skip to content

Tailscale & OPNsense Configuration


This covers running Tailscale on OPNsense: the firewall as the tailnet's DNS server (ad-blocking everywhere), an exit node, and subnet routes, plus the firewall rules that make it work. The client side (installing on Debian servers, ACLs, key expiry) is on the Tailscale page.

How it's set up here

Tailscale runs in two places, for different jobs:

Where Job
OPNsense (os-tailscale plugin) The tailnet's DNS server, so phones get AdGuard Home filtering on mobile data; an exit node; a subnet router for the VLANs remote devices actually need
The Debian servers (Tailscale client) Direct, independent admin access (SSH) when the Cloudflare tunnel or the reverse proxy is the thing that's broken

On the servers, Tailscale's DNS is off (tailscale set --accept-dns=false). Why: with it on, tailscaled rewrites /etc/resolv.conf at runtime to point at MagicDNS, and the host then depends on the tailnet path for every lookup. When that path broke (see the firewall rule pitfall below), the hosts couldn't resolve anything while their containers still worked, which is a confusing failure. The servers use the router's LAN resolver directly and nothing on them needs *.ts.net names.

On the tailnet side, remote devices may reach only the VLANs they need (servers and trusted clients). The management and IoT networks and the firewall's own GUI/SSH stay blocked from the tailnet.

1. Install the plugin

  1. System → Firmware → Plugins, tick Show community plugins, install os-tailscale.
  2. VPN → Tailscale → Authentication: log in (an auth key or the login URL).
  3. Interfaces → Assignments: add tailscale0 as a new interface, enable it (no IP config), apply. It then gets its own tab under the firewall rules.

2. Advertise routes and the exit node (OPNsense)

  1. Navigate to VPN > Tailscale > Settings.
  2. Check Advertise Routes and enter the subnets remote devices should reach (e.g. <SERVERS_SUBNET>).
  3. Enable Advertise Exit Node if you want phones to send all traffic home.
  4. Click Save.

CLI Alternative

Without the plugin GUI:

tailscale set --advertise-routes=<YOUR_SUBNET> --advertise-exit-node

Approve them (Tailscale Console)

  1. Open the Tailscale Admin Console.
  2. Select the OPNsense machine → Edit route settings.
  3. Toggle on the advertised subnet routes and Use as exit node, then Save.

3. Firewall rules on the Tailscale interface

Don't use Tailscale net as the source

The obvious rule, source = Tailscale net, doesn't work on OPNsense. The interface "net" for tailscale0 resolves to the firewall's own tailnet address as a /32, not the tailnet range. No rule ever matches a peer, and pf silently drops everything: DNS, subnet routes, the exit node. Symptoms look like a broken Tailscale link even though tailscale ping works.

Create an alias instead: Firewall → Aliases, tailnet_v4 = 100.64.0.0/10 (Tailscale's CGNAT range), and use it as the source of every rule below. More on this trap: Tailscale peers are blocked by the firewall although the rules say "pass".

Rules on the Tailscale interface, in this order (first match wins):

# Action Source Destination Port Why
1 Pass tailnet_v4 the firewall's tailnet IP 53 TCP/UDP DNS for remote devices
2 Block (log) tailnet_v4 This Firewall (self) any no GUI/SSH on the firewall from the tailnet
3 Block (log) tailnet_v4 MANAGEMENT net, IOT net any keep sensitive VLANs off the tailnet
4 Pass tailnet_v4 the VLANs remote devices need (e.g. SERVERS) any subnet routes
5 Pass tailnet_v4 ! PrivateNetworks (inverted) any exit node: internet only

Rule 2 matters because of rule 5: the built-in private-networks alias doesn't include 100.64.0.0/10, so without an explicit block the "internet only" rule would also open the firewall's own tailnet address.

4. Source NAT for the exit node

Exit-node traffic leaves OPNsense with a 100.x source address. The FreeBSD Tailscale client doesn't masquerade it the way the Linux client does, so it needs an outbound NAT rule:

  1. Firewall → NAT → Source NAT (Outbound): set the mode to Hybrid (automatic rules stay, yours are added).
  2. Add a rule: Interface WAN, Source tailnet_v4, Destination any, Translation Interface address.
  3. Save, apply, then test from a phone on mobile data with the exit node on (check your public IP is the home one).

While you're on that page: the Tailscale docs also recommend a Static Port outbound rule for UDP from the firewall so peers can make direct connections instead of relaying (see the sources).

5. DNS for the tailnet (Tailscale Console)

Make the firewall the tailnet's resolver, so every device on the tailnet gets AdGuard Home filtering and your local names.

  1. In OPNsense, Interfaces → Overview: copy the Tailscale IP (100.x.y.z).
  2. Tailscale Admin Console → DNS → Nameservers → Add nameserver → Custom: paste it.
  3. Choose one:
    • Global nameserver (with Override DNS servers on): all queries from tailnet devices go to OPNsense. Ad-blocking works everywhere. This is what I use for phones.
    • Restrict to domain = <YOUR_LOCAL_DOMAIN>: split DNS, only local names go to OPNsense.

AdGuard Home and Unbound must accept queries from 100.64.0.0/10: AdGuard listening on all interfaces (or tailscale0), and Unbound's access list allowing the range (check Services → Unbound DNS → Access Lists if queries are refused).

Local names over the tailnet

<SERVICE>.<YOUR_LOCAL_DOMAIN> resolves to NPM's LAN IP through the AdGuard rewrite (AdGuard Home → DNS rewrites), so a remote device also needs a subnet route to that IP (rule 4) and has to trust your private CA.

5A. Unbound only (no AdGuard)

If Unbound answers DNS directly, add the local names under Services > Unbound DNS > Overrides (one host override per name, pointing at NPM's IP). Unbound overrides have no wildcard field, which is one reason the AdGuard rewrite is easier.

Interfaces & Rebinds

If it still isn't working, check these common configuration errors:

  1. Interface binding: AdGuard Home must be listening on the Tailscale interface (or all interfaces).
  2. Bootstrap DNS: in AdGuard Home (Settings > DNS Settings), bootstrap servers resolve upstream names only; 127.0.0.1 plus a public resolver is fine.
  3. DNS rebind check: System > Settings > Administration → DNS Rebind Check. If enabled, the GUI may refuse to load via a name that resolves to a private IP; add your local domain to the alternate hostnames instead of turning the check off.
  4. Boot order: the plugin can start after the interfaces are configured, so tailscale0 doesn't exist yet when rules and DNS listeners load. After a reboot, check that tailnet DNS answers; restarting AdGuard Home or Unbound fixes a missed bind.

Verification

  1. Disconnect your phone from Wi-Fi (mobile data).
  2. Enable Tailscale (and the exit node, if used).
  3. DNS: a known ad domain is blocked, and <SERVICE>.<YOUR_LOCAL_DOMAIN> resolves.
  4. A service on an allowed VLAN loads by IP and by name.
  5. A management IP and the firewall GUI do not load (rules 2 and 3).
  6. With the exit node on, your public IP is your home one.
  7. From a server: dig @<OPNSENSE_TAILNET_IP> example.com answers.

Sources & further reading