Find Expiring ACM Certificates Before They Break HTTPS

A metal padlock resting on a laptop keyboard lit by a blue screen

Photo by Towfiqu barbhuiya on Unsplash

To find expiring ACM certificates, call ListCertificates in every Region you use (including us-east-1, where CloudFront certificates live) with all key types included, and compare each NotAfter date with today. Then call DescribeCertificate: imported certificates, certificates not in use, and DNS-validated ones whose CNAME record is gone won’t renew on their own.

AWS Certificate Manager (ACM) renews most of the certificates it issues, which is why an expired one is such a surprise when it happens. This example is for engineers who want to find expiring ACM certificates before a browser warning does, and know which ones need a person. You’ll get a read-only TypeScript script for the AWS SDK for JavaScript v3 that checks every enabled Region and explains, per certificate, what will happen at renewal time.

It’s part of our AWS SDK v3 practical examples. If you’re setting up a custom domain on CloudFront, the example to point www to CloudFront with a Route 53 alias record covers issuing the certificate; this one covers keeping it valid, and the script to check the CloudFront minimum TLS version covers which protocols the distribution accepts with it.

Why do ACM certificates expire when ACM renews them?

ACM’s managed renewal has conditions. For DNS-validated public certificates, ACM checks two things 45 days before expiry: the certificate is in use by an AWS service, and every ACM-provided validation CNAME record is still in public DNS. If both hold, it renews and the ARN stays the same. The ways a certificate slips through:

  • Imported certificates. ACM never renews a certificate you imported. You have to get a new one from your CA and reimport it.
  • Certificates not in use. A certificate that isn’t associated with a service such as a load balancer or CloudFront, and hasn’t been exported, isn’t eligible.
  • A deleted validation CNAME. Someone cleaned up the “strange underscore records” in the zone, and renewal can no longer validate.
  • Email validation. ACM sends renewal emails, and someone has to approve them.

When ACM can’t validate, it sends AWS Health and EventBridge events at 30, 15, 7, 3 and 1 day before expiry. Those only help if someone reads them, which is why a scheduled report is still worth having.

How long are ACM certificates valid now?

Public certificates issued by ACM since 18 February 2026 are valid for 198 days, down from 395. Certificates with 198-day validity renew 45 days before expiry; older 395-day certificates renew 60 days before and come back with 198 days. The change follows the CA/Browser Forum’s Ballot SC-081v3, which cuts the maximum lifetime of public TLS certificates from 398 days in steps, starting with 200 days from 15 March 2026 and reaching 47 days in March 2029. Shorter lifetimes mean more renewals a year, so every broken renewal path shows up sooner.

What does the script do?

  1. Picks RegionsEvery enabled Region from DescribeRegions, always including us-east-1, or the list you pass with --regions=.
  2. Lists certificates of every key typepaginateListCertificates with all seven keyTypes. Without that filter the API returns only RSA 1024 and 2048-bit certificates and silently skips ECDSA ones.
  3. Keeps the ones expiring soonAnything whose NotAfter falls within --days (default 45), plus certificates already expired.
  4. Explains each oneDescribeCertificate returns Type, InUseBy, RenewalSummary and the validation records.
  5. Checks the validation CNAMEsFor DNS-validated certificates, resolves each record with Node’s dns module and compares it with the value ACM expects.

Prerequisites

  • Node.js 18 or later, npm and tsx.
  • The @aws-sdk/client-acm and @aws-sdk/client-ec2 packages.
  • Outbound DNS from the machine you run it on, for the CNAME check.

Which IAM permissions does it need?

Three read actions. acm:DescribeCertificate can be scoped to certificate ARNs in your account (replace 123456789012).

expiring-acm-certificates-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListCertificatesAndRegions",
      "Effect": "Allow",
      "Action": ["acm:ListCertificates", "ec2:DescribeRegions"],
      "Resource": "*"
    },
    {
      "Sid": "DescribeCertificates",
      "Effect": "Allow",
      "Action": "acm:DescribeCertificate",
      "Resource": "arn:aws:acm:*:123456789012:certificate/*"
    }
  ]
}

The IAM policy generator for TypeScript SDK code builds the same list from the script, if you adapt it.

The full script to find expiring ACM certificates

find-expiring-acm-certificates.ts

// find-expiring-acm-certificates.ts
// Lists ACM certificates in every enabled Region (always including us-east-1, where CloudFront
// certificates live) that expire within N days, and says whether ACM will renew each one on its own.
// For DNS-validated certificates it also checks that the validation CNAME still resolves.
// Read-only: it never renews, imports or deletes anything.
// Usage: npx tsx find-expiring-acm-certificates.ts [--days 45] [--regions=us-east-1,eu-west-1]
import { resolveCname } from "node:dns/promises";
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
  ACMClient,
  DescribeCertificateCommand,
  paginateListCertificates,
  type CertificateDetail,
  type KeyAlgorithm,
} from "@aws-sdk/client-acm";

const args = process.argv.slice(2);
const days = Number(args[args.indexOf("--days") + 1]) || 45;
const regionArg = args.find((a) => a.startsWith("--regions="))?.split("=")[1];

