# 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.

URL: https://zrazu.com/en/docs/ssl-auto-aws/

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**.

```js file=zrazu-aws-lambda.mjs
// 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}`;
};
```

3. **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**.

```json
{
  "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/](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).
