Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Use Traefik and cert-manager to serve a secured website

profile picture
Author
Matthias Ludwig
Published
2020-01-28
Time to read
16 minutes reading time

Introduction

This tutorial will guide you through the installation of Traefik on top of a fresh kubernetes cluster. With help of a simple nginx service different solutions will be shown on how to serve the page with a self-signed certificate, a let's encrypt staging and production certificate. All certificate stuff is done with cert-manager.

You should be familiar with basic kubernetes usage and have a working k8s cluster on hand. If you do not have a cluster yet, simply build your own in a few minutes following this tutorial.

Prerequisites

  • A working kubernetes cluster (build your own with this tutorial)
  • A floating IP pointing to your cluster or the public IP of one of your nodes (<10.0.0.1> in this tutorial; the <> are not part of the IP)
  • Familiarity with linux and working on the shell
  • kubectl command line tool installed
  • An email address for usage with Let's Encrypt, this tutorial uses: mail@example.com

Recommended

*This tutorial was tested on Ubuntu 18.04 Hetzner Cloud server and Kubernetes version v1.15.7 and v1.16.4, Ubuntu 18.04 was used as local machine.

All steps can be executed as is on the machine where kubectl is installed. Most steps are using heredoc notation to prevent config file creation. The used kubernetes services will use different syntax, where possible. This shall show you different possibilities.

Step 1 - Deploy Traefik as ingress controller

The following setup is based on the official setup instructions.

Step 1.1 - Create the required RBAC roles

Traefik needs some kubernetes roles configured, before the setup of the service can be done.

kubectl apply -f https://raw.githubusercontent.com/containous/traefik/v1.7/examples/k8s/traefik-rbac.yaml

Step 1.2 - Setup Traefik

As stated in the docs, traefik can be installed as Controller or a DaemonSet. In this tutorial we use the DaemonSet deployment. If you have a cluster consisting of multiple nodes (master or workers), exactly one traefik pod will be created on every node without additional configuration.

!!! If you just have a single node cluster, take care to taint the master node to accept pods: kubectl taint nodes --all node-role.kubernetes.io/master-

You can define the traefik configuration with cli flags or using a toml file. The toml file is easier to handle for complex setups. Due to simplicity, cli flags are used in this tutorial.

!!! The config uses the type LoadBalancer instead of NodePort. This should work for bare metall setups with metallb like in this tutorial. If you're using a cloud provider with an external load balancer, you might use NodePort instead.

!!! The used traefik configuration is not production ready! You should use http basic auth to secure the dashboard in combination with automatic https redirection. For a production environment it's also recommended to setup traefik into a seperate namespace.

cat << EOF | kubectl apply -f -
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: traefik-ingress-controller
  namespace: kube-system
---
kind: DaemonSet
apiVersion: extensions/v1beta1
metadata:
  name: traefik-ingress-controller
  namespace: kube-system
  labels:
    k8s-app: traefik-ingress-lb
spec:
  template:
    metadata:
      labels:
        k8s-app: traefik-ingress-lb
        name: traefik-ingress-lb
    spec:
      serviceAccountName: traefik-ingress-controller
      terminationGracePeriodSeconds: 60
      containers:
      - image: traefik:v1.7
        name: traefik-ingress-lb
        ports:
        - name: http
          containerPort: 80
          hostPort: 80
        - name: https
          containerPort: 443
          hostPort: 443
        - name: dashboard
          containerPort: 8080
          hostPort: 8080
        securityContext:
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE
        args:
        - --api
        - --accesslog
        - --logLevel=INFO
        - --kubernetes
        - --defaultentrypoints=http,https
        - --entrypoints=Name:https Address::443 TLS
        - --entrypoints=Name:http Address::80
---
kind: Service
apiVersion: v1
metadata:
  name: traefik-ingress-service
  namespace: kube-system
spec:
  selector:
    k8s-app: traefik-ingress-lb
  ports:
    - protocol: TCP
      port: 80
      name: http
    - protocol: TCP
      port: 443
      name: https
    - protocol: TCP
      port: 8080
      name: dashboard
  type: LoadBalancer
EOF

The above config publishes the Traefik dashboard on port 8080 and publishes the default ports used for HTTP traffic: 80 and 443.

Step 1.3 - Validate setup

Check if all pods are Running.

