Here's a scenario I've seen more than once. Someone on the marketing team spins up a campaign site on Azure App Service. Platform adds a CNAME from promo.example.com to example-promo.azurewebsites.net. The campaign ends, someone deletes the App Service to save forty dollars a month, and nobody touches DNS. The record sits there for a year. Then one morning a certificate for promo.example.com shows up in Certificate Transparency logs, issued by a CA you've never used, and a login page that looks a lot like yours is live on it.
Nobody broke in. No credentials leaked. The CA followed the rules. Every system involved worked exactly as designed, and someone else now holds a publicly trusted certificate for your domain. That's what makes subdomain takeover so annoying to explain in a postmortem.
How takeover becomes issuance
A dangling DNS record is a CNAME, or sometimes an A record, pointing at a cloud resource that no longer exists. The record still resolves, or resolves into a shared namespace, but whatever lived on the other end is gone. If the provider lets anyone claim that name again, an attacker creates a resource with the same name in their own account. Your CNAME now sends traffic to them.
The certificate is the easy part. ACME's http-01 challenge asks one question: can the requester serve a specific token at a well-known path on port 80 of this hostname? The attacker can, because your DNS points at their bucket or app. tls-alpn-01 asks roughly the same thing on port 443. In both cases the CA is checking whether the applicant controls what gets served for that name, and they do. Under the CA/Browser Forum Baseline Requirements, that counts as domain control.
On a lot of platforms the attacker never touches an ACME client. Heroku, GitHub Pages, Azure App Service managed certificates and plenty of SaaS products with a custom domains feature request a cert automatically once a hostname is attached to an account. The attacker types your subdomain into a settings page and the platform does the rest, usually through Let's Encrypt, DigiCert or Google Trust Services.
Why your existing controls miss it
CAA allows what you allowed
Most CAA deployments I've seen look like this: issue "letsencrypt.org" at the apex, maybe "amazon.com" for ACM, done. That record lets any Let's Encrypt account get a cert for your names, including the attacker's. CAA follows the CNAME during lookup and then climbs your own tree (RFC 8659), so your apex policy does apply to the dangling name. It just doesn't block anything, because the attacker is using the same CA you are.
RFC 8657 adds two parameters that actually do something. accounturi pins issuance to a specific ACME account. validationmethods restricts which challenge types count. A CAA record that says "only my Let's Encrypt account, only via dns-01" stops an attacker who controls nothing but HTTP content on a hijacked name. Let's Encrypt supports both.
Hardly anyone deploys them, and laziness isn't the main reason. If any platform issues certs for you under its own ACME account (Heroku, Netlify, Vercel, App Service managed certs, your CDN), an account pin at the apex breaks it. You can scope CAA per subdomain to work around that, but now CAA is a per-name inventory problem, which is the same visibility problem that left the record dangling in the first place. My take: if your issuance is centralized, say cert-manager with dns-01 or ACM with DNS validation, pin the account and method. It's cheap and it works. If it isn't centralized, CAA is a partial fix and you shouldn't count on it.
The browser trusts the subdomain because you told it to
A lock icon on promo.example.com tells users nothing about who controls it, but it's enough to make a phishing page look legitimate. The real damage usually comes from what your own apps send to that subdomain:
- Cookies scoped to the parent. A session cookie set with Domain=example.com goes to every subdomain, the hijacked one included. HttpOnly stops JavaScript from reading it. The browser still sends it to the attacker's server.
- Wildcard trust in your config. CORS allowlists that match any subdomain of example.com, CSP sources, OAuth redirect URI patterns and postMessage origin checks all treat the hijacked name as first-party.
- Email and brand reputation. A real subdomain with a real cert walks straight past the "check the URL" advice from security awareness training.
Which providers are actually exposed
It comes down to one thing. Does the provider release names back into a shared pool anyone can claim, or does it bind a custom domain to an account that proved ownership?
Historically exposed, and still exposed if you skip their mitigations:
- Azure: *.cloudapp.net (classic Cloud Services), *.cloudapp.azure.com (public IP DNS labels), *.azurewebsites.net, *.trafficmanager.net and a few others. Microsoft's own Learn page, "Prevent dangling DNS entries and avoid subdomain takeover", lists the affected resource types and walks through the attack.
- S3 website endpoints. Bucket names are globally unique and become available again after deletion. Recreate the bucket in the same region and the CNAME to its s3-website endpoint serves the attacker's content. S3 website hosting is HTTP-only, which is precisely what http-01 needs.
- Elastic Beanstalk. The CNAME prefix under elasticbeanstalk.com can be re-registered once the environment is deleted.
- Heroku, GitHub Pages and assorted SaaS custom domain features. Mostly edge cases now. Heroku moved to randomized DNS targets and GitHub added domain verification for Pages, but older configurations and orgs that never set up verification are still exposed.
Mostly closed:
- CloudFront won't let you add an alternate domain name unless you attach a certificate covering it, and it blocks the same alias on two distributions. The attacker needs the cert before they can serve anything, which kills the http-01 route. Clean up dangling CloudFront records anyway, just not first.
- Anything that requires a TXT ownership proof before routing a custom domain to an account.
The best public reference is can-i-take-over-xyz on GitHub, started by EdOverflow and maintained by the community. It tracks which services are vulnerable, which are edge cases, and what each one shows for an unclaimed name ("NoSuchBucket", "There isn't a GitHub Pages site here" and so on). Before you take a vendor's word that they're safe, check whether their entry changed in the last year.
Detection
Watch CT for issuance you didn't request
Every publicly trusted certificate has to be logged in Certificate Transparency, and precertificates usually land in the logs before the cert is in use. That gives you a window. It only helps if you compare what shows up against what you actually issued.
The catch is that CT entries don't include the ACME account. You get the issuing intermediate, the SANs and the validity period. So "issued from an account you don't use" has to be inferred by matching each entry against your own issuance sources: ACM, Key Vault, GCP Certificate Manager, cert-manager, and every SaaS platform that issues on your behalf. What you're looking for is a CT entry none of those systems can explain. An unfamiliar issuer makes it more suspicious, but don't filter on issuer alone. Attackers mostly use Let's Encrypt. So do you.
Cross-check CNAME targets against live resources
Export your zones. For every CNAME into a provider namespace (azurewebsites.net, cloudapp.azure.com, s3-website, elasticbeanstalk.com, github.io, herokudns.com), ask whether the target exists in one of your accounts. Don't bother checking whether the name resolves. A deleted bucket's S3 hostname still resolves; the bucket just isn't yours anymore. Match against real inventory: bucket lists from every AWS account, App Service and public IP resources from every Azure subscription, Beanstalk environments in every region.
Microsoft publishes a PowerShell tool, Get-DanglingDnsRecords, that does this for Azure via Resource Graph. On AWS you're writing it yourself. The logic is simple enough: list the zone, list the resources, diff them. Any CNAME with no matching resource in your accounts is either dangling or belongs to someone else, and you need to know which.
Severity rules that match reality
A new cert on api.example.com the week after a deploy is noise. A new cert on a subdomain whose DNS record hasn't changed in a year, that no pipeline has deployed to, and that none of your issuance sources requested should wake someone up. Write the alert that way. Teams that dump every CT match into a shared ticket queue tend to find these incidents three weeks late, during some unrelated cleanup.
Prevention and response
Decommission in the right order. Delete the DNS record, wait out the TTL, then delete the resource. Almost every takeover starts with someone doing it backwards. Put the order in your runbooks and teardown modules. If Terraform manages both the record and the resource, make the dependency explicit so destroy runs them in the right sequence.
Use the provider's binding mechanisms. In Azure, add the asuid TXT record with your subscription's custom domain verification ID. With that in place, App Service won't bind your hostname to someone else's app even when the CNAME is dangling. Use Azure DNS alias records and Route 53 alias records wherever the target supports them. They point at the resource itself rather than a name, so deleting the resource makes the record stop resolving instead of leaving a reusable name behind.
Sweep for dangling records in your DNS pipeline. Run the inventory cross-check weekly, and as a CI check on every DNS change. A new CNAME into a provider namespace that doesn't match a resource you own should fail the build.
When it happens anyway:
- Delete or repoint the DNS record immediately. That pulls the attacker's content off your name for everyone whose resolver cache has expired.
- Reclaim the resource name if the provider allows it, so the record can't be turned against you again.
- Revoke the cert. With Let's Encrypt, any ACME account holding valid authorizations for every name on the cert can revoke it. Once you control DNS again, validate the name from your own account and revoke. For other CAs, file a certificate problem report; the Baseline Requirements give them 24 hours to investigate. Browser revocation checking is still unreliable, so think of this as cleanup. Fixing DNS is what actually ends the attack.
- Rotate session cookies scoped to the parent domain and check your logs for what the subdomain received while it was hijacked.
The short version
Subdomain takeover is an inventory problem. A DNS record outlived the resource it pointed to, and a CA turned that into a trusted certificate. Fix your decommission order, turn on provider ownership verification, sweep your zones on a schedule, and watch CT for certs you didn't issue.
That last one only works if you can match CT entries against your actual cloud inventory, and it's the reason we built CertPulse. It compares CT entries for your domains against what's in ACM, Key Vault and GCP Certificate Manager and flags anything nothing in your estate asked for. You can build the same matching yourself, and if you have the time, you should. Whichever way you go, someone needs to see that CT entry before the phishing page goes live.
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.