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