How to Obtain a Let’s Encrypt SSL Certificate for Internal/LAN Servers: DNS Validation Guide


2 views

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:

  1. A domain name you control (even if it's not publicly resolvable)
  2. DNS provider API access (or ability to manually update records)
  3. 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