In this short tutorial, you'll be setting up TLS Termination on an (nginx) Ingress controller. We know you're busy, so let's get right to it.
You'll be performing the following steps:
- If needed: setup a cluster
- Define a simple test http service deployment
- Define the plain
Ingressobject for our service - Create a (self-signed) certificate using openssl
- create the Kubernetes Secrets
- Update the
Ingressobject with TLS termination
You will be creating a https-service for the imaginary company Evil Corp.
Define the cluster and namespace
You can use any cluster with an ingress controller for this tutorial. If you want to set-up a fresh clean minikube cluster for this tutorial use the following command:
$ minikube start -p edc4it
This will spin up a minikube cluster using the default driver and default memory/cpu resource.
Either way we will assume you created the following namespace while following this tutorial:
$ kubectl create namespace tls-tutorial
Define a sample service
In order to test our TLS termination, we'll need a web service/site. We will quickly create a Deployment and Service for this purpose.
Define the Deployment inside a file named evilcorp-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: evilcorp
namespace: tls-tutorial
labels:
app.kubernetes.io/name: evilcorp
spec:
selector:
matchLabels:
app.kubernetes.io/name: evilcorp
template:
metadata:
labels:
app.kubernetes.io/name: evilcorp
spec:
containers:
- name: main
image: edc4it/generic-service:1.0
ports:
- containerPort: 80
protocol: TCP
name: http
Then the service in evilcorp-svc.yaml:
apiVersion: v1
kind: Service
metadata:
name: evilcorp
namespace: tls-tutorial
labels:
app.kubernetes.io/name: evilcorp
spec:
selector:
app.kubernetes.io/name: evilcorp
ports:
- port: 80
targetPort: http
appProtocol: http
Apply both files using kubectl
kubectl apply -f .
Perform the following verification steps:
- Your deployment has span up its single pod:
kubectl -n tls-tutorial get deploy evilcorp - Your service was created:
kubectl -n tls-tutorial get svc evilcorp - And that its endpoints have been populated:
kubectl -n tls-tutorial get ep evilcorp
Define a standard Ingress (without TLS)
To start and to work progressively, let's start with a plain Ingress for our service.
First make sure you have an ingress controller installed. To enable ingress for minikube run the following command:
$ minikube -p edc4it addons enable ingress
This creates an ingress controller in the ingress-nginx namespace:
$ kubectl get deployments -l app.kubernetes.io/instance=ingress-nginx --all-namespaces
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
ingress-nginx ingress-nginx-controller 1/1 1 1 127d21h
Create a new file evilcorp-ing.yaml and paste the following contents in it:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: evilcorp
namespace: tls-tutorial
labels:
app.kubernetes.io/name: evilcorp
spec:
rules:
- host: www.evilcorp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: evilcorp
port:
name: http
get the HTTP NodePort of your Ingress controller:
$ kg svc -l app.kubernetes.io/instance=ingress-nginx --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx ingress-nginx-controller NodePort 10.107.156.187 <none> 80:32115/TCP,443:31957/TCP 127d22h
ingress-nginx ingress-nginx-controller-admission ClusterIP 10.106.58.204 <none> 443/TCP 127d22h
The NodePort is available under the Ports header. In the example above it is 32115 for HTTP (to port 80)
For the ip address, we just need an ip address of one of your nodes. If you using minikube for this tutorial, you can get its ip using the following command:
$ minikube -p edc4it ip
You can then use that to make a request to our service
$ curl http://192.168.49.2:32115/version -H "Host: www.evilcorp.com"
You can also use the following command, which obtains the minikube ip and ingress controller's node port using sub commands:
$ IP=$(minikube -p edc4it ip) \
PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}') && \
curl http://$IP:$PORT/version -H "Host: www.evilcorp.com"
Define the (SAN) Certificate
In order to define a certificate to be used with an Ingress Controller, we need to use a SAN Certificate. Kubernetes still supports the Common Name Field, but it has been deprecated. In case you are not familiar with these concepts: in the "past" we would use the CN= field in the certificate to denote the server's domain name. The problem with this was that it could only hold one entry (wildcard or non-wildcard). SAN ("Subject Alternate Name") was introduced to solve this limitation.
To create a SAN certificate, define the following configuration file (we assume you name this file evilcorp.conf:
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = NY
L = New York
O = E Corp
OU = Security
CN = www.evilcorp.com
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.evilcorp.com
DNS.2 = evilcorp.com
Note the distinguished_name points to the details in the req_distinguished_name. When using this command later, you won't be prompted for certificate fields (due to the prompt field set to false). SAN is an extension, therefore we added x509_extensions which refers to the v3_req section. Beside the SAN extension, we are using the Key usage and Extended Key Usage.
The subjectAltName refers to the list under the alt_names extension. As you can see it may contain multiple domain names associated with this certificate.
To create the key and certificate, use the following command:
$ openssl req -x509 -nodes -days 730 -newkey rsa:2048 \
-keyout evilcorp.key \
-out evilcorp.crt \
-config evilcorp.conf \
-extensions 'v3_req'
For those that are unfamiliar with openssl and curious:
- With the
reqcommand we use the certificate request and certificate generating utility - By using the
-newkeyoption, we also create a new private key using RSA with 2048 nBits - In this article we are using a self-signed certificate. This is accomplished by passing the
x509option (By default openssl outputs a certificate signing request) - The certificate will be valid for 730
days - The
nodes("no DES") option makes sure the key won't be encrypted - We use the
keyoutandoutoptions to save the newly created private key and certificate file - Our input configuration (
evilcorp.conf) is specified using theconfigoption - We are enabling/configuring the
extensionswhich can be found under ourv3_reqsection
Out of curiosity let's have a look the certificate (this also verifies if the command above was successful)
$ openssl x509 -in evilcorp.crt -noout -text
We don't want you to type commands and not understanding them, so here is the explanation of the command:
- This time the command is
x509. This is used to summon the "Certificate display and signing utility" - We want to display the certificate we just generated. This is supplied using the
inoption - We don't want to sign but merely display our
- We don't need to see the encoded certificate. Therefore we use
nooutoption - And finally, the goal is to display the certificate. Hence the
textoption (which prints the certificate in text form)
After running the command, you should be able to see your certificate. You might want to reassure yourself and notice the Issuer value (which you should recognize as Evil Corp). You can see this is a SAN certificate when looking for the "X509v3 Subject Alternative Name" extension settings. You should find your two DNS entries.
Great we've got a private key (evilcorp.key) and a certificate (evilcorp.crt).
Define the Kubernetes Secrets
Kubernetes secrets hold sensitive information. This includes arbitrary data (type: Opaque) service-accounts tokens (kubernetes.io/service-account-token), docker configuration (kubernetes.io/dockerconfigjson) and and among others, our kubernetes.io/tls
It is always best to define your kubernetes objects using the declarative approach using yaml files. In this article we will however resort to the imperative way (feel free to append -oyaml --dry-run=client > evilcorp-tls.yaml to the command below to obtain the yaml file)
$ kubectl -n tls-tutorial create secret tls evilcorp-tls \
--key evilcorp.key \
--cert evilcorp.crt
This defines a Kubernetes secret named evtls of type kubernetes.io/tls with the required data entries tls.crt and tls.key as shown below (we have abbreviated the base64 encoded certificate and key data):
apiVersion: v1
kind: Secret
metadata:
name: evilcorp-tls
namespace: tls-tutorial
type: kubernetes.io/tls
data:
tls.crt: LS0tL…UtLS0tLQo=
tls.key: LS0tL…S0tLS0K
Check if the secret has been successfully created on your cluster:
$ kubectl -n tls-tutorial describe secrets evilcorp-tls
Define TLS Termination on our Ingress
In order to configure TLS termination, we need to add tls configuration to our Ingress object.
Open your evilcorp-svc.yaml and add the following yaml under the spec:
tls:
- hosts:
- www.evilcorp.com
secretName: evilcorp-tls
Apply your updated evilcorp-svc.yaml:
$ kubectl -f evilcorp-svc.yam
Then Check the logs of your ingress controller, that your ingress controller configuration has been updated:
$ kubectl -n ingress-nginx -l app.kubernetes.io/instance=ingress-nginx
We need to obtain the https port on your ingress controller. Use the same command you used before when you needed the standard http port:
$ kg svc -l app.kubernetes.io/instance=ingress-nginx --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx ingress-nginx-controller NodePort 10.107.156.187 <none> 80:32115/TCP,443:31957/TCP 127d22h
ingress-nginx ingress-nginx-controller-admission ClusterIP 10.106.58.204 <none> 443/TCP 127d22h
In the example above, the https port is available on the ephemeral port 31957. Let's use this to send an https request.
This time we cannot just set the Host header (with SAN there's actually a preflight request). We could update your host file, but that would just leave skeletons on your machine after this tutorial. There's an --resolve option on cUrl to resolve a hostname.
Use all of this to send the following cUrl request (recall the ip address is one of your node's. With minikube you can get such an ip using minikube -p edc4it ip)
(Spoiler it will fail!)
$ curl https://www.evilcorp.com:31957/version --resolve "www.evilcorp.com:31957:192.168.49.2"
Or using a one-liner (will still fail :)
$ IP=$(minikube -p edc4it ip)
PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
curl https://www.evilcorp.com:$PORT/version --resolve "www.evilcorp.com:$PORT:$IP"
It fails because the certificate we're using is self-signed. You can obtain the certificate using the following command:
$ curl --insecure -vvI https://www.evilcorp.com:31957 --resolve "www.evilcorp.com:31957:192.168.49.2" 2>&1 | awk 'BEGIN { cert=0 } /^\* SSL connection/ { cert=1 } /^\*/ { if (cert) print }'
or using a one-liner:
$ IP=$(minikube -p edc4it ip)
PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
curl --insecure -vvI https://www.evilcorp.com:31957 --resolve "www.evilcorp.com:$PORT:$IP" 2>&1 | awk 'BEGIN { cert=0 } /^\* SSL connection/ { cert=1 } /^\*/ { if (cert) print }'
You should recognise your old friend (notice the subject)
For purposes of this tutorial, we can safely ignore the fact that we are using a self-signed certificate. To do so using cUrl, just add the --insecure (or the abbreviated -k) option to our original request:
$ IP=$(minikube -p edc4it ip)
PORT=$(kubectl -n ingress-nginx get service ingress-nginx-controller -o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}') && \
curl --insecure https://www.evilcorp.com:$PORT/version --resolve "www.evilcorp.com:$PORT:$IP"
And that should work. You have successfully configured your Ingress with its TLS secret to apply TLS Termination.
Even though we used a self-signed certificate, the steps to configure Kubernetes (TLS Secret and the Ingress) are the same regardless of what kind of certificate you use.
Hope this tutorial was helpful for you.