// Without Includes.keyTypes, ListCertificates returns only RSA_1024 and RSA_2048 certificates.
const ALL_KEY_TYPES: KeyAlgorithm[] = [
  "RSA_1024", "RSA_2048", "RSA_3072", "RSA_4096", "EC_prime256v1", "EC_secp384r1", "EC_secp521r1",
];

interface Row { Region: string; Domain: string; Type: string; Expires: string; DaysLeft: number; InUseBy: number; Renewal: string; Action: string }

const bare = (name: string): string => name.replace(/\.$/, "").toLowerCase();

// true if every ACM validation CNAME is still published in public DNS.
async function cnamesInPlace(cert: CertificateDetail): Promise<boolean> {
  const records = (cert.DomainValidationOptions ?? []).map((d) => d.ResourceRecord).filter((r) => r !== undefined);
  const unique = [...new Map(records.map((r) => [bare(r.Name ?? ""), bare(r.Value ?? "")])).entries()];
  for (const [name, value] of unique) {
    try {
      const answers = await resolveCname(name);
      if (!answers.map(bare).includes(value)) return false;
    } catch {
      return false; // NXDOMAIN or no CNAME
    }
  }
  return unique.length > 0;
}

async function action(cert: CertificateDetail, left: number, exported: boolean): Promise<string> {
  const inUse = (cert.InUseBy ?? []).length > 0;
  if (left < 0) return inUse ? "EXPIRED AND IN USE: replace now" : "expired, unused: delete";
  if (cert.Type === "IMPORTED") return inUse ? "reimport a new certificate before expiry" : "imported, unused: delete or reimport";
  if (cert.CertificateKeyPairOrigin === "ACME") return "renewed by your ACME client";
  if (cert.Type === "PRIVATE") return "private CA: check renewal or export";
  if (!inUse && !exported) return "not in use: ACM won't renew";
  const method = cert.DomainValidationOptions?.[0]?.ValidationMethod;
  if (method === "EMAIL") return "approve the renewal email";
  if (method === "DNS") return (await cnamesInPlace(cert)) ? "auto-renew expected" : "validation CNAME missing: renewal will fail";
  return `check ${method ?? "validation"} renewal`;
}

async function scanRegion(region: string): Promise<Row[]> {
  const acm = new ACMClient({ region });
  const rows: Row[] = [];
  const cutoff = Date.now() + days * 86_400_000;
  for await (const page of paginateListCertificates({ client: acm }, {
    CertificateStatuses: ["ISSUED", "EXPIRED"],
    CertificateKeyPairOrigins: ["AWS_MANAGED", "CUSTOMER_PROVIDED", "ACME"], // ACME is excluded by default
    Includes: { keyTypes: ALL_KEY_TYPES },
  })) {
    for (const s of page.CertificateSummaryList ?? []) {
      if (!s.NotAfter || s.NotAfter.getTime() > cutoff) continue;
      const cert = (await acm.send(new DescribeCertificateCommand({ CertificateArn: s.CertificateArn }))).Certificate;
      if (!cert?.NotAfter) continue;
      const left = Math.floor((cert.NotAfter.getTime() - Date.now()) / 86_400_000);
      rows.push({
        Region: region,
        Domain: cert.DomainName ?? "",
        Type: cert.Type ?? "",
        Expires: cert.NotAfter.toISOString().slice(0, 10),
        DaysLeft: left,
        InUseBy: (cert.InUseBy ?? []).length,
        Renewal: cert.RenewalSummary?.RenewalStatus ?? cert.RenewalEligibility ?? "",
        Action: await action(cert, left, s.Exported === true),
      });
    }
  }
  return rows;
}

