SSL: automatic renewal
Kubernetes — the certificate in a TLS secret renews by itself
A CronJob fetches the fresh certificate once a day and updates the TLS secret your ingress uses; the key stays in that same secret.
Renews by itself
Use this if your site runs in Kubernetes behind an ingress (ingress-nginx, Traefik, HAProxy Ingress and so on) and the certificate is stored in a secret of type kubernetes.io/tls. Ingress controllers watch secrets themselves: once you update the secret, the site serves the new certificate within a minute or two.
How it works: a CronJob fetches the certificate from the link once a day, compares it with the one in the secret, checks that it matches the key from that same secret, and replaces only tls.crt. The key is never copied anywhere. The job's permissions are to read and change one secret in one namespace.
What you need
- The certificate link from your account:
https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN. kubectlwith access to your site's namespace.- Outbound internet access from the cluster (to
api.zrazu.comand to the Alpine Linux mirrors — the image installscurl,opensslandjqon every run).
Step 1. The TLS secret (if it does not exist yet)
Download https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN/fullchain.pem into a file named fullchain.pem and create the secret with your key:
curl -fsS -o fullchain.pem "https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN/fullchain.pem"
kubectl -n NAMESPACE_NAME create secret tls zrazu-tls --cert=fullchain.pem --key=key.pemIn your site's Ingress, specify this secret: spec.tls[].secretName: zrazu-tls.
Step 2. The CronJob
Save the text below to a file named zrazu-ssl.yaml. If your secret has a different name, replace zrazu-tls (it appears twice).
# ZRAZU: updates the certificate in a TLS secret using the personal link. Runs once a day.
apiVersion: v1
kind: ServiceAccount
metadata:
name: zrazu-ssl
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: zrazu-ssl
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["zrazu-tls"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: zrazu-ssl
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: zrazu-ssl
subjects:
- kind: ServiceAccount
name: zrazu-ssl
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: zrazu-ssl
spec:
schedule: "17 4 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
serviceAccountName: zrazu-ssl
restartPolicy: Never
containers:
- name: update
image: alpine:3.20
env:
- name: PULL
value: "https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN"
- name: SECRET
value: "zrazu-tls"
command: ["/bin/sh", "-c"]
args:
- |
set -eu
apk add --no-cache curl openssl jq >/dev/null
SA=/var/run/secrets/kubernetes.io/serviceaccount
API="https://kubernetes.default.svc/api/v1/namespaces/$(cat $SA/namespace)/secrets/$SECRET"
k() { curl -fsS --cacert "$SA/ca.crt" -H "Authorization: Bearer $(cat $SA/token)" "$@"; }
cd /tmp
curl -fsS --retry 3 --max-time 60 -o new.pem "$PULL/fullchain.pem"
grep -q "BEGIN CERTIFICATE" new.pem || { echo "ZRAZU: not a certificate"; exit 1; }
k "$API" > secret.json
jq -r '.data["tls.crt"]' secret.json | base64 -d > current.pem
if cmp -s new.pem current.pem; then echo "ZRAZU: unchanged"; exit 0; fi
jq -r '.data["tls.key"]' secret.json | base64 -d > key.pem
cert_pub=$(openssl x509 -in new.pem -noout -pubkey | openssl sha256)
key_pub=$(openssl pkey -in key.pem -pubout | openssl sha256)
rm -f key.pem
[ "$cert_pub" = "$key_pub" ] || { echo "ZRAZU: certificate is for another key"; exit 1; }
openssl x509 -in new.pem -noout -checkend 0 >/dev/null || { echo "ZRAZU: expired"; exit 1; }
crt=$(base64 < new.pem | tr -d '\n')
k -X PATCH -H "Content-Type: application/merge-patch+json" --data "{\"data\":{\"tls.crt\":\"$crt\"}}" "$API" >/dev/null
echo "ZRAZU: updated, valid until $(openssl x509 -in new.pem -noout -enddate | cut -d= -f2)"Apply it in your site's namespace:
kubectl -n NAMESPACE_NAME apply -f zrazu-ssl.yamlCheck
- Run the job once by hand:
kubectl -n NAMESPACE_NAME create job --from=cronjob/zrazu-ssl zrazu-ssl-test. - Log:
kubectl -n NAMESPACE_NAME logs job/zrazu-ssl-test— "updated" or "unchanged". - Site: https://zrazu.com/tools/ssl/example.com/.
- Remove the test job:
kubectl -n NAMESPACE_NAME delete job zrazu-ssl-test.
If it did not work
403in the log — the Role is for the wrong secret or the wrong namespace: the name inresourceNamesmust match the secret's name.apk: … network error— the cluster has no internet access to the Alpine mirrors. Build your own image withcurl,opensslandjqand remove theapk addline.- "certificate is for another key" — the certificate was reissued with a new key: recreate the secret (step 1) with the new key.
- The secret is managed by cert-manager — then you do not need this guide: cert-manager renews the certificate by itself through the ZRAZU ACME server (in your account, "Installation" → ACME).