Harden a Raspberry Pi with Tailscale
Secure a Raspberry Pi server with SSH keys, UFW, Fail2Ban, and Tailscale.
Introduction
30 Second Summary
Your home network is full of devices that are always on and always connected. Without proper security, any one of them could become an open door for attackers.
In this project, you will turn a Raspberry Pi into a hardened server protected by three layers of security, then make it accessible from anywhere using a mesh VPN on your Mac and iPhone.
What You'll Build
A production-grade secure Raspberry Pi server running Ubuntu Server 24.04 LTS, accessible remotely from your Mac and iPhone via Tailscale and iOS Shortcuts.
By the end of this project, you'll have:
- A Raspberry Pi with SSH key-only authentication and password login completely disabled.
- A UFW firewall and Fail2Ban intrusion prevention system that automatically bans brute-force attackers.
- A Tailscale mesh network connecting your Pi, Mac, and iPhone so the server is reachable from anywhere without port forwarding.
- Secret Mission: Build an "Ask My Pi" iOS Shortcut that lets you type any command and get the result back on your phone from anywhere in the world.
Are there any prerequisites?
You will need a Raspberry Pi (Model 3, 4, or 5) with a power supply, a microSD card (8GB or larger), and either an Ethernet cable or Wi-Fi credentials.
You will also need a free Tailscale account. You can sign up at tailscale.com using Google, Apple, or GitHub.
Flash Ubuntu Server and First Boot
Before you can harden a server, you need a server to harden. In this step, you will set up your Mac's terminal for remote access and flash a fresh Ubuntu Server image onto your Raspberry Pi's microSD card.
You will use Ghostty as your terminal emulator and Raspberry Pi Imager to write the operating system. By the end of this step, you will be logged into your Pi remotely over SSH.
In this step, get ready to:
- Install and configure Ghostty on your Mac for SSH compatibility.
- Flash Ubuntu Server 24.04 LTS onto a microSD card using Raspberry Pi Imager.
- Boot the Pi and SSH in from your Mac.
Install and configure Ghostty
Ghostty is a fast, GPU-accelerated terminal emulator by Mitchell Hashimoto. It has a built-in SSH integration feature that automatically installs its terminal information on remote servers when you connect.
Without this feature, the Pi would not recognise Ghostty's custom terminal type (xterm-ghostty) and commands like nano or htop could break with errors about a missing terminal.
- Press Cmd+Space to open Spotlight Search.
- Type Terminal and press Enter to open the built-in macOS Terminal app.
- Install Ghostty by running this command:
brew install --cask ghostty
What if I don't have Homebrew?
Homebrew is the standard package manager for macOS. If the brew command is not found, install Homebrew first by visiting brew.sh and following the one-line install instructions. Then come back and run the Ghostty install command.
Alternatively, download Ghostty directly from ghostty.org and drag it into your Applications folder.
Once Ghostty finishes installing, verify it is ready.
- Press Cmd+Space to open Spotlight Search.
- Type Ghostty and press Enter to launch it.
- In the menu bar, click Ghostty, then click About Ghostty.
✔️ I see version 1.3.0 or higher
Ghostty is ready to go. Close the About window and continue to the next part below.
ⓧ I see a version lower than 1.3.0
The SSH shell integration features this project depends on require Ghostty 1.3.0 or higher.
- Update Ghostty by running this command in your current terminal:
brew upgrade --cask ghostty
- After the upgrade finishes, quit Ghostty completely (Cmd+Q) and relaunch it.
- Click Ghostty > About Ghostty to confirm the version is now 1.3.0 or higher.
ⓧ Ghostty did not install
If the Homebrew install failed or you cannot find Ghostty in Spotlight, try the direct download approach.
- Visit ghostty.org in your browser.
- Download the macOS binary.
- Open the downloaded file and drag Ghostty into your Applications folder.
- Launch Ghostty from Applications or via Spotlight.
Now you need to configure Ghostty's SSH integration so it automatically handles remote terminal compatibility.
- In your Ghostty window, create the config file by running this command:
mkdir -p ~/Library/Application\ Support/com.mitchellh.ghostty && echo 'shell-integration-features = sudo,ssh-terminfo,ssh-env' > ~/Library/Application\ Support/com.mitchellh.ghostty/config.ghostty
What does this config do?
This single line enables three shell integration features:
- sudo preserves the TERM environment variable when you run sudo on the Pi, preventing terminal errors.
- ssh-terminfo automatically installs Ghostty's terminfo entry on the remote host the first time you SSH in.
- ssh-env forwards environment variables like COLORTERM so the remote shell knows it is running inside Ghostty.
- Quit Ghostty completely by pressing Cmd+Q.
- Relaunch Ghostty by pressing Cmd+Space, typing Ghostty, and pressing Enter.
- Verify the config was saved correctly by running:
cat ~/Library/Application\ Support/com.mitchellh.ghostty/config.ghostty
You should see shell-integration-features = sudo,ssh-terminfo,ssh-env printed back.
Config file not found?
Make sure you included the backslash before the space in Application\ Support. Without the backslash, the terminal interprets the space as a separator between two arguments.
help me troubleshoot my Ghostty config file not being created
Flash Ubuntu Server to the microSD card
Raspberry Pi Imager writes operating system images to microSD cards and lets you pre-configure settings like hostname, username, and SSH access before the Pi even boots.
- Install Raspberry Pi Imager by running this command in Ghostty:
brew install --cask raspberry-pi-imager
- Insert your microSD card into your Mac's card reader (or a USB adapter).
- Press Cmd+Space, type Raspberry Pi Imager, and press Enter to launch it.
Rosetta 2 prompt on Apple Silicon
If you are on an Apple Silicon Mac (M1, M2, M3, or M4), macOS may prompt you to install Rosetta 2 when launching Raspberry Pi Imager for the first time. Click Install to allow it.
- Under Raspberry Pi Device, click Choose Device and select your Pi model (Pi 3, Pi 4, or Pi 5).
- Under Operating System, click Choose OS.
- Select Other general-purpose OS, then Ubuntu, then Ubuntu Server 24.04 LTS (64-bit).
- Under Storage, click Choose Storage and select your microSD card.
- Click Next.
- When prompted to customise the OS, click Edit Settings.
This opens the advanced settings panel where you pre-configure the Pi before its first boot.
- In the General tab, set the hostname to piserver.
- Set the username to piuser and choose a password you will remember.
- If you are using Wi-Fi instead of Ethernet, check Configure wireless LAN and enter your Wi-Fi network name (SSID) and password.
- Switch to the Services tab.
- Check Enable SSH and select Use password authentication.
- Click Save.
- Click Yes to apply the customisation, then confirm you want to write to the SD card.
Why enable password authentication temporarily?
Password authentication is less secure than SSH keys, but you need it for the initial connection. In Step 2, you will generate SSH keys and then disable password authentication permanently.
Wait for the write and verification to complete. This typically takes 3-5 minutes depending on your SD card speed.
Write failed or verification error?
Try a different USB port or card reader. Some USB hubs do not provide enough power for reliable writes. If the issue persists, try reformatting the SD card (FAT32) and running the Imager again.
help me fix my Raspberry Pi Imager write failure
Boot the Pi and connect via SSH
With the SD card flashed, your Pi is ready for its first boot. On first boot, Ubuntu runs a process called cloud-init that applies your pre-configured settings (hostname, user, SSH, Wi-Fi).
- Eject the microSD card from your Mac.
- Insert the microSD card into the Raspberry Pi's card slot (on the underside of the board).
- Connect an Ethernet cable from your Pi to your router (skip this if you configured Wi-Fi).
- Connect the power supply to the Pi.
Wait 2-3 minutes for the Pi to complete its first boot and cloud-init setup.
- In Ghostty on your Mac, find the Pi on your network by running:
ping piserver.local
You should see replies with an IP address. Press Ctrl+C to stop the ping once you see replies.
Not seeing replies from piserver.local?
The Pi may still be booting. Wait another minute and try again. If it still does not respond, check that the Pi's power LED is lit and that your Ethernet cable is firmly connected (or that your Wi-Fi credentials are correct).
If piserver.local never resolves, you can find the Pi's IP address by checking your router's admin page for connected devices named piserver.
help me find my Raspberry Pi on the network
- SSH into the Pi by running:
ssh piuser@piserver.local
- Type yes when prompted to accept the host fingerprint.
- Enter the password you set in Raspberry Pi Imager.
On first connect, you may see a brief pause as Ghostty automatically installs its terminfo entry on the Pi. This is normal and only happens once.
What just happened behind the scenes?
Because you enabled ssh-terminfo in your Ghostty config, Ghostty wrapped the ssh command and automatically ran tic on the Pi to install the xterm-ghostty terminfo entry. This means full-screen programs like nano, htop, and less will work correctly over SSH without terminal errors.
- Update the system packages by running:
sudo apt update && sudo apt upgrade -y
This may take a few minutes. The Pi is downloading and applying the latest security patches and package updates.
- Verify Ghostty's terminfo is working correctly by running:
echo $TERM
You should see xterm-ghostty printed. This confirms Ghostty's automatic terminfo install succeeded.
Seeing xterm-256color instead of xterm-ghostty?
This means the automatic terminfo install did not run. Disconnect from the Pi with exit, then verify your Ghostty config file contains shell-integration-features = sudo,ssh-terminfo,ssh-env by running cat ~/Library/Application\ Support/com.mitchellh.ghostty/config.ghostty on your Mac.
If the config is correct, quit Ghostty (Cmd+Q), relaunch it, and SSH in again. The shell integration needs a fresh Ghostty session to activate.
help me fix Ghostty terminfo not installing on my Pi
✔️ Awesome, I've got everything!
Great work. Make sure you have saved the Ghostty config file and that your Pi session is still connected.
ⓧ I'd like to double check the full config
Your Ghostty config file should contain exactly this:
shell-integration-features = sudo,ssh-terminfo,ssh-env
Your Pi should be accessible at piserver.local over SSH as piuser, with the system fully updated and echo $TERM returning xterm-ghostty.
Your Pi is online, updated, and your terminal is fully compatible. Next up, you will lock down SSH access by replacing password authentication with cryptographic keys.
Harden SSH with Key Authentication
Your Pi is up and running, accessible from Ghostty over SSH. Now it's time to lock down that connection so only your Mac can get in.
Password authentication is the default, but it's vulnerable to brute-force attacks where attackers try thousands of passwords per minute. SSH keys replace passwords with a cryptographic key pair that's practically impossible to guess.
In this step, get ready to:
- Generate an ed25519 SSH key pair on your Mac.
- Copy the public key to the Pi and test key-based login.
- Disable password authentication so only key holders can connect.
Generate an SSH key pair and copy it to the Pi
SSH key pairs work like a lock and key. The private key stays on your Mac. The public key goes on the Pi. Only someone with the matching private key can unlock the connection.
- In Ghostty on your Mac (not the Pi's SSH session), generate a new ed25519 key pair by running this command:
ssh-keygen -t ed25519 -C "piserver-key"
What will this command ask you?
The command asks two questions. Press Enter for the default file path (~/.ssh/id_ed25519). For the passphrase, either type a passphrase for extra security or press Enter twice to skip it.
The -t ed25519 flag selects the Ed25519 algorithm, which produces shorter, faster, and more secure keys than older RSA. The -C "piserver-key" flag adds a comment label so you can identify this key later.
- Confirm the key files were created by running:
ls ~/.ssh/id_ed25519*
You should see two files: id_ed25519 (your private key) and id_ed25519.pub (your public key).
- Copy your public key to the Pi by running:
ssh-copy-id piuser@piserver.local
What does this command do?
This connects to the Pi using your current password and appends your public key to the Pi's ~/.ssh/authorized_keys file. It will ask for piuser's password one last time.
You should see a message saying the key was added and suggesting you try logging in.
Connection refused or host not found?
Make sure you're running this on your Mac, not inside the Pi's SSH session. If piserver.local isn't resolving, try using the Pi's IP address instead (find it with ping piserver.local from a fresh Ghostty tab).
If you see "Host key verification failed", the Pi's IP may have changed since first boot. Remove the old entry with ssh-keygen -R piserver.local and try again.
help me fix my ssh-copy-id connection issue to my Raspberry Pi
Disable password authentication
Now that your key is on the Pi, you can disable password logins entirely. First, verify that key-based login works before you lock yourself out.
- Disconnect from any existing SSH session to the Pi by typing exit.
- Reconnect using your key by running:
ssh piuser@piserver.local
You should connect directly without being asked for a password. This confirms your key is working.
Still being asked for a password?
Check that ssh-copy-id completed successfully in the previous substep. You can also verify manually by running cat ~/.ssh/authorized_keys on the Pi. Your key ending in piserver-key should appear in the output.
help me troubleshoot why SSH key authentication isn't working on my Raspberry Pi
- Now that key login is confirmed, create a hardening config file on the Pi by running:
sudo nano /etc/ssh/sshd_config.d/hardening.conf
- Type the following three lines into the file:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
What do these settings do?
- PasswordAuthentication no rejects any login attempt that uses a password.
- PermitRootLogin no blocks the root user from logging in via SSH, even with a key.
- PubkeyAuthentication yes explicitly enables key-based authentication (it's on by default, but setting it explicitly makes your intent clear).
- Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
Here's what your hardening.conf file should look like:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
Restart SSH and verify key-only access
The SSH service needs to be restarted to pick up the new config. This is a critical moment. If something is wrong with your key setup, restarting SSH with password auth disabled could lock you out.
Keep your current session open
Do not close your existing SSH session until you confirm you can connect in a new tab. Your current session stays alive even after restarting the SSH service.
- Apply the new SSH settings by running:
sudo systemctl restart ssh
- Open a second Ghostty tab by pressing Cmd+T (macOS).
- In the new tab, test that key-based login still works by running:
ssh piuser@piserver.local
You should connect without being asked for a password. If this works, your key setup is solid.
- Now verify that password authentication is truly rejected. Disconnect from the new tab and run:
ssh -o PubkeyAuthentication=no piuser@piserver.local
You should see Permission denied (publickey) immediately. This confirms the Pi rejects password-based login attempts.
Still able to log in with a password?
Make sure you saved hardening.conf correctly and that you restarted SSH with sudo systemctl restart ssh. Double-check the file contents with cat /etc/ssh/sshd_config.d/hardening.conf on the Pi.
Also check that no other config file is overriding your settings. Run sudo sshd -T | grep passwordauthentication to see the effective value.
help me fix SSH password authentication that won't disable on my Raspberry Pi
Your SSH access is now hardened with key-only authentication. Next, you'll set up a UFW firewall to control which traffic can reach your Pi at the network level.
Configure UFW Firewall
Your Pi now rejects any SSH login that doesn't present the correct key. That's a strong first layer of defense, but it only protects SSH itself.
A firewall controls ALL network traffic reaching your Pi, not just SSH. UFW (Uncomplicated Firewall) comes pre-installed on Ubuntu and lets you define simple rules for what gets through. In this step, you'll configure UFW to deny everything by default, then poke a single hole for SSH so you can still connect.
In this step, get ready to:
- Set UFW default rules to deny incoming and allow outgoing traffic.
- Allow SSH connections so you don't lock yourself out.
- Enable the firewall and confirm the rules are active.
Set default rules and allow SSH
Before enabling the firewall, you need to set the rules first. If you enable UFW without allowing SSH, you'll immediately lose your connection to the Pi.
- In your Ghostty session on the Pi, set the default policies to deny all incoming traffic and allow all outgoing traffic by running these commands:
sudo ufw default deny incoming && sudo ufw default allow outgoing
- Allow SSH connections through the firewall by running:
sudo ufw allow OpenSSH
What do these commands do?
- default deny incoming blocks all unsolicited traffic reaching your Pi.
- default allow outgoing lets your Pi make requests to the internet (for updates, Tailscale connections, etc.).
- allow OpenSSH creates an exception for port 22 so your SSH sessions keep working.
Seeing 'command not found'?
UFW comes pre-installed on Ubuntu 18.04 and later. If the command isn't found, make sure you're running this on the Pi (not your Mac). Check your terminal prompt shows piuser@piserver.
Help me troubleshoot UFW on my Pi
Enable the firewall
With the rules set, you can safely enable UFW. UFW will ask you to confirm because enabling a misconfigured firewall could cut off your access.
- Enable UFW by running:
sudo ufw enable
- Type y and press Enter when prompted to confirm.
What will I see?
UFW will display a warning that enabling the firewall may disrupt existing SSH connections. Since you already allowed OpenSSH, your session stays connected after you confirm.
- Verify the firewall is active and your rules are in place by running:
sudo ufw status verbose
You should see output showing the status as active, with default deny for incoming, default allow for outgoing, and an OpenSSH rule allowing connections from anywhere.
Lost your SSH connection?
If your session disconnected after enabling UFW, you'll need physical access to the Pi. Connect a keyboard and monitor, log in locally, and run sudo ufw allow OpenSSH followed by sudo ufw reload.
Help me recover SSH access after enabling UFW
Confirm your firewall rules
- View your rules with numbered entries by running:
sudo ufw status numbered
You should see two rules listed: rule 1 allowing OpenSSH from anywhere (IPv4), and rule 2 allowing OpenSSH from anywhere (IPv6).
Why is the OpenSSH rule temporary?
This rule allows SSH from any network interface, meaning anyone who can reach your Pi's IP on port 22 can attempt to connect. In Step 5, you'll replace this with a Tailscale-only rule that restricts SSH to the encrypted Tailscale tunnel.
For now, the OpenSSH rule keeps you connected while you set up the remaining security layers.
Your Pi now has a firewall blocking all unsolicited traffic except SSH. Next up, you'll add Fail2Ban to automatically ban anyone who tries to brute-force their way through that SSH door.
Install and Configure Fail2Ban
Your firewall is blocking uninvited traffic, but what about someone who keeps knocking on the door you left open for SSH?
Brute-force attacks try thousands of username and password combinations against your SSH port. Even though you disabled password authentication, these attempts still flood your logs and waste resources. Fail2Ban monitors those logs and automatically bans any IP address that fails too many times.
In this step, get ready to:
- Install Fail2Ban and create a hardened jail configuration.
- Restart the service and verify the SSH jail is active.
- Understand how to test bans and unban IPs.
Install Fail2Ban and create the jail config
Fail2Ban ships in Ubuntu's default repositories, so a single command installs it. The service starts automatically after installation.
- Install Fail2Ban on your Pi by running this command:
sudo apt install -y fail2ban
What does this command do?
This installs the Fail2Ban package from Ubuntu's repositories. The -y flag auto-confirms the installation so you don't have to type yes. Fail2Ban starts running immediately as a systemd service.
Now you need to tell Fail2Ban how aggressively to ban attackers. Fail2Ban reads its defaults from jail.conf, but you should never edit that file directly because package updates overwrite it.
Why use jail.local instead of jail.conf?
Fail2Ban loads jail.conf first, then applies anything in jail.local as an override. When you run sudo apt upgrade in the future, the package manager may replace jail.conf with a newer version. Your jail.local file is safe from those upgrades.
- Create the local configuration override by running:
sudo nano /etc/fail2ban/jail.local
- Paste the following configuration into the file:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
banaction = ufw
banaction_allports = ufw
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = ssh
maxretry = 3
findtime = 10m
bantime = 1h
What does this configuration do?
- bantime = 1h bans an offending IP for 1 hour.
- findtime = 10m is the window Fail2Ban watches. If failures happen within 10 minutes, they count together.
- maxretry = 3 means 3 failed attempts within the findtime window triggers a ban.
- banaction = ufw tells Fail2Ban to use UFW to enforce bans, keeping your firewall rules in one place.
- ignoreip = 127.0.0.1/8 ::1 prevents Fail2Ban from banning localhost (your Pi banning itself would be bad).
- The [sshd] section enables the SSH jail with the same thresholds, explicitly protecting your SSH service.
- Save the file by pressing Ctrl+O, then Enter, then exit nano with Ctrl+X.
Nano not saving?
Make sure you ran the command with sudo. Without it, you don't have write permission to /etc/fail2ban/. If nano shows "Permission denied", exit and re-run with sudo nano at the start.
help me fix a permission error when saving jail.local
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
banaction = ufw
banaction_allports = ufw
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = ssh
maxretry = 3
findtime = 10m
bantime = 1h
Restart and verify Fail2Ban
Fail2Ban only reads its configuration at startup. You need to restart the service for your new jail.local to take effect.
- Restart Fail2Ban and check the SSH jail status by running:
sudo systemctl restart fail2ban && sudo fail2ban-client status sshd
- You should see output showing the sshd jail is active with 0 currently banned IPs:
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
What does this output mean?
- Currently failed shows how many IPs have recent failed attempts that haven't yet hit the ban threshold.
- Currently banned shows how many IPs are actively blocked right now.
- Journal matches confirms Fail2Ban is reading SSH events from the systemd journal (the correct source on Ubuntu 24.04).
Seeing an error about the sshd jail?
If you see a message that the sshd jail doesn't exist, there's likely a typo in your jail.local file. Re-open it with sudo nano /etc/fail2ban/jail.local and check that [sshd] is spelled exactly with square brackets and lowercase letters.
help me troubleshoot my Fail2Ban sshd jail not loading
Understand the ban and unban workflow
Your Fail2Ban setup is now live and protecting SSH. Any IP that fails 3 SSH authentication attempts within 10 minutes gets banned for 1 hour automatically.
How would I test this?
To trigger a ban, you would need to attempt SSH connections from a different device or user that doesn't have a valid key. Each rejected connection counts as a failed attempt in Fail2Ban's log. After 3 failures within 10 minutes, that IP gets blocked.
You can check for bans at any time by running sudo fail2ban-client status sshd on the Pi. If you accidentally ban yourself, you can unban an IP with sudo fail2ban-client set sshd unbanip <IP> (replacing <IP> with the actual address).
Your Pi now has three layers of defense: key-only SSH, a UFW firewall, and Fail2Ban auto-banning attackers. Next up, you'll connect the Pi to a Tailscale mesh network so it's reachable from anywhere without exposing it to the public internet.
Set Up Tailscale for Remote Access
Your Pi now has three layers of defense: SSH key-only access, a UFW firewall, and Fail2Ban intrusion prevention. There is one problem remaining. The Pi is only reachable from your local network.
Tailscale creates a mesh VPN that connects your Pi, Mac, and iPhone into a private network called a tailnet. Once connected, you can reach your Pi from anywhere in the world without opening ports or configuring dynamic DNS.
In this step, get ready to:
- Install and authenticate Tailscale on the Pi.
- Connect your Mac and iPhone to the same Tailscale network.
- Lock down UFW to only allow traffic through the Tailscale interface.
Install and authenticate Tailscale on the Pi
Tailscale is built on WireGuard, the same encryption protocol used in enterprise VPNs. The difference is that Tailscale handles all the key exchange and NAT traversal automatically. No port forwarding, no dynamic DNS, no manual config files.
- In your SSH session on the Pi, install Tailscale by running this command:
curl -fsSL https://tailscale.com/install.sh | sh
What does this command do?
- curl -fsSL downloads the Tailscale install script silently, follows redirects, and fails on HTTP errors.
- The script detects your OS and architecture, then installs Tailscale from their official package repository.
- You should see output ending with a success message confirming Tailscale is installed.
Seeing a curl error?
- If you see "command not found", install curl first with sudo apt install -y curl then re-run the Tailscale install command.
- If the download stalls, check that your Pi has internet access. Try ping google.com to verify.
Help me troubleshoot the Tailscale install on my Raspberry Pi
- Authenticate your Pi with Tailscale by running:
sudo tailscale up
- Tailscale prints a URL in the terminal. Copy this URL and open it in a browser on your Mac.
- Log in with your Tailscale account (Google, Apple, or GitHub).
- After authenticating, return to your terminal. You should see a confirmation that the device is connected.
- Confirm the Pi appears in your Tailscale admin console at https://login.tailscale.com/admin/machines.
Authentication URL not appearing?
- Make sure you used sudo with the command. Tailscale needs root privileges to configure the network interface.
- If the Pi does not have internet access, check your Ethernet cable or Wi-Fi configuration.
Help me connect my Pi to Tailscale
Connect your Mac and iPhone to the tailnet
Your Pi is now part of your Tailscale network. To reach it from your Mac and iPhone, those devices need to join the same tailnet using the same account.
- On your Mac, download Tailscale from tailscale.com/download or search for "Tailscale" in the Mac App Store.
- Install and open Tailscale on your Mac.
- Sign in with the same account you used to authenticate the Pi.
- On your iPhone, download Tailscale from the App Store.
- Open Tailscale on your iPhone and sign in with the same account.
- Confirm all three devices (Pi, Mac, iPhone) appear in your Tailscale admin console.
Why the same account on all devices?
Tailscale groups devices into a private network (tailnet) based on your account. All devices signed in to the same account can see each other automatically. The Tailscale Personal plan supports this for free with up to 6 users.
- Back in Ghostty, find the Pi's Tailscale IP by running this command on the Pi:
tailscale ip -4
- You should see a 100.x.y.z address. This is your Pi's private Tailscale IP, reachable from any device on your tailnet.
- Test SSH over the Tailscale IP from your Mac by running this command in a new Ghostty tab:
ssh piuser@[[TAILSCALE_IP="your Pi's Tailscale IP (100.x.y.z)"]]
- You should connect successfully using your SSH key, just like connecting via piserver.local.
Connection refused or timed out?
- Make sure Tailscale is running on both your Mac and Pi. Check the Tailscale menu bar icon on your Mac. It should show "Connected".
- Verify you used the correct IP from tailscale ip -4, not the Pi's local IP.
- If the connection hangs, wait 30 seconds for Tailscale to establish its peer-to-peer tunnel. The first connection can be slower.
Help me SSH to my Pi over Tailscale
Lock down UFW to Tailscale traffic only
Now that SSH works over Tailscale, you can remove the public SSH rule from UFW. This makes your Pi invisible on the local network while remaining accessible through Tailscale from anywhere.
- On the Pi, allow all traffic on the Tailscale interface and remove the public SSH rule by running these commands:
sudo ufw allow in on tailscale0
sudo ufw delete allow OpenSSH
sudo ufw reload
What do these commands do?
- sudo ufw allow in on tailscale0 allows all incoming traffic on the Tailscale virtual network interface. Only devices in your tailnet can send traffic through this interface.
- sudo ufw delete allow OpenSSH removes the rule that allowed SSH from any network. Port 22 is now blocked on all interfaces except tailscale0.
- sudo ufw reload applies the changes immediately without disconnecting your Tailscale session.
- Confirm the new rules by running:
sudo ufw status verbose
- You should see Anywhere on tailscale0 ALLOW IN Anywhere in the output and no OpenSSH rule.
What should I see?
The output shows your default policies (deny incoming, allow outgoing) and one rule: Anywhere on tailscale0 ALLOW IN Anywhere. The absence of an OpenSSH rule means port 22 is blocked on the physical network interfaces.
- Open a new Ghostty tab and verify SSH still works over the Tailscale IP:
ssh piuser@[[TAILSCALE_IP="your Pi's Tailscale IP (100.x.y.z)"]]
- You should connect successfully. Your SSH key authentication works the same way over Tailscale.
- Now verify that SSH over the local IP is blocked by running:
ssh piuser@piserver.local
- This should time out or return "Connection refused". Your Pi is now invisible on the local network.
- Press Ctrl+C to cancel if the connection hangs.
- On your iPhone, open the Tailscale app and confirm the Pi appears in your device list as connected.
Locked yourself out?
If you cannot connect via the Tailscale IP either, you need physical access to the Pi. Connect a keyboard and monitor, log in directly, and restore local access:
sudo ufw allow OpenSSH
This re-enables SSH on all interfaces while you troubleshoot your Tailscale configuration.
Help me regain access to my Pi after a UFW lockout
Your Pi is now hardened and accessible from anywhere. It is invisible to the public internet, protected behind three security layers, and reachable only through your private Tailscale network. Next up, you will build iOS Shortcuts that execute commands on the Pi with a single tap from your iPhone.
Secret mission
Build "Ask My Pi" iOS Shortcut
Your Pi is hardened, firewalled, and reachable from anywhere via Tailscale. But what if you could control it from your pocket? In this secret mission, you will build an iOS Shortcut that prompts you for any command, runs it on your Pi over SSH through Tailscale, and displays the output on your phone. One tap from your Home Screen to full server control.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local hardware and uses Tailscale's free Personal plan. There are no ongoing cloud costs to worry about.
Decide whether to keep your Pi running, power it off temporarily, or tear everything down completely.
Resources you used:
- Raspberry Pi running Ubuntu Server 24.04 LTS (local hardware, ~5W power draw).
- Tailscale Personal account with three devices (Pi, Mac, iPhone). Free forever, no charges.
- SSH key pair on your Mac (~/.ssh/id_ed25519 and ~/.ssh/id_ed25519.pub).
- Ghostty SSH config modification (config.ghostty).
- "Ask My Pi" iOS Shortcut on your iPhone.
Keep everything running
No action needed. Choose this if you want the Pi to stay accessible from anywhere via Tailscale.
- Leave the Pi powered on and connected to your network. Power consumption is minimal (~5W).
- Tailscale keeps the Pi, Mac, and iPhone connected automatically. No maintenance required.
- Your "Ask My Pi" shortcut continues to work from anywhere.
- Fail2Ban and UFW continue protecting the Pi in the background.
Pause - I'll come back to this later
Power off the Pi to save electricity. All configuration persists on the SD card.
- SSH into the Pi and run a graceful shutdown:
- Tailscale will show the Pi as offline in your admin console and on your other devices.
- When you power the Pi back on, Tailscale reconnects automatically. SSH, UFW, and Fail2Ban resume without any reconfiguration.
- Your "Ask My Pi" shortcut will work again as soon as the Pi is back online.
sudo shutdown now
Delete - I don't want to use this again
Remove all project resources from your devices and start fresh.
Remove devices from your Tailscale network
- Open the Machines page of the Tailscale admin console in your browser.
- For each device (piserver, your Mac, your iPhone), select the menu next to the device and select Remove.
- In the confirmation dialog, select Remove machine.
Uninstall Tailscale from your devices
- On the Pi (before powering it off), uninstall the Tailscale package:
- On your Mac, delete Tailscale.app from your Applications folder, empty the Trash, and reboot.
- On your iPhone, long-press the Tailscale app icon and tap Remove App to delete it.
sudo apt remove tailscale
Delete SSH keys from your Mac
- Remove the SSH key pair you created for this project:
rm ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
Remove the Ghostty SSH config line
- Open your Ghostty config file and delete the shell-integration-features line:
nano ~/Library/Application\ Support/com.mitchellh.ghostty/config.ghostty
- Delete the line shell-integration-features = sudo,ssh-terminfo,ssh-env and save the file.
Delete the iOS Shortcut
- On your iPhone, open the Shortcuts app.
- Long-press the "Ask My Pi" shortcut and tap Delete.
Wipe the Raspberry Pi SD card
- Power off the Pi and remove the microSD card.
- Insert the microSD card into your Mac.
- Open Raspberry Pi Imager and use its Erase option to format the card. This removes Ubuntu Server and all configurations completely.
Nice Work!
Nice Work!
You just built a production-grade secure Raspberry Pi server with three layers of defense and remote access from anywhere in the world.
You've learned how to:
- Harden a Raspberry Pi running Ubuntu Server with SSH key-only authentication and Fail2Ban intrusion prevention that auto-bans attackers after 3 failed attempts.
- Connect the Pi, Mac, and iPhone via a Tailscale mesh network and lock down UFW to only allow traffic through the Tailscale virtual interface, making the Pi invisible to the public internet.
- Configure Ghostty's SSH shell integration to automatically install terminfo on remote servers for seamless terminal sessions.
- Secret Mission: Built an "Ask My Pi" iOS Shortcut that lets you type any command and get the result back on your iPhone from anywhere in the world.
Ready to quiz yourself?