Setting up secure endpoints in Kubernetes

Note This post requires a basic understanding of Kubernetes and how it works for it to be most useful.

Source Code All the code and configuration files for this post are available here.

One Simple App, Two Endpoints

One of the most basic needs of a new application that's deployed on Kubernetes is to expose a couple of web endpoints on the public internet with different URLs and secure them with SSL. For example, your app might want to have the following 2 endpoints publicly available:

app.acme.org and api.acme.org both secured with SSL certificates.

Regardless of what your app is written in (Rails, Node, Go,...) this should be easy enough to do in Kubernetes.

The Application + API

For this example, I'm going to use a simple Node.js app, but you can replace it with your favourite language / framework.

My Application

const http = require('http')
const port = 3000  // let's host this on port 3000

const requestHandler = (request, response) => {
  console.log(request.url)
  response.end('Hello! This is my app!')
}

const server = http.createServer(requestHandler)

server.listen(port, (err) => {
  if (err) {
    return console.log('something bad happened', err)
  }

  console.log(`application server is listening on ${port}`)
})

I want this to be available at app.acme.org.

My API

const http = require('http')
const port = 4000 // I'm going to host this one on port 4000

const requestHandler = (request, response) => {
  console.log(request.url)
  response.end('Hello! This is my API!')
}

const server = http.createServer(requestHandler)

server.listen(port, (err) => {
  if (err) {
    return console.log('something bad happened', err)
  }

  console.log(`API server is listening on ${port}`)
})

I want this to be available at api.acme.org.

As you can see, both are identical (for the sake of simplicity) with the small difference of the port they serve on: the application is on port 3000 and the API is on port 4000. Obviously when I roll this into production, I want both of them to be on HTTPS port 443.

The Setup

Pre-requisites

For this example, I am using a DigitalOcean managed Kubernetes cluster and DigitalOcean DNS service. Please note this example is not going to work on Minikube without modification.
If you would like to try the code in this post, make sure you have a running Kubernetes cluster on DigitalOcean and can communicate with it using kubectl

Step 0: Creating the container images

If you're using Docker, you will need to build your application into a container image first. I am including this step since the image is used in the following steps. The following steps use the following 2 images: 1 for your application called cloud66/k8s_secure_endpoints_app and cloud66/k8s_secure_endpoints_api. Those images are built on Dockerhub as well so you can just use the configuration as it is.

Step 1: Creating Deployments

The first step to hosting our application on Kubernetes is to deploy them as Deployments:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - image: cloud66/k8s_secure_endpoint_app
        imagePullPolicy: Always
        name: app
        ports:
        - containerPort: 3000
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - image: cloud66/k8s_secure_endpoint_app
        imagePullPolicy: Always
        name: app
        ports:
        - containerPort: 4000

Save this to deployment.yml and apply it to your cluster and see it come up:

Step 2: Creating the Services

Now that you have the deployments, you need to expose them to outside of the cluster using Services:

apiVersion: v1
kind: Service
metadata:
  name: app
  labels:
    app: app
spec:
  type: ClusterIP
  ports:
  - port: 3000
    targetPort: 3000
  selector:
    app: app
---
apiVersion: v1
kind: Service
metadata:
  name: api
  labels:
    app: api
spec:
  type: ClusterIP
  ports:
  - port: 4000
    targetPort: 4000
  selector:
    app: api

Save this to services.yml and apply to to your cluster.

$ kubectl apply -f services.yml
$ kubectl get svc

NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
api          ClusterIP   10.109.49.232    <none>        4000/TCP   2s
app          ClusterIP   10.109.121.170   <none>        3000/TCP   2s

Step 3: Setting up the Ingress Controller

First, what's Ingress? Ingress is a Kubernetes feature that makes it possible to give access (usually through HTTP) to the Services. You can think of Ingress, like an nginx instance sitting in front of your services and dispatching traffic to the right one based on the incoming URL.

The following configuration files will setup nginx Ingress on your cluster. This is written for a cluster with RBAC support (which is most of the clusters you get if you use a managed provider like DigitalOcean).

