In one of my previous articles, I deployed an OpenVSCode instance in my Proxmox VE using Docker and Traefik. The project was enjoyable, and I learned a lot. It was essential for me because I wanted an online IDE that I could use on any machine at home or when I am outside of my home network.
After completing my deployment, I was able to use the service on my local network as usual. Still, one of the critical parts was missing, which was accessing this particular service from outside my home network. In one of my earlier articles, I deployed a self-managed Pangolin server, which turned out to be the perfect solution for exposing private services securely.
An overview of my Pangolin setup
Here, I will explain how to safely expose self-hosted services (like WordPress or OpenVSCode) to the internet without putting your entire home network at risk.
The architecture combines three key components:
- Pangolin > secure tunnelling from a cloud VM to your home lab
- Traefik > reverse proxy, TLS certificates, and middlewares
- CrowdSec > intrusion detection and automatic banning
The result is a layered, zero-trust style setup:
- Public services run behind tunnels, not directly exposed.
- CrowdSec inspects traffic and bans malicious actors.
- Traefik handles SSL and adds extra protection with headers and authentication.
Here’s the high-level flow:

Key points:
- Oracle VM (1 GB, 1 vCPU) hosts Pangolin server – a single entry point on the internet.
- Home services remain private, only reachable via Pangolin tunnels.
- Traefik proxies requests and manages TLS (via Cloudflare DNS-01 challenge).
- CrowdSec bans malicious IPs in real-time, both at the cloud edge and in the home Docker VM.
Setup Steps
1. Oracle VM Setup
- Launch a free-tier Ubuntu VM in Oracle.
- Assign a reserved public IP.
- Open security group ports:
22
(SSH),80
(HTTP),443
(HTTPS), and Pangolin ports (51820
,21820
). - For security, change SSH to a custom port (via
sshd_config
) and persist withiptables
.
2. Install Pangolin Server
Pangolin provides a one-line script:
curl -fsSL https://get.pangolin.dev | sh
BashDuring install:
- Provide the domain (e.g.,
pangolin.example.com
). - Let’s Encrypt issues certificates automatically.
Deploy Pangolin Edge + Traefik + CrowdSec (Home VM)
docker-compose.yml
# docker-compose.yml
secrets:
cf-token:
file: ./cf-token # contains only the Cloudflare API token
networks:
proxy:
external: true # use your existing 'proxy' network
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
secrets:
- cf-token
networks:
- proxy
ports:
- "80:80"
- "443:443"
environment:
- TZ=Europe/London
- CF_API_EMAIL=<your-cloudflare-email> # optional if using API key
- CF_DNS_API_TOKEN_FILE=/run/secrets/cf-token
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik.yaml:/traefik.yaml:ro
- ./config/acme.json:/acme.json
- ./config:/etc/traefik/config:ro
- ./logs:/var/log/traefik
labels:
- "traefik.enable=true"
# HTTP -> HTTPS redirect
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
- "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.traefik.middlewares=https-redirect"
# HTTPS dashboard (with auth + CrowdSec)
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=example.com"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.example.com"
- "traefik.http.routers.traefik-secure.service=api@internal"
- "traefik.http.routers.traefik-secure.middlewares=traefik-auth@file,crowdsec-bouncer@file"
YAMLtraefik.yaml
# traefik.yaml
# Static Traefik configuration
api:
dashboard: true # set to false in production if you don’t want the dashboard exposed
debug: true
entryPoints:
http:
address: ":80"
forwardedHeaders:
trustedIPs:
- "172.18.0.0/16" # Docker network range
- "127.0.0.1/32"
http:
middlewares:
- crowdsec-bouncer@file
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
forwardedHeaders:
trustedIPs:
- "172.18.0.0/16" # Docker network range
- "127.0.0.1/32"
http:
middlewares:
- crowdsec-bouncer@file
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /etc/traefik/config
watch: true
certificatesResolvers:
cloudflare:
acme:
# caServer: https://acme-v02.api.letsencrypt.org/directory # production
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging (testing)
email: <your-cloudflare-email>
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
log:
level: "INFO"
filePath: "/var/log/traefik/traefik.log"
accessLog:
filePath: "/var/log/traefik/access.log"
YAMLSecurity Enhancements
1. CrowdSec Bouncer API Key
Generate a key inside CrowdSec:
docker exec -it crowdsec cscli bouncers add bouncer-traefik
BashPaste into .env
as CROWDSEC_BOUNCER_API_KEY
.
2. Whitelist Home IP
/etc/crowdsec/parsers/s02-enrich/whitelist-home.yaml
:
name: whitelist_home
description: "Allow traffic from my home IP"
whitelist:
reason: "Home Admin"
ip:
- "YOUR.HOME.IP"
YAMLReload CrowdSec and verify:
docker exec -it crowdsec cscli parsers list | grep whitelist
Bash3. Forward Real Client IPs
In traefik.yaml
:
entryPoints:
https:
address: ":443"
forwardedHeaders:
trustedIPs:
- "172.18.0.0/16" # Docker network
- "127.0.0.1/32"
YAMLNow Traefik logs (and CrowdSec) show the actual client IP instead of the tunnel address.
Doing this, you will achieve
Tunnelling (Pangolin): lets you expose services securely without NAT or direct exposure.
Traefik: flexible proxy with auto TLS, middlewares (auth, headers, redirects).
CrowdSec: community-driven intrusion prevention, bans brute force, scans, and exploits in real time.
Layered security: multiple checkpoints (Traefik auth + Pangolin auth + CrowdSec bans).
Practical troubleshooting: Oracle firewall rules, SSH port hardening, iptables, and real client IP forwarding.
A few notes:
Oracle VM requires manual iptables rules for a custom SSH port and all other necessary ports.
Configured forwardedHeaders > trusted IPs in Traefik to view real client IPs instead of internal Docker IPs.
CrowdSec whitelist config was mounted incorrectly at first, fixed by mapping the entire config directory into the container.
Main takeaways from this project:
- How to securely expose private services using a zero-trust layered model.
- Combining Pangolin tunnelling (for safe external access), Traefik (for routing, TLS, and auth), and CrowdSec (for intrusion prevention).
- Troubleshooting real-world issues: firewall rules, SSH hardening, Docker network forwarding, and middleware misconfigurations.
- Importance of log visibility (forwarded headers for client IPs) for auditing and IDS/IPS effectiveness.
This project reinforced modern DevOps/SRE practices: infrastructure hardening, automation, and defence-in-depth security design.