async function main(): Promise<void> {
  const regions = new Set(regionArg ? regionArg.split(",") : ["us-east-1"]);
  if (!regionArg) {
    const out = await new EC2Client({}).send(new DescribeRegionsCommand({}));
    for (const r of out.Regions ?? []) if (r.RegionName) regions.add(r.RegionName);
  }
  const rows: Row[] = [];
  for (const region of [...regions].sort()) {
    try {
      rows.push(...(await scanRegion(region)));
    } catch (err) {
      console.error(`${region}: skipped (${err instanceof Error ? err.name + ": " + err.message : String(err)})`);
    }
  }
  rows.sort((a, b) => a.DaysLeft - b.DaysLeft);
  console.table(rows);
  const risky = rows.filter((r) => r.Action !== "auto-renew expected");
  console.log(`${rows.length} certificates expire within ${days} days; ${risky.length} need attention.`);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-acm @aws-sdk/client-ec2
npm install --save-dev tsx typescript

# Certificates expiring in the next 45 days, every enabled Region
AWS_PROFILE=readonly npx tsx find-expiring-acm-certificates.ts

# Look further ahead in two Regions
AWS_PROFILE=readonly npx tsx find-expiring-acm-certificates.ts --days 90 --regions=us-east-1,eu-west-1

Sample output

Output

┌─────────┬─────────────┬───────────────────────┬─────────────────┬──────────────┬──────────┬─────────┬────────────────────────┬───────────────────────────────────────────────┐
│ (index) │ Region      │ Domain                │ Type            │ Expires      │ DaysLeft │ InUseBy │ Renewal                │ Action                                        │
├─────────┼─────────────┼───────────────────────┼─────────────────┼──────────────┼──────────┼─────────┼────────────────────────┼───────────────────────────────────────────────┤
│ 0       │ 'us-east-1' │ 'shop.example.com'    │ 'AMAZON_ISSUED' │ '2026-10-09' │ 12       │ 1       │ 'PENDING_VALIDATION'   │ 'validation CNAME missing: renewal will fail' │
│ 1       │ 'eu-west-1' │ 'legacy.example.com'  │ 'IMPORTED'      │ '2026-10-19' │ 22       │ 2       │ 'INELIGIBLE'           │ 'reimport a new certificate before expiry'    │
│ 2       │ 'eu-west-1' │ 'staging.example.com' │ 'AMAZON_ISSUED' │ '2026-10-25' │ 28       │ 0       │ 'INELIGIBLE'           │ "not in use: ACM won't renew"                 │
│ 3       │ 'us-east-1' │ 'mail.example.com'    │ 'AMAZON_ISSUED' │ '2026-11-02' │ 36       │ 1       │ 'PENDING_VALIDATION'   │ 'approve the renewal email'                   │
│ 4       │ 'us-east-1' │ 'www.example.com'     │ 'AMAZON_ISSUED' │ '2026-11-08' │ 42       │ 1       │ 'PENDING_AUTO_RENEWAL' │ 'auto-renew expected'                         │
└─────────┴─────────────┴───────────────────────┴─────────────────┴──────────────┴──────────┴─────────┴────────────────────────┴───────────────────────────────────────────────┘
5 certificates expire within 45 days; 4 need attention.

Domains are illustrative. The first row is the emergency. shop.example.com will fail to renew because its validation CNAME is gone; recreate the record from the ResourceRecord that DescribeCertificate returns so ACM can validate the domain again. www.example.com is the healthy case: in use, DNS-validated and with its CNAME in place.

How do you fix each result?

  • Imported and in use. Get a new certificate from your CA and reimport it with ImportCertificate and the existing CertificateArn, which keeps the certificate’s AWS service associations. Better, replace it with an ACM-issued certificate that renews itself.
  • Validation CNAME missing. Add the record back. If the domain’s DNS is in Route 53, the guide to troubleshoot a Route 53 domain not serving CloudFront checks the rest of that chain too. Before anyone deletes a zone that looks empty, the checks to find unused Route 53 hosted zones confirm whether the domain is still delegated to it.
  • Not in use. If nothing will use it, delete it. If a deployment is about to, attach it soon enough for ACM’s 45-day check.
  • Email validation. Make sure the approval emails reach someone, or request a new DNS-validated certificate and switch to it.

For ongoing alerts, create an EventBridge rule on the ACM Certificate Approaching Expiration event. ACM sends it daily for every active certificate, starting 30 days before expiry for public certificates and 45 days for imported and private ones, and PutAccountConfiguration changes that window. A scheduled run of the script can publish its own findings to the same bus, as shown in the guide to send events to EventBridge with AWS SDK v3 (PutEvents). The same idea of watching dates before they bite applies to the script to find EC2 Reserved Instances about to expire, and to credentials in the script to find IAM access keys older than 90 days.

Troubleshooting

  • A certificate you can see in the console is missing. Check its Region. CloudFront only uses certificates from us-east-1, so those never show up in your application Region.
  • The CNAME check fails but the record exists. Your machine may use a DNS server with a private view of the zone. Run from a network that resolves public DNS, or compare with dig.
  • AccessDeniedException. The profile can list but not describe certificates. Add acm:DescribeCertificate for your certificate ARNs.
  • ThrottlingException in accounts with many certificates. SDK v3 retries throttled calls automatically; raise maxAttempts on the client if the scan still stops.

Ask ChatWithCloud instead

You can also ask ChatWithCloud “Which ACM certificates in us-east-1 expire in the next 60 days, and are they in use?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your AWS profile and explains the answer, like the checks in the guide to troubleshoot AWS infrastructure with an AI CLI. One session covers one profile and Region, so repeat the question for us-east-1 and your application Region. Generated code runs without a confirmation step, so connect ChatWithCloud to your AWS account with a read-only profile; the ChatWithCloud security model covers what leaves your machine.

Frequently asked questions

Does ACM renew certificates automatically?

Yes, for certificates ACM issued, if they’re in use (or exported) and validation still works. DNS-validated certificates renew without any action. Imported certificates are never renewed.

How many days before expiry does ACM renew?

For current 198-day public certificates, ACM checks renewal criteria 45 days before expiry. Older 395-day certificates renew 60 days before.

Why doesn’t ListCertificates show my ECDSA certificate?

By default it returns only RSA 1024 and 2048-bit certificates. Pass the other key types in Includes.keyTypes, as the script does.

How do I get alerted before an ACM certificate expires?

Create an EventBridge rule for the ACM Certificate Approaching Expiration event and send it to SNS or a chat channel. Run the script above on a schedule as a second check.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud