GuidesSecurity

Tailscale SSH for an AI agent server: tags, ACLs, key expiry

By default an AI agent's server on your tailnet can reach your laptop. Tag it, let only your devices connect, SSH in with no keys, and turn off key expiry.

October 1, 2026The Everpod team
The short answer

Out of the box, a tailnet lets every device start connections to every other, so a server you add for an AI agent can reach your laptop, your phone and whatever they serve, and so can any command the agent is tricked into running. Tag the server, replace the allow-all rule with a grant that only your own devices match, and add a Tailscale SSH rule from your devices to the tag. You then SSH in with no keys to manage and no need for a public port 22, and the server cannot open a connection to anything else on the tailnet. Check the result in your devices’ own packet filters, and turn off the server’s key expiry yourself: tagging it in the admin console leaves the expiry in place.

Why the default is wrong for this server

Tailscale’s default policy has one network rule, every device to every device, and in Tailscale’s words it “lets all devices in the tailnet access all other devices in the tailnet” (Tailscale’s description of the default policy). Between your own laptop and phone that is convenient. A server running an agent is a different kind of device. The agent reads web pages, email and code that other people wrote, and text it reads can carry instructions it will follow. Whatever it runs inherits the server’s place on your tailnet, and by default that place has a route to every device you own and anything listening on them, such as a dev server on your laptop or a file share at home.

The SSH rules have the same shape. Tailscale’s default SSH rule lets each person SSH into their own devices, and a server you signed in to Tailscale as yourself is one of your own devices, as a source as well as a destination. Tailscale’s SSH documentation lists “machines that have outbound Tailscale SSH access, where you do not trust the code running on those machines” among the setups it does not suit (Tailscale SSH’s limitations).

The policy: only your own devices start connections

Tailscale’s rules are directional: a rule lets a source start connections to a destination, which answers on them but cannot start its own unless another rule allows it (how access rules behave). So the fix is to name who may start connections. This policy does it with a tag for the server, in the grants syntax Tailscale recommends for new policies; the older ACL syntax still works but gets no new features (grants versus ACLs). On a tailnet still on the default policy, it replaces the whole file on the admin console’s Access controls page:

{
  "tagOwners": {
    "tag:agent-server": ["autogroup:admin"]
  },
  "grants": [
    {
      "src": ["autogroup:member"],
      "dst": ["*"],
      "ip": ["*"]
    }
  ],
  "ssh": [
    {
      "action": "check",
      "src": ["autogroup:member"],
      "dst": ["autogroup:self"],
      "users": ["autogroup:nonroot", "root"]
    },
    {
      "action": "accept",
      "src": ["autogroup:member"],
      "dst": ["tag:agent-server"],
      "users": ["autogroup:nonroot", "root"]
    }
  ]
}

What quietly undoes it

The policy file can also carry tests, assertions checked every time the file changes, which make the editor reject an edit that breaks them. One with tag:agent-server as its source and your own login with a port, such as 22, under deny catches a later edit that reopens the route.

Tag the server, after the policy

Save the policy first. Tailscale’s default SSH rule covers a person’s own devices and never tagged ones, so a server tagged while the default policy is in force loses Tailscale SSH until the rule for its tag exists.

A server joining the tailnet now

After installing Tailscale on the server, run:

sudo tailscale up --ssh --advertise-tags=tag:agent-server

Open the link it prints and sign in as the tailnet’s owner or an admin. The server joins as tag:agent-server with Tailscale SSH switched on, and a device that authenticates with a tag for the first time gets key expiry disabled by default, so the expiry step below becomes a check rather than a change.

A server already on your tailnet

In the admin console, open Machines, then the menu at the end of the server’s row, then Edit tags, and add tag:agent-server. An admin can do this without re-authenticating the device. If Tailscale SSH is not on yet, run sudo tailscale set --ssh on the server over some connection other than its Tailscale address, since Tailscale warns that the command makes existing SSH sessions to that address hang. Then do the key expiry step below, because this route leaves the expiry on.

From any of your devices, connect with an ordinary SSH client, using the server’s name on the tailnet:

ssh <user>@<server-name>

There is no key and no password: Tailscale already knows which of your devices is connecting. From a phone, run the Tailscale app alongside an SSH client. If the client refuses to connect without a password, Tailscale’s documented workaround is to add +password to the username and type anything at the password prompt.

