Secure Self-Hosting with Pangolin, Traefik, and CrowdSec

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 with iptables.

2. Install Pangolin Server

Pangolin provides a one-line script:

curl -fsSL https://get.pangolin.dev | sh
Bash

During 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"
YAML

traefik.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"
YAML

Security Enhancements

1. CrowdSec Bouncer API Key

Generate a key inside CrowdSec:

docker exec -it crowdsec cscli bouncers add bouncer-traefik
Bash

Paste 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"
YAML

Reload CrowdSec and verify:

docker exec -it crowdsec cscli parsers list | grep whitelist
Bash

3. 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"
YAML

Now 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.