|
| 1 | +# Securing Traffic using Let's Encrypt and Cert-Manager |
| 2 | + |
| 3 | +Securing client server communication is a crucial part of modern application architectures. One of the most important |
| 4 | +steps in this process is implementing HTTPS (HTTP over TLS/SSL) for all communications. This encrypts the data |
| 5 | +transmitted between the client and server, preventing eavesdropping and tampering. To do this, you need an SSL/TLS |
| 6 | +certificate from a trusted Certificate Authority (CA). However, issuing and managing certificates can be a complicated |
| 7 | +manual process. Luckily, there are many services and tools available to simplify and automate certificate issuance and |
| 8 | +management. |
| 9 | + |
| 10 | +This guide will demonstrate how to: |
| 11 | + |
| 12 | +- Configure HTTPS for your application using a [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/). |
| 13 | +- Use [Let’s Encrypt](https://letsencrypt.org) as the Certificate Authority (CA) issuing the TLS certificate. |
| 14 | +- Use [cert-manager](https://cert-manager.io) to automate the provisioning and management of the certificate. |
| 15 | + |
| 16 | +## Prerequisities |
| 17 | + |
| 18 | +1. Administrator access to a Kubernetes cluster. |
| 19 | +2. [Helm](https://helm.sh) and [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) must be installed locally. |
| 20 | +3. Deploy NGINX Kubernetes Gateway (NKG) following the [deployment instructions](/docs/installation.md). |
| 21 | +4. A DNS resolvable domain name is required. It must resolve to the public endpoint of the NKG deployment, and this |
| 22 | + public endpoint must be an external IP address or alias accessible over the internet. The process here will depend |
| 23 | + on your DNS provider. This DNS name will need to be resolvable from the Let’s Encrypt servers, which may require |
| 24 | + that you wait for the record to propagate before it will work. |
| 25 | + |
| 26 | +## Overview |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | +The diagram above shows a simplified representation of the cert-manager ACME Challenge and certificate issuance process |
| 31 | +using Gateway API. Please note that not all of the Kubernetes objects created in this process are represented in |
| 32 | +this diagram. |
| 33 | + |
| 34 | +At a high level, the process looks like this: |
| 35 | + |
| 36 | +1. We deploy cert-manager and create a ClusterIssuer which specifies Let’s Encrypt as our CA and Gateway as our ACME |
| 37 | + HTTP01 Challenge solver. |
| 38 | +2. We create a Gateway resource for our domain (cafe.example.com) and configure cert-manager integration using an |
| 39 | + annotation. |
| 40 | +3. This kicks off the certificate issuance process – cert-manager contacts Let’s Encrypt to obtain a certificate, and |
| 41 | + Let’s Encrypt starts the ACME challenge. As part of this challenge, a temporary HTTPRoute resource is created by |
| 42 | + cert-manager which directs the traffic through NKG to verify we control the domain name in the certificate request. |
| 43 | +4. Once the domain has been verified, the certificate is issued. Cert-manager stores the keypair in a Kubernetes secret |
| 44 | + that is referenced by the Gateway resource. As a result, NGINX is configured to terminate HTTPS traffic from clients |
| 45 | + using this signed keypair. |
| 46 | +5. We deploy our application and our HTTPRoute which defines our routing rules. The routing rules defined configure |
| 47 | + NGINX to direct requests to https://cafe.example.com/coffee to our coffee-app application, and to use the https |
| 48 | + Listener defined in our Gateway resource. |
| 49 | +6. When the client connects to https://cafe.example.com/coffee, the request is routed to the coffee-app application |
| 50 | + and the communication is secured using the signed keypair contained in the cafe-secret Secret. |
| 51 | +7. The certificate will be automatically renewed when it is close to expiry, the Secret will be updated using the new |
| 52 | + Certificate, and NKG will dynamically update the keypair on the filesystem used by NGINX for HTTPS termination once |
| 53 | + the Secret is updated. |
| 54 | + |
| 55 | +## Details |
| 56 | + |
| 57 | +### Step 1 – Deploy cert-manager |
| 58 | + |
| 59 | +The first step is to deploy cert-manager onto the cluster. |
| 60 | + |
| 61 | +- Add the Helm repository. |
| 62 | + |
| 63 | + ```shell |
| 64 | + helm repo add jetstack https://charts.jetstack.io |
| 65 | + helm repo update |
| 66 | + ``` |
| 67 | + |
| 68 | +- Install cert-manager, and enable the GatewayAPI feature gate: |
| 69 | + |
| 70 | + ```shell |
| 71 | + helm install \ |
| 72 | + cert-manager jetstack/cert-manager \ |
| 73 | + --namespace cert-manager \ |
| 74 | + --create-namespace \ |
| 75 | + --version v1.12.0 \ |
| 76 | + --set installCRDs=true \ |
| 77 | + --set "extraArgs={--feature-gates=ExperimentalGatewayAPISupport=true}" |
| 78 | + ``` |
| 79 | + |
| 80 | +### Step 2 – Create a ClusterIssuer |
| 81 | + |
| 82 | +Next we need to create a [ClusterIssuer](https://cert-manager.io/docs/concepts/issuer/), a Kubernetes resource that |
| 83 | +represents the certificate authority (CA) that will generate the signed certificates by honouring certificate signing |
| 84 | +requests. |
| 85 | + |
| 86 | +We are using the ACME Issuer type, and Let's Encrypt as the CA server. In order for Let's Encypt to verify that we own |
| 87 | +the domain a certificate is being requested for, we must complete "challenges". This is to ensure clients are |
| 88 | +unable to request certificates for domains they do not own. We will configure the Issuer to use a HTTP01 challenge, and |
| 89 | +our Gateway resource that we will create in the next step as the solver. To read more about HTTP01 challenges, see |
| 90 | +[here](https://cert-manager.io/docs/configuration/acme/http01/). Use the following YAML definition to create the |
| 91 | +resource, but please note the `email` field must be updated to your own email address. |
| 92 | + |
| 93 | +```yaml |
| 94 | +apiVersion: cert-manager.io/v1 |
| 95 | +kind: ClusterIssuer |
| 96 | +metadata: |
| 97 | + name: letsencrypt-prod |
| 98 | +spec: |
| 99 | + acme: |
| 100 | + # You must replace this email address with your own. |
| 101 | + # Let's Encrypt will use this to contact you about expiring |
| 102 | + # certificates, and issues related to your account. |
| 103 | + email: my-name@example.com |
| 104 | + server: https://acme-v02.api.letsencrypt.org/directory |
| 105 | + privateKeySecretRef: |
| 106 | + # Secret resource that will be used to store the account's private key. |
| 107 | + name: issuer-account-key |
| 108 | + # Add a single challenge solver, HTTP01 using NKG |
| 109 | + solvers: |
| 110 | + - http01: |
| 111 | + gatewayHTTPRoute: |
| 112 | + parentRefs: # This is the name of the Gateway that will be created in the next step |
| 113 | + - name: gateway |
| 114 | + namespace: default |
| 115 | + kind: Gateway |
| 116 | +``` |
| 117 | +
|
| 118 | +### Step 3 – Deploy our Gateway with the cert-manager annotation |
| 119 | +
|
| 120 | +Next we need to deploy our Gateway. Use can use the below YAML manifest, updating the `spec.listeners[1].hostname` |
| 121 | +field to the required value for your environment. |
| 122 | + |
| 123 | +```yaml |
| 124 | +apiVersion: gateway.networking.k8s.io/v1beta1 |
| 125 | +kind: Gateway |
| 126 | +metadata: |
| 127 | + name: gateway |
| 128 | + annotations: # This is the name of the ClusterIssuer created in the previous step |
| 129 | + cert-manager.io/cluster-issuer: letsencrypt-prod |
| 130 | +spec: |
| 131 | + gatewayClassName: nginx |
| 132 | + listeners: |
| 133 | + - name: http |
| 134 | + port: 80 |
| 135 | + protocol: HTTP |
| 136 | + - name: https |
| 137 | + # Important: The hostname needs to be set to your domain |
| 138 | + hostname: "cafe.example.com" |
| 139 | + port: 443 |
| 140 | + protocol: HTTPS |
| 141 | + tls: |
| 142 | + mode: Terminate |
| 143 | + certificateRefs: |
| 144 | + - kind: Secret |
| 145 | + name: cafe-secret |
| 146 | +``` |
| 147 | + |
| 148 | +It's worth noting a couple of key details in this manifest: |
| 149 | + |
| 150 | +- The cert-manager annotation is present in the metadata – this enables the cert-manager integration, and tells |
| 151 | + cert-manager which ClusterIssuer configuration it should use for the certificates. |
| 152 | +- There are two Listeners configured, an HTTP Listener on port 80, and an HTTPS Listener on port 443. |
| 153 | + - The http Listener on port 80 is required for the HTTP01 ACME challenge to work. This is because as part of the |
| 154 | + HTTP01 Challenge, a temporary HTTPRoute will be created by cert-manager to solve the ACME challenge, and this |
| 155 | + HTTPRoute requires a Listener on port 80. See the [HTTP01 Gateway API solver documentation](https://cert-manager.io/docs/configuration/acme/http01/#configuring-the-http-01-gateway-api-solver) |
| 156 | + for more information. |
| 157 | + - The https Listener on port 443 is the Listener we will use in our HTTPRoute in the next step. Cert-manager will |
| 158 | + create a Certificate for this Listener block. |
| 159 | +- The hostname needs to set to the required value. A new certificate will be issued from the `letsencrypt-prod` |
| 160 | + ClusterIssuer for the domain, e.g. "cafe.example.com", once the ACME challenge is successful. |
| 161 | + |
| 162 | +Once the certificate has been issued, cert-manager will create a Certificate resource on the cluster and the |
| 163 | +`cafe-secret` Secret containing the signed keypair in the same Namespace as the Gateway. We can verify the Secret has |
| 164 | +been created successfully using `kubectl`. Note it will take a little bit of time for the Challenge to complete and the |
| 165 | +Secret to be created: |
| 166 | + |
| 167 | +```shell |
| 168 | +kubectl get secret cafe-secret |
| 169 | +``` |
| 170 | + |
| 171 | +```text |
| 172 | +NAME TYPE DATA AGE |
| 173 | +cafe-secret kubernetes.io/tls 2 20s |
| 174 | +``` |
| 175 | + |
| 176 | +### Step 4 – Deploy our application and HTTPRoute |
| 177 | +Now we can create our coffee Deployment and Service, and configure the routing rules. You can use the following manifest |
| 178 | +to create the Deployment and Service: |
| 179 | + |
| 180 | +```yaml |
| 181 | +apiVersion: apps/v1 |
| 182 | +kind: Deployment |
| 183 | +metadata: |
| 184 | + name: coffee |
| 185 | +spec: |
| 186 | + replicas: 1 |
| 187 | + selector: |
| 188 | + matchLabels: |
| 189 | + app: coffee |
| 190 | + template: |
| 191 | + metadata: |
| 192 | + labels: |
| 193 | + app: coffee |
| 194 | + spec: |
| 195 | + containers: |
| 196 | + - name: coffee |
| 197 | + image: nginxdemos/nginx-hello:plain-text |
| 198 | + ports: |
| 199 | + - containerPort: 8080 |
| 200 | +--- |
| 201 | +apiVersion: v1 |
| 202 | +kind: Service |
| 203 | +metadata: |
| 204 | + name: coffee |
| 205 | +spec: |
| 206 | + ports: |
| 207 | + - port: 80 |
| 208 | + targetPort: 8080 |
| 209 | + protocol: TCP |
| 210 | + name: http |
| 211 | + selector: |
| 212 | + app: coffee |
| 213 | +``` |
| 214 | + |
| 215 | +Deploy our HTTPRoute to configure our routing rules for the coffee application. Note the `parentRefs` section in the |
| 216 | +spec refers to the Listener configured in the previous step. |
| 217 | + |
| 218 | +```yaml |
| 219 | +apiVersion: gateway.networking.k8s.io/v1beta1 |
| 220 | +kind: HTTPRoute |
| 221 | +metadata: |
| 222 | + name: coffee |
| 223 | +spec: |
| 224 | + parentRefs: |
| 225 | + - name: gateway |
| 226 | + sectionName: https |
| 227 | + hostnames: # Update the hostname to match what is configured in the Gateway resource |
| 228 | + - "cafe.example.com" |
| 229 | + rules: |
| 230 | + - matches: |
| 231 | + - path: |
| 232 | + type: PathPrefix |
| 233 | + value: /coffee |
| 234 | + backendRefs: |
| 235 | + - name: coffee |
| 236 | + port: 80 |
| 237 | +``` |
| 238 | + |
| 239 | +## Testing |
| 240 | + |
| 241 | +To test everything has worked correctly, we can use curl to the navigate to our endpoint, e.g. |
| 242 | +https://cafe.example.com/coffee. To verify using curl, we can use the `-v` option to increase verbosity and inspect the |
| 243 | +presented certificate. The output will look something like this: |
| 244 | + |
| 245 | +```shell |
| 246 | +curl https://cafe.example.com/coffee -v |
| 247 | +``` |
| 248 | + |
| 249 | +```text |
| 250 | +* Trying 54.195.47.105:443... |
| 251 | +* Connected to cafe.example.com (54.195.47.105) port 443 (#0) |
| 252 | +* ALPN: offers h2,http/1.1 |
| 253 | +* (304) (OUT), TLS handshake, Client hello (1): |
| 254 | +* CAfile: /etc/ssl/cert.pem |
| 255 | +* CApath: none |
| 256 | +* (304) (IN), TLS handshake, Server hello (2): |
| 257 | +* (304) (IN), TLS handshake, Unknown (8): |
| 258 | +* (304) (IN), TLS handshake, Certificate (11): |
| 259 | +* (304) (IN), TLS handshake, CERT verify (15): |
| 260 | +* (304) (IN), TLS handshake, Finished (20): |
| 261 | +* (304) (OUT), TLS handshake, Finished (20): |
| 262 | +* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 |
| 263 | +* ALPN: server accepted http/1.1 |
| 264 | +* Server certificate: |
| 265 | +* subject: CN=cafe.example.com |
| 266 | +* start date: Aug 11 08:22:11 2023 GMT |
| 267 | +* expire date: Nov 9 08:22:10 2023 GMT |
| 268 | +* subjectAltName: host "cafe.example.com" matched cert's "cafe.example.com" |
| 269 | +* issuer: C=US; O=Let's Encrypt; CN=R3 |
| 270 | +* SSL certificate verify ok. |
| 271 | +* using HTTP/1.1 |
| 272 | +> GET /coffee HTTP/1.1 |
| 273 | +> Host: cafe.example.com |
| 274 | +> User-Agent: curl/7.88.1 |
| 275 | +> Accept: */* |
| 276 | +> |
| 277 | +< HTTP/1.1 200 OK |
| 278 | +< Server: nginx/1.25.1 |
| 279 | +< Date: Fri, 11 Aug 2023 10:03:21 GMT |
| 280 | +< Content-Type: text/plain |
| 281 | +< Content-Length: 163 |
| 282 | +< Connection: keep-alive |
| 283 | +< Expires: Fri, 11 Aug 2023 10:03:20 GMT |
| 284 | +< Cache-Control: no-cache |
| 285 | +< |
| 286 | +Server address: 192.168.78.136:8080 |
| 287 | +Server name: coffee-9bf875848-xvkqv |
| 288 | +Date: 11/Aug/2023:10:03:21 +0000 |
| 289 | +URI: /coffee |
| 290 | +Request ID: e64c54a2ac253375ac085d48980f000a |
| 291 | +* Connection #0 to host cafe.example.com left intact |
| 292 | +``` |
| 293 | + |
| 294 | +## Troubleshooting |
| 295 | + |
| 296 | +- For troubeshooting anything related to the cert-manager installation or Issuer setup, see |
| 297 | + [the cert-manager troubleshooting guide](https://cert-manager.io/docs/troubleshooting/). |
| 298 | +- For troubleshooting the HTTP01 ACME Challenge, please see the cert-manager |
| 299 | + [ACME troubleshooting guide](https://cert-manager.io/docs/troubleshooting/acme/). |
| 300 | + - Note that for the HTTP01 Challenge to work using the Gateway resource, HTTPS redirect must not be configured. |
| 301 | + - The temporary HTTPRoute created by cert-manager routes the traffic between cert-manager and the Let's Encrypt server |
| 302 | + through NKG. If the Challenge is not successful, it may be useful to inspect the NGINX logs to see the ACME |
| 303 | + Challenge requests. You should see something like the following: |
| 304 | + |
| 305 | + ```shell |
| 306 | + kubectl logs <pod-name> -n nginx-gateway -c nginx |
| 307 | + <...> |
| 308 | + 52.208.162.19 - - [15/Aug/2023:13:18:12 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "cert-manager-challenges/v1.12.0 (linux/amd64) cert-manager/bd192c4f76dd883f9ee908035b894ffb49002384" |
| 309 | + 52.208.162.19 - - [15/Aug/2023:13:18:14 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "cert-manager-challenges/v1.12.0 (linux/amd64) cert-manager/bd192c4f76dd883f9ee908035b894ffb49002384" |
| 310 | + 52.208.162.19 - - [15/Aug/2023:13:18:16 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "cert-manager-challenges/v1.12.0 (linux/amd64) cert-manager/bd192c4f76dd883f9ee908035b894ffb49002384" |
| 311 | + 52.208.162.19 - - [15/Aug/2023:13:18:18 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "cert-manager-challenges/v1.12.0 (linux/amd64) cert-manager/bd192c4f76dd883f9ee908035b894ffb49002384" |
| 312 | + 52.208.162.19 - - [15/Aug/2023:13:18:20 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "cert-manager-challenges/v1.12.0 (linux/amd64) cert-manager/bd192c4f76dd883f9ee908035b894ffb49002384" |
| 313 | + 3.128.204.81 - - [15/Aug/2023:13:18:22 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" |
| 314 | + 23.178.112.204 - - [15/Aug/2023:13:18:22 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" |
| 315 | + 35.166.192.222 - - [15/Aug/2023:13:18:22 +0000] "GET /.well-known/acme-challenge/bXQn27Lenax2AJKmOOS523T-MWOKeFhL0bvrouNkUc4 HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)" |
| 316 | + <...> |
| 317 | + ``` |
| 318 | + |
| 319 | +## Links |
| 320 | + |
| 321 | +- Gateway docs: https://gateway-api.sigs.k8s.io |
| 322 | +- Cert-manager Gateway usage: https://cert-manager.io/docs/usage/gateway/ |
| 323 | +- Cert-manager ACME: https://cert-manager.io/docs/configuration/acme/ |
| 324 | +- Let’s Encrypt: https://letsencrypt.org |
| 325 | +- NGINX HTTPS docs: https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/ |
0 commit comments