Introduction
In this article, I’ll show you how I secured my Pangolin reverse proxy with Mutual TLS (mTLS). This ensures that only devices with a valid client certificate can access the dashboard and the services behind it.
What is Pangolin?
Pangolin is a self-hosted reverse proxy built on Traefik. It allows you to securely expose internal services over the internet while providing a user-friendly interface for management.
What is mTLS?
With a normal HTTPS connection, only the server authenticates itself to the client (one-way TLS). With Mutual TLS (mTLS), both parties must authenticate:
-
The server presents its certificate (like normal HTTPS)
-
The client must also present a valid certificate
This means: Even if someone knows the URL, they cannot access the service without the matching client certificate. This is an additional security layer that goes beyond passwords.
Benefits of mTLS
-
Strong authentication: Certificates are harder to steal than passwords
-
No password fatigue: No complex passwords to remember
-
Device binding: The certificate is tied to a specific device
-
Phishing protection: No access without certificate, even with leaked credentials
Step 1: Create Directory Structure
First, I created the necessary directory structure for the certificates:
mkdir -p config/traefik/certs/ca
mkdir -p config/traefik/certs/clients
The structure looks like this:
config/traefik/certs/
├── ca/ # Certificate Authority (CA) certificates
│ ├── ca.crt # CA certificate (public)
│ └── ca.key # CA private key (secret!)
├── clients/ # Client certificates
│ ├── user1.crt
│ ├── user1.key
│ ├── user1.p12 # For browser import
│ └── ...
└── create-client.sh # Certificate creation script
Step 2: Create Certificate Authority (CA)
Before I can issue client certificates, I need my own Certificate Authority (CA). This CA will later sign all client certificates.
cd config/traefik/certs/ca
# Create CA private key (4096 bit for high security)
openssl genrsa -out ca.key 4096
# Create CA certificate (valid for 10 years)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
-subj "/C=US/ST=California/L=San Francisco/O=My Organization/OU=IT/CN=My mTLS CA"
Important: Keep the
ca.keysecure! Anyone with this key can issue valid client certificates.
Step 3: Configure Traefik Dynamic Configuration
The core of the mTLS configuration lies in the Traefik dynamic configuration. It’s important to enable mTLS on all three routes: next-router, api-router, and ws-router. Otherwise, Traefik will throw errors about routes using different TLS options. Additionally, this means that all domains you protect via the Pangolin dashboard are also secured with mTLS, since you must first authenticate at the Pangolin dashboard.
I created/modified the following file:
# config/traefik/dynamic_config.yml
tls:
options:
# mTLS erforderlich - Client MUSS gültiges Zertifikat haben
mtls-strict:
clientAuth:
caFiles:
- /etc/traefik/certs/ca/ca.crt
clientAuthType: RequireAndVerifyClientCert
minVersion: VersionTLS12
sniStrict: true
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
main-app-router-redirect:
rule: "Host(`pangolin.domain.de`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
next-router:
rule: "Host(`pangolin.domain.de`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
options: mtls-strict
api-router:
rule: "Host(`pangolin.domain.de`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
options: mtls-strict
ws-router:
rule: "Host(`pangolin.domain.de`)"
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
options: mtls-strict
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002"
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000"
Explanation of Key Settings
| Setting | Description |
|———|————-|
| clientAuthType: RequireAndVerifyClientCert | Client certificate is mandatory |
| caFiles | Path to CA that signed client certificates |
| minVersion: VersionTLS12 | Minimum TLS 1.2 (for security) |
| sniStrict: true | Strict SNI verification enabled |
| options: mtls-strict | Applies mTLS option to the router |
Step 4: Update Docker Compose
In my docker-compose.yml, I mounted the certificate volume for Traefik:
traefik:
image: traefik:v3.3.3
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik:rw
- ./config/traefik/certs:/etc/traefik/certs:ro #Volume to store mtls certs
Step 5: Create Client Certificates
To simplify client certificate creation, I wrote the following script:
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "Usage: $0 <client-name> [email]"
exit 1
fi
CLIENT_NAME="$1"
EMAIL="${2:-$CLIENT_NAME@email.com}"
DAYS="${3:-730}"
cd "$(dirname "$0")"
echo "=== Erstelle Client-Zertifikat für: $CLIENT_NAME ==="
# Config erstellen
cat > clients/${CLIENT_NAME}.cnf << CNFEOF
[req]
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
C = DE
ST = City
L = State
O = User
OU = Benutzer
CN = ${CLIENT_NAME}
emailAddress = ${EMAIL}
CNFEOF
# Key erstellen
openssl genrsa -out clients/${CLIENT_NAME}.key 4096
# CSR erstellen
openssl req -new -key clients/${CLIENT_NAME}.key \
-out clients/${CLIENT_NAME}.csr \
-config clients/${CLIENT_NAME}.cnf
# Mit CA signieren
openssl x509 -req -days $DAYS \
-in clients/${CLIENT_NAME}.csr \
-CA ca/ca.crt \
-CAkey ca/ca.key \
-CAcreateserial \
-out clients/${CLIENT_NAME}.crt \
-extfile <(echo "basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth")
# P12 für Browser erstellen
openssl pkcs12 -export \
-out clients/${CLIENT_NAME}.p12 \
-inkey clients/${CLIENT_NAME}.key \
-in clients/${CLIENT_NAME}.crt \
-certfile ca/ca.crt \
-name "${CLIENT_NAME} mTLS Zertifikat"
echo ""
echo "=== Fertig! ==="
echo "Dateien erstellt:"
echo " - clients/${CLIENT_NAME}.crt (Zertifikat)"
echo " - clients/${CLIENT_NAME}.key (Private Key)"
echo " - clients/${CLIENT_NAME}.p12 (Browser-Import)"
Using the Script
# Make script executable
chmod +x config/traefik/certs/create-client.sh
# Create certificate for a user
./create-client.sh john john@example.com
# The script will prompt for an export password for the .p12 file
Step 6: Import Client Certificate in Browser
Firefox
-
Open Settings
-
“Privacy & Security” → “Certificates”
-
Click “View Certificates”
-
“Your Certificates” tab → “Import”
-
Select the
.p12file and enter password
Chrome / Edge
-
Settings → “Privacy and security”
-
“Security” → “Manage certificates”
-
“Personal” tab → “Import”
-
Select the
.p12file
iOS (iPhone/iPad)
-
Transfer the
.p12file to the device via AirDrop or email -
Tap on the file → “Profile Downloaded”
-
Settings → “Profile Downloaded” → “Install”
-
Enter password
Android
-
Settings → “Security” → “Encryption & credentials”
-
“Install a certificate” → “VPN & app user certificate”
-
Select the
.p12file
Result
After configuration, here’s what happens when someone accesses pangolin.domain.de:
-
With valid certificate: The browser shows a certificate selection dialog, you choose your certificate and gain access
-
Without certificate: The connection is immediately rejected - no login page, no error message, just “Connection refused”
This provides very strong protection since attackers can’t even reach the login page.
Tips and Best Practices
Revoking Certificates
If a certificate becomes compromised, there are two options:
-
Simple: Create new CA and reissue all client certificates
-
Professional: Maintain a Certificate Revocation List (CRL)
Don’t Forget Backups
Make sure to backup these files:
-
ca/ca.key- Without this key, no new certificates can be created -
ca/ca.crt- Required by Traefik
Transfer Certificates Securely
Never send .p12 files unencrypted via email. Better options:
-
Hand over in person
-
Via secure channel (Signal, etc.)
-
Temporarily on a password-protected share
Conclusion
With mTLS, I’ve set up an additional, very strong security layer for my Pangolin reverse proxy. Only devices with a valid client certificate can even establish a connection. This effectively protects against unauthorized access, even if credentials should leak.
The initial effort is worth it - once set up, management with the script is very simple and security is significantly enhanced.