Fedora

Install Tailscale on Fedora 45 / 44: Exit Node and Subnet Router

When someone on the team asks to reach the office NAS from a hotel, the shortest honest answer on Fedora is Tailscale. One package and one login put the laptop on a private WireGuard mesh, and nothing gets forwarded on the office router. The catch on current Fedora is that the install line Tailscale publishes still uses dnf4 syntax, and dnf5 rejects it. After that, the subnet router and exit node work with SELinux enforcing, but firewalld quietly drops forwarded traffic when the LAN interface is not in a zone.

Original content from computingforgeeks.com - post 179719

This guide shows how to install Tailscale on Fedora with dnf5, log in with tailscale up, confirm MagicDNS through systemd-resolved, and turn one Fedora box into a subnet router and exit node behind firewalld. It also covers pointing the client at a self-hosted Headscale server with --login-server. We ran every step in October 2026 on a Fedora 45 router and a Fedora 44 laptop with Tailscale 1.104.4 and SELinux enforcing, and every output below comes from them.

Planning the setup

Every Fedora machine that should join the tailnet needs the same install, and only the box that forwards traffic needs the router steps. You do not have to decide on the router role up front. On our test network that box is vpn-gw, a Fedora server sitting next to an office LAN (172.20.30.0/24) with a NAS at 172.20.30.10 that will never run Tailscale itself. The laptop, laptop44, reaches that NAS by asking vpn-gw to forward for it.

Tailscale on Fedora architecture diagram: laptop44 and vpn-gw register with the control server, laptop44 reaches vpn-gw over WireGuard UDP 41641, vpn-gw forwards to printer-nas 172.20.30.10 and acts as exit node

The control server only hands out keys and routes. Traffic between the two Fedora machines goes straight over WireGuard on UDP 41641, and our tailscale ping confirmed a direct path rather than a relay. Before you start, have these ready:

  • Fedora 45 or 44 (Workstation, Server or Cloud) with sudo
  • A Tailscale account, or a Headscale server you control
  • firewalld running on the router box
  • Outbound TCP 443 and UDP 3478 from every machine

Fedora Server and Workstation ship firewalld, but the Fedora Cloud image we tested on doesn’t, so install and start it on the router (and on a Headscale host, if you build one) with sudo dnf install firewalld followed by sudo systemctl enable --now firewalld. The core path is the first three sections. Everything after that is optional, and you only need the pieces that match what you want the box to do:

SectionNeeded forExtra things you need
Headscale login serverSelf-hosted control plane, no Tailscale accountA server for Headscale, TCP 8080 (or 443 behind TLS)
Subnet routerReaching LAN devices that can’t run TailscaleIP forwarding, LAN interface in a firewalld zone
Exit nodeSending all client traffic out through home or officeIP forwarding, route approval

Adding the Tailscale repo with dnf5

Tailscale publishes signed RPMs for Fedora, so you don’t need COPR or a tarball. The command on pkgs.tailscale.com is dnf config-manager --add-repo, which is dnf4 syntax. Current Fedora ships dnf5 as dnf, and on our VM that line stopped with an error:

Unknown argument "--add-repo" for command "config-manager". Add "--help" for more information about the arguments.

The dnf5 form takes a subcommand and a URL to a .repo file. It fetches that file and drops it into /etc/yum.repos.d/:

sudo dnf config-manager addrepo --from-repofile=https://pkgs.tailscale.com/stable/fedora/tailscale.repo

Check what landed. Both gpgcheck and repo_gpgcheck are on, so dnf verifies the repo metadata as well as every package.

cat /etc/yum.repos.d/tailscale.repo

You should see the stable channel pointing at the $basearch tree, with no release number anywhere in the path.

