# Kubernetes — сертификат в TLS-секрете обновляется сам

> CronJob раз в сутки забирает свежий сертификат и обновляет TLS-секрет, с которым работает ingress; ключ остаётся в том же секрете.

URL: https://zrazu.com/ru/docs/ssl-auto-kubernetes/

Подходит, если сайт работает в 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` и создайте секрет со своим ключом:

```sh
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` (он встречается дважды).

```yaml file=zrazu-ssl-k8s.yaml
# 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 сайта:

```sh
kubectl -n ИМЯ_NAMESPACE apply -f zrazu-ssl.yaml
```

## Проверка

1. Запустите задачу один раз вручную: `kubectl -n ИМЯ_NAMESPACE create job --from=cronjob/zrazu-ssl zrazu-ssl-test`.
2. Журнал: `kubectl -n ИМЯ_NAMESPACE logs job/zrazu-ssl-test` — «updated» или «unchanged».
3. Сайт: [https://zrazu.com/tools/ssl/example.com/](https://zrazu.com/tools/ssl/example.com/).
4. Уберите пробную задачу: `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).
