Running Cloudflared with Traefik on My K3s Cluster

By switching to Cloudflared, I was able to remove the firewall rules I originally had that allowed external traffic from Cloudflare’s network to communicate directly with my cluster.
This not only simplifies the overall design but also adds another layer of security to the setup.

As with any technology, things are constantly evolving and changing. I mention this because before I even finished documenting what I had done and how I set it up, Cloudflared came along and turned out to be a much better fit for my environment.

I finally made the call — I’m keeping Traefik in my setup, but I’ve added Cloudflared into the mix.
This gives me the best of both worlds: Traefik handles all my in-cluster routing and middleware, while Cloudflared securely bridges everything through Cloudflare’s network.

Now, all external traffic flows like this:

Cloudflare → Cloudflared → Traefik → Services (like WordPress)

So far, resource utilization has remained about the same, and I haven’t noticed any difference in response times.
The setup feels just as fast and responsive as before, but it’s far more secure and easier to manage.


How It’s Set Up

Cloudflared runs in its own Kubernetes namespace and Deployment.
Instead of replacing Traefik, I just pointed the Cloudflared routes to my internal Traefik endpoint:

https://traefik.kube-system.svc.cluster.local:443

Each route uses an originServerName matching my hostname — for example, www.thedougie.com.
When requests hit Traefik, it recognizes that hostname and forwards them to the right service.
That means www.thedougie.com hits WordPress, mealie.thedougie.com hits Mealie, and so on — just like before.


The Playbook

Here’s the cleaned-up Ansible playbook that handles Cloudflared.
Everything wrapped in {{ }} should live in your Ansible vars file, ideally encrypted with ansible-vault since you’ll be storing your Cloudflare token there. Please note this is not the same as the api key used. this is a token that is generated when you setup zero trust.

# cloudflared-deploy.yml
- name: Deploy Cloudflared (remote-managed) via Helm using provided token
  hosts: master
  gather_facts: false

  vars:
    namespace: cloudflared
    release: cloudflared
    chart: cloudflare/cloudflare-tunnel

  tasks:
    - name: Ensure namespace exists
      kubernetes.core.k8s:
        kubeconfig: /etc/rancher/k3s/k3s.yaml
        api_version: v1
        kind: Namespace
        name: "{{ namespace }}"
        state: present

    - name: Deploy Cloudflared using Deployment manifest and token
      kubernetes.core.k8s:
        kubeconfig: /etc/rancher/k3s/k3s.yaml
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: cloudflared
            namespace: "{{ namespace }}"
          spec:
            replicas: 3
            selector:
              matchLabels:
                app: cloudflared
            template:
              metadata:
                labels:
                  app: cloudflared
              spec:
                containers:
                  - name: cloudflared
                    image: cloudflare/cloudflared:latest
                    args:
                      - tunnel
                      - --no-autoupdate
                      - run
                      - --token
                      - "{{ cloudflaredtoken }}"

Example Vars File

Here’s what your vars/cloudflared.yml might look like:

cloudflaredtoken: <your-cloudflare-token>

Then encrypt it:

ansible-vault encrypt vars/cloudflared.yml

And run the playbook:

ansible-playbook cloudflared-deploy.yml --ask-vault-pass

Anything in {{ }} comes from your vars file.
If you’re using multiple clusters, remember you can always set {{ kube_context }} in your vars as well — though that’s optional if you only manage one cluster in your kubeconfig.


Verifying Everything

You can check the deployment with:

kubectl get pods -n cloudflared

To see logs:

kubectl logs -n cloudflared deployment/cloudflared

To confirm Traefik’s endpoint:

kubectl get svc -n kube-system traefik

And to test routing internally:

curl -vk https://traefik.kube-system.svc.cluster.local -H "Host: www.thedougie.com"

If you see your app respond, the connection is good.


Wrapping Up

Running Cloudflared inside the cluster with Traefik has been a solid move.
It adds another layer of security, removes the need for open firewall ports, and keeps my existing ingress logic intact.
It also gives me room to grow — whether I eventually migrate to a full Cloudflared standalone setup or keep this hybrid design.

As with everything in the homelab, nothing’s ever truly “done.” Things evolve, better options appear, and sometimes you pivot mid-way. That’s half the fun.

Leave a Comment