Updated: 2020-06-18
If you followed my last post, I automated DNS using external-dns. Now it’s time to automate SSL Certificates with cert-manager using Let’s Encrypt.
Install
Get latest version from https://hub.helm.sh/charts/jetstack/cert-manager.
# Install the CustomResourceDefinition
# Kubernetes 1.15+
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.15.1/cert-manager.crds.yaml
# Kubernetes <1.15
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.15.1/cert-manager-legacy.crds.yaml
# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io
# Update your local Helm chart repository cache
helm repo update # create namespace kubectl create ns cert-manager
# Install the cert-manager Helm chart
helm install cert-manager \
-n cert-manager \
jetstack/cert-manager
Verify
kubectl get pods -n cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5d669ffbd8-zhzm8 1/1 Running 0 2m18s
cert-manager-cainjector-79b7fc64f-rlcgx 1/1 Running 0 2m19s
cert-manager-webhook-6484955794-nmh84 1/1 Running 0 2m19s
Test Installation
Create a ClusterIssuer to test the webhook works okay.
cat <<EOF > test-resources.yaml
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager-test
---
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: test-selfsigned
namespace: cert-manager-test
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: selfsigned-cert
namespace: cert-manager-test
spec:
commonName: example.com
secretName: selfsigned-cert-tls
issuerRef:
name: test-selfsigned
EOF
Create the test resources.
kubectl apply -f test-resources.yaml
Check the status of the newly created certificate. You may need to wait a few seconds before cert-manager processes the certificate request.
kubectl describe certificate.cert-manager.io -n cert-manager-test
Name: selfsigned-cert
Namespace: cert-manager-test
Labels: <none>
Annotations: API Version: cert-manager.io/v1alpha3
Kind: Certificate
Metadata:
Creation Timestamp: 2020-06-13T04:27:41Z
Generation: 1
Resource Version: 9390932
Self Link: /apis/cert-manager.io/v1alpha3/namespaces/cert-manager-test/certificates/selfsigned-cert
UID: 2a513598-80c2-465d-aa9f-6b318bf9c7a9
Spec:
Common Name: example.com
Issuer Ref:
Name: test-selfsigned
Secret Name: selfsigned-cert-tls
Status:
Conditions:
Last Transition Time: 2020-06-13T04:27:41Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-09-11T04:27:41Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 80s cert-manager Generated a new private key
Normal Requested 80s cert-manager Created new CertificateRequest resource "selfsigned-cert-2334779822"
Normal Issued 80s cert-manager Certificate issued successfully
Clean up the test resources.
kubectl delete -f test-resources.yaml
Create Zone
I created a public zone within Google Cloud DNS.
https://cloud.google.com/dns/docs/quickstart
Create GCP service account and download key
Create a GCP service account with DNS admin access. The ClusterIssuer will use it to access DNS from the K8s cluster.
export PROJECT_NAME=[YOUR_PROJECT_NAME]
# create service account
gcloud iam service-accounts create k8s-cert-manager \
--display-name="Service Account to manage SSL Certificates." \
--project=$PROJECT_NAME
# create and download service account key
gcloud iam service-accounts keys create ./credentials.json \
--iam-account=k8s-cert-manager@$PROJECT_NAME.iam.gserviceaccount.com \
--project=$PROJECT_NAME
# give dns admin permissions
gcloud projects add-iam-policy-binding $PROJECT_NAME \
--member=serviceAccount:k8s-cert-manager@$PROJECT_NAME.iam.gserviceaccount.com \
--role=roles/dns.admin
Create secret from GCP service account
kubectl create secret generic cert-manager \
--from-file=./credentials.json \
--namespace=cert-manager
ClusterIssuer
I am using a ClusterIssuer instead of an Issuer.
“ClusterIssuers are a resource type similar to Issuers. They are specified in exactly the same way, but they do not belong to a single namespace and can be referenced by Certificate resources from multiple different namespaces.”
https://docs.cert-manager.io/en/latest/reference/clusterissuers.html
https://docs.cert-manager.io/en/latest/tutorials/acme/dns-validation.html
I will also be using a DNS-01 challenge mechanism (versus HTTP-01). More about this later.
There is a Let’s Encrypt staging and production API. I suggest you use the staging until you’ve worked out any bugs. The Let’s Encrypt production API will rate limit you.
“We highly recommend testing against our staging environment before using our production environment. This will allow you to get things right before issuing trusted certificates and reduce the chance of your running up against rate limits.”
https://letsencrypt.org/docs/staging-environment/
Create ClusterIssuer Staging
cat <<EOF > clusterissuer-staging.yaml
apiVersion: certmanager.k8s.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
namespace: cert-manager
spec:
acme:
# The ACME server URL
server: https://acme-staging-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: username@domain.com
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-staging
solvers:
# ACME DNS-01 provider configurations
- dns01:
# Google Cloud DNS
clouddns:
# Secret from the google service account key
serviceAccountSecretRef:
name: cert-manager
key: credentials.json
# The project in which to update the DNS zone
project: $PROJECT_NAME
EOF
Apply.
kubectl apply -f clusterissuer-staging.yaml
Describe.
kubectl describe clusterissuer letsencrypt-staging
Create ClusterIssuer Production
cat <<EOF > clusterissuer-production.yaml
apiVersion: certmanager.k8s.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt
namespace: cert-manager
spec:
acme:
# The ACME server URL
server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: username@domain.com
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt
solvers:
# ACME DNS-01 provider configurations
- dns01:
# Google Cloud DNS
clouddns:
# Secret from the google service account key
serviceAccountSecretRef:
name: cert-manager
key: credentials.json
# The project in which to update the DNS zone
project: $PROJECT_NAME
EOF
Apply, describe.
kubectl apply -f clusterissuer-production.yaml
kubectl describe clusterissuer letsencrypt
Certificate
Once we have created the above ClusterIssuer, we can use it to obtain a certificate.
To request a certificate from Let’s Encrypt (or any Certificate Authority), you need to provide some kind of proof that you are entitled to receive the certificate for a given domain. Let’s Encrypt support two methods of validation to prove control of your domain, http-01 (validation over HTTP) and dns-01 (validation via DNS). Wildcard domain certificates (those covering *.yourdomain.com) can only be requested using DNS validation.
cert-manager will periodically check its validity and attempt to renew it if it gets close to expiry. cert-manager considers certificates to be close to expiry when the ‘Not After’ field on the certificate is less than the current time plus 30 days.
Create Certificate: Staging Wildcard
cat <<EOF > certificate-itsmetommy-io-tls-staging.yaml
apiVersion: cert-manager.io/v1alpha3
kind: Certificate
metadata:
name: itsmetommy-io-tls-staging
namespace: itsmetommy
spec:
secretName: itsmetommy-io-tls-staging
dnsNames:
- '*.itsmetommy.io'
issuerRef:
name: letsencrypt-staging
kind: ClusterIssuer
EOF
Create, describe.
kubectl create -f certificate-itsmetommy-io-tls-staging.yaml
kubectl describe secret itsmetommy-io-tls-staging
kubectl describe cert itsmetommy-io-tls-staging
Create Certificate: Production Wildcard
cat <<EOF > certificate-itsmetommy-io-tls.yaml
apiVersion: cert-manager.io/v1alpha3
kind: Certificate
metadata:
name: itsmetommy-io-tls
namespace: itsmetommy
spec:
secretName: itsmetommy-io-tls
dnsNames:
- '*.itsmetommy.io'
issuerRef:
name: letsencrypt
kind: ClusterIssuer
EOF
Create, describe.
kubectl create -f certificate-itsmetommy-io-tls.yaml
kubectl describe secret itsmetommy-io-tls
kubectl describe cert itsmetommy-io-tls
Create Deployment
kubectl create deployment nginx --image=nginx -n itsmetommy
Create Service
kubectl expose deployment nginx --port=80 --target-port=80 --type=NodePort -n itsmetommy
Ingress
In this blog, I will show you two ways of utilizing Certificates within an Ingress.
- Ingress with an existing certificate
- Ingress with an autogenerated certificate
Note: The ingress and certificate have to be in the same namespace.
Ingress w/ existing Certificate: Staging
Uses existing certificate itsmetommy-io-tls-staging, which includes *.itsmetommy.io.
Note: There is no need to include the annotations cert-manager.io section if you’ve already created a certificate.
cat <<EOF > ingress-itsmetommy-io-staging.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: itsmetommy-io-staging
namespace: itsmetommy
annotations:
# use ingress-nginx controller
kubernetes.io/ingress.class: nginx
spec:
tls:
# use an already existing certificate within the same namespace
- secretName: itsmetommy-io-tls-staging
hosts:
- staging-1.itsmetommy.io
- staging-2.itsmetommy.io
rules:
- host: staging-1.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
- host: staging-2.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
EOF
Create, get, describe.
kubectl create -f ingress-itsmetommy-io-staging.yaml
kubectl get ingress itsmetommy-io-staging
kubectl describe secret itsmetommy-io-tls-staging
kubectl describe ingress itsmetommy-io-staging
Ingress w/ existing Certificate: Production
Note: There is no need to include the annotations cert-manager.io section if you’ve already created a certificate.
cat <<EOF > ingress-itsmetommy-io.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: itsmetommy-io
namespace: itsmetommy
annotations:
# use ingress-nginx controller
kubernetes.io/ingress.class: nginx
spec:
tls:
# use an already existing certificate within the same namespace
- secretName: itsmetommy-io-tls
hosts:
- prod-1.itsmetommy.io
- prod-2.itsmetommy.io
rules:
- host: prod-1.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
- host: prod-2.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
EOF
Create, get, describe.
kubectl create -f ingress-itsmetommy-io.yaml
kubectl get ingress itsmetommy-io
kubectl describe secret itsmetommy-io-tls
kubectl describe ingress itsmetommy-io
Ingress w/ autogenerated Certificate: Staging
This Ingress will automatically create a certificate for you because the name of the secret does not exist, which hold the actual certificate. The most important part of having auto generated certificates is the secretName. Make sure you do not use the same name as a certificate that you already created before. For example, we already created a certificate called itsmetommy-io-tls-staging, which created a secret called itsmetommy-io-tls-staging with the certificate in it. In this case, we do not want to use the same name. This will cause changes that will break the certificate.
You will also notice that I included cert-manager.io in the annotations section. This tells tells which issuer to use to generate the certificate.
cat <<EOF > ingress-itsmetommy-io-auto-staging.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: itsmetommy-io-auto-staging
namespace: itsmetommy
annotations:
# use ingress-nginx controller
kubernetes.io/ingress.class: nginx
# automatically create ssl certificate using cert-manager
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
tls:
- secretName: itsmetommy-io-auto-tls-staging
hosts:
- staging-1-auto.itsmetommy.io
- staging-2-auto.itsmetommy.io
rules:
- host: staging-1-auto.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
- host: staging-2-auto.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
EOF
Create, get, describe.
kubectl create -f ingress-itsmetommy-io-auto-staging.yaml
kubectl get ingress itsmetommy-io-staging
kubectl describe secret itsmetommy-io-auto-tls-staging
kubectl describe ingress itsmetommy-io-auto-staging
Ingress w/ autogenerated Certificate: Production
cat <<EOF > ingress-itsmetommy-io-auto.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: itsmetommy-io-auto
namespace: itsmetommy
annotations:
# use ingress-nginx controller
kubernetes.io/ingress.class: nginx
# automatically create ssl certificate using cert-manager
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- secretName: itsmetommy-io-auto-tls
hosts:
- prod-1-auto.itsmetommy.io
- prod-2-auto.itsmetommy.io
rules:
- host: prod-1-auto.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
- host: prod-2-auto.itsmetommy.io
http:
paths:
- path:
backend:
serviceName: nginx
servicePort: 80
EOF
Create, get, describe.
kubectl create -f ingress-itsmetommy-io-auto.yaml
kubectl get ingress itsmetommy-io
kubectl describe secret itsmetommy-io-auto-tls
kubectl describe ingress itsmetommy-io-auto
2 responses to “Kubernetes: cert-manager on GKE using Let’s Encrypt”
[…] previously went over how to create SSL Certificates using cert-manager, but Google also has a GKE specific way of somewhat doing the same thing by using a custom resource […]
[…] https://itsmetommy.com/2019/06/23/kubernetes-cert-manager-on-gke-using-lets-encrypt/ […]