Skip to content

Commit aa973b2

Browse files
authored
Add a cert-manager integration guide (#957)
* Add cert-manager guide
1 parent 9cc25a3 commit aa973b2

File tree

2 files changed

+325
-0
lines changed

2 files changed

+325
-0
lines changed
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
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+
![cert-manager ACME Challenge and certificate management with Gateway API](/docs/images/cert-manager-gateway-workflow.png)
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/
148 KB
Loading

0 commit comments

Comments
 (0)