Tutorial

How to Audit Every Certificate Across 70+ AWS Accounts

March 19, 202611 min readCertPulse Engineering

I've managed certificates across environments with 70+ AWS accounts. At one point I had a spreadsheet with 400 rows in it, each one an ACM certificate that someone had provisioned at some point for some reason. About a third of the rows had question marks in the "owner" column.

The AWS console shows you certificates one account at a time, one region at a time. If you have 70 accounts and 16 regions, that is 1,120 console views to check. Nobody does that. So you write a script. Then you maintain the script. Then the person who wrote the script leaves and the script breaks and you write another script.

I want to walk through how cross-account certificate audits actually work, what goes wrong, and at what point you should stop building it yourself.

The pattern: AssumeRole across every account

Cross-account access in AWS comes down to IAM role assumption. You have a "hub" account (or your local machine) that assumes a role in each target account. That role has read-only access to ACM.

The setup is three steps: create a read-only IAM role in every target account (something like CertAuditReadOnly) with a trust policy that allows your hub to assume it, give the role permission to call acm:ListCertificates, acm:DescribeCertificate, and acm:ListTagsForCertificate, then iterate through your account list from the hub, assuming into each one and pulling the cert inventory.

If you use AWS Organizations, you can deploy the role with a CloudFormation StackSet and hit every account in one go. If you don't, you're deploying it manually or through Terraform in each account, which is tedious but not complicated.

Add an ExternalId condition to prevent the confused deputy problem. Not strictly required, but good hygiene.

What the enumeration needs to do

Once the roles are in place, the actual enumeration is straightforward. For each account, you assume the role and then scan every region. In each region, you paginate through ListCertificates, then call DescribeCertificate on each one to get the full details: domain name, status, issuer, expiration date, whether it was Amazon-issued or imported, the renewal status, and what AWS resources reference it.

You want to scan regions concurrently to keep the total runtime reasonable. For 70 accounts with 10 regions each, a sequential scan can take 30+ minutes. Running regions in parallel within each account gets it down to 3-5 minutes.

The output you care about is three things: a full cert inventory, a list of certs expiring soon, and a list of imported certs that will never auto-renew. Those last two lists are the ones that matter most.

Gotchas you will run into

I've been running audits like this for a few years. Here are the things that trip people up.

Rate limits

The ACM DescribeCertificate API has a rate limit of 10 requests per second per account. If you are scanning an account with 300 certs across 10 regions concurrently, you will hit it. The AWS SDK retries with exponential backoff, so things slow down but don't break.

If you want to be a good API citizen, limit concurrent DescribeCertificate calls to 5-8 per account. Or scan regions sequentially within each account and scan accounts concurrently. Depends on whether you care more about per-account speed or total throughput.

Imported certs don't auto-renew

This is worth repeating because it is the number one source of surprise certificate expirations in AWS. If the Type field is IMPORTED, ACM will not renew that certificate. Period. It does not matter that it's in ACM. It does not matter that the cert was originally obtained from a CA that supports auto-renewal. Once it's imported, it's manual.

In the environments I've audited, imported certs typically make up 15-40% of the total ACM inventory. People import certs for various reasons: wildcard certs from commercial CAs, certs that predate their ACM adoption, certs with specific key requirements, certs from internal PKI that terminate on ALBs. All of these need manual tracking.

InUseBy does not mean it's serving traffic

The InUseBy field tells you which AWS resources reference a certificate. An ALB ARN, a CloudFront distribution ARN, an API Gateway domain. But "in use by" means "associated with," not "actively serving traffic."

A certificate can be attached to an ALB that has no listeners, or to a disabled CloudFront distribution, or to an API Gateway custom domain that nobody points DNS at. Conversely, a cert with an empty InUseBy might still be critical if it was downloaded and installed on an EC2 instance or a non-AWS server.

InUseBy is useful for a rough picture of what depends on a cert. It is not reliable for determining whether a cert is actually load-bearing in production.

Some regions have certs you forgot about

Most teams have an "active" set of regions (us-east-1, eu-west-1, whatever) and don't think about the rest. But ACM certs can exist in any region. Someone spins up a test in ap-south-1, provisions a cert, then forgets about it. If you only scan your primary regions, you miss these.

