Securing Kubernetes Ingress with Ambassador and Let's Encrypt
In addition to routing the incoming requests or exposing service API's through a single endpoint, the ingress gateways does other tasks, such as rate limiting, SSL termination, load balancing, authentication, circuit breaking and more. In this article I will show you how to install the Ambassador Gateway and other components to be able to obtain an SSL certificate for your application.
Introduction
What is SSL?
helm version
to make sure Helm is installed:$ helm version
version.BuildInfo{Version:"v3.2.4", GitCommit:"0ad800ef43d3b826f31a5ad8dfbb4fe05d143688", GitTreeState:"dirty", GoVersion:"go1.14.3"}
Note
Helm is a package manager for Kubernetes. Instead of dealing with individual deployments, services, configuration maps, secrets, and other Kubernetes resources, Helm packages them into "charts". A chart is a collection of different Kubernetes resource files. You can then take thee charts and version, deploy, upgrade, and manage them as a single unit.
Deploying the sample application
apiVersion: apps/v1
kind: Deployment
metadata:
name: dogpic-web
labels:
app.kubernetes.io/name: dogpic-web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: dogpic-web
template:
metadata:
labels:
app.kubernetes.io/name: dogpic-web
spec:
containers:
- name: dogpic-container
image: learncloudnative/dogpic-service:0.1.0
ports:
- containerPort: 3000
---
kind: Service
apiVersion: v1
metadata:
name: dogpic-service
labels:
app.kubernetes.io/name: dogpic-web
spec:
selector:
app.kubernetes.io/name: dogpic-web
ports:
- port: 3000
name: http
dogpic-app.yaml
file and use kubectl apply -f dogpic-app.yaml
to create the deployment and service.Deploying cert-manager
$ kubectl create ns cert-manager
namespace/cert-manager created
jetstack
Helm repository and refresh the local repository cache:$ helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
Update Complete. ⎈ Happy Helming!⎈
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v0.15.1 \
--set installCRDs=true
ClusterIssuer
or an Issuer
resource and configure it. This resource represents a certificate signing authority (CA) and allows cert-manager to issue certificates.ClusterIssuer
and an Issuer
is that the ClusterIssuer
operates at the cluster level, while an Issuer
resource works on a namespace. For example, you could configure different Issuer
resources for each namespace. Alternatively, you could create a ClusterIssuer
to issue certificates in any namespace.Challenges
HTTP-01
and DNS-01
challenge. You can read more details about each one of these on Let's Encrypt website.http://[my-cool-domain]/.well-known/acme-challenge/[token-file]
.ClusterIssuer
we will be using. Make sure you replace the email with your email address:apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: hello@example.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
selector: {}
kubectl describe clusterissuer
and confirming that the ACME account was registered (i.e. the email address you provided):...
Status:
Acme:
Last Registered Email: hello@example.com
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/89498526
Conditions:
Last Transition Time: 2020-06-22T20:36:04Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
kubectl get clusterissuer
you should see the indication that the ClusterIssuer
is ready:$ kubectl get clusterissuer
NAME READY AGE
letsencrypt-prod True 2m30s
Certificate
resource.Ambassador Gateway
$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-crds.yaml
...
$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-rbac.yaml
apiVersion: v1
kind: Service
metadata:
name: ambassador
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
service: ambassador
ambassador-svc.yaml
file and run kubectl apply -f ambassador-svc.yaml
.Note
Deploying the above service will create a load balancer in your cloud providers' account.
ambassador
service:$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ambassador LoadBalancer 10.0.78.66 51.143.120.54 80:31365/TCP 97s
ambassador-admin NodePort 10.0.65.191 <none> 8877:30189/TCP 4m20s
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 30d
http://[my-domain.com]
in your browser and it will resolve to the above IP address (the ingress controller inside the cluster).startkubernetes.com
. I will set up a subdomain dogs.startkubernetes.com
to point to the IP address of my load balancer (e.g. 51.143.120.54
) using an A record. Regardless of where you registered your domain, you should be able to update the DNS records. Check the documentation on your domain registrars website on how to do that.dogs.startkubernetes.com
with your domain or subdomain name):apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: ambassador
spec:
rules:
- host: dogs.startkubernetes.com
http:
paths:
- backend:
serviceName: dogpic-service
servicePort: 3000
http://dogs.startkubernetes.com
you should see the Dog Pic website as shown below.Requesting a certificate
Certificate
resource. This resource includes the issuer reference (ClusterIssuer
we created earlier), DNS names we want to request certificates for (dogs.startkubernetes.com
), and the Secret name where the certificate will be stored.apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: ambassador-certs
namespace: default
spec:
secretName: ambassador-certs
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- dogs.startkubernetes.com
dogs.startkubernetes.com
with your domain name. Once you've done that, save the YAML in cert.yaml
and create the certificate using kubectl apply -f -cert.yaml
.cm-acme-http-solver
:$ kubectl get po
NAME READY STATUS RESTARTS AGE
ambassador-9db7b5d76-jlcdg 1/1 Running 0 22h
ambassador-9db7b5d76-qcwgk 1/1 Running 0 22h
ambassador-9db7b5d76-xsfw4 1/1 Running 0 22h
cm-acme-http-solver-qzh6l 1/1 Running 0 25m
dogpic-web-7bf547bd54-f2pff 1/1 Running 0 22h
$ kubectl logs cm-acme-http-solver-qzh6l
I0622 20:39:26.712391 1 solver.go:39] cert-manager/acmesolver "msg"="starting listener" "expected_domain"="dogs.startkubernetes.com" "expected_key"="iqUZlG9v1K8czpAKaTpLfL278piwf-mN4VZNvuwD0Ks.xonKHFvEQg2Ox_mI0cPM7UpCUHfu6H4aKtRcdrpiLik" "expected_token"="iqUZlG9v1K8czpAKaTpLfL278piwf-mN4VZNvuwD0Ks" "listen_port"=8089
Mapping
resource from Ambassador. This resource defines a mapping to redirect requests with prefix ./well-known/acme-challenge
to the Kubernetes service that goes to the pod.apiVersion: getambassador.io/v2
kind: Mapping
metadata:
name: challenge-mapping
spec:
prefix: /.well-known/acme-challenge/
rewrite: ''
service: challenge-service
---
apiVersion: v1
kind: Service
metadata:
name: challenge-service
spec:
ports:
- port: 80
targetPort: 8089
selector:
acme.cert-manager.io/http01-solver: 'true'
challenge.yaml
and deploy it using kubectl apply -f challenge.yaml
. The cert-manager will retry the challenge and issue the certificate.kubectl get cert
and confirm the READY
column shows True
, like this:$ kubectl get cert
NAME READY SECRET AGE
ambassador-certs True ambassador-certs 35m
- Request the certificate by creating the
Certificate
resource. - Cert-manager creates the
http-solver
pod (exposed through thechallenge-service
we created) - Cert-manager uses the issuer referenced in the
Certificate
and requests the certificates for thednsNames
from the authority (Let's Encrypt) - The authority sends the challenge for the
http-solver
to prove that we own the domains and checks that the challenges are solved (i.e. downloads the file from/.well-known/acme-challenge/
) - Issued certificate and key are stored in the secret, referenced by the Issuer resource
Configuring TLS in Ingress
ambassador-certs
secret name in the Certificate
resource we created earlier.apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: ambassador
spec:
tls:
- hosts:
- dogs.startkubernetes.com
secretName: ambassador-certs
rules:
- host: dogs.startkubernetes.com
http:
paths:
- path: /
backend:
serviceName: dogpic-service
servicePort: 3000
spec
), we use the tls
key to specify the hosts and the secret name where the certificate and private key are stored.ingress-tls.yaml
and apply it with kubectl apply -f ingress-tls.yaml
.https://dogs.startkubernetes.com
) you will see that the connection is secure and it is using a valid certificate from Let's Encrypt.Cleanup
kubectl delete cert ambassador-certs
kubectl delete secret ambassador-certs
kubectl delete -f https://www.getambassador.io/yaml/ambassador/ambassador-crds.yaml
kubectl delete -f https://www.getambassador.io/yaml/ambassador/ambassador-rbac.yaml
kubectl delete svc ambassador
helm uninstall cert-manager -n cert-manager
kubectl delete svc dogpic-service challenge-service
kubectl delete deploy dogpic-web
kubectl delete ing my-ingress