kubectl -n kube-system get pod
# Output:
NAME                               READY   STATUS    RESTARTS   AGE
coredns-5d4dd4b4db-frkpb           1/1     Running   0          12m
coredns-5d4dd4b4db-t8z2s           1/1     Running   0          12m
etcd-k1                            1/1     Running   1          11m
kube-apiserver-k1                  1/1     Running   1          6m49s
kube-controller-manager-k1         1/1     Running   1          11m
kube-flannel-ds-amd64-hbk58        1/1     Running   0          5m47s
kube-proxy-dmng9                   1/1     Running   1          12m
kube-scheduler-k1                  1/1     Running   1          12m
traefik-ingress-controller-z8xdn   1/1     Running   0          36s

Get the EXTERNAL-IP assigned to the traefik-ingress-service.

kubectl -n kube-system get service
# Output:
NAME                      TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                                                    AGE
kube-dns                  ClusterIP      10.96.0.10     <none>        53/UDP,53/TCP,9153/TCP                                     16m
traefik-ingress-service   LoadBalancer   10.109.23.93   10.0.0.1    80:31681/TCP,443:31856/TCP,8080:32684/TCP                  4m23s

You can also show the logs for the traefik pod using its given label.

  • logs for a specific pod

    kubectl -n kube-system logs -f $(kubectl -n kube-system get pods -l k8s-app=traefik-ingress-lb  -o jsonpath='{.items[0].metadata.name}')
  • logs for all pods, useful if you have multiple nodes

    kubectl -n kube-system logs -f -l k8s-app=traefik-ingress-lb

Step 1.4 - Open Traefik dashboard

