Self-Hosting Pangolin: Zero Trust Access to Your Home Network

There's a ton of information out there on setting up Pangolin for zero trust access to your network resources. Say you want to reach Plex or Jellyfin remotely. Do you have a DHCP WAN IP address that changes every time you lose power? This may be the solution for you.


What is Pangolin?

Pangolin is a self-hosted, open source ZTNA (Zero Trust Network Access) solution. It essentially replaces a traditional VPN, but uses WireGuard "under the hood." Think Cloudflare Tunnels, but you're in charge.

One of my biggest hesitations with hosting services is opening ports on my firewall. It introduces a ton of risk: the application itself could be vulnerable to attack, automated scanners are constantly knocking, and you're more exposed to things like DDoS (distributed denial-of-service) attacks. I'm also not a fan of publicizing my home IP for privacy reasons. And with the increased use of AI, I'm a big fan of keeping things as closed off as possible.

Pangolin also comes with a ton of enterprise-level features you can use if you so desire. I'll do another blog post about the features I use later.


Where to host?

Hey! I have a blog post about that: [LINK]. Take a look and let me know your thoughts. The big takeaway is that if you don't have a static IP, you'll need DDNS (or you'll be manually changing your DNS records at your DNS provider every time your IP changes).


What does the infrastructure look like?

It's really not too bad!

Piece What it does, in one line
Pangolin The dashboard and brains: users, sites, apps, logins
Gerbil Runs the WireGuard side on the Pangolin server so tunnels can connect
Traefik The front door: takes web requests and handles HTTPS certificates
Newt Runs at home (or wherever your apps live) and opens the tunnel

The big pieces to know are Pangolin (because you log in and manage everything there) and Newt (the site connector, where the tunnel comes out on your end). Your resources typically sit behind your Newt instance. The other pieces are good to know, but they largely work in the background.


What do you need?

  • A server to host Pangolin
    • Option A: a VPS with a public IP. You can go small on these. I run RackNerd at $3/mo and it runs great.
    • Option B: an always-on Linux box at home (a virtual machine, a Proxmox CT, or a mini PC), plus access to your router's port forwarding settings.
  • A domain name that you control. I host mine at Cloudflare personally. They don't mark up domain registrations or renewals. We'll use example.com in this post.
  • A Linux box at home. For Option A, this runs Docker, which runs Newt. For Option B, it's optional. Since I went with Option A, I have a VM in Proxmox that acts as my Newt site connector.
  • An hour of your time (varies).

Step 1: Prepare the server

Option B: same steps, just on your home box (Proxmox server, mini PC, whatever). Give it a fixed IP address (a DHCP reservation in your home router) so the port forward in Step 2 doesn't break!

Install the OS. I typically run Ubuntu Server LTS, but I know there are die-hard Debian fans out there too.

Update it:

sudo apt update && sudo apt upgrade -y

Step 2: Open firewall ports

Pangolin needs a few ports open on the server it runs on:

Port Protocol What it's for
80 TCP HTTP, and getting certificates from Let's Encrypt
443 TCP HTTPS, the actual website traffic
51820 UDP WireGuard tunnels from Newt
21820 UDP Client connections (the Pangolin docs list it as "UDP for clients")

Two places can block these, so check both:

  1. Your VPS provider's firewall, typically in their web dashboard under "Security groups" or "Firewall."
  2. The firewall on the server itself, if you turned on something like ufw:
sudo ufw allow 22/tcp      # SSH, so you don't lock yourself out
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 51820/udp
sudo ufw allow 21820/udp
sudo ufw enable
Option B: forward the ports on your router. On a home box there's a third place to open ports: your router. Forward TCP 80 and 443 to the Pangolin box's local IP. You only need to forward UDP 51820 (and 21820) if you'll connect Newt sites from outside your home, like a second house or a friend's server.

Heads up: some ISPs block incoming 80/443 on home plans. If certificates never issue and you've checked everything else, that's a likely culprit, and Option A (a VPS) is the way out.

Step 3: Point your domain at the server

In your DNS provider of choice, create two A records that point at either your VPS's IP or your home IP:

Name Type Value
pangolin.example.com A 203.0.113.10
*.example.com A 203.0.113.10

The first record is your dashboard, and you can name it whatever you want. I use ztna.example.com, for instance. The second is a wildcard: it means any subdomain you use later for a new service, like plex.example.com, already points at your server without touching DNS again.

Option B: use your home public IP for now, then set up DDNS in Step 3b so things update when your public IP changes.

Special note: if you use Cloudflare, make sure both records are set to DNS only (the orange cloud turns grey). Newt's tunnel connects to your dashboard name over UDP, and Cloudflare's proxy only passes web traffic.

After a few minutes, check that it works. It should return the public IP of your VPS, or your home IP if you're doing Option B:

nslookup pangolin.example.com

Step 3b (Option B only): Keep DNS pointed at home with DDNS

Home internet service typically comes with a DHCP WAN IP address, which can change at any point. It's annoying and I hate it. We'll fix that with Dynamic DNS, or DDNS. Pick one of these two routes:

Route 1: Cloudflare updater (recommended) Route 2: DuckDNS + CNAME
Needs Your domain's DNS on Cloudflare (free plan is fine) Any DNS provider that allows CNAME records
How it works The updater edits your example.com records directly DuckDNS tracks your IP; your domain points at the DuckDNS name
Moving parts One container DuckDNS updater, plus two CNAME records set once

Route 1: Your domain is on Cloudflare

Make an API token. This lets the updater change your DNS records for you. It can't change anything else.

  1. In Cloudflare, go to My Profile → API Tokens → Create Token.
  2. Use the Edit zone DNS template.
  3. Under Zone Resources, pick Include → Specific zone → example.com.
  4. Create it and copy the token. Treat it like a password! You only see it once.

Run the updater. Put it on the same box that hosts Pangolin, for ease of use. Create a folder and a compose file:

sudo mkdir -p /opt/docker-apps/cloudflare-ddns
cd /opt/docker-apps/cloudflare-ddns
sudo nano compose.yaml

Paste this in, with your own token and domain:

services:
  cloudflare-ddns:
    image: favonia/cloudflare-ddns:1
    container_name: cloudflare-ddns
    network_mode: host
    restart: always
    environment:
      - CLOUDFLARE_API_TOKEN=your-token-here
      - DOMAINS=pangolin.example.com,*.example.com
      - PROXIED=false
      - IP6_PROVIDER=none

Start it and check the logs:

sudo docker compose up -d
sudo docker logs cloudflare-ddns

You want to see it find your IP and set (or confirm) both records, with no errors. It re-checks every 5 minutes by default.

Route 2: DuckDNS (any DNS provider)

Just a heads up: AI helped me write this route. I don't have personal experience with it, so your mileage may vary.

Your domain's DNS provider doesn't need to support DDNS at all for this. You let DuckDNS do the IP tracking, then point your domain at DuckDNS once.

  1. Get a DuckDNS name and run its updater. Follow Steps 2 and 3 of my WireGuard and DuckDNS post (signup, then the cron script or the Docker container). You end up with something like myhome.duckdns.org that always points home.
  2. Point your domain at it with CNAME records. In your domain's DNS settings, replace the two A records from Step 3 with:
Name Type Value
pangolin.example.com CNAME myhome.duckdns.org
*.example.com CNAME myhome.duckdns.org

A CNAME is a nickname: "for this name, go look up that other name." Whenever DuckDNS updates, your domain follows along automatically, and you never touch these records again. Cloudflare supports wildcard CNAMEs; if you use a different DNS provider, check that yours does too.

One limit: a CNAME can't go on the bare example.com itself, only on names in front of it. That's fine here, because Pangolin only uses subdomains.

Router has DDNS built in? Many do (ASUS, UniFi, pfSense, OPNsense, OpenWrt, Fritz!Box). If yours supports DuckDNS or Cloudflare, you can let the router do the updating instead of running a container. The CNAME records in Route 2 work the same either way.

Check that it works

From any computer:

nslookup pangolin.example.com
nslookup anything.example.com

Both should return the same address that ifconfig.me shows from home. The second one tests the wildcard: any made-up name should work.

The real test: reboot your router (or wait for your IP to change), give it 5 to 10 minutes, and run the checks again. If they show the new IP, you're set.

Good to know

  • Expect a few minutes of downtime when your IP changes. The updater has to notice, then DNS caches around the internet have to catch up. Keeping the record TTL low keeps this short. Cloudflare's "Auto" TTL is 300 seconds (5 minutes) for DNS-only records. For a homelab, that's usually fine.
  • Pangolin itself doesn't care about your IP. Its config uses names like pangolin.example.com, not addresses, so nothing in Pangolin needs changing when your IP moves.
  • Sites outside your home connect using that same name. If one stays offline after your IP changes, restart its connector container.
  • Can't open it from inside your home? Some routers can't send you "out and back in" to your own public IP (this is called NAT loopback or hairpin NAT). Turn on NAT loopback in your router if it has the option, or add local DNS entries in Pi-hole / AdGuard Home pointing *.example.com at the Pangolin box's local IP.

Step 4: Run the installer

Pangolin has a great built-in installer that asks questions as you go and writes all the config files for you. The first line downloads the installer, and the second one launches it:

curl -fsSL https://static.pangolin.net/get-installer.sh | bash
sudo ./installer
Question What to put
Install the Enterprise version? Your call. The installer says it's free for personal use. I went Enterprise.
Use PostgreSQL? No (the installer itself says "not recommended for most users")
Base domain example.com
Dashboard domain pangolin.example.com
Email for Let's Encrypt An email you actually read
Use Gerbil for tunneled connections? Yes for a VPS. For Option B, still yes if you'll use Newt
Enable email (SMTP)? Optional. Skip for now if you don't have SMTP details
Is your server IPv6 capable? Yes if it is, no if you're not sure
Download the MaxMind GeoLite2 databases? Yes if you want to block by country later

If you get an option to enable CrowdSec, I enabled it too. It comes with a basic configuration that includes the CrowdSec Bouncer Traefik plugin. It's a very powerful tool that I'll probably write a blog post about soon.

Once that's done, the installer starts the containers. Run sudo docker ps and you should see pangolin, gerbil, and traefik in the list.


Step 5: Log in and create your admin account

The first admin account requires a setup token. The installer prints it at the end of the installation, along with the setup link. You can also find it in the logs (run this from your install folder):

sudo docker compose logs pangolin

Then open https://pangolin.example.com/auth/initial-setup in a browser, paste the token, and create your admin account.

If you get a certificate error, wait a few minutes and refresh the page. Traefik is the service that gets certificates (over port 80), and that can take a little while at first.


Step 6: Add your first site (install the connector at home)

Option B: apps running on the Pangolin box itself don't need a site connector. Pick Local Site instead, which doesn't create a tunnel.

A site is a place your apps live, like your home network. It creates the tunnel between the VPS and your home network.

  1. In the dashboard, go to Sites → Add Site.
  2. Give it a name (e.g. home) and choose Pangolin Site.
  3. Pangolin shows you an ID and a secret together, with a ready-made command. You only see the secret once, so copy it now. I usually pick Docker, then the Docker Compose method on this screen.
services:
  pangolin-site:
    image: fosrl/pangolin-cli
    container_name: pangolin-site
    restart: unless-stopped
    environment:
      - PANGOLIN_ENDPOINT=https://pangolin.example.com
      - SITE_ID=put-id-here
      - SITE_SECRET=your-secret-here
docker compose up -d
docker logs pangolin-site

Back in the dashboard, you should see the site come Online and turn green. Try refreshing a couple of times.


Step 7: Add your first app (a "resource")

A resource is an application you want to access remotely through your newly created tunnel.

  1. Go to Resources → Add Resource.
  2. Pick the site (home), choose HTTP, and set a subdomain (e.g. plex).
  3. Under targets, put the app's local address (e.g. 192.168.1.50) and its port (Plex uses 32400; Jellyfin uses 8096).
  4. Open https://plex.example.com. It should load, with a valid certificate.

Step 8: Lock it down

Out of the box, Pangolin puts a login screen in front of each resource. Leave that on unless you have a good reason.

  • Pangolin login (SSO): visitors must sign in with a Pangolin account.
  • PIN or password: kind of obvious; visitors enter a code to access the app.

Just a note: be careful with Bypass Auth rules. Pangolin lets you add rules for certain paths, IPs, or countries, and a Bypass Auth rule gives matching visitors unfettered access to the resource, with no login at all. I'll cover Pangolin rules in depth in another post. It seems like every time I write one post, three more ideas come out of it.


Troubleshooting

Problem Likely cause Fix
Certificate warning that won't go away DNS not pointing at your server yet, or port 80 blocked Check nslookup, open port 80, then sudo docker logs traefik
Dashboard won't load at all Containers not running sudo docker ps, then sudo docker logs pangolin
Site stuck on Offline UDP 51820 blocked, wrong ID/secret, or Cloudflare proxy (orange cloud) on Open UDP 51820 in both firewalls, set the records to grey cloud, recheck docker logs pangolin-site
App shows Bad Gateway Target IP/port wrong, or the connector can't reach it From the connector box, curl http://192.168.1.50:32400
Hit Let's Encrypt rate limit Too many retries while DNS was wrong Fix DNS, then wait (it resets on its own)
Option B: works at home, not from outside Port forward missing, or ISP blocks 80/443 Test from your phone on mobile data. Recheck the router forward to the Pangolin box's local IP
Option B: everything broke after a router reboot Home IP changed, DNS not updated Check your DDNS updater's logs, then nslookup pangolin.example.com
Option B: DDNS updater log shows auth errors Wrong Cloudflare token, or token scoped to the wrong zone Make a new token from the Edit zone DNS template, scoped to your domain
Option B: anything.example.com doesn't resolve Wildcard record missing Add *.example.com to DOMAINS, or add the wildcard CNAME
Option B: works from outside, not from inside your home Router doesn't support NAT loopback Enable NAT loopback, or add local DNS entries (Pi-hole / AdGuard Home)

Wrapping up

The whole thing boils down to:

  1. Pick a server (a small VPS, or a home box with a public IP) and point your domain at it.
  2. At home? Set up DDNS so your domain follows your IP.
  3. Run the Pangolin installer.
  4. Run the site connector at home (unless your apps live on the Pangolin box itself).
  5. Add apps, and keep the login in front of them.

Thanks for reading!