[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=1
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg

The repo path has no Fedora release number in it, so the same file worked on both releases we tested and won’t break when you upgrade. That’s a nice change from repos that 404 on a new release, which our third-party repo 404 fixes deal with.

Installing Tailscale and starting tailscaled

The package is called tailscale and carries both the CLI and the tailscaled daemon. The first install imports Tailscale’s signing key, so read the fingerprint dnf prints before you trust it:

sudo dnf -y install tailscale

Our run imported key 0x957F5868 and installed a single 75 MiB package:

Importing OpenPGP key 0x957F5868:
 UserID     : "Tailscale Inc. (Package repository signing key) <[email protected]>"
 Fingerprint: 2596A99EAAB33821893C0A79458CA832957F5868
 From       : https://pkgs.tailscale.com/stable/fedora/repo.gpg
The key was successfully imported.
Repositories loaded.
Package    Arch   Version     Repository            Size
Installing:
 tailscale x86_64 0:1.104.4-1 tailscale-stable  74.6 MiB

Fedora’s preset leaves new services disabled (the unit shows preset: disabled), so enable and start the daemon yourself. Skip this and tailscale up fails because nothing is listening on its local socket.

sudo systemctl enable --now tailscaled

Confirm the version and that the service is up before going further, because every later step talks to this daemon.

tailscale version
systemctl status tailscaled --no-pager

The first line of tailscale version is the one you’ll compare against the release notes later, and the service should be active (running):

1.104.4
  tailscale commit: 9382131983ca16de1357091618fd903e35db3c4e
  long version: 1.104.4-t938213198-gd38c96d6a
  other commit: d38c96d6afce903d0a81e5f3e32d7048bc33b660
  go version: go1.27.3 (tailscale/go 7ef7b40364)
● tailscaled.service - Tailscale node agent
     Loaded: loaded (/usr/lib/systemd/system/tailscaled.service; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: active (running) since Thu 2026-10-29 10:33:22 UTC; 8ms ago

Under SELinux enforcing, tailscaled runs in the unconfined_service_t domain because Fedora ships no dedicated policy for it. That is why nothing here needs a boolean or a custom module, and it’s also why you shouldn’t treat SELinux as a fence around this daemon. We ran sudo ausearch -m avc,user_avc -ts today on all three lab machines after the full run and got <no matches> each time.

Logging in with tailscale up

With a Tailscale account, one command registers the machine. It prints a login URL and waits until someone approves the device in a browser, so on a headless server you open that link on your own laptop:

sudo tailscale up

Until you finish the browser step, the daemon sits logged out and repeats the same link in tailscale status:

To authenticate, visit:

	https://login.tailscale.com/a/1a2b3c4d5e6f7

Logged out.
Log in at: https://login.tailscale.com/a/1a2b3c4d5e6f7

After approval, tailscale ip -4 prints the machine’s 100.x address and tailscale status lists every peer you can see. For servers you’d rather not approve by hand, generate an auth key in the admin console and pass it as --authkey=, which is also how a Headscale client joins.

Checking MagicDNS on Fedora

MagicDNS lets you use machine names instead of 100.x addresses, and Tailscale turns it on for tailnets created since October 2022. Fedora uses systemd-resolved, so tailscaled registers its resolver on the tailscale0 link instead of rewriting /etc/resolv.conf, and you can see exactly which queries it takes:

resolvectl status tailscale0

A working link lists 100.100.100.100 as its DNS server and your tailnet domain as the search domain. On Tailscale’s service that domain ends in .ts.net, and on our Headscale lab it was tailnet.example.com:

Link 3 (tailscale0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
       DNS Servers: 100.100.100.100 fd7a:115c:a1e0::53
        DNS Domain: tailnet.example.com ~.
     Default Route: yes

Short names now resolve, so ping -c 2 vpn-gw from the laptop answered as vpn-gw.tailnet.example.com (100.64.0.1). Read the ~. on the domain line carefully, though. It makes 100.100.100.100 the resolver for every name, not only tailnet names. Our control server pushed that, and Headscale’s example config is the reason. It sets override_local_dns: true with public nameservers, so per that config, names that only your LAN DNS server knows would no longer reach it while the laptop is connected. If you rely on a LAN DNS server, either turn that override off on the control server or opt the Fedora box out with sudo tailscale set --accept-dns=false. That is the core install done. The sections below are optional.

Joining a Headscale server instead

Headscale is an open source control server that speaks the same protocol, so the Fedora client stays exactly the same and only the login target changes. Use it when you don’t want a Tailscale account in the picture. We ran it on a third Fedora VM, hs01 (10.0.1.10), from the release binary, since the project publishes a .deb but no RPM. For a hardened setup with TLS, follow our Headscale install guide or the Docker version with a web UI, then come back for the client step.

Detect the latest release and install the binary to /usr/bin, because that’s the path the upstream systemd unit calls:

HS_VER=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest | grep tag_name | cut -d '"' -f 4 | tr -d v)
echo "$HS_VER"
sudo curl -sL -o /usr/bin/headscale "https://github.com/juanfont/headscale/releases/download/v${HS_VER}/headscale_${HS_VER}_linux_amd64"
sudo chmod +x /usr/bin/headscale
sudo restorecon -v /usr/bin/headscale
headscale version

The detection printed 0.29.6 on our run, and the binary reported headscale version v0.29.6+dirty. Next, create the service account and pull the example config and unit file that match that release:

sudo useradd --system --create-home --home-dir /var/lib/headscale --shell /usr/sbin/nologin headscale
sudo mkdir -p /etc/headscale
sudo curl -sL -o /etc/headscale/config.yaml "https://raw.githubusercontent.com/juanfont/headscale/v${HS_VER}/config-example.yaml"
sudo curl -sL -o /etc/systemd/system/headscale.service "https://raw.githubusercontent.com/juanfont/headscale/v${HS_VER}/packaging/systemd/headscale.service"

Three keys in the example config need changing: the URL clients connect to, the listen address, and the MagicDNS domain. The base domain must not be the same as the server’s own hostname. Our lab used plain HTTP on the private network; in production you’d put https://headscale.example.com here behind TLS. If clients must keep resolving LAN names through your own DNS server, also set override_local_dns: false under dns:, because the example file overrides local DNS by default:

sudo sed -i -e "s|^server_url: .*|server_url: http://10.0.1.10:8080|" -e "s|^listen_addr: .*|listen_addr: 0.0.0.0:8080|" -e "s|^  base_domain: example.com|  base_domain: tailnet.example.com|" /etc/headscale/config.yaml

Start it and open the port. Do not run headscale configtest as root before the first start: it creates the database and Noise key owned by root, and the service then fails with a permission error (the fix is in the errors section below):

sudo systemctl daemon-reload
sudo systemctl enable --now headscale
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
sleep 3
systemctl is-active headscale
curl -s http://127.0.0.1:8080/health

The unit can sit in activating for a few seconds on first start, which is why the commands wait before asking. A healthy server reports active and answers {"status":"pass"}. Now create a user and a reusable key the Fedora machines can join with. In the Headscale release we tested, --user on preauthkeys takes the numeric ID from users list, not the name:

sudo headscale users create mary
sudo headscale users list
sudo headscale preauthkeys create --user 1 --reusable --expiration 24h

The last command prints a key starting with hskey-auth-, which we’ve shortened here. Treat it like a password for 24 hours:

User created
ID | Name | Username | Email | Created
1  |      | mary     |       | 2026-10-29 10:35:29
hskey-auth-xxxxxxxxxxxxxxxxxxxxxxxxxxxx

On each Fedora client, add --login-server and the key to tailscale up. No browser step is needed:

sudo tailscale up --login-server=http://10.0.1.10:8080 --authkey=hskey-auth-xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Back on hs01, sudo headscale nodes list shows each machine as online with its 100.64.x address. The headscale service also runs as unconfined_service_t, and the SELinux log stayed empty on hs01.

Turning a Fedora box into a subnet router

A subnet router lets tailnet clients reach devices that can’t run Tailscale: printers, NAS boxes, IPMI. The router needs three things before it advertises anything: kernel forwarding, a firewalld zone that allows forwarding to the LAN side, and route approval on the control server. Miss the first and Tailscale warns you; miss the second and it doesn’t, which is the one that cost us time.

Enable IPv4 and IPv6 forwarding persistently. A file in /etc/sysctl.d/ survives reboots, and our router still read net.ipv4.ip_forward = 1 after two:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Now check which interfaces firewalld has placed in a zone. firewalld only forwards between interfaces it knows about, and an interface with no zone gets its forwarded packets rejected:

sudo firewall-cmd --get-active-zones

On a single-NIC router the LAN is on eth0, which is usually listed already, and you can skip ahead. Our router reaches the office LAN on a second interface, veth-lan, which wasn’t in any zone, so we added it to the default zone. Use your own interface name and the zone name your system prints, since Server and Workstation installs don’t default to public. Reading the default zone into a variable keeps the command right on any edition:

ZONE=$(sudo firewall-cmd --get-default-zone)
sudo firewall-cmd --permanent --zone="$ZONE" --add-interface=veth-lan
sudo firewall-cmd --reload
sudo firewall-cmd --get-active-zones

Both interfaces should now share the zone. Run sudo firewall-cmd --list-all as well and look for forward: yes, since a zone with forwarding off would still drop the traffic.

public (default)
  interfaces: veth-lan eth0

Tailscale’s subnet router docs also suggest masquerading on firewalld hosts because of an old forwarding bug. We tested with and without it on the firewalld build Fedora ships, and both the subnet route and the exit node kept working with it removed, because tailscaled adds its own MASQUERADE rule for tailnet traffic. It costs nothing, so we left it on.

sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reload

Direct connections worked without any inbound port open, since both peers punch out to each other. On a router that clients hit from awkward NATs, opening UDP 41641 raises the odds of a direct path instead of a relay. Fedora has no firewalld service definition for Tailscale, so open the port number.

sudo firewall-cmd --permanent --add-port=41641/udp
sudo firewall-cmd --reload

Advertise the LAN. On a machine already logged in, tailscale set changes one setting without repeating your other flags. In our Headscale lab we passed the same --advertise-routes and --advertise-exit-node flags to tailscale up at join time, and the result is identical:

sudo tailscale set --advertise-routes=172.20.30.0/24 --advertise-exit-node

Advertised isn’t the same as active. On Tailscale’s service, open Machines in the admin console, find the router’s Subnets section, choose Edit and enable the route. On Headscale, list the routes first to find the router’s node ID:

sudo headscale nodes list-routes

Our router was ID 1, with all three routes available and none approved yet.

ID | Hostname | Approved | Available      | Serving (Primary)
1  | vpn-gw   |          | 0.0.0.0/0      |                  
   |          |          | 172.20.30.0/24 |                  
   |          |          | ::/0           |                  

Approve the LAN and the default route using the ID from your own output, then list again:

sudo headscale nodes approve-routes --identifier 1 --routes 172.20.30.0/24,0.0.0.0/0
sudo headscale nodes list-routes

Approving 0.0.0.0/0 approved ::/0 with it, and the routes moved into the Serving column.

Node updated
ID | Hostname | Approved       | Available      | Serving (Primary)
1  | vpn-gw   | 0.0.0.0/0      | 0.0.0.0/0      | 172.20.30.0/24
   |          | 172.20.30.0/24 | 172.20.30.0/24 | 0.0.0.0/0
   |          | ::/0           | ::/0           | ::/0             

The router afterwards, with SELinux still enforcing and the laptop connected directly. Every value in this capture is one you can check on your own router with the same commands.

Tailscale 1.104.4 running on Fedora 45 with SELinux enforcing, tailscale status showing vpn-gw offering an exit node

Note the veth-lan eth0 line at the bottom. If your LAN interface is missing from that list, the next test fails even though the router looks healthy everywhere else.

Accepting the route on a Fedora client

Linux clients ignore advertised subnets until you opt in, unlike macOS or Windows. Our laptop joined, warned about it, and could not reach the NAS:

Some peers are advertising routes but --accept-routes is false

Turn route acceptance on, then confirm the new route sits in Tailscale’s policy routing table. Tailscale uses table 52 for this, so the main table will not show it.

sudo tailscale set --accept-routes
ip route show table 52

The 172.20.30.0/24 line is the one that was not there before.

100.64.0.1 dev tailscale0
100.100.100.100 dev tailscale0
172.20.30.0/24 dev tailscale0 

Test against a real LAN host. On our first try this returned No route to host even with the route present, because the router’s LAN interface had no firewalld zone yet. Once it did, the NAS answered:

curl -s http://172.20.30.10/

The response came from a host with no Tailscale on it, so the subnet router is doing its job.

printer-nas on the office LAN

Because tailscaled masquerades subnet traffic by default, the NAS sees requests coming from the router’s LAN address and needs no return route. Keep that in mind if you rely on NAS logs to see who connected, since every tailnet user will appear as the router.

Using the Fedora box as an exit node

An exit node sends all of a client’s internet traffic out through the router, which is what you want on hotel or airport Wi-Fi. If you already ran the subnet router steps, the router side is already done. Forwarding is on, --advertise-exit-node was set, and 0.0.0.0/0 is approved. On Tailscale’s service the exit node has its own approval: open Machines, use the router’s menu, choose Edit exit node and enable Use as exit node.

On the client, pick the exit node by name. Add --exit-node-allow-lan-access so the laptop can still print or cast on whatever local network it’s sitting on:

sudo tailscale set --exit-node=vpn-gw --exit-node-allow-lan-access
tailscale status

The router’s line changes to active; exit node, and the connection is direct rather than relayed.

100.64.0.2  laptop44  mary  linux  -
100.64.0.1  vpn-gw    mary  linux  active; exit node; direct 10.0.1.20:41641, tx 924 rx 828  

A status line can be stale, so prove the path. tracepath ships with Fedora, and its first hop should be the exit node’s 100.64 address:

tracepath -n -m 3 1.1.1.1

Hop 1 is vpn-gw, hop 2 is the office gateway, and hop 3 is the office ISP. The 1280 path MTU is the WireGuard tunnel’s, another sign the traffic is inside Tailscale:

 1?: [LOCALHOST]                      pmtu 1280
 1:  100.64.0.1                                            0.526ms
 1:  100.64.0.1                                            0.296ms
 2:  10.0.1.1                                              0.613ms
 3:  203.0.113.1                                           3.689ms
     Too many hops: pmtu 1280

The full client session, subnet route and MagicDNS included, looked like this:

Fedora 44 Tailscale client reaching a LAN host through the subnet route, resolving vpn-gw with MagicDNS and routing through the exit node

Stop using the exit node with an empty value. The router goes back to offers exit node and your traffic leaves through the local network again:

sudo tailscale set --exit-node=

If you’d rather not keep a Fedora server running at home just for this, a small travel router can do the same job. Our travel routers with Tailscale and WireGuard roundup compares the ones that run it natively. The GL.iNet exit node setup pairs a home unit with a travel one.

Fixing the errors we hit

Each of these came up during our run on the lab VMs. The headings carry the exact text the tools printed, so a search for the message lands on the fix.

Unknown argument “–add-repo” for command “config-manager”

You copied the dnf4 line from Tailscale’s install page onto a Fedora that runs dnf5. Use sudo dnf config-manager addrepo --from-repofile= with the same URL. Nothing was written on the failed attempt, so there’s nothing to clean up.

Warning: IP forwarding is disabled, subnet routing/exit nodes will not work

tailscaled printed this when we advertised routes before touching sysctl, and tailscale status kept a health check line about it. Create /etc/sysctl.d/99-tailscale.conf as shown above and load it. The health line can linger for a while after the fix, so trust sysctl net.ipv4.ip_forward over it. The same run also printed a UDP GRO warning on eth0; that’s a throughput hint for busy routers, not a failure, and Tailscale’s linked page explains the ethtool change.

No route to host from a client to a LAN address

The route was in table 52 and the router answered pings, yet curl failed with connect to 172.20.30.10 port 80 from 100.64.0.2 ... failed: No route to host. That message is firewalld rejecting a forwarded packet, not a missing route. firewall-cmd --get-zone-of-interface=veth-lan printed no zone; adding the interface to the default zone fixed it immediately. The exit node worked the whole time because its traffic left through eth0, which was already in a zone, and that’s what makes this one confusing.

unexpected state: NoState

After the router’s first reboot, tailscale status returned only this for over a minute. The journal showed tailscaled re-registering with the control server and then waiting on a network map that never came. sudo systemctl restart tailscaled brought it back within seconds. A second reboot came up cleanly in under a minute, so we treat it as a one-off rather than a reason to change unit ordering. If it keeps happening on yours, check journalctl -u tailscaled -b for the control server URL it’s trying.

headscale: open /var/lib/headscale/noise_private.key: permission denied

We ran sudo headscale configtest before the first start, and as root it created db.sqlite and noise_private.key owned by root. The service runs as the headscale user, so it looped on reading or creating Noise protocol private key: reading private key file: open /var/lib/headscale/noise_private.key: permission denied and the CLI timed out on its socket. On a fresh install, delete those files and start again:

sudo systemctl stop headscale
sudo sh -c "rm -f /var/lib/headscale/db.sqlite* /var/lib/headscale/noise_private.key"
sudo systemctl start headscale

On a server that already has nodes, do not delete anything: sudo chown -R headscale:headscale /var/lib/headscale keeps the database and fixes the ownership instead.

Keeping Tailscale running on Fedora

The Tailscale repo updates through normal sudo dnf upgrade, so a Fedora box that gets patched weekly stays current with no extra work. A machine you rarely log into is better off with Tailscale’s own updater, which works on Fedora because it calls dnf underneath:

sudo tailscale update --dry-run
sudo tailscale set --auto-update

The dry run answered already running stable version 1.104.4; no update needed, and tailscale debug prefs then showed AutoUpdate with Check and Apply both true. A few habits keep a router from going dark while you’re away:

  • Disable key expiry on the router in the admin console
  • Keep /etc/sysctl.d/99-tailscale.conf in your config backups
  • Re-check firewall-cmd --get-active-zones after NIC changes
  • Watch journalctl -u tailscaled -f during upgrades

Key expiry matters most, because a router whose node key expires drops off the tailnet, and every client routing through it loses the LAN and the exit node until someone logs the router back in. The journal is the one log to watch: route approvals, DNS changes and control server problems all land there before they show up in tailscale status. If you need a tunnel that doesn’t depend on any control server at all, a plain WireGuard setup on Fedora is the fallback, and our firewalld zones guide covers the zone rules above in more depth. The Tailscale guide for Rocky and AlmaLinux follows the same pattern on RHEL-family servers.

Keep reading

How to Upgrade Fedora 44 to Fedora 45 (GUI, CLI and Rollback) Fedora How to Upgrade Fedora 44 to Fedora 45 (GUI, CLI and Rollback) Enable RPM Fusion and Install Multimedia Codecs on Fedora 45 / 44 / 43 Fedora Enable RPM Fusion and Install Multimedia Codecs on Fedora 45 / 44 / 43 Harden Fedora 45 / 44 for Desktop and Server: Complete Guide Fedora Harden Fedora 45 / 44 for Desktop and Server: Complete Guide Switch Fedora From GRUB to systemd-boot With bootctl Fedora Switch Fedora From GRUB to systemd-boot With bootctl DNF5 Cheatsheet for Fedora 45 / 44: Tested Commands and Fixes Fedora DNF5 Cheatsheet for Fedora 45 / 44: Tested Commands and Fixes Install Apache Solr on CentOS / Fedora / Rocky Linux CentOS Install Apache Solr on CentOS / Fedora / Rocky Linux

Leave a Comment

Press ESC to close