SSL: автоматическое обновление

AWS — балансировщик, CloudFront, API Gateway

Функция Lambda раз в сутки забирает свежий сертификат и обновляет его в AWS Certificate Manager на том же ARN — балансировщик подхватит сам.

Обновляется сам

Подходит, если сайт отдаёт сертификат через AWS: Application или Network Load Balancer, CloudFront, API Gateway, Elastic Beanstalk. Сертификат там лежит в AWS Certificate Manager (ACM). Мы не просим ключей от вашего AWS: функция работает в вашем аккаунте и имеет право обновлять только один сертификат.

Как это работает: ACM позволяет импортировать новый сертификат в тот же ARN, и все сервисы, которые им пользуются, переходят на новый без перенастройки. Функция Lambda раз в сутки сравнивает сертификат по ссылке с тем, что в ACM, и если он новый — импортирует его вместе с вашим ключом, который лежит в Secrets Manager.

Что понадобится

  • Ссылка на сертификат из кабинета: https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН.
  • Закрытый ключ без пароля (RSA 2048 или ECDSA P-256) — тот, с которым заказывали сертификат.
  • Доступ к AWS Console с правами создавать функции Lambda, секреты и роли IAM.
  • 20 минут.

Регион. Для CloudFront сертификат должен быть в регионе US East (N. Virginia) us-east-1 — и секрет, и функцию делайте там же. Для балансировщика — регион балансировщика.

Шаг 1. Ключ — в Secrets Manager

  1. Secrets Manager → Store a new secret.
  2. Other type of secret → вкладка Plaintext → удалите пример и вставьте весь текст ключа со строками -----BEGIN … PRIVATE KEY----- и -----END … PRIVATE KEY-----.
  3. Next → название zrazu-ssl-key → Next → Next → Store.
  4. Откройте секрет и скопируйте Secret ARN.

Шаг 2. Первый импорт в ACM (если сертификата там ещё нет)

  1. Certificate Manager → Import a certificate.
  2. Certificate body — содержимое https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН/cert.pem (откройте в браузере, скопируйте весь текст); Certificate private key — ваш ключ; Certificate chain — содержимое https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН/chain.pem.
  3. Import. Откройте сертификат и скопируйте ARN.
  4. Подключите сертификат к сервису, если ещё не подключили: в балансировщике — Listeners → HTTPS:443 → Edit → Default SSL/TLS certificate; в CloudFront — Distribution → Edit → Custom SSL certificate.

Шаг 3. Функция Lambda

  1. Lambda → Create function → Author from scratch. Название zrazu-ssl-update, Runtime — Node.js 22.x, Create function.
  2. Во вкладке Code откройте index.mjs, замените весь текст на код ниже. Вставьте свои значения в строки CERT_ARN (из шага 2) и KEY_SECRET (из шага 1). Нажмите Deploy.
// ZRAZU: обновление сертификата в AWS Certificate Manager по личной ссылке. Запускается раз в сутки.
import { ACMClient, DescribeCertificateCommand, ImportCertificateCommand } from "@aws-sdk/client-acm";
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { X509Certificate, createPrivateKey } from "node:crypto";

const PULL = "https://api.zrazu.com/ssl-pull/zsp_ВАШ_ТОКЕН";
const CERT_ARN = "arn:aws:acm:РЕГИОН:АККАУНТ:certificate/…";
const KEY_SECRET = "arn:aws:secretsmanager:РЕГИОН:АККАУНТ:secret:zrazu-ssl-key-…";

async function pem(part) {
  const res = await fetch(`${PULL}/${part}.pem`);
  if (!res.ok) throw new Error(`ZRAZU: ${part}.pem — HTTP ${res.status}`);
  const text = await res.text();
  if (!text.includes("BEGIN CERTIFICATE")) throw new Error("ZRAZU: по ссылке не сертификат");
  return text;
}

const serial = (s) => (s ?? "").replace(/:/g, "").replace(/^0+/, "").toUpperCase();

export const handler = async () => {
  const acm = new ACMClient({ region: CERT_ARN.split(":")[3] });
  const [cert, chain] = await Promise.all([pem("cert"), pem("chain")]);
  const leaf = new X509Certificate(cert);
  const current = await acm.send(new DescribeCertificateCommand({ CertificateArn: CERT_ARN }));
  if (serial(current.Certificate?.Serial) === serial(leaf.serialNumber)) return "ZRAZU: сертификат не изменился";
  if (new Date(leaf.validTo) <= new Date()) throw new Error("ZRAZU: сертификат уже не действует");
  const secrets = new SecretsManagerClient({ region: KEY_SECRET.split(":")[3] });
  const { SecretString: key } = await secrets.send(new GetSecretValueCommand({ SecretId: KEY_SECRET }));
  if (!leaf.checkPrivateKey(createPrivateKey(key))) {
    throw new Error("ZRAZU: сертификат выпущен на другой ключ — положите новый ключ в секрет");
  }
  await acm.send(new ImportCertificateCommand({
    CertificateArn: CERT_ARN,
    Certificate: Buffer.from(cert),
    CertificateChain: Buffer.from(chain),
    PrivateKey: Buffer.from(key),
  }));
  return `ZRAZU: новый сертификат установлен, действует до ${leaf.validTo}`;
};
  1. Configuration → General configuration → Edit → Timeout 30 секунд → Save.

Шаг 4. Права функции — только на этот сертификат и этот секрет

  1. Configuration → Permissions → щёлкните название роли (откроется IAM).
  2. Add permissions → Create inline policy → вкладка JSON → вставьте текст ниже, подставив два ARN.
  3. Next → название zrazu-ssl-update → Create policy.
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["acm:DescribeCertificate", "acm:ImportCertificate"], "Resource": "ARN_СЕРТИФИКАТА" },
    { "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "ARN_СЕКРЕТА" }
  ]
}

Шаг 5. Расписание

  1. В функции Add trigger → EventBridge (CloudWatch Events).
  2. Create a new rule → название zrazu-ssl-daily → Schedule expression → rate(1 day) → Add.

Проверка

  1. В функции вкладка Test → Test (событие любое). Результат — «новый сертификат установлен» или «сертификат не изменился».
  2. В Certificate Manager у сертификата — новая дата Expires.
  3. Сайт: https://zrazu.com/tools/ssl/example.com/. CloudFront разносит новый сертификат по своим узлам несколько минут.
  4. Журнал каждого запуска — Monitor → View CloudWatch logs.

Если не получилось

  • AccessDeniedException — в политике из шага 4 не тот ARN или не тот регион.
  • HTTP 404 — ссылка отключена или заменена: создайте новую в карточке сертификата и замените строку PULL.
  • «сертификат выпущен на другой ключ» — сертификат перевыпускали с новым ключом: обновите секрет (Retrieve secret value → Edit).
  • ValidationException про ключ — ключ с паролем или не тот формат: нужен PEM без пароля.
  • Task timed out — увеличьте Timeout (шаг 3).

Эта страница в Markdown