The next thing I’m adding to my cluster is Traefik.
In this post, I’ll cover installing Traefik with Argo CD and using Traefik’s built-in basic auth middleware to control access to its control panel.
Overview
This is part 8 of my kubernetes homelab cluster setup series - Add Traefik to the cluster
- Part 1 - Setting up Talos with a Cilium CNI on proxmox
- Part 2 Add SSL to Kubernetes using Cilium, cert-manager and LetsEncrypt with domains hosted on Amazon Route 53
- Part 3 - Set up Secret Management with SOPS
- Part 4 - Back up your Talos etcd cluster to a SMB share
- Part 5 - Install ArgoCD
- Part 6 - Install MQTT into a k8s cluster
- Part 7 - Add an ArgoCD git generator
ApplicationSet - Part 8 - Add Traefik to the cluster
In this article, we’re going to:
- Install Traefik with Argo CD,
- Create an Argo CD
Applicationresource with the git generator to install the extra k8s manifests needed to enable using the Traefik dashboard with the Traefik basic auth middleware to control access.
Software Versions
Here are the versions of the software I used while writing this post. Later versions should work, but this is what these instructions were tested with.
| Software | Version |
|---|---|
argocd |
3.2.6 (chart 9.4.8) |
cert-manager |
1.19.2 |
kubectl |
1.34 |
kubernetes |
1.34.1 |
traefik |
3.3.1 |
sops |
3.11.0 |
Pre-requisites
To follow along with this post, you will need:
- A homelab kubernetes cluster. I’m using the talos cluster from Part 1 - Setting up Talos with a Cilium CNI on proxmox, but this will work with any kubernetes cluster.
- Argo CD installed in your cluster, with a git generator installed. If you don’t have them installed, I have instructions in Part 5 - Install ArgoCD and Part 7 - Add an ArgoCD git generator
ApplicationSet. - A GitHub account. You can also use GitLab or your own
gitserver, but all the examples in this post assume you’re storing your Argo CD repository on GitHub. - If you’ve been following along with this series, you already have
kubectl. Otherwise you canbrew install kubectlon macOS or follow the install instructions for kubectl. - The
sopsoperator installed into your cluster. Instructions are at Part 3 - Set up Secret Management with SOPS - cert-manager so you can set up SSL for the gateway. Instructions are at Part 2 Add SSL to Kubernetes using Cilium, cert-manager and LetsEncrypt with domains hosted on Amazon Route 53.
Installation
First, create the traefik namespace and a SSL certificate
Make it a privileged namespace so Talos doesn’t complain later.
# traefik-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
kubernetes.io/metadata.name: traefik
pod-security.kubernetes.io/enforce: privileged
name: traefik
Creating a SSL certificate can take a minute or two for LetsEncrypt to issue the cert, so let’s get that started ASAP. Even if you have a certificate in another namespace, go ahead and create one in the traefik namespace, it’ll make things less of a hassle later.
# traefik-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-YOUR_DOMAIN-tls
namespace: traefik
spec:
secretName: wildcard-YOUR_DOMAIN-tls
issuerRef:
name: letsencrypt-dns-r53
kind: ClusterIssuer
commonName: "*.YOUR_DOMAIN"
dnsNames:
- "*.YOUR_DOMAIN"
- "YOUR_DOMAIN"
In the interest of speed, apply them now - Argo CD will take over the resources when we create the Application resource.
kubectl apply -f traefik-cert.yaml && \
kubectl apply -f traefik-namespace.yaml
Create a traefik-values.yaml to configure Traefik
We’re going to use Argo CD to install Traefik using a helm chart. To configure it, we’re first going to set up a traefik-values.yaml file in our configuration git repository.
I created mine in argocd/infrastructure/traefik/traefik-values.yaml
# argocd/infrastructure/traefik/traefik-values.yaml
api:
dashboard: true
insecure: false # We're going to put the dashboard behind basic auth
deployment:
kind: Deployment
replicas: 2
podAnnotations:
"networking.k8s.io/mtu": "1450"
service:
enabled: true
type: LoadBalancer
externalTrafficPolicy: Local
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9100"
ports:
web:
port: 8000
exposedPort: 80
expose:
default: true
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
port: 8443
exposedPort: 443
expose:
default: true
tls:
enabled: true
traefik: # This is the dashboard port
port: 9000
expose:
default: true
exposedPort: 9000
protocol: TCP
providers:
kubernetesIngress:
publishedService:
enabled: true
pathOverride: "traefik/traefik"
kubernetesGateway:
enabled: true
infrastructure:
entryPoints:
- web
- websecure
# Let traefik find resources in all namespaces to make using git-generated
# apps easier
namespaces: []
logs:
general:
level: INFO
access:
enabled: true
Create a Argo CD app to install traefik
We’re going to use a multi-source Argo application to install Traefik. We’ll use one source to pull in the helm chart and the second source to pull in our traefik-values.yaml file.
# argocd/infrastructure/traefik.application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: traefik
namespace: argocd
spec:
project: default
sources:
- repoURL: https://traefik.github.io/charts
chart: traefik
targetRevision: 34.0.0
helm:
valueFiles:
- $values/argocd/infrastructure/traefik/traefik-values.yaml
- repoURL: https://github.com/YOUR_USERNAME/YOUR_CONFIGURATION_REPO.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: traefik
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
# # This prevents the "Invalid Task" lockup if an API is missing
# - SkipDryRunOnMissingResource=true
Commit them and push them to your git repository so that Argo will be able to find the traefik-values.yaml file.
Now we can add the application to Argo
kubectl apply -f traefik.application.yaml
In a minute or so, you should see that Traefik is installed
$ kubectl get -n argocd Application/traefik
NAME SYNC STATUS HEALTH STATUS
traefik Synced Healthy
$ k get -n traefik all
NAME READY STATUS RESTARTS AGE
pod/traefik-69d9f94ff9-8p6tr 1/1 Running 0 16m
pod/traefik-69d9f94ff9-tht4x 1/1 Running 0 16m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/traefik LoadBalancer 10.106.40.190 10.0.1.43 9000:30443/TCP,80:31178/TCP,443:31727/TCP 16m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/traefik 2/2 2 2 18m
deployment.apps/whoami 1/1 1 1 18m
NAME DESIRED CURRENT READY AGE
replicaset.apps/traefik-69d9f94ff9 2 2 2 16m
Dashboard Access
Now that traefik is running, let’s get access set up. We’re going to set up a traefik-dashboard Argo App so we can have Argo CD install and update our dashboard setup.
In your configuration repository, create an argocd/apps/traefik-dashboard directory. Put all the following manifests in that directory.
Update DNS
It can take a couple of minutes for DNS to propagate after you make a change, so start that now and hopefully by the time you create the manifests and Argo installs them DNS will be up to date.
Find out what IP the LoadBalancer was assigned:
$ kubectl get -n traefik svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.106.40.190 10.9.8.7 9000:30443/TCP,80:31178/TCP,443:31727/TCP 18m
Add an A record in your DNS of traefik-dashboard.YOUR_DOMAIN that is set to the EXTERNAL-IP shown by kubectl.
Set up basic auth
We’re going to need a username and password.
# basic-auth.yaml
apiVersion: v1
kind: Secret
metadata:
name: dashboard-auth
namespace: traefik
type: Opaque
stringData:
# Generate the BASE64 string with echo $(htpasswd -nb user password) | base64
users: admin:BASE64_STRING
If you installed the sops operator, run sops encrypt -i basic-auth.yaml to encrypt it. If you didn’t, committing unencrypted secrets is a bad habit to get into, so go install sops and then encrypt basic-auth.yaml before you commit it to the repository.
Go ahead and kubectl apply -f basic-auth.yaml to that the secret is available when Argo installs the dashboard configuration.
traefik-gateway.yaml
We need to set up a Traefik Gatewayclass and a traefik-gateway Gateway to route traffic for the dashboard.
---
# traefik-gateway-class.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: traefik
spec:
controllerName: traefik.io/gateway-controller
---
# traefik-owned-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: traefik-gateway
namespace: traefik
spec:
gatewayClassName: traefik
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
- name: https
port: 443
protocol: HTTPS
allowedRoutes:
namespaces:
from: All
tls:
mode: Terminate
certificateRefs:
- name: wildcard-your-domain-tls
namespace: traefik
traefik-dashboard.yaml
Finally, we’re going to create a manifest to:
- Create a basic-auth
Middlewareresource to control access to the dashboard - An
IngressRouteto route traffic - An
HTTPRouteto redirect traffic fromhttps://traefik-dashboard.YOUR_DOMAINtohttps://traefik-dashboard.YOUR_DOMAIN/dashboard/so you don’t have to remember to add the/dashbaord.
# argocd/apps/traefik-configuration/traefik-dashboard.yaml
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: dashboard-auth
namespace: traefik
spec:
basicAuth:
secret: dashboard-auth
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard-api
namespace: traefik
spec:
entryPoints:
- websecure
routes:
- match: Host(`traefik-dashboard.YOUR_DOMAIN`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`))
kind: Rule
services:
- name: api@internal
kind: TraefikService
# This is the "Lock" that uses the Middleware above
middlewares:
- name: dashboard-auth
tls:
secretName: wildcard-yourdomain-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: traefik-dashboard-redirect
namespace: traefik
spec:
parentRefs:
- name: traefik-gateway
namespace: traefik
hostnames:
- "traefik-dashboard.YOUR_DOMAIN"
rules:
- matches:
- path:
type: Exact
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
path:
type: ReplaceFullPath
replaceFullPath: /dashboard/
statusCode: 301
Add an argo-config.yaml to trigger installation by ArgoCD
Commit all the yaml files and push them to github if you haven’t already. Now we can create an argo-config.yaml file to trigger Argo to install our manifests. The argo-config.yaml file must be in the same directory as traefik-cert.yaml, basic-auth.yaml, traefik-gateway.yaml and traefik-dashboard.yaml. The Argo CD git generator ApplicationSet only processes directories that contain an argo-config.yaml file.
# argo-config.yaml
app:
name: "traefik-configuration"
namespace: traefik
clusterName: YOURCLUSTERNAME
# Use the full URL for the destination
clusterUrl: https://kubernetes.default.svc
project: default
Add, commit and push argo-config.yaml. With the default configuration, Argo CD will detect the new app and start installing it in two to three minutes. You can force it to check right away by clicking Sync Apps or running
argocd repo get "$YOUR_CONFIG" --refresh
Update DNS
Once Argo finishes installing, find out what IP the LoadBalancer was assigned
$ kubectl get -n traefik svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
traefik LoadBalancer 10.106.40.190 10.9.8.7 9000:30443/TCP,80:31178/TCP,443:31727/TCP 18m
Update DNS for your domain with an the external IP.
Checking
Once Argo finishes installing your manifests, try going to traefik-dashboard.YOUR_DOMAIN in a browser. If DNS hasn’t propagated, you can check with curl anyway by setting the host header and basic auth information.
curl -vL -k -u admin:YOUR_ADMIN_PASSWORD -H "Host: traefik-dashboard.YOUR_DOMAIN" https://SERVICE_EXTERNAL_IP/dashboard/
What the cli flags do:
-u admin:YOUR_ADMIN_PASSWORD: The magic flag. It takes username:password and converts it into the required HTTP header.-k(or--insecure): The TLS certificate (which is for *.YOUR_DOMAIN) won’t match SERVICE_EXTERNAL_IP. This flag tells curl to ignore that mismatch.-v: Verbose mode. This lets you see the HTTP/1.1 200 OK and confirm the Authorization header is being sent.-L: Follows the redirect (though since you’re hitting/dashboard/directly, it likely won’t need to).
You should see something similar to
* Trying SERVICE_EXTERNAL_IP:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL Trust: peer verification disabled
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
* subject: CN=TRAEFIK DEFAULT CERT
* start date: Apr 4 23:11:05 2026 GMT
* expire date: Apr 4 23:11:05 2027 GMT
* issuer: CN=TRAEFIK DEFAULT CERT
* Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* SSL certificate verification failed, continuing anyway!
* Established connection to SERVICE_EXTERNAL_IP (SERVICE_EXTERNAL_IP port 443) from 10.11.12.13 port 59344
* using HTTP/2
* Server auth using Basic with user 'admin'
* [HTTP/2] [1] OPENED stream for https://SERVICE_EXTERNAL_IP/dashboard/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: traefik-dashboard.YOUR_DOMAIN]
* [HTTP/2] [1] [:path: /dashboard/]
* [HTTP/2] [1] [authorization: Basic FEEDBABEDEADBEEFFEEDBABEDEADBEEFFEEDBABE]
* [HTTP/2] [1] [user-agent: curl/8.18.0]
* [HTTP/2] [1] [accept: */*]
> GET /dashboard/ HTTP/2
> Host: traefik-dashboard.YOUR_DOMAIN
> Authorization: Basic FEEDBABEDEADBEEFFEEDBABEDEADBEEFFEEDBABE
> User-Agent: curl/8.18.0
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< content-security-policy: frame-src 'self' https://traefik.io https://*.traefik.io;
< content-type: text/html; charset=utf-8
< content-length: 1193
< date: Sat, 04 Apr 2026 23:15:25 GMT
<
<!DOCTYPE html><html><head><script>window.APIURL = "/api/"</script><title>Traefik</title><meta charset=utf-8><meta name=description content="Traefik UI"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/png href=./app-logo-128x128.png><link rel=icon type=image/png sizes=16x16 href=./icons/favicon-16x16.png><link rel=icon type=image/png sizes=32x32 href=./icons/favicon-32x32.png><link rel=icon type=image/png sizes=96x96 href=./icons/favicon-96x96.png><link rel=icon type=image/ico href=./icons/favicon.ico><link rel=apple-touch-icon href=./icons/apple-icon-152x152.png><link rel=apple-touch-icon sizes=152x152 href=./icons/apple-icon-152x152.png><link rel=apple-touch-icon sizes=167x167 href=./icons/apple-icon-167x167.png><link rel=apple-touch-icon sizes=180x180 href=./icons/apple-icon-180x180.png> <script type="module" crossorigin src="./assets/index-CLgUCYBL.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-jjiyEA_O.css">
* Connection #0 to host SERVICE_EXTERNAL_IP:443 left intact
</head><body><div id=q-app></div></body></html>HTTP CODE 200
Congratulations
You should now have:
- An Argo CD dual-source Application resource to install Traefik
- An Argo CD application using the git generator that
- Enables access to the Traefik dashboard
- Protects the dashboard with basic auth