Before we jump into a wall of Yaml, let's see what we want to achieve:

  1. Create a load balancer on the cloud provider to sit in front of all of our incoming traffic and distribute it to the servers (nodes)
  2. Create a default backend to handle the traffic that doesn't belong to any of our services.
  3. Use nginx as an Ingress Controller on the cluster.
  4. Add good logging to our nginx to help with debugging
  5. Configure the cluster RBAC so the controller can change the needed settings when our services change.

So what's an Ingress Controller? If Ingress is like a routing table in front of our services, Ingress Controller is like a daemon that sits on top of our cluster and watches services as they are added and removed. It then automatically configures the routing table so we can just add new services to the cluster without having to configure the routing table every time.

First, let's configure the RBAC. For the Ingress Controller to watch what's happening on the cluster and make the needed changes, it needs to have the right access rights to the cluster. Here we're going to create a Service Account that runs the controller (like a Linux user running a daemon) and give it the needed access rights:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: nginx-ingress-clusterrole
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - endpoints
      - nodes
      - pods
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - create
      - patch
  - apiGroups:
      - "extensions"
    resources:
      - ingresses/status
    verbs:
      - update

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: kube-system
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-nginx"
    verbs:
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - endpoints
    verbs:
      - get

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: nginx-ingress-role-nisa-binding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nginx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: nginx-ingress-clusterrole-nisa-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nginx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: kube-system

Now that we have the needed Service Account and the roles, we can move on to creating the default HTTP handler.

Not all traffic that hits your cluster belongs to your services. We also need to take care of the traffic when our services are down. This prevents the load balancers from removing nodes from the cluster. Here I am using a default nginx setup as the default HTTP handler.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: default-http-backend
  labels:
    k8s-app: default-http-backend
  namespace: kube-system
spec:
  replicas: 1
  template:
    metadata:
      labels:
        k8s-app: default-http-backend
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: default-http-backend
        # Any image is permissable as long as:
        # 1. It serves a 404 page at /
        # 2. It serves 200 on a /healthz endpoint
        image: gcr.io/google_containers/defaultbackend:1.0
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 5
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: 10m
            memory: 20Mi
          requests:
            cpu: 10m
            memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
  namespace: kube-system
  name: default-http-backend
  labels:
    k8s-app: default-http-backend
spec:
  ports:
  - port: 80
    targetPort: 8080
  selector:
    k8s-app: default-http-backend

You only need one of these on your cluster (hence replicas: 1)

Now it's time to setup our Ingress Controller. There are multiple options to use as Ingress Controller. Here I am using the nginx Ingress Controller, which basically is pod with nginx running inside of it and its configuration file is changed whenever Ingress changes happen.

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configuration
  namespace: kube-system
  labels:
    app: nginx-ingress-lb
data:
  log-format-upstream: '{ "time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr",
    "x-forward-for": "$proxy_add_x_forwarded_for", "request_id": "$request_id", "remote_user":
    "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, "status":
    $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri",
    "request_query": "$args", "request_length": $request_length, "duration": $request_time,
    "method": "$request_method", "http_referrer": "$http_referer", "http_user_agent":
    "$http_user_agent" }'
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  namespace: kube-system
  name: nginx-ingress-controller
  labels:
    k8s-app: nginx-ingress-controller
spec:
  template:
    metadata:
      labels:
        k8s-app: nginx-ingress-controller
    spec:
      terminationGracePeriodSeconds: 60
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
      - image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.17.1
        name: nginx-ingress-controller
        readinessProbe:
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
        livenessProbe:
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
          initialDelaySeconds: 10
          timeoutSeconds: 1
        ports:
        - containerPort: 80
          hostPort: 80
        - containerPort: 443
          hostPort: 443
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
        args:
        - /nginx-ingress-controller
        - --default-backend-service=kube-system/default-http-backend
        - --publish-service=kube-system/nginx-ingress-lb
        - --configmap=$(POD_NAMESPACE)/nginx-configuration

The first part, configures how nginx should log. The second part creates the Ingress Controller. In this sample I've chosen to use a DaemonSet to deploy the Ingress Controller. This means I will have a single pod running nginx on each node of my cluster as a result. You can choose a different strategy by using Deployment with various replicas count combined perhaps with affinity configuration to tell Kubernetes which servers should handle the incoming traffic.

