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/