CloudFront makes this worse. It uses us-east-1 exclusively for its certificates, regardless of where your distribution is configured. If a team mostly works in eu-west-1 but uses CloudFront, their CDN certs are in us-east-1. I have seen audit scripts that missed these because they only scanned the "European" regions.

New accounts appear after your audit

If your organization uses AWS Organizations, new accounts get created regularly. Developers spin up sandbox accounts. Teams get dedicated accounts for new projects. If your audit uses a hardcoded list of account IDs, it will miss any account created after the list was written.

You can fix this by pulling the account list dynamically from the Organizations API instead of hardcoding it. But then you need the IAM role deployed in every new account automatically, which means StackSets configured to auto-deploy on account creation.

The automation gap

Let's say you build the script, deploy the IAM roles, set up StackSets for new accounts, add rate limiting, scan all regions. You run it every day on a cron job and pipe the output to a Slack channel. You have a working cross-account certificate audit.

Now: who maintains it?

I have built and maintained scripts like this. Here is what happens over time:

The AWS SDK releases a breaking change and the script stops compiling. The STS role in one account gets modified and the script starts printing errors for that account, but nobody notices because the rest of the accounts still work. A new region launches and you forget to add it to the list. The cron server gets decommissioned in an infrastructure migration and the audit silently stops running. Someone changes the Slack webhook URL and the alerts go nowhere.

Every one of these has happened to me or to teams I've worked with.

The script is easy. Keeping the script running reliably for years is the hard part. And the script only covers AWS. If you also have Azure subscriptions, GCP projects, on-prem servers, or certificates from commercial CAs, you need a separate enumeration path for each one.

What ACM cannot tell you

Even a perfectly running audit has blind spots. ACM only knows about certificates that are in ACM. It tells you nothing about:

  • Certificates on EC2 instances (nginx, Apache, HAProxy, etc.)
  • Certificates in Kubernetes secrets or cert-manager
  • Certificates on non-AWS infrastructure
  • Certificates issued for your domains by someone else
  • Whether the cert actually matches what's being served on the endpoint
  • Chain completeness, protocol versions, cipher configuration

The ACM inventory tells you what certificates exist in AWS. It does not tell you what certificates your users actually see when they connect to your services. Those are two different questions, and the second one is the one that matters when a cert expires and breaks your site.

To answer the second question, you need external endpoint monitoring: actually connecting to your HTTPS endpoints from outside your network and checking what cert is being served, its expiry, its chain, its protocol configuration. ACM tells you what should be there. Endpoint monitoring tells you what is there.

The certificates you don't know about

Beyond ACM, there are certificates issued for your domains that you might not know about at all. A developer uses their personal AWS account to issue a cert for staging.yourcompany.com. A former contractor left a Let's Encrypt cert auto-renewing on a server you forgot about. A phishing site gets a DV cert for yourconpany.com (typosquat).

Certificate Transparency logs record every publicly issued cert. Monitoring them is the only way to know about certs issued outside your control. This is separate from the ACM audit, but it fills a gap that the ACM inventory cannot.

When to stop writing scripts

A homegrown audit script is fine for answering the question "what certificates do I have right now?" once. Run it, save the output, move on.

When you need to answer that question continuously, you're building something different. Continuous monitoring means: scheduled execution, persistent storage for historical data, change detection (new certs, removed certs, status changes), alerting on approaching expirations and failed renewals, a dashboard your team can look at without running a script, and coverage across AWS, Azure, GCP, and external endpoints.

That is no longer a script. That is a product. And the engineering time to build and maintain it adds up fast.

I have seen teams spend 2-3 engineer-weeks building a custom cert monitoring stack, then spend another week per quarter maintaining it when things break. At senior engineer rates, that is $15K-$25K per year in time spent on what is basically a solved problem.

This is why we built CertPulse

CertPulse connects to your AWS, Azure, and GCP accounts, enumerates every certificate, monitors your external endpoints, and watches Certificate Transparency logs. One dashboard for every cert. Alerts when auto-renewal fails. Alerts when certs approach expiry. Alerts when someone issues a cert for your domain that you didn't request.

If you're looking for complete certificate visibility without maintaining scripts, we can get you there in about 5 minutes.

How to Audit Every Certificate Across 70+ AWS Accounts | CertPulse