Traefik Ingres with Mutual TLS in Kubernetes

February 24, 2023
Sergio Rua

Introduction

At Digitalis we plan deployments with a security-first mentality. We have many customers in the financial sector and we adhere to PCI-DSS. We encrypt everything, both in transit and at rest. And sometimes we need to go the extra mile and ensure only certain applications are able to connect to the https endpoints.

This is why we use Mutual TLS (mTLS) for some ingresses.

Traefik vs NGINX

We use both on most deployments but we tend to prefer Traefik for outside load balancers because it offers more routing configurations that we often require.

HashiCorp Vault

We extensible use HashiCorp Vault. In the last couple of years, it has become core and essential in all our customer-managed deployments. As we need to issue SSL certificates to both clients and servers, the vault pki support fits nicely within our needs.

We simply configure cert-manager to issue SSL certificates via Vault. The clients can also request and get their signed SSL certificates to connect to the server. See below a sample terraform code to configure cert-manager is as follows:

resource "helm_release" "certs" {
  name       = "jetstack"
  repository = "https://charts.jetstack.io"
  chart      = "cert-manager"
  version    = "v1.11.0"

  create_namespace = true
  namespace        = "cert-manager"
  wait             = true
  wait_for_jobs    = true
}

resource "kubernetes_service_account" "vault_issuer" {
  metadata {
    name      = "vault-issuer"
    namespace = "cert-manager"
    annotations = {
      "kubernetes.io/service-account.name" = "vault-issuer"
    }
  }
  depends_on = [
    helm_release.certs,
  ]
}

resource "kubernetes_secret" "vault_issuer" {
  metadata {
    name      = "vault-issuer-token"
    namespace = "cert-manager"
    annotations = {
      "kubernetes.io/service-account.name" = kubernetes_service_account.vault_issuer.metadata[0].name
    }
  }
  type = "kubernetes.io/service-account-token"
}

resource "kubectl_manifest" "vault_issuer" {
  yaml_body = yamlencode({

    apiVersion = "cert-manager.io/v1"
    kind       = "ClusterIssuer"
    metadata = {
      name      = "vault-issuer"
      namespace = "cert-manager"
    }
    spec = {
      vault = {
        auth = {
          kubernetes = {
            mountPath = "/v1/auth/kubernetes"
            # you'll need this role in Vault with permssions to issue
            # and sign ssl certs
            role      = "vault-issuer"
            secretRef = {
              key  = "token"
              name = kubernetes_secret.vault_issuer.metadata[0].name
            }
          }
        }
        # FIXME: add here your vault config
        path   = "pki/sign/my-domain"
        server = "https://vault:8200"
      }
    }
  })
  depends_on = [
    helm_release.certs,
    kubernetes_secret.vault_issuer,
  ]
}

Traefik

Now with cert-manager up and running, the next part is to deploy your application with the mTLS configuration.

For the example below, let’s assume I’m deploying an app called httpbin for the domain httpbin.my-domain.com

We need two configuration objects, one for the ingress and one for TLSoptions. See below the most important settings.

  • cert-manager.io/cluster-issuer tells the ingress controller how to request the SSL certs via cert-manager and
  • cert-manager.io/common-name the domain name to use. The SSL certs will be saved into a Kubernetes secret called secretName. This is also used by the TLSoptions configuration.
  • traefik.ingress.kubernetes.io/router.tls.options points to the mTLS configuration to use
  • clientAuthType sets the ingress to only allow connections with the right SSL certificates

Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: vault-issuer
    cert-manager.io/common-name: httpbin.my-domain.com
    cert-manager.io/private-key-size: "4096"
    external-dns.alpha.kubernetes.io/hostname: httpbin.my-domain.com
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls.options: default-mtls@kubernetescrd
  name: httpbin
  namespace: default
spec:
  ingressClassName: traefik
  rules:
  - host: httpbin.my-domain.com
    http:
      paths:
      - backend:
          service:
            name: httpbin
            port:
              number: 8080
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - httpbin.my-domain.com
    secretName: httpbin-certs

TLSOptions

apiVersion: traefik.containo.us/v1alpha1
kind: TLSOption
metadata:
  name: mtls
  namespace: default
spec:
  clientAuth:
    clientAuthType: RequireAndVerifyClientCert
    secretNames:
    - httpbin-certs
  minVersion: VersionTLS12

Gotchas

It took me a long time to find the documentation of the syntax for traefik.ingress.kubernetes.io/router.tls.options. See the example below:

traefik.ingress.kubernetes.io/router.tls.options: {namespace}-{resource-name}@kubernetescrd

Conclusion

Security can never be underestimated. TLS is challenging and mutual TLS even more. That’s why having the right automation and CI/CD is fundamental to success. Be it with Apache Cassandra or web services, we implement it always: it’s our default.

Cert-manager and HashiCorp Vault are core applications to me and for good reason. They work fantastically well and I use them as part of every single deployment I implement.

Subscribe to newsletter

Subscribe to receive the latest blog posts to your inbox every week.

By subscribing you agree to with our Privacy Policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Ready to Transform 

Your Business?