← All guidesGitHub
Development and Deployment

Lock down a Droplet with Tailscale and private SSH

Lock down a Droplet with Tailscale, close public SSH, test access rules, and keep a recovery route. Follow the guide before adding private server applications.

Jump to stepsGitHub: TylorMayfield/private-droplet-tailscale

Make SSH private before adding applications

To lock down a Droplet with Tailscale, first establish an authenticated route to the server, then remove public SSH access. This guide uses a fresh Ubuntu 24.04 LTS DigitalOcean Droplet, an SSH key, and a small tailnet. The result is an ordinary OpenSSH session carried over Tailscale, with public inbound connections blocked.

The order matters more than the number of security tools. Keep your original session open until a second private connection succeeds. A closed port is useful only if you can still administer the machine and recover when something goes wrong. We will test allowed access, denied access, and a restart before treating the setup as ready.

This is a private-server guide. A public website, mail server, or public game server needs different inbound rules. Do not copy the final firewall state onto a machine serving those workloads. Use a new Droplet for the exercise so you can understand every exception you add.

Choose this when the server has known users

I would use this setup for a personal automation server, an internal dashboard, or the Home Assistant follow-up. Tailscale removes the need to keep updating a home-IP allowlist when you move between Wi-Fi networks. Its tradeoff is another account, a client on each connecting device, and an access policy that somebody must maintain.

An IP allowlist is still reasonable for one fixed office connection. Plain public SSH with keys can be operated responsibly, but it leaves a reachable service to maintain. Tailscale reduces that exposure; it does not repair vulnerable applications or protect a stolen, already-authorized laptop. Use device screen locks and account multifactor authentication as part of the same setup.

Scroll horizontally to see all columns.

Choose an administration route
ApproachUseful forWork you retain
Public SSH with keysMachines that need broad SSH reachabilityPublic service maintenance and login monitoring
SSH restricted to an IPA fixed trusted office addressUpdating rules when that address changes
SSH over TailscaleKnown users on changing networksClient enrollment, policy review and recovery

Prepare accounts, cost and a recovery path

You need a DigitalOcean account, a Tailscale account, and an SSH key on your computer. Install the Tailscale client locally and sign in before touching the server. The commands assume a Unix-like terminal; Windows readers can use WSL with a working local Tailscale route. Keep private keys on your own computer. Only the public key belongs on the Droplet.

Choose the smallest plan appropriate to the eventual application, rather than buying extra memory for SSH alone. Check the current price in the creation screen, including backups and storage. CPU Droplet billing continues while a machine is powered off and ends when you destroy it. Tailscale has a Personal offering for eligible personal use; check its current limits and business terms separately.

Before closing SSH, locate the DigitalOcean Recovery Console in the Droplet settings and verify you have console login credentials. The ordinary browser Droplet Console uses the network and is not the same recovery route. Keep a local account password in your password manager for console recovery; disabling SSH passwords later does not remove that local password.

Create a fresh server and a named administrator

Create an Ubuntu 24.04 LTS Droplet with your SSH public key selected. Attach a dedicated Cloud Firewall whose only temporary inbound rule allows TCP 22 from your current public IPv4 address with a /32 mask. If connecting over IPv6, use your exact address with /128 instead. Keep the default outbound allowances for this exercise. Confirm the firewall is actually attached to the new machine.

Connect as root using the public address. Replace PUBLIC_IP in the command. Verify the host-key fingerprint through a trusted console before accepting it. In the root session, install updates and create the operator account with adduser operator. Choose its local password, then run usermod -aG sudo operator. Copy your existing public authorized_keys file into the new account using the commands below.

Open another terminal and connect as operator at the public address. Run sudo -v there. Continue only when key login and sudo both work. Leave the root session open as a temporary fallback. Do not change SSH authentication options until this named account has been tested.

  1. Run in a new local terminalLocal terminal
    Replace every highlighted value before running this command.
    ssh root@PUBLIC_IP

Before continuing: Use the fresh tutorial Droplet and keep a tested recovery path. Read the preceding checkpoint before running.

Install updates and the operator public keyConnected Droplet
apt-get update
apt-get upgrade -y
install -d -m 700 -o operator -g operator /home/operator/.ssh
install -m 600 -o operator -g operator /root/.ssh/authorized_keys /home/operator/.ssh/authorized_keys

Join the tailnet and prove private SSH works

