Automatic issuance of SSL certificates. We use Kubernetes and FreeIPA
Recently I started using a new way to obtain SSL certificates. I saved a small cheat sheet for myself so that I don’t have to remember the steps and commands every time and that's all. Then I decided that such instructions might be useful to someone else.
Important! To use this method of issuing SSL certificates, you must have an extra Kubernetes cluster and FreeIPA, between which network connectivity is configured. If so, congratulations, let's fly for certificates.
Briefly about the task
First, let me remind you what we are talking about. Issuing SSL certificates for a company's web resources is a typical administrative task. It can be solved in different ways. There is always an option to buy, the price is several tens of dollars.
You can also use free methods, including those based on Automated Certificate Management Environment (ACME) protocol algorithms. Thanks to them, you can order a domain-validated SSL certificate. This is the simplest certificate that you can get for free for 90 days. Next update is required.
You can simplify working with certificates using Kubernetes. Or rather, thanks to the cert-manager plugin. Its task is precisely to automate the issuance and renewal of SSL certificates.
As it turned out, the network connectivity of Kubernetes with IdM solutions (from identity management, in case you forgot) allows you to make the process even easier. In my case it's FreeIPA.
Advantages of the method:
issuing an SSL certificate takes a maximum of 5 minutes (if you do everything manually, you can be stuck with the task for half a day),
some of the actions are performed once, which means that the issuance of the second and next certificates is even faster,
all updates occur automatically, there is no need to view certificates, track their statuses, etc.
Let's get down to business
1. First you need to enable ACME support in FreeIPA:
(idm)# ipa-acme-manage enable
2. In order for cert-manager to add the _acme-challenge DNS record in FreeIPA, we use the cert-manager provider (in my case RFC-2136). To do this we must create a new TSIG key on our IPA server:
tsig-keygen -a hmac-sha512 acme-update >> /etc/named/ipa-ext.conf
3. Next, you need to connect cert-manager to Kubernetes. This action is performed with the following command:
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.4 \
4. Once we have all the necessary manifests, we will need to create a secret in Kubernetes for the TSIG key. To do this, take the TSIG key and use it to create a Kubernetes secret:
kubectl -n cert-manager create secret generic ipa-tsig-secret --from-literal=tsig-secret-key="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
At this stage, the basic Kubernetes settings are completed. When issuing the next certificate, we start from step 5.
5. We form a series of manifestos.
Let's move on to the ClusterIssuer resource, which provides TLS certificates. In it we indicate our IdM server and the zone within which we will issue certificates:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: myservice.testzone.nubes.ru
namespace: testzone
spec:
acme:
email: test@testzone.nubes.ru
server: https://idm1.testzone.nubes.ru/acme/directory
privateKeySecretRef:
name: ipa-issuer-account-key
solvers:
- dns01:
rfc2136:
nameserver: idm.testzone.nubes.ru
tsigKeyName: acme-update
tsigAlgorithm: HMACSHA512
tsigSecretSecretRef:
name: ipa-tsig-secret
key: tsig-secret-key
selector:
dnsZones:
- 'testzone.nubes.ru'
7. Next, create NetworkPolicy.
NetworkPolicy is a network policy management mechanism. It is designed to define access rules for network traffic between pods in a cluster.
Using NetworkPolicy, we specify the necessary hooks in the white lists:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
annotations:
name: myservice-acme-http-solver
namespace: testzone
spec:
ingress:
- {}
podSelector:
matchExpressions:
- key: acme.cert-manager.io/http-domain
operator: Exists
- key: acme.cert-manager.io/http-token
operator: Exists
policyTypes:
- Ingress
8. In the certificate we indicate which ClusterIssuer we will contact and to which address we want to issue the certificate. We also write secretName (we will need this for the last manifest):
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: myservice-cert
namespace: testzone
spec:
commonName: 'myservice.testzone.nubes.ru'
dnsNames:
- myservice.testzone.nubes.ru
issuerRef:
name: myservice.testzone.nubes.ru
kind: ClusterIssuer
privateKey:
algorithm: RSA
encoding: PKCS1
size: 4096
secretName: myservice-tls
9. And, in fact, the last of the manifestos is Ingress. Specify the annotation 'kubernetes.io/tls-acme: “true”' and our secretName:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: myservice.testzone.nubes.ru
kubernetes.io/tls-acme: "true"
name: myservice-ingress
namespace: testzone
spec:
ingressClassName: nginx
rules:
- host: myservice.testzone.nubes.ru
http:
paths:
- backend:
service:
name: myservice-service
port:
number: 8080
path: /
pathType: Prefix
tls:
- hosts:
- myservice.testzone.nubes.ru
secretName: myservice-tls
10. On the IdM side we get:
And in Kubernetes we see:
kubectl -n testzone describe certificaterequests.cert-manager.io
...
...
...
Conditions:
Last Transition Time: 2024-04-17T12:19:01Z
Message: Certificate request has been approved by cert-manager.io
Reason: cert-manager.io
Status: True
Type: Approved
Last Transition Time: 2024-04-17T12:20:03Z
Message: Certificate fetched from issuer successfully
Reason: Issued
Status: True
Type: Ready
That's it, the certificate has been issued. In general, of course, you can get confused and do everything through Helm, and this is even more correct. But that's a completely different story…