Give the Server an Identity That Survives the Router
A physical LAN is useful for local recovery, but it is a poor long-term identity layer for a server that may move behind different routers, firewalls, or ISPs.
Tailscale gives the admin workstation, physical Fedora host, and Fedora Server VM private identities that do not depend on their current LAN addresses.
This guide assumes you completed KVM, libvirt, and the First Fedora Server VM. You should be able to administer the physical host over SSH and reach the guest through the virt-manager console or the optional ProxyJump path.
private Tailnet
admin workstation ─────┬───── Fedora host
│
└───── Fedora Server VM
The underlying networks still exist:
local LAN
│
└── Fedora host
│
└── libvirt NAT
└── Fedora Server VM
This design uses normal OpenSSH over Tailscale and does not enable Tailscale SSH. Tailscale supplies the private transport; the OpenSSH keys and sshd policy from the previous guides remain the authentication boundary.
Example values: Hostnames, usernames, addresses, interface names, device paths, and other identifiers are illustrative unless explicitly described as software defaults. Discover the values on your own systems before running commands.
The VM Should Have Its Own Private Identity
Without Tailscale inside the VM, the workstation normally reaches the guest through the physical host:
admin workstation
│
▼
Fedora host
│ ProxyJump
▼
Fedora Server VM
With Tailscale installed directly inside the guest:
admin workstation
├──────► Fedora host
└──────► Fedora Server VM
The physical host no longer needs to be the routine SSH jump server, and the VM's private SSH key can remain exclusively on the admin workstation.
Tailscale can use direct peer-to-peer UDP, DERP relay, or a peer relay depending on reachability. All of those paths remain end-to-end encrypted; what changes is path and performance.1
Put the Fedora Host on the Tailnet First
Tailscale's official Linux instructions support Fedora and other mainstream package-manager distributions.2
Choose the installation method before you run anything. If you do not want to pipe a downloaded script into a shell, follow Tailscale's distribution-specific repository instructions instead, then continue with systemctl and tailscale up.2:1 The convenience script path is:
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
sudo tailscale up --hostname=local-server
On first use, tailscale up prints a login URL. Complete authentication in a browser before continuing.
Then inspect the state you actually got:
tailscale status
tailscale ip -4
ip -br address
A Linux node normally has a tailscale0 interface and an address from Tailscale's 100.64.0.0/10 CGNAT range.
A Tailnet address is not reachable from the public Internet, but it still identifies a node in your Tailnet. Do not publish real Tailnet addresses in logs, screenshots, or articles.
Keep Overlay Networking Separate from the Physical Firewall Zone
Do not move tailscale0 into the physical local-server firewalld zone for visual tidiness.
Tailscale on Linux manages its own netfilter rules by default. Its documented on mode installs the rules needed for Tailscale traffic while leaving the operator free to keep separate firewall policy for other interfaces.3
Your physical zones can therefore stay focused on the interfaces they already own:
local-server
interfaces: eno2
libvirt
interfaces: virbr0
Tailscale manages the overlay path through tailscale0 separately from those zones.
If you later change Tailscale's netfilter mode to nodivert or off, you take responsibility for more of the firewall behavior and rule ordering.3:1
Put the Guest on the Tailnet Too
Inside the Fedora Server VM, use the same installation method you chose for the host. With the convenience script:
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
sudo tailscale up --hostname=server-vm-01
tailscale status
tailscale ip -4
The physical host and guest should now appear as distinct Tailnet nodes. That separation is the reason to install Tailscale in the guest instead of routing all VM administration through the host.
On macOS, Pick One Tailscale Distribution and Stick with It
Tailscale currently documents three macOS variants: the Standalone app, the Mac App Store build, and the open-source CLI-only tailscaled variant. Tailscale recommends the Standalone application for most macOS users.4
The variants are not cosmetic packaging differences. The Standalone and App Store clients use macOS extension frameworks, while the open-source variant behaves more like the Linux tailscale + tailscaled split.4:1
Tailscale explicitly warns against installing the App Store and Standalone variants side-by-side because multiple variants can interfere with extension startup.4:2
Choose one supported installation model, launch it, and confirm the client is live:
open -a Tailscale
tailscale status
Know Which macOS CLI You Are Actually Running
Current Standalone builds can install a CLI launcher at:
/usr/local/bin/tailscale
The official CLI documentation also describes the app-bundled CLI behavior and the TAILSCALE_BE_CLI=1 escape hatch for scripts.5
Check the shell path before deleting or reinstalling anything:
type -a tailscale
which tailscale
brew list --versions tailscale 2>/dev/null || true
Compare the application-bundled client when useful:
TAILSCALE_BE_CLI=1 /Applications/Tailscale.app/Contents/MacOS/Tailscale version
TAILSCALE_BE_CLI=1 /Applications/Tailscale.app/Contents/MacOS/Tailscale status
Tailscale's issue history includes both genuine CLI/daemon mismatches and confusing warnings caused by version metadata.6 Start by identifying which binary and daemon are running.
If Homebrew is an unintended duplicate and the Standalone app is your chosen installation:
brew uninstall tailscale
hash -r
type -a tailscale
tailscale version
tailscale status
Current Standalone builds may legitimately own /usr/local/bin/tailscale. That path alone is not evidence of a leftover installation.5:1
Prove Tailnet Connectivity Before You Rewire SSH
From the admin workstation, verify that both nodes are reachable on the Tailnet:
tailscale ping local-server
tailscale ping server-vm-01
Tailscale reports whether peers are connecting directly or through a relay. Direct paths usually perform better, but a relayed path remains end-to-end encrypted.1:1
Then test the host with normal OpenSSH by Tailnet IP:
ssh -i ~/.ssh/local_server_ed25519 admin@<TAILSCALE_IP_HOST>
Exit that session. Back on the admin workstation, test the VM separately:
ssh -i ~/.ssh/server_vm_01_ed25519 admin@<TAILSCALE_IP_VM>
When SSH sees the same machine under a new address, compare its ED25519 host-key fingerprint with the already trusted LAN identity instead of accepting a new key blindly.
Make Tailscale Primary Without Throwing Away LAN Recovery
Once direct SSH works, update the workstation's SSH config. A fictional macOS example:
Host local-server
HostName 100.64.10.10
User admin
IdentityFile ~/.ssh/local_server_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
Host server-vm-01
HostName 100.64.10.20
User admin
IdentityFile ~/.ssh/server_vm_01_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
Host local-server-lan
HostName 192.168.50.50
User admin
IdentityFile ~/.ssh/local_server_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
Host server-vm-01-lan
HostName 192.168.122.50
User admin
IdentityFile ~/.ssh/server_vm_01_ed25519
IdentitiesOnly yes
ProxyJump local-server-lan
AddKeysToAgent yes
UseKeychain yes
The primary VM alias has no ProxyJump. First validate what OpenSSH resolves; these commands exit without opening a session:
ssh -G local-server | grep -E '^(hostname|proxyjump|identityfile) '
ssh -G server-vm-01 | grep -E '^(hostname|proxyjump|identityfile) '
Then open the host session from the admin workstation:
ssh local-server
Exit the host session, return to the admin workstation, and open the guest session separately:
ssh server-vm-01
The LAN aliases remain useful recovery paths during later physical-network changes.
Keep Guest Networking Isolated from the LAN
The VM's libvirt address belongs to the host's NAT network. From a normal workstation on the LAN, this commonly fails:
ssh admin@192.168.122.50
The workstation has no route to 192.168.122.0/24. The physical host can reach the guest directly, and the workstation can still use the fallback ProxyJump alias.
Do not add LAN routes just to expose the private libvirt subnet when Tailscale already gives the VM a cleaner independent administrative path.
A Private Tailnet Still Needs Least Privilege
Admission to a Tailnet should not automatically mean every device can reach every other device on every port.
Tailscale's current access-control documentation recommends Grants for most new policy work. Grants are deny-by-default and can express both network and application-layer permissions.7
For a small administrative Tailnet, the policy can be simple:
admin workstation
├── SSH + ICMP → Fedora host
└── SSH + ICMP → Fedora Server VM
host → workstation denied by default
VM → workstation denied by default
host ↔ VM over Tailnet denied unless needed
Fictional example:
{
"hosts": {
"admin-workstation": "100.64.10.5",
"local-server": "100.64.10.10",
"server-vm-01": "100.64.10.20"
},
"grants": [
{
"src": ["admin-workstation"],
"dst": ["local-server", "server-vm-01"],
"ip": ["tcp:22", "icmp:*"]
}
],
"tests": [
{
"src": "admin-workstation",
"proto": "tcp",
"accept": ["local-server:22", "server-vm-01:22"],
"deny": ["local-server:9090", "server-vm-01:9090"]
}
]
}
Replace every example address or selector with the policy identities that belong to your Tailnet.
Remove pre-existing wildcard allow rules if they would make the restrictive Grant meaningless. Tailscale permissions are additive, so a broad rule can continue allowing traffic even after a narrow Grant is added.7:1
If you are not using Tailscale SSH, Funnel, or another optional capability, leave its policy section out until you need it.
Test Both the Allowed and Denied Paths
From the admin workstation, test Tailnet reachability first:
tailscale ping --icmp local-server
tailscale ping --icmp server-vm-01
Then open the allowed host SSH path:
ssh local-server
Exit that session. Back on the admin workstation, test the allowed guest path separately:
ssh server-vm-01
Then test something that should be denied, such as an ungranted service or lateral server-to-server Tailnet connection.
Tailscale's security guidance explicitly recommends least privilege and policy tests so a policy change does not silently expose resources.8
Tailnet policy does not rewrite unrelated paths. Host-to-guest traffic over the private libvirt network is governed by that network and the guest's own firewall and SSH policy, not by a Grant that applies to Tailnet traffic.
Add Tailnet Lock After Ordinary Connectivity Is Stable
Tailnet Lock adds a different security boundary. Enable it only after the workstation, host, and guest connect reliably and the Grants above have passed their allowed and denied tests. That way a signing problem is not confused with an ordinary connectivity or policy problem.
Grants answer:
What may an admitted node access?
Tailnet Lock answers:
Which node keys are cryptographically trusted to participate?
Tailnet Lock requires trusted signing nodes to sign node keys before peers accept them, reducing trust placed in the coordination server for key distribution.9
For a small setup, choose signing nodes across failure domains. The admin workstation plus the physical host is a reasonable pattern, while the VM remains a normal signed node.
Follow the admin console's current Tailnet Lock flow and generated commands rather than copying an initialization command from an old article. After enabling it, check its status on each node:
tailscale lock status
Store the Disablement Secrets Independently
Tailnet Lock documentation warns that losing disablement secrets without a viable recovery path can make a locked Tailnet unrecoverable.9:1
Do not paste those secrets into Git repositories, public articles, shell-history examples, screenshots, or chat transcripts.
A complete secrets and recovery architecture is outside this guide, but the independence of that recovery material is part of the Tailnet Lock design.
Key Expiry Is an Availability Decision by Device Role
Tailscale node keys expire and require reauthentication according to Tailnet policy. Tailscale explicitly documents trusted servers, subnet routers, and remote or IoT devices as cases where disabling key expiry can be appropriate.10
A practical split can be:
interactive admin workstation expiry enabled
trusted physical server expiry disabled
trusted server VM expiry disabled
That is a policy choice, not a universal rule. A server with easy physical access may reasonably keep expiry enabled.
Tailnet Lock is separate; disabling ordinary node-key expiry does not disable Tailnet Lock.
Client Updates Are Part of the Security Boundary
Timely client updates are part of Tailscale's own security guidance.8:1
Where supported, enable auto-updates through current client or admin controls and verify the setting instead of assuming it is active. Tailscale documents auto-update behavior and notes that the underlying platform or package mechanism applies the updates.11
On clients that expose the setting through the CLI, inspect current help before copying an old command:
tailscale version
tailscale set --help
On macOS, update behavior differs by installation variant. That is another reason not to mix them.4:3
Remote Access Is Not Proven Until the Local LAN Is Gone
Connect the admin workstation through a phone hotspot or another trusted Internet connection. With Tailscale connected, verify both peers first:
tailscale ping local-server
tailscale ping server-vm-01
Open the host session from the admin workstation:
ssh local-server
Exit it, then open the guest session from the admin workstation:
ssh server-vm-01
When the policy and routing are correct, both SSH sessions open without router port forwarding, a public SSH listener, a static public IP, or a direct route to the libvirt subnet.
Reboot the Whole Stack, Not Just Individual Services
Reboot the physical host:
sudo reboot
If the system disk is LUKS-encrypted, unlock it locally. After Fedora boots, reconnect from the admin workstation:
ssh local-server
On the physical Fedora host, verify Tailscale and the autostarted VM:
systemctl is-active tailscaled
tailscale status
tailscale lock status
sudo virsh -c qemu:///system list --all
The VM should be running. Return to the admin workstation, connect to the guest, and then repeat the Tailscale checks there:
ssh server-vm-01
Inside the VM:
systemctl is-active tailscaled
tailscale status
tailscale lock status
A clean reboot proves that the private administration plane survives the lifecycle event that matters most.
Add Features Only When They Solve a Real Problem
A stable administrative Tailnet does not require every Tailscale feature.
Leave these out until you have a specific requirement:
Tailscale SSH
subnet routing
exit nodes
Serve
Funnel
broad wildcard Grants
The baseline stays layered:
identity provider / MFA
↓
Tailnet Lock
↓
least-privilege Grants
↓
Tailscale encrypted transport
↓
OpenSSH public-key authentication
↓
Fedora firewall + SELinux
Tailscale's own security guidance makes the same larger point: access controls, identity security, client updates, and the host or network firewall remain separate responsibilities.8:2