From the operator session, download the official installer to a file, read it, and run it only after reviewing it. The installer configures the appropriate package source. Then run sudo tailscale up and open its authentication URL on your own computer. Do not post that enrollment URL in a public issue or save reusable auth keys in the companion repo.

Run tailscale ip -4 on the Droplet and record its 100.x address. In a fresh local terminal, connect to operator at that address. This still uses your SSH key and the system OpenSSH server. We are not enabling Tailscale SSH, whose authentication and policy are a separate feature. Run hostname and sudo -v to confirm the new session reaches the intended machine.

If the connection fails, stop here. Check that both devices are online in the same tailnet, that the account can reach the node, and that sshd is running. Do not remove public SSH to see whether it helps. A successful old connection does not prove that a new connection can be established.

Download the Tailscale installerConnected Droplet
curl -fsSL https://tailscale.com/install.sh -o /tmp/tailscale-install.sh
Review the installer; press q to exitConnected Droplet
less /tmp/tailscale-install.sh

Before continuing: Use the fresh tutorial Droplet and keep a tested recovery path. Read the preceding checkpoint before running.

Install Tailscale on the DropletConnected Droplet
sudo sh /tmp/tailscale-install.sh

Before continuing: Use the fresh tutorial Droplet and keep a tested recovery path. Read the preceding checkpoint before running.

Enroll the Droplet in TailscaleConnected Droplet
sudo tailscale up
Find the private IPv4 addressConnected Droplet
tailscale ip -4
  1. Run in a new local terminalLocal terminal
    Replace every highlighted value before running this command.
    ssh operator@TAILSCALE_IP

Restrict who can reach the tagged server

Open the tailnet access-policy editor and save a copy of its current contents. For a new teaching tailnet, start with the companion ssh-only.json policy. Replace operator@example.com with your exact Tailscale login and outsider@example.com with a second enrolled test user who must not have access. Keep your laptop user-owned; tag only the server as tag:private-server using the Machines page after saving the tag owner definition.

The policy allows the operators group to reach TCP 22 on that tag. It includes tests for allowed SSH and denied application ports. Run the policy editor validation before saving. The example intentionally grants no TCP 443 or 8123 access. Its empty permissions for everyone else are part of the design.

For an existing tailnet, adapt the policy rather than replacing unrelated rules. Grants and legacy ACL allows are additive. Adding a narrow grant does not cancel an existing allow-all rule. Remove or narrow any broader rule that also reaches this server, then run the negative tests. Reconnect privately after assigning the tag because tagging changes the node identity and may change which rules match.

Close public inbound access without losing the session

In the working private session, install UFW if needed with sudo apt-get install ufw. Set the incoming default to deny, keep outgoing allowed, and add the tailscale0 interface rule below before enabling UFW. Make sure IPv6 is enabled in /etc/default/ufw so the host policy covers it too. Read the enable prompt before answering yes.

Inspect sudo ufw status numbered. Remove each public allow rule by its displayed number using sudo ufw delete NUMBER, re-listing after every removal because numbers change. Keep the tailscale0 rule. A default deny does not override an old allow rule. Do not blindly reset an existing firewall or delete rules copied from a different machine.

Now remove the temporary TCP 22 inbound rule from the attached DigitalOcean Cloud Firewall. The final cloud policy has no inbound rules and retains outbound TCP, UDP and ICMP allowances for IPv4 and IPv6. Inspect every firewall attached directly or by tag; a second permissive firewall can reopen access. Do not add the Tailscale 100.x address as a cloud SSH source, because the cloud firewall sees the outer transport rather than the decrypted private session.

With no inbound UDP allowance, Tailscale may use a relay. That is acceptable for administration if the connection test succeeds. Do not open TCP 22, 443 or 8123 publicly to improve it. Investigate connection diagnostics before considering the documented optional UDP transport allowance and its narrower purpose.

Before continuing: Use the fresh tutorial Droplet and keep a tested recovery path. Read the preceding checkpoint before running.

Configure and inspect the host firewallConnected Droplet
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0
sudo ufw enable
sudo ufw status numbered

Keep SSH keys and verify the effective configuration

Once the operator key works over Tailscale, disable password authentication, keyboard-interactive authentication, and root SSH login. On this fresh Ubuntu host, write the following drop-in with sudo privileges. Keep the tested operator console password; these settings apply to SSH. Check the effective settings, because included files and Match blocks can alter the result.

