Securing Your Local AI

Lock down your Ollama server so it is unreachable from the internet but accessible from anywhere you are. Tailscale, UFW, API keys, ACLs, and HTTPS - every layer explained.

90 min Advanced Free Updated June 2026

The Problem With "Just Use Local Network"

The standard advice for running Ollama is to keep it on your local network and never expose it to the internet. That's safe. It's also useless the moment you leave your house.

The other common approach - set OLLAMA_HOST=0.0.0.0 and open port 11434 in your firewall - works, but you've just put an unauthenticated AI inference server on the public internet. Ollama has no authentication by default. Anyone who finds port 11434 open can pull your models, run unlimited inference on your hardware, read anything in your context window, and if your system prompt contains credentials or private data, read those too.

Security researchers have found tens of thousands of open Ollama instances on the public internet via Shodan. Many of them are running on home hardware and the owners have no idea.

What this guide actually builds

A setup where:

  • Your Ollama API and Open WebUI are reachable from your laptop, your phone, anywhere - without a VPN app being "on" all the time feeling like a chore
  • Port 11434 does not appear on your public IP at all - a port scan returns nothing
  • Even on your private Tailscale network, requests require an API key
  • Your devices are the only things that can talk to your AI server at the network level - a correctly stolen API key is useless from an IP not on your Tailscale network
  • Open WebUI is served over HTTPS with a real certificate
  • If someone does get onto your Tailscale network somehow, ACL policy limits what they can reach
This is not a beginner guide. You need to be comfortable with Linux, systemd service files, UFW, and reading error messages. Every step here has a way to silently appear to work while not actually doing what you think. The verify section at the end is not optional - run every test or you don't know what you've built.

What you need before starting

  • A running Ollama + OpenClaw setup - either Mini PC or AWS EC2
  • Root or sudo access to the machine
  • A free Tailscale account (free tier covers up to 100 devices)
  • A second device to test from - phone on cellular is ideal

The architecture in one sentence

Tailscale creates a private overlay network between your devices. Your AI server stays on that network only. UFW blocks everything from the public internet. Ollama requires an API key even inside the private network. Two independent layers have to fail before anything is exposed.

Firewall First

Tailscale comes later. UFW comes now. The order matters: if you set up Tailscale first and assume it handles your firewall, you're trusting a single point of failure. UFW is the outermost wall. Tailscale is the gate through it. You want both.

Do not run ufw enable before adding the SSH rule. If you lock yourself out of an EC2 instance, you will need to use the AWS console to recover it. On a headless mini PC, you'll need a monitor and keyboard. Read the full step before running anything.

Check what's currently open

Server
sudo ufw status verbose

If UFW is inactive (common on fresh installs), that means no firewall rules are enforced at all. If it's active, read the existing rules before changing anything.

Set the defaults and allow SSH

Server
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH - do this BEFORE enabling UFW
# If SSH runs on a non-standard port, use: sudo ufw allow PORT/tcp
sudo ufw allow ssh

# If you're on EC2, also check your security group allows port 22
# UFW and EC2 security groups are independent - both must allow SSH

Enable UFW

Server
sudo ufw enable
# Type 'y' when prompted

# Verify SSH still works - open a new terminal and test before closing this one
sudo ufw status verbose

Open a second terminal window and confirm you can still SSH in before proceeding. If you've locked yourself out, sudo ufw disable from the existing session restores access.

Allow Tailscale traffic

Tailscale uses the 100.64.0.0/10 address range (IANA CGNAT space) for all devices on your private network. After you install Tailscale, you'll allow the entire range through UFW so your devices can reach each other freely. Add the rule now - it won't do anything until Tailscale is installed.

Server
# Allow all traffic from Tailscale IP range
sudo ufw allow from 100.64.0.0/10 to any

# Allow Tailscale's UDP port (used for encrypted tunnel traffic)
sudo ufw allow 41641/udp

Explicitly close the ports that matter most

With default deny incoming these are already blocked, but being explicit means the rules show up in ufw status so you can audit them later and know exactly what was considered.

Server
# Ollama API
sudo ufw deny 11434

# Open WebUI (deny both common ports)
sudo ufw deny 3000
sudo ufw deny 8080

# Verify final state
sudo ufw status numbered

Your output should show SSH allowed, 100.64.0.0/10 allowed, and 11434/8080/3000 denied. Everything else is covered by the default deny.

SSH hardening (while you're here)

Password-based SSH auth has no place on a server that's going to be on the internet. Key-only authentication should already be set up if you followed the EC2 or Mini PC guides, but verify:

Server
sudo grep -E "^PasswordAuthentication|^PubkeyAuthentication|^PermitRootLogin" /etc/ssh/sshd_config

You want to see PasswordAuthentication no on all three. If the grep returns nothing at all, the settings may be in an included file - also check:

Server
sudo grep -rE "PasswordAuthentication|PubkeyAuthentication|PermitRootLogin" \
  /etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
Empty output does not mean you're secure. On Ubuntu 22.04 and later, PasswordAuthentication may be commented out in the config file. If the setting is absent or commented, SSH uses its compiled-in default which is yes on most systems - meaning password login is active. If you see no output from either grep, assume password auth is enabled and add the line explicitly.

If PasswordAuthentication is yes or missing, edit /etc/ssh/sshd_config and add or change the line to PasswordAuthentication no, then sudo systemctl restart sshd. Confirm your key auth still works from a second terminal before closing the current session.

Also install fail2ban if it's not already running. It automatically bans IPs that fail SSH login repeatedly: sudo apt install fail2ban && sudo systemctl enable --now fail2ban

Tailscale: Your Private Network

Tailscale builds a WireGuard-based mesh network between your devices. Every device gets a stable IP in the 100.64.0.0/10 range that stays the same regardless of which network it's on. Your phone on cellular, your laptop at a coffee shop, your server at home - they're all on the same private network and can talk to each other directly, but nothing else can reach them.

The personal plan is free. Check tailscale.com/pricing for current limits - they've changed over time and personal use has always been well within the free tier.

Install on the server

Server
curl -fsSL https://tailscale.com/install.sh | sh

Then start Tailscale and authenticate. The command outputs a URL - open it in a browser to log in and authorize the machine. The --ssh flag enables Tailscale SSH, which lets you SSH to this machine via Tailscale without needing port 22 open at all. Recommended:

Server
sudo tailscale up --ssh

If you prefer not to use Tailscale SSH and want to keep using regular SSH on port 22, omit the --ssh flag: sudo tailscale up

After authenticating, get your server's Tailscale IP:

Server
tailscale ip -4
# Returns something like: 100.72.14.203

tailscale status
# Shows all devices on your Tailscale network and their status

Install on your other devices

Install the Tailscale app on every device you want to access your AI from:

  • Mac/Linux: brew install tailscale or the installer from tailscale.com
  • Windows: installer from tailscale.com
  • iOS/Android: App Store / Play Store - search Tailscale

Sign into the same Tailscale account on each device. They'll appear in your admin console at tailscale.com/admin.

Enable MagicDNS

In the Tailscale admin console, go to DNS and enable MagicDNS. This gives every machine a stable hostname instead of a raw IP address:

With MagicDNS enabled
# Instead of:
curl http://100.72.14.203:11434/api/tags

# You can use:
curl http://your-server-name:11434/api/tags

# Or with the full FQDN:
curl http://your-server-name.tailnet-name.ts.net:11434/api/tags

The hostname is whatever you named the machine during setup. Check it with tailscale status on the server.

Test Tailscale connectivity to Ollama

From your laptop (on your home WiFi is fine for this first test), try reaching Ollama via the Tailscale IP:

Your laptop
# Replace with your server's Tailscale IP from tailscale ip -4
curl http://100.72.14.203:11434/api/tags

If you get a connection refused error, Ollama is still bound to localhost only - that's fine, we'll fix it in the next step. If you get a timeout, check that both machines show as connected in tailscale status and that UFW allows the 100.64.0.0/10 range (Step 1).

If you used --ssh: test it from a second terminal before removing port 22 from UFW. Run ssh user@your-server-name from a new terminal window. Confirm it connects successfully before running sudo ufw delete allow ssh in the original session. If you close port 22 and Tailscale SSH isn't working, you are locked out.

Ollama API Key + Network Binding

Tailscale limits who can reach your machine at the network level. But if a device on your Tailscale network is ever compromised - or if you share Tailscale access with someone - you want a second layer. Ollama's API key requirement means every request must present a valid secret, regardless of where it comes from on the network.

You also need to tell Ollama to accept connections on the Tailscale interface, not just localhost. Right now it's almost certainly only listening on 127.0.0.1, which is why the Tailscale test in the previous step returned a connection refused.

Generate a strong API key

Server
openssl rand -hex 32

Copy the output - this is your Ollama API key. Store it somewhere safe (a password manager). You'll need it in OpenClaw's config and anywhere else you call the API.

Configure Ollama via systemd override

Never edit /etc/systemd/system/ollama.service directly - package updates will overwrite it. Use a systemd drop-in override file instead. It survives updates and is the correct way to add environment variables to a managed service.

Server
sudo systemctl edit ollama

This opens an editor for the override file. Add the following - replacing the key with the one you generated:

/etc/systemd/system/ollama.service.d/override.conf
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_API_KEY=YOUR-32-CHAR-HEX-KEY-HERE"

Save and exit. Then reload and restart:

Server
sudo systemctl daemon-reload
sudo systemctl restart ollama

# Confirm the service came back up cleanly
sudo systemctl status ollama

Verify the API key is enforced

Server (via localhost)
# This should now return 401 Unauthorized
curl http://localhost:11434/api/tags

# This should return your model list
curl http://localhost:11434/api/tags \
  -H "Authorization: Bearer YOUR-KEY-HERE"

If the first request still returns data without a key, the environment variable didn't load. Check with:

Server
sudo systemctl show ollama --property=Environment

You should see both OLLAMA_HOST and OLLAMA_API_KEY in the output. If not, the override file has a syntax error - check it with sudo systemctl cat ollama.

Update OpenClaw to pass the API key

In your OpenClaw config, update the Ollama endpoint and add the authorization header. In openclaw.json (in ~/.openclaw/):

~/.openclaw/openclaw.json
{
  "model": {
    "provider": "ollama",
    "base_url": "http://your-server-name:11434",
    "api_key": "YOUR-KEY-HERE",
    "primary": "ollama/qwen3:8b"
  }
}

Use the Tailscale hostname (your-server-name) or Tailscale IP (100.x.x.x) here, not localhost - if you're running Ollama on a dedicated mini PC and OpenClaw on your laptop, this is how they find each other regardless of which network you're on.

OLLAMA_HOST=0.0.0.0 is safe only because UFW is blocking port 11434 from the internet. The two configurations depend on each other. If you ever disable UFW or flush iptables rules without thinking, Ollama becomes publicly accessible. Confirm UFW is enabled after any system updates or reboots: sudo ufw status should show Status: active.

HTTPS for Open WebUI

Open WebUI running over plain HTTP on your Tailscale network is acceptable but not ideal. Tailscale encrypts all traffic in the tunnel, so HTTP between two Tailscale devices is actually encrypted at the network layer. That said, getting a real HTTPS certificate and serving over 443 means a proper padlock in the browser and compatibility with any tool that requires HTTPS.

Tailscale provides automatic HTTPS certificates for every machine on your network via tailscale serve. You don't need to generate certificates, configure Let's Encrypt, or manage renewals. Tailscale handles all of it.

Verify Open WebUI is running

Server
# Check what port Open WebUI is listening on
sudo ss -tlnp | grep -E '3000|8080'

# If using Docker
docker ps | grep open-webui

Note the port. Common defaults are 3000 (Docker older installs) or 8080 (Linux native install). The rest of this section assumes 8080 - adjust if yours differs.

Enable Tailscale Serve

Server
# Expose Open WebUI over HTTPS on your Tailscale network
# Replace 8080 with your actual Open WebUI port
sudo tailscale serve https / http://localhost:8080

# Check what's being served and get your HTTPS URL
tailscale serve status

The serve configuration persists automatically across reboots - no daemon or background flag needed. After running this, Open WebUI is accessible at:

Access from any Tailscale device
https://your-server-name.tailnet-name.ts.net

Get the exact URL by running tailscale serve status on the server - it shows the full HTTPS endpoint in the output.

How the certificate works

Tailscale issues certificates from Let's Encrypt for the *.ts.net subdomain using DNS-01 challenge validation. The certificate is automatically renewed before expiry. You can inspect it:

Server
# View certificate details (optional)
sudo tailscale cert your-server-name.tailnet-name.ts.net

Restrict Open WebUI to Tailscale only

Tailscale Serve exposes the URL only on the Tailscale network, but Open WebUI itself is still listening on 0.0.0.0:8080. UFW already blocks 8080 from the internet (Step 1), but add an explicit rule to bind Open WebUI to localhost only so it's not reachable even from other interfaces.

If running via Docker, add the bind address to the port mapping:

Server - Docker
# Stop the running container
docker stop open-webui

# Remove it (required before creating a new container with the same name)
docker rm open-webui

# Recreate with localhost-only port binding
# Tailscale Serve connects to localhost:8080 from within the same machine - this works
docker run -d \
  --name open-webui \
  --restart always \
  -p 127.0.0.1:8080:8080 \
  -v open-webui:/app/backend/data \
  ghcr.io/open-webui/open-webui:main

The 127.0.0.1:8080:8080 binding tells Docker to only expose the port on the loopback interface. Tailscale Serve connects to http://localhost:8080 from within the same machine, so this works correctly - but no other machine can reach port 8080 directly, even within Tailscale.

Add Open WebUI authentication

Even with all of this in place, Open WebUI should have its own user accounts enabled. By default the first account you create becomes the admin. Go to Open WebUI settings and verify user authentication is required and registration is disabled (unless you want others to be able to sign up).

For power users - Caddy as an alternative: If you want more control over headers, rate limiting, or want to put multiple services behind one reverse proxy, install Caddy and use tailscale cert to get the certificate manually. Caddy's tls /path/to/cert /path/to/key directive handles the rest. The Tailscale Serve approach is simpler and sufficient for personal use.

Tailscale ACLs

By default, every device on your Tailscale network can talk to every other device on any port. That's fine when it's just you with two devices. It becomes a liability when you add more devices, share access with anyone, or simply want defense in depth - if one device is compromised, it shouldn't automatically have full access to everything else.

Tailscale ACLs (Access Control Lists) use a JSON policy file to define exactly which devices can talk to which other devices on which ports. You manage it in the Tailscale admin console under Access Controls at tailscale.com/admin.

Understanding the default policy

The default policy looks like this:

Default Tailscale ACL
{
  "acls": [
    {
      "action": "accept",
      "src":    ["*"],
      "dst":    ["*:*"]
    }
  ]
}

That single rule means every device can reach every other device on every port. Replace this with a policy that grants only what's needed.

The ACL policy

You need to paste the full policy FIRST before tagging any devices - the tags must exist in tagOwners before Tailscale will accept them. Go to tailscale.com/admin, click Access Controls, and replace the default policy with this:

Read this before saving. The moment you save a new ACL policy, it takes effect immediately across your entire Tailscale network. An incorrect policy can cut you off from your own server with no undo button in the UI. Keep your existing SSH session open, open a second terminal and confirm connectivity after saving before closing the first. If you do get locked out: go to tailscale.com/admin, click Access Controls, and revert to the default policy ("acls": [{"action":"accept","src":["*"],"dst":["*:*"]}]). This restores full access immediately.
Tailscale uses HuJSON, not standard JSON. The policy file allows // comments and trailing commas. Do not run this through a standard JSON formatter or validator - it will strip the comments and may reject valid syntax. Paste it directly into the Tailscale admin console as-is.
tailscale.com/admin - Access Controls
{
  // Who is allowed to assign each tag
  "tagOwners": {
    "tag:ai-server": ["autogroup:admin"],
    "tag:client":    ["autogroup:admin"]
  },

  "acls": [
    // Clients can reach the AI server on specific ports only
    {
      "action": "accept",
      "src": ["tag:client"],
      "dst": [
        "tag:ai-server:11434",   // Ollama API
        "tag:ai-server:443",     // Open WebUI HTTPS (via Tailscale Serve)
        "tag:ai-server:22"       // SSH - remove this line if using Tailscale SSH
      ]
    },
    // Admin (your Tailscale account) has full access for management
    {
      "action": "accept",
      "src": ["autogroup:admin"],
      "dst": ["*:*"]
    }
  ],

  // Tailscale SSH block - only needed if you used tailscale up --ssh
  "ssh": [
    {
      "action": "accept",
      "src":    ["autogroup:admin"],
      "dst":    ["tag:ai-server"],
      "users":  ["autogroup:nonroot"]
    }
  ]
}

Tag your devices

After saving the policy, tag your machines. The tags must already be in tagOwners (which you just saved) for this to work. On the server:

Server
sudo tailscale set --advertise-tags=tag:ai-server

For client devices (laptop, phone), assign the tag:client tag from the admin console under Machines - click the machine name, then Edit Tags.

What this policy does

  • Devices tagged client can only reach the AI server on ports 11434, 443, and 22 - nothing else
  • The AI server cannot initiate connections to client devices at all (no src: tag:ai-server rule exists)
  • Your admin account (autogroup:admin) retains full access so you can always manage the network
  • Any untagged device that somehow joins your network has no access to anything

Verify ACLs are working

Use tailscale ping and direct port tests from a client device to confirm the policy:

Your laptop (on Tailscale)
# Should work - port 11434 is allowed
curl http://your-server-name:11434/api/tags \
  -H "Authorization: Bearer YOUR-KEY"

# Try a port that should be blocked (e.g., 8080 - Open WebUI raw)
# Should timeout or refuse
curl --connect-timeout 5 http://your-server-name:8080

The Tailscale admin console also has an ACL testing tool under Access Controls - "Test connectivity" lets you simulate whether a source device can reach a destination without needing to actually test from the device. Use it to verify your policy before deploying.

Verify It Actually Works

Most people finish a security setup and test it from the same network the server is on, which proves nothing. You need to verify that the server is unreachable from the internet and reachable from Tailscale. These are different tests and both matter.

The failure mode to avoid: everything "works" in testing because you're on your home network where the Tailscale IP and the local network IP both resolve, and UFW on port 11434 appears to be blocked - but you forgot to add the OLLAMA_HOST env var, so it's only listening on localhost anyway, and you never actually verified it works over Tailscale from outside. You push this to production and the first time you try to use it from a hotel, nothing works and you don't know why.

Find your server's public IP

Server
curl -s ifconfig.me

Note this IP. Every test against it should fail.

Test 1: Port 11434 must not be reachable from the internet

Use a second device that is not on Tailscale, or temporarily disable Tailscale on your phone and connect via cellular. From that device:

Device without Tailscale (phone on cellular)
# This must fail - timeout, connection refused, or no route
curl --connect-timeout 10 http://YOUR-PUBLIC-IP:11434/api/tags

If this returns data, your firewall is not configured correctly. Go back to the Firewall section and verify UFW is active and the deny rules are in place. Do not proceed until this test fails.

Test 2: Ollama requires the API key even over Tailscale

Now enable Tailscale on your phone (or use your laptop from a different network if possible). From a device on Tailscale:

Device on Tailscale
# This must fail with 401 Unauthorized
curl http://your-server-name:11434/api/tags

# This must succeed
curl http://your-server-name:11434/api/tags \
  -H "Authorization: Bearer YOUR-KEY-HERE"

If the first request succeeds without a key, the OLLAMA_API_KEY env var is not loaded. Check sudo systemctl show ollama --property=Environment on the server.

Test 3: Open WebUI is only accessible via HTTPS over Tailscale

Device on Tailscale
# This should work - Tailscale Serve HTTPS
curl -I https://your-server-name.tailnet-name.ts.net

# This should fail - port 8080 is only on localhost
curl --connect-timeout 5 http://your-server-name:8080

# This should fail - public internet
curl --connect-timeout 5 http://YOUR-PUBLIC-IP:8080

Test 4: Port scan your public IP

This is the definitive external view of what's exposed. From your laptop (not the server):

Mini PC users: do not run this from your home network. If your server is a mini PC behind your home router, scanning your public IP from inside the same network scans your router's NAT - not the mini PC. The results will not reflect what the internet actually sees. Use your phone as a hotspot, or an online port scanner like portscanner.io, to scan from an external network. EC2 users can run this scan from their laptop on any network - the EC2 public IP routes through the internet regardless.
Your laptop (from outside your home network)
# Install nmap if needed: brew install nmap (Mac) or sudo apt install nmap
nmap -p 22,443,3000,8080,11434 YOUR-PUBLIC-IP

Expected results:

  • Port 22 (SSH): open (or filtered if you're using Tailscale SSH only)
  • Port 443: filtered or closed - Tailscale Serve HTTPS is on the Tailscale interface only
  • Port 3000: filtered or closed
  • Port 8080: filtered or closed
  • Port 11434: filtered or closed

Filtered means the packet was dropped (UFW doing its job). Closed means nothing is listening (also correct). Open on 11434, 8080, or 3000 means something is wrong - debug before continuing.

You can also check Shodan: https://www.shodan.io/host/YOUR-PUBLIC-IP. If your server has been up for a while with port 11434 open, it may already be indexed there. After applying these steps and waiting 24-48 hours, Shodan should show no open services except SSH.

Test 5: UFW survives a reboot

Tailscale and UFW both auto-start on boot, but confirm this explicitly rather than assuming it.

Server
sudo reboot

Wait about 60 seconds, then SSH back in (or use Tailscale SSH). After the server comes back up, confirm UFW is still active and Ollama is running with the correct environment:

Server (after reboot)
sudo ufw status
sudo systemctl status ollama
sudo systemctl show ollama --property=Environment | grep OLLAMA

UFW should show Status: active and OLLAMA_API_KEY should be present in the environment output. If either is missing after reboot, the service wasn't configured for persistence.

Add a monthly reminder to rotate your API key. Set a calendar reminder, generate a new key with openssl rand -hex 32, update the systemd override file and your OpenClaw config, and restart Ollama. Rotating keys means a leaked key has a limited window of usefulness. If you suspect a key has been compromised, rotate immediately.

You're done. Here's what you built.

Your AI server now has three independent security layers that all have to be bypassed for anyone to reach Ollama without authorization:

  1. UFW - port 11434 not visible from the internet at all
  2. Tailscale - reachable only from your enrolled devices, encrypted WireGuard tunnel
  3. Ollama API key - even authenticated network access requires a valid bearer token

From anywhere in the world with a device on your Tailscale network, you have full access to your AI. From the rest of the internet, the server is effectively invisible on the ports that matter. That's what "properly secured" actually looks like.

Where to go from here

Your server is locked down. Now make it more capable:

  • Ollama + MCP - give your local AI access to files, live web content, and databases via tool calling
  • Ollama Advanced - tune model parameters, manage context, and wire Ollama into OpenClaw for bot responses
  • Open WebUI - the ChatGPT-style interface you just secured deserves a full setup guide
  • Mini PC Setup - if you're still on EC2 and want to cut the monthly bill, this is the hardware guide