Many developers face SSL/TLS certificate challenges when dealing with internal servers that aren't publicly accessible. Traditional Let's Encrypt validation methods (HTTP-01 or TLS-ALPN-01) require your server to be reachable from the internet, which creates problems for:
- Development servers
- Internal applications
- LAN-only services
- IoT devices on local networks
Let's Encrypt's DNS-01 challenge provides a perfect solution for this scenario. Instead of verifying control through web server accessibility, it verifies by checking for specific DNS TXT records.
Here's why DNS validation works better:
- Doesn't require port 80/443 to be open to internet
- Works for any domain you control
- Can issue wildcard certificates (*.yourdomain.com)
- Suitable for automated renewal
Here's a complete example using Certbot with DNS plugins. We'll use the Cloudflare plugin as an example:
# Install certbot and Cloudflare plugin
sudo apt install certbot python3-certbot-dns-cloudflare
# Create Cloudflare API credentials file
echo "dns_cloudflare_email = your@email.com" > ~/.secrets/cloudflare.ini
echo "dns_cloudflare_api_key = your_global_api_key" >> ~/.secrets/cloudflare.ini
chmod 600 ~/.secrets/cloudflare.ini
# Request certificate (replace with your domain)
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d internal.example.com \
-d *.internal.example.com
Certbot supports multiple DNS providers through plugins. Some popular options:
# For Route53 (AWS)
certbot certonly --dns-route53 -d server.local.example.com
# For DigitalOcean
certbot certonly --dns-digitalocean -d dev.example.com
# For manual DNS (any provider)
certbot certonly --manual --preferred-challenges dns -d private.example.com
For production environments, set up automatic renewals:
# Create renewal script
echo "#!/bin/bash" > /usr/local/bin/renew_certs.sh
echo "certbot renew --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini --post-hook \"systemctl reload apache2\"" >> /usr/local/bin/renew_certs.sh
chmod +x /usr/local/bin/renew_certs.sh
# Add to crontab (runs daily at 3AM)
(crontab -l 2>/dev/null; echo "0 3 * * * /usr/local/bin/renew_certs.sh") | crontab -
After obtaining certificates, configure Apache:
<VirtualHost *:443>
ServerName internal.example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/internal.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/internal.example.com/privkey.pem
# HSTS and other security headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options nosniff
</VirtualHost>
Common issues and solutions:
# Check certificate chain
openssl s_client -connect localhost:443 -servername internal.example.com | openssl x509 -noout -text
# Verify DNS propagation
dig +short TXT _acme-challenge.internal.example.com
# Dry run for renewal testing
certbot renew --dry-run
When dealing with internal servers not exposed to the public internet, traditional Let's Encrypt certificate issuance methods (HTTP-01 or TLS-ALPN-01 challenges) won't work since they require public accessibility. This creates a dilemma for developers maintaining:
- Development/staging environments
- Internal tools dashboards
- LAN-only services
- VPN-connected infrastructure
The most reliable method for private servers is the DNS-01 challenge, which verifies domain ownership through DNS records rather than HTTP servers. Here's how it works:
# Example certbot command with DNS plugin
certbot certonly \
--manual \
--preferred-challenges dns \
-d internal.example.com \
--server https://acme-v02.api.letsencrypt.org/directory
Prerequisites:
- A domain name you control (even if it's not publicly resolvable)
- DNS provider API access (or ability to manually update records)
- Certbot installed on any internet-accessible machine
Automated DNS Validation (Cloudflare example):
# Install certbot DNS plugin
sudo apt install certbot python3-certbot-dns-cloudflare
# Create API credential file
echo "dns_cloudflare_api_token = YOUR_API_TOKEN" > ~/.secrets/certbot/cloudflare.ini
chmod 600 ~/.secrets/certbot/cloudflare.ini
# Obtain certificate
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d internal.example.com
Manual DNS Challenge: For providers without API support:
certbot certonly --manual --preferred-challenges dns -d internal.example.com
Follow the prompts to create TXT records manually.
Port Forwarding Temporary Workaround: If feasible for brief periods:
# Temporarily forward port 80 to your internal server
ssh -R 80:localhost:80 your_vps
# Then run standard HTTP challenge
certbot certonly --standalone -d internal.example.com
After obtaining certificates, transfer them to your internal server and configure Apache:
<VirtualHost *:443>
ServerName internal.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/internal.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/internal.example.com/privkey.pem
# ... other configuration
</VirtualHost>
For DNS plugins with API support, automate renewals via cron:
0 3 * * * certbot renew --dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
--post-hook "systemctl reload apache2"
- Store API credentials with minimal permissions
- Use certificate directories with proper permissions (chmod 700)
- Consider certificate transparency logs for monitoring
Common Issues:
Error | Solution |
---|---|
DNS propagation delays | Add --dns-cloudflare-propagation-seconds 60 |
Permission denied | Check certbot's access to private key files |
Rate limits | Use --dry-run for testing |