The last 3 lines are important, so let's go through them: Here we are telling the nginx Ingress Controller:

  1. If any traffic comes in and it doesn't belong to any of the upstream Ingress, then default-http-backend is where it should go.
  2. publish-service=kube-system/nginx-ingress-lb tells the Controller to use the IP address of the physical load balancer for each one of the Ingress. We don't have the load balancer yet (it's coming up next) but using this help when we configure our DNS service.
  3. configmap=... tells our Controller where to get it's configuration, which we created on the top part of the file. The ConfigMap and this line are optional and only improve the logging for better debugging.

Now we can create the inbound service and the load balancer.

apiVersion: v1
kind: Service
metadata:
  namespace: kube-system
  name: nginx-ingress-lb
  labels:
    app: nginx-ingress-lb
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  ports:
  - port: 80
    name: http
    targetPort: 80
  - port: 443
    name: https
    targetPort: 443
  selector:
    # Selects nginx-ingress-controller pods
    k8s-app: nginx-ingress-controller

This creates a physical load balancer on the cloud provider (DigitalOcean in this case) and points all traffic that comes through to our Ingress Controller.

I've put all of this together here so you can just run it using this command to get it setup:

$ kubectl apply -f https://raw.githubusercontent.com/cloud66-samples/k8s-secure-endpoints/master/k8s/nginx.yml

Step 4: Setting up Ingress

We can now create Ingress objects on the cluster to tell our controller where to send traffic based on the URL.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
  labels:
    app: app
spec:
  rules:
  - host: app.acme.org
    http:
      paths:
      - backend:
          serviceName: app
          servicePort: 3000
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
  labels:
    app: api
spec:
  rules:
  - host: api.acme.org
    http:
      paths:
      - backend:
          serviceName: api
          servicePort: 4000
Note You will need to change api.acme.org and app.acme.org to domains you actually have control over. Unless you own acme.org!

Here we are creating 2 Ingress objects which will setup our "routing table". The creation of these objects are observed by our Ingress Controller (which we setup in step 3) and will lead into the nginx configuration being modified accordingly.

Save this file as ingress_01.yml and apply it to your cluster:

$ kubectl apply -f ingress_01.yml

Testing it so far

This could be a good point to stop and see how we're doing! Let's begin by testing the services directly. To do that, we're going to use port-forward to connect to the pods directly from our dev machine and see if the server is responding.

$ kubectl port-forward app-9c5fbd865-8bl6n 3000:3000

Now in another window, try hitting the service:

$ curl http://localhost:3000
Hello! This is my app!

You can try the same with the API service as well.

Let's see how our Ingress is doing:

$ kubectl get ing

NAME          HOSTS          ADDRESS               PORTS     AGE
api-ingress   api.acme.org   136.23.12.89          80        1h
app-ingress   app.acme.org   136.23.12.89          80        1h

Here, 136.23.12.89 should be the IP address of your load balancer. You can tell that by checking the Ingress Controller service:

$ kubectl get svc -n kube-system

NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
default-http-backend   ClusterIP      10.245.117.145   <none>          80/TCP                       1h
nginx-ingress-lb       LoadBalancer   10.245.208.224   136.23.12.89    80:31874/TCP,443:32004/TCP   1h

Let's check to see if our Ingress Controller is up and running (your kube-system namespace might have other pods running in it as well):

$ kubectl get pod -n kube-system

NAME                                    READY     STATUS    RESTARTS   AGE
default-http-backend-64c956bc67-95km7   1/1       Running   0          3h
nginx-ingress-controller-kgtzc          1/1       Running   0          3h
nginx-ingress-controller-n7dps          1/1       Running   0          3h
nginx-ingress-controller-t6vfv          1/1       Running   0          3h

Now that we know our service is running, our load balancer is in place and our Ingress Controller is up and running, we can test the flow.

First try hitting the default HTTP backend by targeting the load balancers directly:

$ curl http://136.23.12.89

default backend - 404

This looks good. Without sending traffic that belongs to any specific Ingress, this is the expected behaviour.

Now let's trick nginx into thinking we're hitting the endpoint from a URL by adding a Host HTTP header (make sure to replace the header with the appropriate domain you used in the Ingress controller above):

$ curl -H 'Host: app.acme.org' http://136.23.12.89
Hello! This is my app!

$ curl -H 'Host: api.acme.org' http://136.23.12.89
Hello! This is my api!

