As part of my effort to reduce US dependencies in my day-to-day life, one key tool I relied on was Cloudflare Tunnels. I currently use it on my Kubernetes homelab to expose services to friends and family from anywhere. It’s easy to set up and free to use.
However, Cloudflare is a US-based company, and with the current geopolitical climate it has become increasingly difficult to rely on American infrastructure. Cloudflare has already threatened to pull its servers from certain markets rather than comply with local regulations, a move that highlights how dependent we are on their goodwill, even if the regulations themselves are debatable.
Sadly, free non-US alternatives to Cloudflare Tunnels don’t exist. One alternative was to self-host an open source alternative called Pangolin on a VPS, which allows you to have a similar setup without relying on a third party.
Choosing a VPS was pretty straightforward to me. It was either Hetzner or OVH. Since I was also hosting my domain DNS on Cloudflare, I chose to go with OVH so I can host my domain DNS on OVH as well, and have the VPS on the same provider. I took the VPS-1 which is 5 euro per month and the cheapest option (still pretty overkill for my needs). (Disclosure: I joined OVH as a software engineer in March 2026, so take that recommendation with the appropriate grain of salt.)
How it works
An incoming request hits the VPS, where Traefik handles TLS termination and routing. On the cluster side, Newt runs as a Deployment and handles the WireGuard tunnel back to the VPS. Traffic is forwarded through that tunnel to your in-cluster gateway (kgateway in my case), which routes it to the appropriate service. Pangolin acts as the control plane, managing sites, resources, and access rules across that tunnel. Gerbil runs alongside Pangolin on the VPS and handles the network-level plumbing between Traefik and the tunnel.
internet -> VPS (Traefik + Gerbil) -> WireGuard tunnel (Newt) -> cluster (kgateway -> service)The Setup
Pangolin setup is just a Docker Compose file that runs Pangolin, Traefik, and Gerbil (with CrowdSec as an optional dependency). docker-compose.yml
To help a fast deployment I also created an Ansible playbook that automates the setup of the VPS and the deployment of Pangolin, with security hardening and gitops in place with doco-cd. playbook.yml My Ansible will set up doco-cd, deploy Pangolin, apply security rules, and push secrets generated by Pangolin to 1Password.
Now that we have Pangolin set up and running, we need to configure the link between Pangolin and our cluster. For that, the Pangolin team developed Newt, which is based on WireGuard.
Newt can push configuration directly to Pangolin, letting you manage exposed services from the cluster using Blueprints:
jellyfin: name: 'Jellyfin' protocol: http ssl: true full-domain: 'jellyfin.erwanleboucher.dev' tls-server-name: jellyfin.erwanleboucher.dev targets: - site: glistening-desert-rosy-boa hostname: kgateway-external.network.svc.cluster.local method: https port: 443This defines what is called a resource: the link between the external world and the cluster. The site id is a unique identifier that is generated by Newt when you create a site.
If you are experienced with Cloudflare Tunnels, you might think about having a wildcard domain to route all the services that are available in your gateway (here kgateway). Sadly this is not possible with Newt/Wireguard. Therefore, either you define each of them manually or you can use the sidecar service I created to automatically discover and configure services from your cluster.
Newt-Sidecar
Newt-sidecar watches all HTTPRoute resources in your cluster and configures them in Pangolin automatically. This runs as a sidecar container alongside the Newt deployment.
newt-sidecar: image: repository: ghcr.io/home-operations/newt-sidecar tag: latest args: - --gateway-name=kgateway-external - --site-id=glistening-desert-rosy-boa - --target-hostname=kgateway-external.network.svc.cluster.local - --deny-countries=RU,CN,KP,IR,BY,IL # GeoIP blocking via Badger, see Security sectionTLS With OVH DNS challenge
As my DNS is hosted on OVH, we need to set up the OVH DNS challenge to automatically configure TLS certificates.
Traefik has a built-in OVH DNS challenge provider, which makes it easier than cert-manager which needs an external webhook (which I will write about soon).
The OVH API key is a bit tricky to get. You need to provide the endpoint path to the OVH API, which is not the most obvious process, but you can follow this guide to get your credentials (which is also the guide for the OVH cert-manager webhook).
Once you have your credentials (ApplicationKey, ApplicationSecret and ConsumerKey), make them available as environment variables in your deployment:
container_name: traefikrestart: unless-stoppedenvironment: - OVH_ENDPOINT=ovh-eu - OVH_APPLICATION_KEY=${OVH_APPLICATION_KEY} - OVH_APPLICATION_SECRET=${OVH_APPLICATION_SECRET} - OVH_CONSUMER_KEY=${OVH_CONSUMER_KEY} - CROWDSEC_API_KEY=${CROWDSEC_API_KEY}And in traefik_config.yaml:
letsencrypt: acme: dnsChallenge: provider: ovh email: 'erwanleboucher@gmail.com' storage: '/letsencrypt/acme.json' caServer: 'https://acme-v02.api.letsencrypt.org/directory'This will allow you to generate certificates for your domains using the OVH DNS challenge and protect your services with HTTPS.
Security
As this opens a door into our homelab cluster, we should be careful to protect our environment as much as possible.
In my case, I added ufw rules to restrict the VPS access to only the services I need.
port: 22- name: HTTP port: 80- name: HTTPS port: 443- name: Wireguard port: 51820 protocol: udp- name: Gerbil port: 21820 protocol: udpPangolin also gives you two ways to secure your cluster:
- Pangolin’s own security features with the Badger Traefik middleware
- CrowdSec which is an open-source security platform that provides threat detection and prevention capabilities.
I decided to use both, as Badger also adds monitoring to the Pangolin dashboard. Badger also offers authentication with SSO capabilities, shareable links, and more. I also use it for GeoIP blocking, as seen in the newt-sidecar config above.
Badger
Pangolin uses the MaxMind GeoIP database to block access from countries you don’t want reaching your services, and you can easily configure it with Docker Compose.
container_name: geoipupdateimage: ghcr.io/maxmind/geoipupdate:v7.1.1restart: unless-stoppedenvironment: - GEOIPUPDATE_ACCOUNT_ID=${MAXMIND_ACCOUNT_ID} - GEOIPUPDATE_LICENSE_KEY=${MAXMIND_LICENSE_KEY} - GEOIPUPDATE_EDITION_IDS=GeoLite2-Country GeoLite2-ASN - GEOIPUPDATE_FREQUENCY=72volumes: - './config/GeoLite2:/usr/share/GeoIP'And in the Pangolin config:
maxmind_asn_path: './config/GeoLite2/GeoLite2-ASN.mmdb'This will automatically download and update the GeoIP database every 72 hours. To learn more and get the license key.
CrowdSec
I haven’t dug too deeply into CrowdSec yet, but to make it work with Pangolin, you need to configure it with Docker Compose like:
environment: COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules ENROLL_INSTANCE_NAME: pangolin-crowdsec ENROLL_TAGS: docker GID: '1000' PARSERS: crowdsecurity/whitelists healthcheck: interval: 10s retries: 3 start_period: 30s test: - CMD - cscli - lapi - status timeout: 5s image: docker.io/crowdsecurity/crowdsec:v1.7.6 labels: - traefik.enable=false ports: - 6060:6060 restart: unless-stopped volumes: - /opt/crowdsec/config:/etc/crowdsec - /opt/crowdsec/data:/var/lib/crowdsec/data - ./config/traefik/logs:/var/log/traefik configs: - source: crowdsec_acquis target: /etc/crowdsec/acquis.yamlconfig: crowdsec_acquis: content: | source: file filenames: - /var/log/traefik/access.log labels: type: traefik --- source: appsec listen_addr: 0.0.0.0:7422 path: / appsec_config: crowdsecurity/virtual-patching labels: type: appsecThen create a bouncer API key for Traefik. If you want CrowdSec to auto-generate one:
cscli bouncers add traefik-bouncerCopy the output key and set it as CROWDSEC_API_KEY in your environment. If you already have a key (e.g. from an Ansible vault), you can pass it directly:
cscli bouncers add traefik-bouncer --key "$CROWDSEC_API_KEY"Migrating away from Cloudflare Tunnels took an afternoon and has been rock solid since. The setup is more involved than a single cloudflared token and some wildcard configuration, but you gain full control over your traffic, no vendor lock-in, and no dependency on US infrastructure. If you’re already running a Kubernetes homelab, the extra complexity is well worth it. Questions? Come find me on the home-operations Discord.
Everything explained above is in my own homelab repository here.