github.com/projectcontour/contour@v1.28.2/site/content/docs/1.27/guides/cert-manager.md (about) 1 --- 2 title: Deploying HTTPS services with Contour and cert-manager 3 --- 4 5 This tutorial shows you how to securely deploy an HTTPS web application on a Kubernetes cluster, using: 6 7 - Kubernetes 8 - Contour, as the Ingress controller 9 - [JetStack's cert-manager][1] to provision TLS certificates from [the Let's Encrypt project][6] 10 11 ## Prerequisites 12 13 - A Kubernetes cluster deployed in either a data center or a cloud provider with a Kubernetes as a service offering. This tutorial was last tested on a GKE cluster running Kubernetes 1.22 14 - RBAC enabled on your cluster 15 - Your cluster must be able to request a public IP address from your cloud provider, using a load balancer. If you're on AWS or GKE this is automatic if you deploy a Kubernetes service object of type: LoadBalancer. If you're on your own datacenter you must set it up yourself 16 - A DNS domain that you control, where you host your web application 17 - Administrator permissions for all deployment steps 18 19 **NOTE:** To use a local cluster like `minikube` or `kind`, see the instructions in [the deployment guide][7]. 20 21 ## Summary 22 23 This tutorial walks you through deploying: 24 25 1. [Contour][0] 26 2. [Jetstack cert-manager][1] 27 3. A sample web application using HTTPProxy 28 29 **NOTE:** If you encounter failures related to permissions, make sure the user you are operating as has administrator permissions. 30 31 After you've been through the steps the first time, you don't need to repeat deploying Contour and cert-manager for subsequent application deployments. Instead, you can skip to step 3. 32 33 ## 1. Deploy Contour 34 35 Run: 36 37 ```bash 38 $ kubectl apply -f {{< param base_url >}}/quickstart/contour.yaml 39 ``` 40 41 to set up Contour as a deployment in its own namespace, `projectcontour`, and tell the cloud provider to provision an external IP that is forwarded to the Contour pods. 42 43 Check the progress of the deployment with this command: 44 45 ```bash 46 $ kubectl -n projectcontour get po 47 NAME READY STATUS RESTARTS AGE 48 contour-5475898957-jh9fm 1/1 Running 0 39s 49 contour-5475898957-qlbs2 1/1 Running 0 39s 50 contour-certgen-v1.19.0-5xthf 0/1 Completed 0 39s 51 envoy-hqbkm 2/2 Running 0 39s 52 ``` 53 54 After all the `contour` & `envoy` pods reach `Running` status and fully `Ready`, move on to the next step. 55 56 ### Access your cluster 57 58 Retrieve the external address of the load balancer assigned to Contour's Envoys by your cloud provider: 59 60 ```bash 61 $ kubectl get -n projectcontour service envoy -o wide 62 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR 63 envoy LoadBalancer 10.51.245.99 35.189.26.87 80:30111/TCP,443:30933/TCP 38d app=envoy 64 ``` 65 66 The value of `EXTERNAL-IP` varies by cloud provider. In this example GKE gives a bare IP address; AWS gives you a long DNS name. 67 68 To make it easier to work with the external load balancer, the tutorial adds a DNS record to a domain we control that points to this load balancer's IP address: 69 70 ```bash 71 $ host gke.davecheney.com 72 gke.davecheney.com has address 35.189.26.87 73 ``` 74 75 On AWS, you specify a `CNAME`, not an `A` record, and it would look something like this: 76 77 ```bash 78 $ host aws.davecheney.com 79 aws.davecheney.com is an alias for a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com. 80 a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com has address 52.63.20.117 81 a4d1766f6ce1611e7b27f023b7e83d33–1465548734.ap-southeast-2.elb.amazonaws.com has address 52.64.233.204 82 ``` 83 84 In your own data center, you need to arrange for traffic from a public IP address to be forwarded to the cluster IP of the Contour service. This is beyond the scope of the tutorial. 85 86 ### Testing connectivity 87 88 You must deploy at least one Ingress object before Contour can configure Envoy to serve traffic. 89 Note that as a security feature, Contour does not configure Envoy to expose a port to the internet unless there's a reason it should. 90 For this tutorial we deploy a version of Kenneth Reitz's [httpbin.org service][3]. 91 92 To deploy httpbin to your cluster, run this command: 93 94 ```bash 95 $ kubectl apply -f {{< param base_url >}}/examples/httpbin.yaml 96 ``` 97 98 Check that the pods are running: 99 100 ```bash 101 $ kubectl get po -l app=httpbin 102 NAME READY STATUS RESTARTS AGE 103 httpbin-85777b684b-8sqw5 1/1 Running 0 24s 104 httpbin-85777b684b-pb26w 1/1 Running 0 24s 105 httpbin-85777b684b-vpgwl 1/1 Running 0 24s 106 ``` 107 108 Then type the DNS name you set up in the previous step into a web browser, for example `http://gke.davecheney.com/`. You should see something like: 109 110 ![httpbin screenshot][8] 111 112 You can delete the httpbin service now, or at any time, by running: 113 114 ```bash 115 $ kubectl delete -f {{< param base_url >}}/examples/httpbin.yaml 116 ``` 117 118 ## 2. Deploy jetstack/cert-manager 119 120 **NOTE:** cert-manager is a powerful product that provides more functionality than this tutorial demonstrates. 121 There are plenty of [other ways to deploy cert-manager][4], but they are out of scope. 122 123 ### Fetch the source manager deployment manifest 124 125 To keep things simple, we skip cert-manager's Helm installation, and use the [supplied YAML manifests][5]. 126 127 ```bash 128 $ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml 129 ``` 130 131 When cert-manager is up and running you should see something like: 132 133 ```bash 134 $ kubectl -n cert-manager get all 135 NAME READY STATUS RESTARTS AGE 136 pod/cert-manager-cainjector-74bb68d67c-8lb2f 1/1 Running 0 40s 137 pod/cert-manager-f7f8bf74d-65ld9 1/1 Running 0 40s 138 pod/cert-manager-webhook-645b8bdb7-2h5t6 1/1 Running 0 40s 139 140 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 141 service/cert-manager ClusterIP 10.48.13.252 <none> 9402/TCP 40s 142 service/cert-manager-webhook ClusterIP 10.48.7.220 <none> 443/TCP 40s 143 144 NAME READY UP-TO-DATE AVAILABLE AGE 145 deployment.apps/cert-manager 1/1 1 1 40s 146 deployment.apps/cert-manager-cainjector 1/1 1 1 40s 147 deployment.apps/cert-manager-webhook 1/1 1 1 40s 148 149 NAME DESIRED CURRENT READY AGE 150 replicaset.apps/cert-manager-cainjector-74bb68d67c 1 1 1 40s 151 replicaset.apps/cert-manager-f7f8bf74d 1 1 1 40s 152 replicaset.apps/cert-manager-webhook-645b8bdb7 1 1 1 40s 153 ``` 154 155 ### Deploy the Let's Encrypt cluster issuer 156 157 cert-manager supports two different CRDs for configuration, an `Issuer`, which is scoped to a single namespace, 158 and a `ClusterIssuer`, which is cluster-wide. 159 160 For Contour to be able to serve HTTPS traffic for an Ingress in any namespace, use `ClusterIssuer`. 161 Create a file called `letsencrypt-staging.yaml` with the following contents: 162 163 ```yaml 164 apiVersion: cert-manager.io/v1 165 kind: ClusterIssuer 166 metadata: 167 name: letsencrypt-staging 168 namespace: cert-manager 169 spec: 170 acme: 171 email: user@example.com 172 privateKeySecretRef: 173 name: letsencrypt-staging 174 server: https://acme-staging-v02.api.letsencrypt.org/directory 175 solvers: 176 - http01: 177 ingress: 178 class: contour 179 ``` 180 181 replacing `user@example.com` with your email address. 182 This is the email address that Let's Encrypt uses to communicate with you about certificates you request. 183 184 The staging Let's Encrypt server is not bound by [the API rate limits of the production server][2]. 185 This approach lets you set up and test your environment without worrying about rate limits. 186 You can then repeat this step for a production Let's Encrypt certificate issuer. 187 188 After you edit and save the file, deploy it: 189 190 ```bash 191 $ kubectl apply -f letsencrypt-staging.yaml 192 clusterissuer.cert-manager.io/letsencrypt-staging created 193 ``` 194 195 Wait for the `ClusterIssuer` to be ready: 196 197 ```bash 198 $ kubectl get clusterissuer letsencrypt-staging 199 NAME READY AGE 200 letsencrypt-staging True 54s 201 ``` 202 203 ## 3. Deploy your first HTTPS site using Ingress 204 205 For this tutorial we deploy a version of Kenneth Reitz's [httpbin.org service][3]. 206 We start with the deployment. 207 Copy the following to a file called `deployment.yaml`: 208 209 ```yaml 210 apiVersion: apps/v1 211 kind: Deployment 212 metadata: 213 labels: 214 app: httpbin 215 name: httpbin 216 spec: 217 replicas: 1 218 selector: 219 matchLabels: 220 app: httpbin 221 strategy: 222 rollingUpdate: 223 maxSurge: 1 224 maxUnavailable: 1 225 type: RollingUpdate 226 template: 227 metadata: 228 labels: 229 app: httpbin 230 spec: 231 containers: 232 - image: docker.io/kennethreitz/httpbin 233 name: httpbin 234 ports: 235 - containerPort: 8080 236 name: http 237 command: ["gunicorn"] 238 args: ["-b", "0.0.0.0:8080", "httpbin:app"] 239 dnsPolicy: ClusterFirst 240 ``` 241 242 Deploy to your cluster: 243 244 ```bash 245 $ kubectl apply -f deployment.yaml 246 deployment.apps/httpbin created 247 $ kubectl get pod -l app=httpbin 248 NAME READY STATUS RESTARTS AGE 249 httpbin-67fd96d97c-8j2rr 1/1 Running 0 56m 250 ``` 251 252 Expose the deployment to the world with a Service. Create a file called `service.yaml` with 253 the following contents: 254 255 ```yaml 256 apiVersion: v1 257 kind: Service 258 metadata: 259 name: httpbin 260 spec: 261 ports: 262 - port: 8080 263 protocol: TCP 264 targetPort: 8080 265 selector: 266 app: httpbin 267 ``` 268 269 and deploy: 270 271 ```bash 272 $ kubectl apply -f service.yaml 273 service/httpbin created 274 $ kubectl get service httpbin 275 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 276 httpbin ClusterIP 10.48.6.155 <none> 8080/TCP 57m 277 ``` 278 279 Expose the Service to the world with Contour and an Ingress object. Create a file called `ingress.yaml` with 280 the following contents: 281 282 ```yaml 283 apiVersion: networking.k8s.io/v1 284 kind: Ingress 285 metadata: 286 name: httpbin 287 spec: 288 rules: 289 - host: httpbin.davecheney.com 290 http: 291 paths: 292 - pathType: Prefix 293 path: / 294 backend: 295 service: 296 name: httpbin 297 port: 298 number: 8080 299 ``` 300 301 The host name, `httpbin.davecheney.com` is a `CNAME` to the `gke.davecheney.com` record that was created in the first section, and must be created in the same place as the `gke.davecheney.com` record was. 302 That is, in your cloud provider. 303 This lets requests to `httpbin.davecheney.com` resolve to the external IP address of the Contour service. 304 They are then forwarded to the Contour pods running in the cluster: 305 306 ```bash 307 $ host httpbin.davecheney.com 308 httpbin.davecheney.com is an alias for gke.davecheney.com. 309 gke.davecheney.com has address 35.189.26.87 310 ``` 311 312 Change the value of `spec.rules.host` to something that you control, and deploy the Ingress to your cluster: 313 314 ```bash 315 $ kubectl apply -f ingress.yaml 316 ingress.networking.k8s.io/httpbin created 317 $ kubectl get ingress httpbin 318 NAME CLASS HOSTS ADDRESS PORTS AGE 319 httpbin <none> httpbin.davecheney.com 80 12s 320 ``` 321 322 Now you can type the host name of the service into a browser, or use curl, to verify it's deployed and everything is working: 323 324 ```bash 325 $ curl http://httpbin.davecheney.com/get 326 { 327 "args": {}, 328 "headers": { 329 "Accept": "*/*", 330 "Content-Length": "0", 331 "Host": "htpbin.davecheney.com", 332 "User-Agent": "curl/7.58.0", 333 "X-Envoy-Expected-Rq-Timeout-Ms": "15000", 334 "X-Envoy-Internal": "true" 335 }, 336 "origin": "10.152.0.2", 337 "url": "http://httpbin.davecheney.com/get" 338 } 339 ``` 340 341 Excellent, it looks like everything is up and running serving traffic over HTTP. 342 343 ### Request a TLS certificate from Let's Encrypt 344 345 Now it's time to use cert-manager to request a TLS certificate from Let's Encrypt. 346 Do this by adding some annotations and a `tls:` section to the Ingress spec. 347 348 We need to add the following annotations: 349 350 - `cert-manager.io/cluster-issuer: letsencrypt-staging`: tells cert-manager to use the `letsencrypt-staging` cluster issuer you just created. 351 - `kubernetes.io/tls-acme: "true"`: Tells cert-manager to do ACME TLS (what Let's Encrypt uses). 352 - `ingress.kubernetes.io/force-ssl-redirect: "true"`: tells Contour to redirect HTTP requests to the HTTPS site. 353 - `kubernetes.io/ingress.class: contour`: Tells Contour that it should handle this Ingress object. 354 355 Using `kubectl edit ingress httpbin`: 356 357 ```yaml 358 apiVersion: networking.k8s.io/v1 359 kind: Ingress 360 metadata: 361 name: httpbin 362 annotations: 363 cert-manager.io/cluster-issuer: letsencrypt-staging 364 ingress.kubernetes.io/force-ssl-redirect: "true" 365 kubernetes.io/ingress.class: contour 366 kubernetes.io/tls-acme: "true" 367 spec: 368 tls: 369 - secretName: httpbin 370 hosts: 371 - httpbin.davecheney.com 372 rules: 373 - host: httpbin.davecheney.com 374 http: 375 paths: 376 - pathType: Prefix 377 path: / 378 backend: 379 service: 380 name: httpbin 381 port: 382 number: 8080 383 ``` 384 385 The certificate is issued in the name of the hosts listed in the `tls:` section, `httpbin.davecheney.com` and stored in the secret `httpbin`. 386 Behind the scenes, cert-manager creates a certificate CRD to manage the lifecycle of the certificate, and then a series of other CRDs to handle the challenge process. 387 388 You can watch the progress of the certificate as it's issued: 389 390 ```bash 391 $ kubectl describe certificate httpbin | tail -n 12 392 Status: 393 Conditions: 394 Last Transition Time: 2019-11-07T00:37:55Z 395 Message: Waiting for CertificateRequest "httpbinproxy-1925286939" to complete 396 Reason: InProgress 397 Status: False 398 Type: Ready 399 Events: 400 Type Reason Age From Message 401 ---- ------ ---- ---- ------- 402 Normal GeneratedKey 26s cert-manager Generated a new private key 403 Normal Requested 26s cert-manager Created new CertificateRequest resource "httpbinproxy-1925286939" 404 ``` 405 406 Wait for the certificate to be issued: 407 408 ```bash 409 $ kubectl describe certificate httpbin | grep -C3 "Certificate is up to date" 410 Status: 411 Conditions: 412 Last Transition Time: 2019-11-06T23:47:50Z 413 Message: Certificate is up to date and has not expired 414 Reason: Ready 415 Status: True 416 Type: Ready 417 ``` 418 419 A `kubernetes.io/tls` secret is created with the `secretName` specified in the `tls:` field of the Ingress. 420 421 ```bash 422 $ kubectl get secret httpbin 423 NAME TYPE DATA AGE 424 httpbin kubernetes.io/tls 2 3m 425 ``` 426 427 cert-manager manages the contents of the secret as long as the Ingress is present in your cluster. 428 429 You can now visit your site, replacing `http://` with `https://` — and you get a huge security warning! 430 This is because the certificate was issued by the Let's Encrypt staging servers and has a fake CA. 431 This is so you can't accidentally use the staging servers to serve real certificates. 432 433 ```bash 434 $ curl https://httpbin.davecheney.com/get 435 curl: (60) SSL certificate problem: unable to get local issuer certificate 436 More details here: https://curl.haxx.se/docs/sslcerts.html 437 438 curl failed to verify the legitimacy of the server and therefore could not 439 establish a secure connection to it. To learn more about this situation and 440 how to fix it, please visit the web page mentioned above. 441 ``` 442 443 ### Switch to Let's Encrypt Production 444 445 To request a properly signed certificate from the Let's Encrypt production servers, we create a new `ClusterIssuer`, as before but with some modifications. 446 447 Create a file called `letsencrypt-prod.yaml` with the following contents: 448 449 ```yaml 450 apiVersion: cert-manager.io/v1 451 kind: ClusterIssuer 452 metadata: 453 name: letsencrypt-prod 454 namespace: cert-manager 455 spec: 456 acme: 457 email: user@example.com 458 privateKeySecretRef: 459 name: letsencrypt-prod 460 server: https://acme-v02.api.letsencrypt.org/directory 461 solvers: 462 - http01: 463 ingress: 464 class: contour 465 ``` 466 467 again replacing `user@example.com` with your email address. 468 469 Deploy: 470 471 ```bash 472 $ kubectl apply -f letsencrypt-prod.yaml 473 clusterissuer.cert-manager.io/letsencrypt-prod created 474 ``` 475 476 Now we use `kubectl edit ingress httpbin` to edit our Ingress to ask for a real certificate from `letsencrypt-prod`: 477 478 ```yaml 479 apiVersion: networking.k8s.io/v1 480 kind: Ingress 481 metadata: 482 name: httpbin 483 annotations: 484 cert-manager.io/cluster-issuer: letsencrypt-prod 485 spec: 486 ... 487 ``` 488 489 The certificate resource will transition to `Ready: False` while it's re-provisioned from the Let's Encrypt production servers, and then back to `Ready: True` once it's been provisioned: 490 491 ```bash 492 $ kubectl describe certificate httpbin 493 ... 494 Events: 495 Type Reason Age From Message 496 ---- ------ ---- ---- ------- 497 ... 498 Normal Issuing 21s cert-manager Issuing certificate as Secret was previously issued by ClusterIssuer.cert-manager.io/letsencrypt-staging 499 Normal Reused 21s cert-manager Reusing private key stored in existing Secret resource "httpbin" 500 Normal Requested 21s cert-manager Created new CertificateRequest resource "httpbin-sjqbt" 501 Normal Issuing 18s (x2 over 48s) cert-manager The certificate has been successfully issued 502 ``` 503 504 Followed by: 505 506 ```bash 507 $ kubectl get certificate httpbin -o wide 508 NAME READY SECRET ISSUER STATUS AGE 509 httpbin True httpbin letsencrypt-prod Certificate is up to date and has not expired 3m35s 510 ``` 511 512 Now revisiting our `https://httpbin.davecheney.com` site should show a valid, trusted, HTTPS certificate. 513 514 ```bash 515 $ curl https://httpbin.davecheney.com/get 516 { 517 "args": {}, 518 "headers": { 519 "Accept": "*/*", 520 "Content-Length": "0", 521 "Host": "httpbin.davecheney.com", 522 "User-Agent": "curl/7.58.0", 523 "X-Envoy-Expected-Rq-Timeout-Ms": "15000", 524 "X-Envoy-Internal": "true" 525 }, 526 "origin": "10.152.0.2", 527 "url": "https://httpbin.davecheney.com/get" 528 } 529 ``` 530 531 ![httpbin.davecheney.com screenshot][9] 532 533 ## Making cert-manager work with HTTPProxy 534 535 cert-manager currently does not have a way to interact directly with HTTPProxy objects in order to respond to the HTTP01 challenge (See [#950][10] and [#951][11] for details). 536 cert-manager, however, can be configured to request certificates automatically using a `Certificate` object. 537 538 When cert-manager finds a `Certificate` object, it will implement the HTTP01 challenge by creating a new, temporary Ingress object that will direct requests from Let's Encrypt to temporary pods called 'solver pods'. 539 These pods know how to respond to Let's Encrypt's challenge process for verifying you control the domain you're issuing certificates for. 540 The Ingress resource as well as the solver pods are short lived and will only be available during the certificate request or renewal process. 541 542 The result of the work steps described previously is a TLS secret, which can be referenced by a HTTPProxy. 543 544 ## Details 545 546 To do this, we first need to create our HTTPProxy and Certificate objects. 547 548 This example uses the hostname `httpbinproxy.davecheney.com`, remember to create that name before starting. 549 550 Firstly, the HTTPProxy: 551 552 ```yaml 553 apiVersion: projectcontour.io/v1 554 kind: HTTPProxy 555 metadata: 556 name: httpbinproxy 557 spec: 558 virtualhost: 559 fqdn: httpbinproxy.davecheney.com 560 tls: 561 secretName: httpbinproxy 562 routes: 563 - services: 564 - name: httpbin 565 port: 8080 566 ``` 567 568 This object will be marked as Invalid by Contour, since the TLS secret doesn't exist yet. 569 Once that's done, create the Certificate object: 570 571 ```yaml 572 apiVersion: cert-manager.io/v1 573 kind: Certificate 574 metadata: 575 name: httpbinproxy 576 spec: 577 commonName: httpbinproxy.davecheney.com 578 dnsNames: 579 - httpbinproxy.davecheney.com 580 issuerRef: 581 name: letsencrypt-prod 582 kind: ClusterIssuer 583 secretName: httpbinproxy 584 ``` 585 586 Wait for the Certificate to be provisioned: 587 588 ```bash 589 $ kubectl get certificate httpbinproxy -o wide 590 NAME READY SECRET ISSUER STATUS AGE 591 httpbinproxy True httpbinproxy letsencrypt-prod Certificate is up to date and has not expired 39s 592 ``` 593 594 Once cert-manager has fulfilled the HTTP01 challenge, you will have a `httpbinproxy` secret, that will contain the keypair. 595 Contour will detect that the Secret exists and generate the HTTPProxy config. 596 597 After that, you should be able to curl the new site: 598 599 ```bash 600 $ curl https://httpbinproxy.davecheney.com/get 601 { 602 "args": {}, 603 "headers": { 604 "Accept": "*/*", 605 "Content-Length": "0", 606 "Host": "httpbinproxy.davecheney.com", 607 "User-Agent": "curl/7.54.0", 608 "X-Envoy-Expected-Rq-Timeout-Ms": "15000", 609 "X-Envoy-External-Address": "122.106.57.183" 610 }, 611 "origin": "122.106.57.183", 612 "url": "https://httpbinproxy.davecheney.com/get" 613 } 614 ``` 615 616 ## Wrapping up 617 618 Now that you've deployed your first HTTPS site using Contour and Let's Encrypt, deploying additional TLS enabled services is much simpler. 619 Remember that for each HTTPS website you deploy, cert-manager will create a Certificate CRD that provides the domain name and the name of the target Secret. 620 The TLS functionality will be enabled when the HTTPProxy contains the `tls:` stanza, and the referenced secret contains a valid keypair. 621 622 See the [cert-manager docs][12] for more information. 623 624 ## Bonus points 625 626 For bonus points, you can use a feature of Contour to automatically upgrade any HTTP request to the corresponding HTTPS site so you are no longer serving any traffic over insecure HTTP. 627 628 To enable the automatic redirect from HTTP to HTTPS, add this annotation to your Ingress object. 629 630 ``` 631 metadata: 632 annotations: 633 ingress.kubernetes.io/force-ssl-redirect: "true" 634 ``` 635 Now any requests to the insecure HTTP version of your site get an unconditional 301 redirect to the HTTPS version: 636 637 ``` 638 $ curl -v http://httpbin.davecheney.com/get 639 * Trying 35.189.26.87… 640 * TCP_NODELAY set 641 * Connected to httpbin.davecheney.com (35.189.26.87) port 80 (#0) 642 > GET /get HTTP/1.1 643 > Host: httpbin.davecheney.com 644 > User-Agent: curl/7.58.0 645 > Accept: */* 646 > 647 < HTTP/1.1 301 Moved Permanently 648 < location: https://httpbin.davecheney.com/get 649 < date: Tue, 20 Feb 2018 04:11:46 GMT 650 < server: envoy 651 < content-length: 0 652 < 653 * Connection #0 to host httpbin.davecheney.com left intact 654 ``` 655 656 __Note:__ For HTTPProxy resources this happens automatically without the need for an annotation. 657 658 [0]: {{< param github_url >}} 659 [1]: https://github.com/jetstack/cert-manager 660 [2]: https://letsencrypt.org/docs/rate-limits/ 661 [3]: http://httpbin.org/ 662 [4]: https://docs.cert-manager.io/en/latest/getting-started/install/kubernetes.html 663 [5]: https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml 664 [6]: https://letsencrypt.org/getting-started/ 665 [7]: ../deploy-options/#get-your-hostname-or-ip-address 666 [8]: /img/cert-manager/httpbinhomepage.png 667 [9]: /img/cert-manager/httpbin.png 668 [10]: {{< param github_url >}}/issues/950 669 [11]: {{< param github_url >}}/issues/951 670 [12]: https://cert-manager.io/docs/usage/ingress/