SSL: automatic renewal

AWS — load balancer, CloudFront, API Gateway

A Lambda function fetches the fresh certificate daily and reimports it into AWS Certificate Manager under the same ARN — ELB and CloudFront follow.

Renews by itself

Use this if your site gets its certificate through AWS: Application or Network Load Balancer, CloudFront, API Gateway, Elastic Beanstalk. The certificate lives in AWS Certificate Manager (ACM). We never ask for keys to your AWS: the function runs in your account and is allowed to update only one certificate.

How it works: ACM lets you import a new certificate under the same ARN, and every service that uses it switches to the new one without reconfiguration. A Lambda function compares the certificate at the link with the one in ACM once a day and, if it is new, imports it together with your key stored in Secrets Manager.

What you need

  • The certificate link from your account: https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN.
  • A private key without a password (RSA 2048 or ECDSA P-256), the one the certificate was ordered with.
  • Access to the AWS Console with permission to create Lambda functions, secrets and IAM roles.
  • 20 minutes.

Region. For CloudFront the certificate must be in the US East (N. Virginia) us-east-1 region, so create both the secret and the function there. For a load balancer, use the load balancer's region.

Step 1. Put the key in Secrets Manager

  1. Secrets Manager → Store a new secret.
  2. Other type of secret → Plaintext tab → delete the example and paste the whole key text, including the -----BEGIN … PRIVATE KEY----- and -----END … PRIVATE KEY----- lines.
  3. Next → name it zrazu-ssl-key → Next → Next → Store.
  4. Open the secret and copy the Secret ARN.

Step 2. First import into ACM (if the certificate is not there yet)

  1. Certificate Manager → Import a certificate.
  2. Certificate body — the contents of https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN/cert.pem (open it in a browser and copy all the text); Certificate private key — your key; Certificate chain — the contents of https://api.zrazu.com/ssl-pull/zsp_YOUR_TOKEN/chain.pem.
  3. Import. Open the certificate and copy its ARN.
  4. Attach the certificate to the service if you have not yet: for a load balancer, Listeners → HTTPS:443 → Edit → Default SSL/TLS certificate; for CloudFront, Distribution → Edit → Custom SSL certificate.

Step 3. The Lambda function

  1. Lambda → Create function → Author from scratch. Name it zrazu-ssl-update, set Runtime to Node.js 22.x, then Create function.
  2. On the Code tab open index.mjs and replace all the text with the code below. Put your values in the CERT_ARN (from step 2) and KEY_SECRET (from step 1) lines. Click Deploy.
// ZRAZU: updates the certificate in AWS Certificate Manager using the personal link. Runs once a day.
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_YOUR_TOKEN";
const CERT_ARN = "arn:aws:acm:REGION:ACCOUNT:certificate/…";
const KEY_SECRET = "arn:aws:secretsmanager:REGION:ACCOUNT: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: the link does not return a certificate");
  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: certificate unchanged";
  if (new Date(leaf.validTo) <= new Date()) throw new Error("ZRAZU: certificate has already expired");
  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: certificate was issued for a different key — put the new key in the secret");
  }
  await acm.send(new ImportCertificateCommand({
    CertificateArn: CERT_ARN,
    Certificate: Buffer.from(cert),
    CertificateChain: Buffer.from(chain),
    PrivateKey: Buffer.from(key),
  }));
  return `ZRAZU: new certificate installed, valid until ${leaf.validTo}`;
};
  1. Configuration → General configuration → Edit → set Timeout to 30 seconds → Save.

Step 4. Function permissions — only for this certificate and this secret

  1. Configuration → Permissions → click the role name (IAM opens).
  2. Add permissions → Create inline policy → JSON tab → paste the text below, putting in your two ARNs.
  3. Next → name it zrazu-ssl-update → Create policy.
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["acm:DescribeCertificate", "acm:ImportCertificate"], "Resource": "CERTIFICATE_ARN" },
    { "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "SECRET_ARN" }
  ]
}

Step 5. Schedule

  1. In the function, Add trigger → EventBridge (CloudWatch Events).
  2. Create a new rule → name zrazu-ssl-daily → Schedule expression → rate(1 day) → Add.

Check

  1. In the function, open the Test tab → Test (any event). The result is "new certificate installed" or "certificate unchanged".
  2. In Certificate Manager, the certificate shows a new Expires date.
  3. Site: https://zrazu.com/tools/ssl/example.com/. CloudFront takes a few minutes to spread the new certificate to its nodes.
  4. The log of each run: Monitor → View CloudWatch logs.

If it did not work

  • AccessDeniedException — the policy from step 4 has the wrong ARN or the wrong region.
  • HTTP 404 — the link is disabled or replaced: create a new one in the certificate card and replace the PULL line.
  • "certificate was issued for a different key" — the certificate was reissued with a new key: update the secret (Retrieve secret value → Edit).
  • ValidationException about the key — the key has a password or is in the wrong format: you need a PEM without a password.
  • Task timed out — increase Timeout (step 3).

This page in Markdown