Traefik & CRD & Let’s Encrypt

    This document is intended to be a fully working example demonstrating how to set up Traefik in Kubernetes, with the dynamic configuration coming from the , and TLS setup with Let’s Encrypt. However, for the sake of simplicity, we’re using docker image for the Kubernetes cluster setup.

    Please note that for this setup, given that we’re going to use ACME’s TLS-ALPN-01 challenge, the host you’ll be running it on must be able to receive connections from the outside on port 443. And of course its internet facing IP address must match the domain name you intend to use.

    In the following, the Kubernetes resources defined in YAML configuration files can be applied to the setup in two different ways:

    • the first, and usual way, is simply with the command.
    • the second, which can be used for this tutorial, is to directly place the files in the directory used by the k3s docker image for such inputs (/var/lib/rancher/k3s/server/manifests).

    Kubectl Version

    Our starting point is the docker-compose configuration file, to start the k3s cluster. You can start it with:

    1. server:
    2. image: rancher/k3s:v1.17.2-k3s1
    3. command: server --disable-agent --no-deploy traefik
    4. environment:
    5. - K3S_CLUSTER_SECRET=somethingtotallyrandom
    6. - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml
    7. - K3S_KUBECONFIG_MODE=666
    8. volumes:
    9. # k3s will generate a kubeconfig.yaml in this directory. This volume is mounted
    10. # on your host, so you can then 'export KUBECONFIG=/somewhere/on/your/host/out/kubeconfig.yaml',
    11. # in order for your kubectl commands to work.
    12. - /somewhere/on/your/host/out:/output
    13. # This directory is where you put all the (yaml) configuration files of
    14. # the Kubernetes resources.
    15. - /somewhere/on/your/host/in:/var/lib/rancher/k3s/server/manifests
    16. ports:
    17. - 6443:6443
    18. node:
    19. image: rancher/k3s:v1.17.2-k3s1
    20. privileged: true
    21. links:
    22. - server
    23. environment:
    24. - K3S_URL=https://server:6443
    25. - K3S_CLUSTER_SECRET=somethingtotallyrandom
    26. volumes:
    27. # this is where you would place a alternative traefik image (saved as a .tar file with
    28. # 'docker save'), if you want to use it, instead of the traefik:v2.4 image.
    29. - /sowewhere/on/your/host/custom-image:/var/lib/rancher/k3s/agent/images

    Cluster Resources

    Let’s now have a look (in the order they should be applied, if using kubectl apply) at all the required resources for the full setup.

    First, the definition of the IngressRoute and the Middleware kinds. Also note the RBAC authorization resources; they’ll be referenced through the serviceAccountName of the deployment, later on.

    1. apiVersion: apiextensions.k8s.io/v1beta1
    2. kind: CustomResourceDefinition
    3. metadata:
    4. name: ingressroutes.traefik.containo.us
    5. spec:
    6. group: traefik.containo.us
    7. version: v1alpha1
    8. names:
    9. kind: IngressRoute
    10. plural: ingressroutes
    11. singular: ingressroute
    12. scope: Namespaced
    13. ---
    14. apiVersion: apiextensions.k8s.io/v1beta1
    15. kind: CustomResourceDefinition
    16. metadata:
    17. name: middlewares.traefik.containo.us
    18. spec:
    19. group: traefik.containo.us
    20. version: v1alpha1
    21. names:
    22. kind: Middleware
    23. plural: middlewares
    24. singular: middleware
    25. scope: Namespaced
    26. ---
    27. apiVersion: apiextensions.k8s.io/v1beta1
    28. kind: CustomResourceDefinition
    29. metadata:
    30. name: ingressroutetcps.traefik.containo.us
    31. spec:
    32. group: traefik.containo.us
    33. version: v1alpha1
    34. names:
    35. kind: IngressRouteTCP
    36. plural: ingressroutetcps
    37. singular: ingressroutetcp
    38. scope: Namespaced
    39. ---
    40. apiVersion: apiextensions.k8s.io/v1beta1
    41. kind: CustomResourceDefinition
    42. metadata:
    43. name: ingressrouteudps.traefik.containo.us
    44. spec:
    45. group: traefik.containo.us
    46. version: v1alpha1
    47. names:
    48. kind: IngressRouteUDP
    49. plural: ingressrouteudps
    50. singular: ingressrouteudp
    51. scope: Namespaced
    52. ---
    53. apiVersion: apiextensions.k8s.io/v1beta1
    54. kind: CustomResourceDefinition
    55. metadata:
    56. name: tlsoptions.traefik.containo.us
    57. spec:
    58. group: traefik.containo.us
    59. version: v1alpha1
    60. names:
    61. kind: TLSOption
    62. singular: tlsoption
    63. ---
    64. apiVersion: apiextensions.k8s.io/v1beta1
    65. kind: CustomResourceDefinition
    66. metadata:
    67. name: tlsstores.traefik.containo.us
    68. spec:
    69. group: traefik.containo.us
    70. version: v1alpha1
    71. names:
    72. kind: TLSStore
    73. plural: tlsstores
    74. singular: tlsstore
    75. scope: Namespaced
    76. ---
    77. apiVersion: apiextensions.k8s.io/v1beta1
    78. kind: CustomResourceDefinition
    79. metadata:
    80. name: traefikservices.traefik.containo.us
    81. spec:
    82. group: traefik.containo.us
    83. version: v1alpha1
    84. names:
    85. kind: TraefikService
    86. plural: traefikservices
    87. singular: traefikservice
    88. scope: Namespaced
    89. ---
    90. apiVersion: apiextensions.k8s.io/v1beta1
    91. kind: CustomResourceDefinition
    92. metadata:
    93. name: serverstransports.traefik.containo.us
    94. spec:
    95. group: traefik.containo.us
    96. version: v1alpha1
    97. names:
    98. kind: ServersTransport
    99. plural: serverstransports
    100. singular: serverstransport
    101. scope: Namespaced
    102. ---
    103. kind: ClusterRole
    104. apiVersion: rbac.authorization.k8s.io/v1beta1
    105. metadata:
    106. name: traefik-ingress-controller
    107. rules:
    108. - apiGroups:
    109. - ""
    110. resources:
    111. - services
    112. - endpoints
    113. - secrets
    114. verbs:
    115. - get
    116. - list
    117. - watch
    118. - apiGroups:
    119. - extensions
    120. - networking.k8s.io
    121. resources:
    122. - ingresses
    123. - ingressclasses
    124. verbs:
    125. - get
    126. - list
    127. - watch
    128. - apiGroups:
    129. - extensions
    130. resources:
    131. - ingresses/status
    132. verbs:
    133. - update
    134. - apiGroups:
    135. - traefik.containo.us
    136. resources:
    137. - middlewares
    138. - ingressroutes
    139. - traefikservices
    140. - ingressroutetcps
    141. - ingressrouteudps
    142. - tlsoptions
    143. - tlsstores
    144. - serverstransports
    145. verbs:
    146. - get
    147. - list
    148. - watch
    149. ---
    150. kind: ClusterRoleBinding
    151. apiVersion: rbac.authorization.k8s.io/v1beta1
    152. metadata:
    153. name: traefik-ingress-controller
    154. roleRef:
    155. apiGroup: rbac.authorization.k8s.io
    156. kind: ClusterRole
    157. name: traefik-ingress-controller
    158. subjects:
    159. - kind: ServiceAccount
    160. namespace: default

    Then, the services. One for Traefik itself, and one for the app it routes for, i.e. in this case our demo HTTP server: .

    1. apiVersion: v1
    2. metadata:
    3. namespace: default
    4. name: traefik-ingress-controller
    5. ---
    6. kind: Deployment
    7. apiVersion: apps/v1
    8. metadata:
    9. namespace: default
    10. name: traefik
    11. labels:
    12. app: traefik
    13. spec:
    14. replicas: 1
    15. selector:
    16. matchLabels:
    17. app: traefik
    18. template:
    19. metadata:
    20. labels:
    21. app: traefik
    22. spec:
    23. serviceAccountName: traefik-ingress-controller
    24. containers:
    25. - name: traefik
    26. image: traefik:v2.4
    27. args:
    28. - --api.insecure
    29. - --accesslog
    30. - --entrypoints.web.Address=:8000
    31. - --entrypoints.websecure.Address=:4443
    32. - --providers.kubernetescrd
    33. - --certificatesresolvers.myresolver.acme.tlschallenge
    34. - --certificatesresolvers.myresolver.acme.email=foo@you.com
    35. - --certificatesresolvers.myresolver.acme.storage=acme.json
    36. # Please note that this is the staging Let's Encrypt server.
    37. # Once you get things working, you should remove that whole line altogether.
    38. - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
    39. ports:
    40. - name: web
    41. containerPort: 8000
    42. - name: websecure
    43. containerPort: 4443
    44. - name: admin
    45. containerPort: 8080
    46. ---
    47. kind: Deployment
    48. apiVersion: apps/v1
    49. metadata:
    50. namespace: default
    51. name: whoami
    52. labels:
    53. app: whoami
    54. spec:
    55. replicas: 2
    56. selector:
    57. matchLabels:
    58. app: whoami
    59. template:
    60. metadata:
    61. labels:
    62. app: whoami
    63. spec:
    64. containers:
    65. - name: whoami
    66. image: traefik/whoami
    67. ports:
    68. - name: web
    69. containerPort: 80

    Now, as an exception to what we said above, please note that you should not let the ingressRoute resources below be applied automatically to your cluster. The reason is, as soon as the ACME provider of Traefik detects we have TLS routers, it will try to generate the certificates for the corresponding domains. And this will not work, because as it is, our Traefik pod is not reachable from the outside, which will make the ACME TLS challenge fail. Therefore, for the whole thing to work, we must delay applying the ingressRoute resources until we have port-forwarding set up properly, which is the next step.

    1. kubectl port-forward --address 0.0.0.0 service/traefik 8000:8000 8080:8080 443:4443 -n default

    Also, and this is out of the scope if this guide, please note that because of the privileged ports limitation on Linux, the above command might fail to listen on port 443. In which case you can use tricks such as elevating caps of kubectl with setcaps, or using authbind, or setting up a NAT between your host and the WAN. Look it up.

    We can now finally apply the actual ingressRoutes, with:

    1. apiVersion: traefik.containo.us/v1alpha1
    2. kind: IngressRoute
    3. metadata:
    4. name: simpleingressroute
    5. namespace: default
    6. spec:
    7. entryPoints:
    8. - web
    9. routes:
    10. - match: Host(`your.example.com`) && PathPrefix(`/notls`)
    11. kind: Rule
    12. services:
    13. - name: whoami
    14. port: 80
    15. ---
    16. apiVersion: traefik.containo.us/v1alpha1
    17. kind: IngressRoute
    18. metadata:
    19. name: ingressroutetls
    20. namespace: default
    21. spec:
    22. entryPoints:
    23. - websecure
    24. routes:
    25. - match: Host(`your.example.com`) && PathPrefix(`/tls`)
    26. kind: Rule
    27. services:
    28. - name: whoami
    29. port: 80
    30. tls:
    31. certResolver: myresolver

    Give it a few seconds for the ACME TLS challenge to complete, and you should then be able to access your whoami pod (routed through Traefik), from the outside. Both with or (just for fun, do not do that in production) without TLS:

    1. curl [-k] https://your.example.com/tls