If this is what you're getting, then you're ready for the next step.

Step 5: Setting up DNS

Technically you can manually setup an A record that points to your load balancer and be done. Here, we're doing more than automatic configuration of your DNS provider: as well as making sure your DNS record is updated if your load balancer is deleted and re-recreated, we are also automating addition of any new Ingress records you might add in future. We only have 2 Ingress records for api.acme.org and app.acme.org at the moment, but if we add more Ingress records in future for more services, this method will create and update all of them automatically.

Also, you can use this to include any services you are hosting on your cluster that don't use Ingress, like TCP services.

To automate this part, we are using a little useful project called External DNS. This tool sits on top of your cluster and listens to any new services or ingress that is created and creates the DNS records you need with your DNS provider. It supports a wide range of DNS providers you can choose from. For this example, I'm going to use DigitalOcean's DNS service.

To use this, first you need to login to your DigitalOcean account and create a new domain under Networking / Domain. Follow their guide to set it up. Once you have your domain setup, head to DigitalOcean API section and create a read / write API key for your account. ExternalDNS needs this key to make changes to your DNS records.

First, let's add our API key as a Secret in the cluster (replace 'XXXX' with the key):

kubectl create secret generic api-keys -n kube-system --from-literal=do-api-key=XXXXXXX
# kubectl create secret generic api-keys -n kube-system --from-literal=do-api-key=XXXXXXX
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
  namespace: kube-system
