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
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.
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
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
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
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.
# 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.
# 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:
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:
sudo grep -rE "PasswordAuthentication|PubkeyAuthentication|PermitRootLogin" \
/etc/ssh/sshd_config /etc/ssh/sshd_config.d/ 2>/dev/null
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.
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
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:
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:
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 tailscaleor 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:
# 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:
# 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).
--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
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.
sudo systemctl edit ollama
This opens an editor for the override file. Add the following - replacing the key with the one you generated:
[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:
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
# 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:
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/):
{
"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.
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
# 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
# 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:
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:
# 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:
# 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).
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:
{
"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:
"acls": [{"action":"accept","src":["*"],"dst":["*:*"]}]).
This restores full access immediately.
// 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.
{
// 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:
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
clientcan 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-serverrule 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:
# 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
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:
# 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:
# 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
# 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):
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.
# 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.
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:
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.
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:
- UFW - port 11434 not visible from the internet at all
- Tailscale - reachable only from your enrolled devices, encrypted WireGuard tunnel
- 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