The Service you created, should have got the EXTERNAL-IP of the server. This should be the same as our floating IP or the public IP of the server, depending on your setup. A LoadBalancer service binds to some random port(s) as you see in column PORT(S). Knowing the port and the IP is all you need to open the Dashboard (http://<10.0.0.1>:32684).

Step 2 - Host a simple nginx service

In the next step we will deploy a simple nginx service and will publish it on your domain.

Step 2.1 - Prepare the domain

If you have your own domain (in this tutorial: <example.com>), take care to create an A-Record in the DNS settings to your cluster's floating IP (<10.0.0.1>) or the server's public IP. Depending on your provider's TTL (Time to Live in seconds) it can take some time, that the DNS changes are propagated. Because your local system DNS will not update as frequently as the server settings, it's also a good idea to do the check on the server.

You can check if your domain points to the right IP with:

host <example.com>
# Outputs:
example.com has address 10.0.0.1

Store your domain as environmental variable for further usage:

DOMAIN=<example.com>

Step 2.2 - Deploy nginx service

We will now use the configured DOMAIN directly inside the config. As you can see, the service will be created in a seperate namespace. This is the recommended way.

cat << EOF | kubectl apply -f -
---
apiVersion: v1
kind: Namespace
metadata:
  name: testing
spec:
  finalizers:
  - kubernetes
status:
  phase: Active
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx
  namespace: testing
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - name: http
          containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: testing
spec:
  selector:
    app: nginx
  ports:
  - name: http
    port: 80
    targetPort: 80
  type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
  name: nginx
  namespace: testing
spec:
  rules:
  - host: ${DOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80
EOF

Check that the nginx pod is created and Running.

kubectl -n testing get pod
NAME                     READY   STATUS    RESTARTS   AGE
nginx-74f6bc9c7c-bvjh5   1/1     Running   0          9s

After applying the changes, open <example.com> in your browser. You should see a very basic html page now.

If you open the pods log and reload the domain in the browser, you'll see the requests in the nginx logs.

kubectl -n testing logs -f $(kubectl -n testing get pods -l app=nginx -o jsonpath='{.items[0].metadata.name}')

Step 3 - Deploy HTTPS

For a production ready page, you should always use https (HTTP over TLS = encrypted http) for security reasons! In days of Let's Encrypt this is a very easy task. No complicated certificate handling anymore.

Traefik can use Let's Encrypt on it's own, but this is not the recommended way and might fail in some cases. A much better way (in the sense of do-one-thing-and-to-it-right) is the usage of cert-manager.

Step 3.1 - Setup cert-manager

kubectl create namespace cert-manager
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.12.0/cert-manager.yaml

After a few seconds all pods should be in RUNNING state.

!!! The watch command is not a special kubernetes command. It's simply a standard unix command that executes the following command every second.

watch kubectl -n cert-manager get pods
#Outputs:
NAME                                      READY   STATUS    RESTARTS   AGE
cert-manager-66c8bc8b67-5qcb4             1/1     Running   0          24s
cert-manager-cainjector-df4dc78cd-s56zv   1/1     Running   0          24s
cert-manager-webhook-5f78ff89bc-hr9ck     1/1     Running   0          24s

cert-manager is now ready to use. In the next steps we will tryout different ways to create testing and production ready certificates.

Step 3.2 - Create self-signed certificate

For some use cases, e.g. internal or test usage, you can easily create a self-signed certificate with cert-manager. To configure a certificate with cert-manager you always need an Issuer, ClusterIssuer and the Certificate ressource.

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
  name: selfsigned
  namespace: testing
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: ${DOMAIN}-cert
  namespace: testing
spec:
  commonName: ${DOMAIN}
  secretName: ${DOMAIN}-cert
  issuerRef:
    name: selfsigned
EOF

You can check the status of the Certificate very easily.

  • short status

    kubectl -n testing get certificate
    # Outputs:
    10.0.0.1.example.com-cert   True    10.0.0.1.example.com-cert   47s
  • full info and status

    kubectl -n testing describe certificate <10.0.0.1>.example.com-cert
    # Outputs:
    ...
    Events:
      Type    Reason        Age   From          Message
      ----    ------        ----  ----          -------
      Normal  GeneratedKey  94s   cert-manager  Generated a new private key
      Normal  Requested     94s   cert-manager  Created new CertificateRequest resource "10.0.0.1.example.com-cert-2241091300"
      Normal  Issued        94s   cert-manager  Certificate issued successfully

Our nginx service doesn't know about the certificate right now. Reconfigure the Ingress to change this.

cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
  name: nginx
  namespace: testing
spec:
  rules:
  - host: ${DOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80
  tls:
  - hosts:
    - ${DOMAIN}
    secretName: ${DOMAIN}-cert
EOF

If you open https://<example.com> in your browser, you have to accept the certificate first. That's because your certificate is not signed by an official, trusted Certificate Authority (CA). After adding the exception, you should see the default nginx page, but secured by https.

Step 3.3 - Create a staging certificate

"Let's Encrypt is a non-profit certificate authority run by Internet Security Research Group (ISRG) that provides X.509 certificates for Transport Layer Security (TLS) encryption at no charge. The certificate is valid for 90 days, during which renewal can take place at any time. The offer is accompanied by an automated process designed to overcome manual creation, validation, signing, installation, and renewal of certificates for secure websites." (Wikimedia)

The process behind Let`s Encrypt is very easy:

  • check if the requested domain points to the same IP the request comes from
  • check for some special files on the pointing server
  • if one of the conditions is not met, refuse the request

A staging certificate is for testing purposes only and has to be accepted manually like a self-signed certificate. Take care, that your domain is configured properly! Please replace the email with a proper mail address!

Issuer

To get a certificate from Let's Encrypt, you need to setup an Issuer or ClusterIssuer. A Issuer is valid for the current namespace, a ClusterIssuer does not depend on the namespace.

# Please replace mail with your mail
YOUR_MAIL_ADDRESS=mail@example.com

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    # You must replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: ${YOUR_MAIL_ADDRESS}
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Secret resource used to store the account's private key.
      name: staging-issuer-account-key
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: traefik
EOF

Get the status with describe or simple status with get.

# check status
kubectl describe clusterissuer letsencrypt-staging
# Outputs:
Status:
  Acme:
    Last Registered Email:  mail@example.com
    Uri:                    https://acme-staging-v02.api.letsencrypt.org/acme/acct/12082973
  Conditions:
    Last Transition Time:  2020-01-13T15:40:13Z
    Message:               The ACME account was registered with the ACME server
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

Certificate

Take care to configure your domain properly like described in Step 2.

The certificates content is stored in a secret. To create a new certificate, we have to delete this secret. Also delete the certificate, otherwise cert-manager will create a new one instantly. You have to add a label to your nginx ingress cert-manager.io/cluster-issuer: letsencrypt-staging. Otherwise cert-manager does not know which issuer to use.

kubectl -n testing delete certificate "${DOMAIN}-cert"
kubectl -n testing delete secret "${DOMAIN}-cert"
cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
    cert-manager.io/cluster-issuer: letsencrypt-staging
  name: nginx
  namespace: testing
spec:
  rules:
  - host: ${DOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80
  tls:
  - hosts:
    - ${DOMAIN}
    secretName: ${DOMAIN}-cert
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: ${DOMAIN}-cert
  namespace: testing
spec:
  commonName: ${DOMAIN}
  secretName: ${DOMAIN}-cert
  issuerRef:
    name: letsencrypt-staging
EOF

The request takes about 30 seconds. You can check the status continuously with the bash command watch.

watch kubectl -n testing describe certificate "${DOMAIN}-cert"
# Output
Status:
  Conditions:
    Last Transition Time:  2020-01-13T16:06:33Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2020-04-12T15:06:33Z
Events:
  Type    Reason        Age    From          Message
  ----    ------        ----   ----          -------
  Normal  GeneratedKey  2m26s  cert-manager  Generated a new private key
  Normal  Requested     2m26s  cert-manager  Created new CertificateRequest resource "10.0.0.1.xip.io-cert-4183194560"
  Normal  Issued        2m     cert-manager  Certificate issued successfully

cert-manager outputs lots of stuff during this operation. If something goes wrong (e.g. no certificate after a minute or two), you can check the logs with:

  • static log

    kubectl -n cert-manager logs $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}')
  • live log

    kubectl -n cert-manager logs -f --tail 20 $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}')

Open in browser

If you open or reload https://example.com or https://$DOMAIN in your browser, the certificate will not be trusted, comparable to the self-signed certificate. The name differs now is the name of the issuer Fake LE Intermediate X1 instead of cert-manager.

Step 3.4 - Create a production certificate

If everything worked well in the last steps, you can continue to obtain a production ready Let's Encrypt certificate. A production certificate is signed by an official CA and will be trusted out of the box by all mainstream browsers (e.g. Chrome/Chromium, Firefox, IE, Edge, Safari, Opera). The resulting certificate is valid for 90 days. cert-manager renews the certificates every 30 days on its own, doing so the certificate should never expire.

!!! To force a renewal, simply delete the secret containing the certificate: kubectl -n testing delete secret "${DOMAIN}-cert".

If you are using your own domain, the process should work.

Issuer

YOUR_MAIL_ADDRESS=mail@example.com

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    # You must replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: ${YOUR_MAIL_ADDRESS}
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      # Secret resource used to store the account's private key.
      name: letsencrypt-issuer-account-key
    # Add a single challenge solver, HTTP01 using nginx
    solvers:
    - http01:
        ingress:
          class: traefik
EOF

Check status

kubectl describe clusterissuer letsencrypt
# Outputs:
Status:
  Acme:
    Last Registered Email:  mail@example.com
    Uri:                    https://acme-v02.api.letsencrypt.org/acme/acct/75756861
  Conditions:
    Last Transition Time:  2020-01-13T17:08:07Z
    Message:               The ACME account was registered with the ACME server
    Reason:                ACMEAccountRegistered
    Status:                True
    Type:                  Ready
Events:                    <none>

Certificate

Again, drop the Certificate and the corresponding Secret.

kubectl -n testing delete certificate "${DOMAIN}-cert"
kubectl -n testing delete secret "${DOMAIN}-cert"

DOMAIN=example.com

cat << EOF | kubectl apply -f -
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.ingress.kubernetes.io/rule-type: "PathPrefixStrip"
    cert-manager.io/cluster-issuer: letsencrypt
  name: nginx
  namespace: testing
spec:
  rules:
  - host: ${DOMAIN}
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80
  tls:
  - hosts:
    - ${DOMAIN}
    secretName: ${DOMAIN}-cert
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: ${DOMAIN}-cert
  namespace: testing
spec:
  commonName: ${DOMAIN}
  secretName: ${DOMAIN}-cert
  issuerRef:
    name: letsencrypt
EOF

Again, check the status of the request and check the logs of cert-manager after a minute if the certificate doesn't show Status: True.

watch kubectl -n testing describe certificate "${DOMAIN}-cert"
# Output
Status:
  Conditions:
    Last Transition Time:  2020-01-13T16:06:33Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2020-04-12T15:06:33Z
Events:
  Type    Reason        Age    From          Message
  ----    ------        ----   ----          -------
  Normal  GeneratedKey  2m26s  cert-manager  Generated a new private key
  Normal  Requested     2m26s  cert-manager  Created new CertificateRequest resource "example.com-cert-4183194560"
  Normal  Issued        2m     cert-manager  Certificate issued successfully
kubectl -n cert-manager logs -f --tail 10 $(kubectl -n cert-manager get pod -l app=cert-manager -o jsonpath='{.items[0].metadata.name}')

Conclusion

This tutorial has shown you how to setup traefik as ingress controller on a plain kubernetes cluster. Additionally you should be able to secure a deployed webservice with a self-signed TLS certificate or one from Let's Encrypt.

!!! The used traefik configuration is not production ready! You should use (at least) http basic auth to secure the dashboard in combination with automatic https redirection.

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue

Discover our

Dedicated Servers

Configure your dream server. Top performance with an excellent connection at an unbeatable price!

Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more