Let's Encrypt Certificates for Self-Hosted Services

- Hariharan

TLS certificates are an important security tool for both, services accessible over the internet and services hosted locally in a home lab. If you are just starting out, self-signed certificates can be a very good option. If you own the domain, you could get valid TLS certificates. Using the dnsChallenge option in Traefik, you can do it without having to expose any services to the internet.

Here I have documented the steps to achieve this on a K3S cluster.

ACME Challenges

ACME standard has different challenge types that can be used to prove that you control the domain. There is a very good write-up about ACME challenges on the Let’s Encrypt site.

Most of these challenge types require the service / reverse proxy (in our case traefik) to be accessible by Let’s Encrypt. This would mean opening up ports on the router if you are self-hosting on your home lab.

DNS challenge is great for such cases. You give the ACME client API token to modify your DNS entries and it uses that to add a TXT record. This record is then queried by Let’s Encrypt to authenticate and issue the certificate.

HelmChartConfig

K3S uses a CRD called HelmChartConfig to configure packaged components like traefik. This CRD can be used to pass any arguments that can be passed using the helm CLI tool.
I created this config to use cloudflare as the DNS provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    env:
      - name: CF_API_EMAIL
        valueFrom:
          secretKeyRef:
            key: email
            name: traefik-cloudflare-secrets
      - name: CF_DNS_API_TOKEN
        valueFrom:
          secretKeyRef:
            key: apikey
            name: traefik-cloudflare-secrets
    additionalArguments:
      - "--log.level=DEBUG"
      - "[email protected]"
      - "--certificatesresolvers.le.acme.storage=/data/acme.json"
      - "--certificatesresolvers.le.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.le.acme.dnsChallenge.provider=cloudflare"
      - "--certificatesResolvers.le.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53"    

Cloudflare email and API key are passed as secrets. It is also important to pass the email in --certificatesresolvers.le.acme.email additional argument. This example uses the Let’s Encrypt staging server (--certificatesresolvers.le.acme.caServer) to safely test because the production server has rate limits.

The Cloudflare API token needs to have the following permissions:

You can also scope the token to use a single zone.

Apply this config using kubectl.

Ingress

Once the HelmChartConfig is deployed, you can create an Ingress.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: default
  annotations:
    ingressClassName: traefik
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.tls.certresolver: le
spec:
  rules:
    - host: "nginx.example.com"
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service: 
              name: nginx-svc
              port: 
                number: 80

You will need to point this domain name to the Ingress IP. I use Pi-Hole, so I added a local DNS entry. It might take a few seconds to retrieve the certificates if they are being issued for the first time. Now you should have valid TLS certificates from Let’s Encrypt even for fully locally hosted services.