rules:
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
  resources: ["ingresses"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:latest
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=acme.org
        - --provider=digitalocean
#        - --dry-run
#        - --log-level=debug
        env:
        - name: DO_TOKEN
          valueFrom:
            secretKeyRef:
              name: api-keys
              key: do-api-key
Make sure to change acme.org to the domain name you added to your DigitalOcean Domains account.

Save this file as external_dns.yml and apply it to the cluster:

$ kubectl apply -f external_dns.yml

This will add ExternalDNS to your cluster. Once started, ExternalDNS will look at all the Ingress records in the cluster and creates DNS records for the ones that have a spec.rules.host that matches acme.org.

You can check the progress by tapping into ExternalDNS log files:

$ kubectl get pod -n kube-system

NAME                                    READY     STATUS    RESTARTS   AGE
...
external-dns-7c84f5c5b4-fk2bk           1/1       Running   0          4h
...

$ kubectl logs external-dns-7c84f5c5b4-fk2bk -n kube-system -f

You can also log into your DigitalOcean account and see the records created or check it from terminal:

$ dig app.acme.org
; <<>> DiG 9.10.6 <<>> app.acme.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64362
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1452
;; QUESTION SECTION:
;app.acme.org.		IN	A

;; ANSWER SECTION:
app.acme.org.	300	IN	A	136.23.12.89

;; Query time: 128 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Fri Mar 22 13:57:37 GMT 2019
;; MSG SIZE  rcvd: 64

$ dig api.acme.org
; <<>> DiG 9.10.6 <<>> app.acme.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64362
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1452
;; QUESTION SECTION:
;api.acme.org.		IN	A

;; ANSWER SECTION:
api.acme.org.	300	IN	A	136.23.12.89

;; Query time: 128 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Fri Mar 22 13:57:37 GMT 2019
;; MSG SIZE  rcvd: 64

Once you have the records you can try reaching the services again, this time using their domain name:

$ curl http://app.acme.org
Hello! This is my app!

$ curl http://api.acme.org
Hello! This is my api!

Step 6: Setting up SSL

To secure our services, we are going to use Let's Encrypt SSL certificates. There is a project called Cert Manager, which is based on or inspired by another project called Lego. Cert Manager can be installed on your cluster to watch any services and ingress records you might have on the cluster and request SSL certificates for them automatically. It also takes care of renewing the certificates for you.

Setting up Cert Manager is not very easy. I have included the file you need to use to set it up in the same repository that comes with this post, but I advise you to follow the steps on Cert Manager's website to install it. However. I would advise against using the Helm chart approach as it has caused my pain several times before. Firstly the Helm chart approach is indeterministic and depending on when you try it might break the rest of the configuration you need to do. This has happened to me before. Another scary part about the Helm approach (which is not limited to Cert Manager) is that it can install things across the cluster without telling you. You'd need to know what the chart (release) you are using does before trying it or you might break the cluster for everyone. One of the biggest advantages of Kubernetes for me is the immutable infrastructure: I can run the same code every time and get the same results back from fresh. Helm breaks this firstly by loose package version management. Secondly, it encourages "copy/paste" insfrastructure setup: copy a helm install and paste it to get to results which is very similar to configuring servers by copy / pasting apt-install commands without use of configuration management tools. This is the topic for another post, so for now you can use the following command to install Cert Manager on your cluster from this script:

Please note that in this section I am using Cluster Issuers instead of Issuers. This means by running this command you are installing Cert Manager on your entire cluster and not just a single domain. You can replace ClusterIssuer with Issuer if you like.
$ kubectl apply -f https://raw.githubusercontent.com/cloud66-samples/k8s-secure-endpoints/master/k8s/cert_manager.yml

This will setup Cert Manager on your cluster. You now need to setup the needed certificates:

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    # You must replace this email address with your own.
    # Let's Encrypt will use this to contact you about expiring
    # certificates, and issues related to your account.
    email: foo@acme.org
    privateKeySecretRef:
      name: letsencrypt-production-key
    http01: {}
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: app-acme-systems
  namespace: central-bootstrap
spec:
  secretName: app-acme-systems-tls
  issuerRef:
    name: letsencrypt-production
    kind: ClusterIssuer
  commonName: app.acme.org
  dnsNames:
  - app.acme.org
  acme:
    config:
    - http01:
        ingressClass: nginx
      domains:
      - app.acme.org
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: api-acme-systems
  namespace: central-bootstrap
spec:
  secretName: api-acme-systems-tls
  issuerRef:
    name: letsencrypt-production
    kind: ClusterIssuer
  commonName: api.acme.org
  dnsNames:
  - api.acme.org
  acme:
    config:
    - http01:
        ingressClass: nginx
      domains:
      - api.acme.org
Make sure to replace foot@acme.org , app.acme.org and api.acme.or with the correct values.

Save this file as certificates.yml and apply it to your cluster.

$ kubectl apply -f certificates.yml

This will get Cert Manager to contact Let's Entrypt and issue, install certificates for your Ingress records. However, we still have 1 more step before we can use our secure endpoints: we need to tell Ingress Controller where to find the certificates. To do this, open ingress_01.yml you created and overwrite it with this one:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    kubernetes.io/tls-acme: "true"
    certmanager.k8s.io/cluster-issuer: "letsencrypt-production"
  labels:
    app: app
spec:
  rules:
  - host: app.acme.org
    http:
      paths:
      - backend:
          serviceName: app
          servicePort: 3000
  tls:
  - secretName: app-acme-org-tls
    hosts:
    - app.acme.org
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    kubernetes.io/tls-acme: "true"
    certmanager.k8s.io/cluster-issuer: "letsencrypt-production"
  labels:
    app: api
spec:
  rules:
  - host: api.acme.org
    http:
      paths:
      - backend:
          serviceName: api
          servicePort: 4000
  tls:
  - secretName: api-acme-org-tls
    hosts:
    - api.acme.org
Make sure to replace app.acme.org api.acme.org app-acme-org-tls and api-acme-org-tls with the correct names.

The changes we made to our ingress are in the annotations and the tls section. As for the annotations we added 2 annotations: kubernetes.io/tls-acme tells Cert Manager that we need a certificate for this endpoint. certmanager.k8s.io/cluster-issuer specifies which certificate issuer we want to use for this endpoint. If you changed your issuer from ClusterIssuer to Issuer make sure to reflect it here too.

The tls changes are simply telling Ingress Controller where to find the TLS certificates generated by Cert Manager by pointing it to the right secret by name.

To check and see if the certificates have been issued (this may take some time), you can run this command:

$ kubectl get certificates 

NAMESPACE           NAME                               AGE
default             api-acme-org-tls                   15h
default             app-acme-org-tls                   15h

See it work!

You can now try and hit your service with https:

$ curl https://app.acme.org
Hello! This is my app!

$ curl https://api.acme.org
Hello! This is my api!

Congratulations! You just setup everything that was needed to run a service with SSL security on a Kubernetes cluster! Now you might want to check out our products that make all this much easier to deal with or send me any questions you might have about these!



Related Content: