SSL: автоматическое обновление
Kubernetes — сертификат в TLS-секрете обновляется сам
CronJob раз в сутки забирает свежий сертификат и обновляет TLS-секрет, с которым работает ingress; ключ остаётся в том же секрете.
Обновляется сам
Подходит, если сайт работает в Kubernetes за ingress (ingress-nginx, Traefik, HAProxy Ingress и т. п.) и сертификат лежит в секрете типа kubernetes.io/tls. Ingress-контроллеры следят за секретами сами: обновили секрет — через минуту-две сайт отдаёт новый сертификат.
Как это работает: CronJob раз в сутки забирает сертификат по ссылке, сравнивает с тем, что в секрете, проверяет, что он подходит к ключу из того же секрета, и заменяет только tls.crt. Ключ никуда не копируется. Права задачи — читать и изменять один секрет в одном namespace.
Что понадобится
- Ссылка на сертификат из кабинета:
https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН. kubectlс доступом к namespace сайта.- Выход из кластера в интернет (к
api.zrazu.comи к зеркалам Alpine Linux — образ ставитcurl,opensslиjqпри каждом запуске).
Шаг 1. TLS-секрет (если его ещё нет)
Загрузите https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН/fullchain.pem в файл fullchain.pem и создайте секрет со своим ключом:
curl -fsS -o fullchain.pem "https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН/fullchain.pem"
kubectl -n ИМЯ_NAMESPACE create secret tls zrazu-tls --cert=fullchain.pem --key=key.pemВ Ingress сайта укажите этот секрет: spec.tls[].secretName: zrazu-tls.
Шаг 2. CronJob
Сохраните текст ниже в файл zrazu-ssl.yaml. Если секрет у вас называется иначе, замените zrazu-tls (он встречается дважды).
# ZRAZU: обновление сертификата в TLS-секрете по личной ссылке. Запускается раз в сутки.
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_ВАШ_ТОКЕН"
- 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)"Примените в namespace сайта:
kubectl -n ИМЯ_NAMESPACE apply -f zrazu-ssl.yamlПроверка
- Запустите задачу один раз вручную:
kubectl -n ИМЯ_NAMESPACE create job --from=cronjob/zrazu-ssl zrazu-ssl-test. - Журнал:
kubectl -n ИМЯ_NAMESPACE logs job/zrazu-ssl-test— «updated» или «unchanged». - Сайт: https://zrazu.com/tools/ssl/example.com/.
- Уберите пробную задачу:
kubectl -n ИМЯ_NAMESPACE delete job zrazu-ssl-test.
Если не получилось
403в журнале — Role не на тот секрет или не в том namespace: название вresourceNamesдолжно совпадать с названием секрета.apk: … network error— у кластера нет выхода в интернет к зеркалам Alpine. Соберите свой образ сcurl,opensslиjqи уберите строкуapk add.- «certificate is for another key» — сертификат перевыпускали с новым ключом: пересоздайте секрет (шаг 1) с новым ключом.
- Секретом управляет cert-manager — тогда эта инструкция не нужна: cert-manager продлевает сертификат сам через ACME-сервер ZRAZU (в кабинете, «Установка» → ACME).