Run sudo sshd -t before reloading SSH. If validation fails, fix or remove only the new drop-in using the session you kept open. After a successful reload, open another operator connection over Tailscale and run sudo -v. Do not close the last working session until this test passes. A local syntax check alone cannot prove remote authentication works.

Before continuing: Use the fresh tutorial Droplet and keep a tested recovery path. Read the preceding checkpoint before running.

Validate SSH settings before reloadingConnected Droplet
sudo tee /etc/ssh/sshd_config.d/00-private-access.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
EOF
sudo sshd -t && sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication) ' && sudo systemctl reload ssh

Test fresh connections from both sides

From your authorized computer, open a new SSH connection to the Tailscale address with connection multiplexing disabled. Then try the public IPv4 address and, if assigned, public IPv6. The private connection must succeed; public SSH must not establish a TCP session. A public permission-denied response means SSH is reachable and the network test has failed.

Use the second enrolled user to attempt private SSH too. It must fail before authentication. Also disconnect Tailscale on a test client and confirm it cannot reach the private address. Restore the client connection afterward. The strongest evidence is a recorded matrix with the source device, destination, time, and result for each attempt, not a screenshot of green firewall switches.

Finally reboot the Droplet from the private session, wait for it to return, and repeat the new-connection tests. Review tailscale status, the SSH service, and UFW after restart. If you rely on key expiry, schedule reauthentication before it expires. If you disable expiry for this server, record why and remove the device promptly if it is retired.

  1. Run in a new local terminalLocal terminal
    Replace every highlighted value before running this command.
    ssh -o ControlMaster=no -o ControlPath=none -o ConnectTimeout=10 operator@TAILSCALE_IP
  1. Run in a new local terminalLocal terminal
    Replace every highlighted value before running this command.
    ssh -o ControlMaster=no -o ControlPath=none -o ConnectTimeout=10 operator@PUBLIC_IP

Recover access and keep the boundary intact

If private access breaks, first inspect the Tailscale admin console for an expired node or a policy change. If necessary, use the Recovery Console and your local password to inspect tailscaled, SSH and UFW. To restore temporary public SSH, add only your current public IP to both the Cloud Firewall and UFW. Repair Tailscale, prove private access again, then remove both temporary exceptions.

The tailscale0 host rule deliberately trusts traffic admitted by Tailscale. It is not a per-port tailnet permission system. Keep least-privilege grants in the tailnet policy. Docker can also change packet filtering independently of UFW; a future published container port requires a new exposure review. Keep the Cloud Firewall attached even when host rules look correct.

Apply Ubuntu, OpenSSH and Tailscale updates, review users and devices, and remove stale SSH keys. Keep backups of application data outside the VM. This configuration prevents unwanted network reachability; it cannot stop an authorized account from doing something its permissions allow.

For the next article, retain the SSH-only policy until Home Assistant has completed private onboarding. The application will get its own TCP 443 grant after its backend is restricted. If this was just a practice server, save any needed notes, destroy the exact Droplet in DigitalOcean, and remove its Tailscale node. Powering it off leaves the bill running.

Does Tailscale replace SSH keys?

Not in this guide. OpenSSH still checks your key and local account permissions. Tailscale supplies the private route and network access policy. Tailscale SSH is a different feature with its own authorization rules, and these instructions do not turn it on.

Can I leave the default tailnet policy?

Only if its access matches your intent. A broad allow rule can admit every member to the server even after you add a narrow grant. Review the combined policy and run denied-access tests with a separate enrolled user before hosting private applications.

Do I need to open a public Tailscale port?

Try the no-inbound configuration first. Tailscale can use relayed connections when a direct path is unavailable. Verify that new sessions and reconnects work from your actual networks. An optional UDP transport rule is separate from opening public SSH or an application port.

What if I lock myself out?

Use the DigitalOcean Recovery Console with local login credentials. The ordinary Droplet Console depends on the network. Repair the node or restore a temporary source-restricted SSH rule in both firewalls, then remove that exception after private access works again.

Is this enough to call the server secure?

It establishes a restricted network entry point. You still need updates, account protection, application authorization, backups and recovery practice. Repeat the public and unauthorized-user tests whenever you add a service, change a policy, or attach another firewall.

Check your result

Expected result
Allowed private connections work after reboot. Public and unauthorized connections fail.
Stop if
Stop if a public address or unauthorized user reaches the service. Restore the last working configuration through the recovery path.
Next step
Record a reboot and recovery test before connecting real workloads.