Check it where it is enforced

The policy file says what you meant. What happens is decided by each device’s own packet filter: Tailscale compiles the policy into rules for each device, and the device enforces them on incoming connections itself, without Tailscale’s servers in the loop. So the proof that the server cannot reach your laptop is on the laptop, and since the laptop enforces it, nothing done on the server, as root or otherwise, changes what the laptop accepts.

Print a device’s current network map, which includes its packet filter (on Linux, with sudo):

tailscale debug netmap

Under PacketFilterRules, each rule’s SrcIPs lists the addresses allowed to start connections to that device, and a * there admits every device. On your laptop, the server’s Tailscale address, which tailscale ip -4 prints on the server, must not appear. On the server, your devices’ addresses should. This is a debug command, and the shape of its output is internal to Tailscale and can change; those are the field names in Tailscale’s source as of October 2026.

We read both filters on a tailnet set up this way on September 29, 2026. The laptop’s admitted only the owner’s own devices and not the server, the server’s admitted the laptop, SSH from the laptop to the server worked, and the server could not open a Tailscale SSH session to itself.

For a quicker check from the server, Tailscale’s docs describe a pair of pings (debugging access rules). A TSMP ping stops before the access check, so it should answer; an ICMP ping goes through it, so it should not:

tailscale ping --tsmp <laptop-name>   # answers: the devices can connect
tailscale ping --icmp <laptop-name>   # no reply: the laptop's rules refuse it

Turn off key expiry yourself

Each device’s key expires unless you turn that off, after 180 days by default on a new tailnet, and when it does, connections to and from the device stop (Tailscale on key expiry). For a server you reach only through Tailscale, that cuts you off on a date you did not pick, until you extend the key from the admin console or get in through your provider’s web console.

Tailscale’s docs say that a device which first authenticates with a tag has key expiry disabled by default, and that changing a device’s tags from the admin console, the CLI or the API leaves its expiry as it was until the device re-authenticates (key expiry for tagged devices). That is what we saw on September 29, 2026: a server tagged in the admin console still had a key expiry about six months out. Turn it off from the same menu (Machines, the menu at the end of the server’s row, Disable key expiry), then confirm on the server:

tailscale status --json --peers=false | grep KeyExpiry

It prints the expiry date while one is set and nothing once expiry is off. Re-authenticating with tailscale up --force-reauth would also clear it, but Tailscale warns that doing so can drop the tailnet connection, so it is not a command to run over Tailscale SSH. If a key has already expired, the same menu offers Temporarily extend key, which gives you 30 minutes to get in and disable expiry.

Then close port 22

Tailscale SSH does not use the server’s own SSH daemon. It takes port 22 on the server’s Tailscale address only, and it leaves sshd_config and authorized_keys untouched, so the ordinary SSH server keeps listening on the public address (Tailscale SSH, how it works). Once Tailscale SSH works, sshd is left with one job: answering the internet. Confirm Tailscale SSH from a second terminal, know where your provider’s web console is, then switch sshd off. On Ubuntu 24.04, where sshd is socket-activated, both units have to go:

sudo systemctl disable --now ssh.socket ssh.service
sudo ss -tlnp 'sport = :22'

Disabling only ssh.service is not enough: the socket keeps listening on port 22 and starts sshd again on the next connection. The ss command should print its header line and nothing else. On Ubuntu 24.04 on September 29, 2026, with sshd switched off, nothing listened on port 22 and Tailscale SSH kept working. On other distributions the unit is usually ssh or sshd. If you would rather keep sshd as a second way in, make it keys-only instead.

The provider’s firewall can then hold no inbound rules at all, a property Tailscale and Cloudflare Tunnel share. Tailscale’s own answer to which ports to open is that most of the time none need opening (Tailscale’s firewall ports FAQ), and in our check the same day, a laptop connected directly, not through a relay, to a server behind a provider firewall with no inbound rules. tailscale status shows which you have: direct with an address and port, or relay with the relay’s city code.

Your own open-source AI agent, set up for you.

Everpod runs OpenClaw on a private, always-on computer of its own: set up, secured and backed up, with model usage included. You name your agent, and say hello about fifteen minutes later.

Create your agent

First month half price, then $29/mo · model usage included · cancel anytime

Wondering what you’d do with one? See what a cloud agent can do