Recently, I was setting up Navidrome, kind of like a self-hosted Spotify, in my home lab and I wanted to set it up to use proxy auth in Authentik. But to do this I needed to use with a reverse proxy, i.e. Traefik. In this article I will show you how to you can point your Cloudflare tunnel to Traefik and have that forward the request to the service.

Prerequisite

In this article, I assume you are already familiar with Cloudflare Tunnels and Traefik.

Background

Cloudflare tunnels are an easy way to allow access to apps running on a server, i.e. home lab. Where you only want expose, say, a few services. When a client connects, they connect to Cloudflare and then Cloudflare makes a secure connection to your server. Which is running the cloudflared daemon.

Where my Cloudflare nix config looks something like this 1:

{
  config,
  lib,
  ...
}:
with lib;
with lib.nixicle; let
  cfg = config.services.nixicle.cloudflared;
in {
  options.services.nixicle.cloudflared = {
    enable = mkEnableOption "Enable The cloudflared (tunnel) service";
  };

  config = mkIf cfg.enable {
    sops.secrets.cloudflared = {
      sopsFile = ../secrets.yaml;
      owner = "cloudflared";
    };

    services = {
      cloudflared = {
        enable = true;
        tunnels = {
          "ec0b6af0-a823-4616-a08b-b871fd2c7f58" = {
            credentialsFile = config.sops.secrets.cloudflared.path;
            default = "http_status:404";
          };
        };
      };
    };
  };
}

The other apps I have set up in Cloudflare tunnels point directly to the service itself. This is the first one I needed to point to Traefik, and then Traefik would forward the request to Navidrome.

Where my Traefik setup looks something 2:

{
  config,
  lib,
  ...
}:
with lib; let
  cfg = config.services.nixicle.traefik;
in {
  options.services.nixicle.traefik = {
    enable = mkEnableOption "Enable traefik";
  };

  config = mkIf cfg.enable {
    services = {
      traefik = {
        enable = true;

        staticConfigOptions = {
          # removed config to simplify

          entryPoints.web = {
            address = "0.0.0.0:80";
            http.redirections.entryPoint = {
              to = "websecure";
              scheme = "https";
              permanent = true;
            };
          };

          entryPoints.websecure = {
            address = "0.0.0.0:443";
            http.tls = {
              certResolver = "letsencrypt";
              domains = [
                {
                  main = "homelab.haseebmajid.dev";
                  sans = ["*.homelab.haseebmajid.dev"];
                }
                {
                  main = "haseebmajid.dev";
                  sans = ["*.haseebmajid.dev"];
                }
              ];
            };
          };
        };
      };
    };
  };
}

Things to note about my setup (now shown here) but Traefik is set up to be able to provision certificates from let’s encrypt using Cloudflare DNS challenge (see full config linked above).

Setup

We can see here this is how I configured my tunnel, first I point to traefik on port 443, i.e. https. As Traefik I have set up always expects traffic on 443. Also notice the origin server name is navidrome.haseebmajid.dev. This will get passed to Traefik.

Or in my Nix config I have defined it like so:

{
services = {
  cloudflared = {
    enable = true;
    tunnels = {
      "ec0b6af0-a823-4616-a08b-b871fd2c7f58" = {
        ingress = {
          "navidrome.haseebmajid.dev" = {
            service = "https://localhost";
            originRequest = {
              originServerName = "navidrome.haseebmajid.dev";
            };
          };
        };
      };
    };
  };
};
}

Finally, the last bit to connect the two bits together, the Traefik config:

{
  traefik = {
    dynamicConfigOptions = {
      http = {
        services = {
          navidrome.loadBalancer.servers = [
            {
              url = "http://localhost:4533";
            }
          ];
        };

        routers = {
          navidrome = {
            entryPoints = ["websecure"];
            rule = "Host(`navidrome.haseebmajid.dev`)";
            service = "navidrome";
            tls.certResolver = "letsencrypt";
            middlewares = ["authentik"];
          };
        };
      };
    };
  };
}

Pointing to our local service http://localhost:4533 where Navidrome is running. We also tell it that if we get a request for host navidrome.haseebmajid.dev it should forward it to that localhost service. We also tell it to provision a certificate using letsencrypt. Though you may not need this as Cloudflare does it for you.

However, Cloudflare will only do it on direct subdomains, i.e. it won’t work on navidrome.homelab.haseebmajid.dev. Because it’s a subdomain of a subdomain.

Then tell it to use the authentik middleware, which you can find out how to set up here. This tells Traefik to forward the request to Authentik, so we can correctly authenticate the user.

That’s it, we should be able to access our service Navidrome when it’s all deployed. The request is going from Client → Cloudflare →Traefik → Navidrome.