6.2.b. Secure with Let's Encrypt

Goal

Obtain a trusted SSL certificate from Let’s Encrypt using DNS-01 challenge via DuckDNS, eliminating browser security warnings and enabling secure HTTPS connections to your application.

What you’ll learn:

  • How Let’s Encrypt’s DNS-01 challenge works for domain verification
  • How to install and configure Certbot with DNS plugins
  • How to configure Nginx for HTTPS with automatic certificate management
  • Best practices for certificate renewal automation

Prerequisites

Before starting, ensure you have:

  • ✓ Completed Tutorial 06 (Nginx Reverse Proxy) with VM running
  • ✓ Completed Tutorial 06.2a (DuckDNS Setup) with subdomain configured
  • ✓ DuckDNS token saved securely
  • ✓ SSH access to your VM (ssh azureuser@<VM_IP>)
  • ✓ Azure CLI logged in (az login)

Exercise Steps

Overview

  1. Understand the DNS-01 Challenge
  2. Open Port 443 for HTTPS
  3. Update DuckDNS with VM IP
  4. Install Certbot and DuckDNS Plugin
  5. Create DuckDNS Credentials File
  6. Request Let’s Encrypt Certificate
  7. Install Certificate into Nginx
  8. Set Up Automatic Renewal
  9. Test Your HTTPS Setup

Step 1: Understand the DNS-01 Challenge

Before requesting a certificate, it’s important to understand how Let’s Encrypt verifies domain ownership. The DNS-01 challenge is ideal for scenarios where you need wildcard certificates or when your server isn’t publicly accessible on port 80.

Let’s Encrypt offers several challenge types to verify you control a domain:

We use DNS-01 because:

The process works like this:

  1. Certbot requests a challenge from Let’s Encrypt
  2. Let’s Encrypt provides a random token
  3. Certbot creates a TXT record: _acme-challenge.yourdomain.duckdns.org
  4. Let’s Encrypt queries DNS for this record
  5. If the token matches, certificate is issued

Concept Deep Dive

DNS-01 is particularly useful because it proves you control the domain at the DNS level, which is a stronger proof of ownership than just having access to a web server. The tradeoff is that it requires API access to your DNS provider, which is why we use DuckDNS - it provides a simple API for this purpose.

Quick check: You understand that certbot will automatically update your DuckDNS TXT record

Step 2: Open Port 443 for HTTPS

Your VM’s Network Security Group currently only allows HTTP traffic on port 80. HTTPS requires port 443 to be open. This step configures Azure’s firewall to allow encrypted traffic.

  1. Open a terminal on your local machine

  2. Run the following Azure CLI command to open port 443:

    az vm open-port \
        --resource-group hellojava-reverseproxy-rg \
        --name hellojava-reverseproxy-vm \
        --port 443 \
        --priority 310
    
  3. Verify the port is open:

    az network nsg rule list \
        --resource-group hellojava-reverseproxy-rg \
        --nsg-name hellojava-reverseproxy-vmNSG \
        --output table
    

    You should see rules for ports 22, 80, and 443.

Concept Deep Dive

Azure Network Security Groups (NSGs) act as virtual firewalls. Each rule has a priority number - lower numbers are evaluated first. We use priority 310 for HTTPS (after 300 for HTTP) to maintain a logical ordering.

Common Mistakes

  • Using the wrong resource group name (check with az group list)
  • Forgetting the NSG name includes “NSG” suffix

Quick check: The command completes without errors and port 443 appears in the rule list

Step 3: Update DuckDNS with VM IP

DuckDNS needs to point to your VM’s current IP address. While you may have set this during Tutorial 06.2a, it’s good practice to verify and update it before requesting a certificate.

  1. Get your VM’s public IP address:

    az vm show -d \
        --resource-group hellojava-reverseproxy-rg \
        --name hellojava-reverseproxy-vm \
        --query publicIps -o tsv
    

    Note this IP address.

  2. Update DuckDNS with the IP (replace SUBDOMAIN and TOKEN):

    curl "https://www.duckdns.org/update?domains=SUBDOMAIN&token=TOKEN&ip=YOUR_VM_IP"
    

    You should see OK in the response.

  3. Verify DNS resolution:

    dig +short SUBDOMAIN.duckdns.org
    

    This should return your VM’s IP address.

Concept Deep Dive

DNS propagation is usually instant with DuckDNS, but can take up to 5 minutes in some cases. If dig returns the wrong IP, wait a few minutes and try again. Let’s Encrypt will query multiple DNS servers during validation, so correct DNS is essential.

Quick check: dig returns your VM’s current public IP address

Step 4: Install Certbot and DuckDNS Plugin

Certbot is the official Let’s Encrypt client. We’ll install it as a snap package along with the DuckDNS DNS plugin that enables automatic DNS record management.

  1. SSH into your VM:

    ssh azureuser@YOUR_VM_IP
    
  2. Install Certbot via snap:

    sudo snap install --classic certbot
    
  3. Create a symlink to make certbot available system-wide:

    sudo ln -sf /snap/bin/certbot /usr/bin/certbot
    
  4. Enable snap plugin trust:

    sudo snap set certbot trust-plugin-with-root=ok
    
  5. Install the DuckDNS plugin:

    sudo snap install certbot-dns-duckdns
    
  6. Connect the plugin to Certbot:

    sudo snap connect certbot:plugin certbot-dns-duckdns
    
  7. Verify the installation:

    certbot --version
    certbot plugins
    

    You should see dns-duckdns in the plugins list.

Concept Deep Dive

We use snap instead of apt because snap provides the latest Certbot version with automatic updates. The trust-plugin-with-root=ok setting allows Certbot to use third-party DNS plugins that need root access to create credentials files.

Common Mistakes

  • Forgetting to connect the plugin (certbot won’t see it)
  • Missing the symlink (certbot command not found)

Quick check: certbot plugins shows dns-duckdns in the list

Step 5: Create DuckDNS Credentials File

Certbot needs your DuckDNS token to create TXT records. We’ll create a secure credentials file that only root can read.

  1. Create the credentials file (replace YOUR_TOKEN with your actual token):

    sudo mkdir -p /etc/letsencrypt
    sudo tee /etc/letsencrypt/duckdns-credentials << EOF
    dns_duckdns_token = YOUR_TOKEN
    EOF
    
  2. Secure the file permissions:

    sudo chmod 600 /etc/letsencrypt/duckdns-credentials
    
  3. Verify the file was created correctly:

    sudo cat /etc/letsencrypt/duckdns-credentials
    

    You should see your token in the output.

Concept Deep Dive

The credentials file must be in /etc/letsencrypt/ because Certbot runs as a snap with restricted filesystem access. The snap cannot read files from /root/ or other user directories. The 600 permission ensures only root can read the file.

Common Mistakes

  • Putting credentials in /root/ (snap can’t access it)
  • Wrong permissions (Certbot will refuse to use world-readable credentials)
  • Spaces around the = sign (use exactly: dns_duckdns_token = TOKEN)

Quick check: File exists with correct permissions (600) and contains your token

Step 6: Request Let’s Encrypt Certificate

Now we’ll request the actual certificate. Certbot will contact Let’s Encrypt, create the DNS TXT record via DuckDNS, and wait for validation.

  1. Run the certificate request (replace SUBDOMAIN and EMAIL):

    sudo certbot certonly \
        --authenticator dns-duckdns \
        --dns-duckdns-credentials /etc/letsencrypt/duckdns-credentials \
        --dns-duckdns-propagation-seconds 90 \
        -d SUBDOMAIN.duckdns.org \
        --agree-tos \
        --email YOUR_EMAIL \
        --non-interactive
    
  2. Wait for the process to complete (about 2 minutes).

  3. Verify the certificate was obtained:

    sudo ls -la /etc/letsencrypt/live/
    

    You should see a directory named SUBDOMAIN.duckdns.org.

Concept Deep Dive

The --dns-duckdns-propagation-seconds 90 tells Certbot to wait 90 seconds after creating the TXT record before asking Let’s Encrypt to verify. This allows time for DNS propagation. The certificate files are stored in /etc/letsencrypt/live/DOMAIN/ with symlinks to the current versions.

Common Mistakes

  • Wrong credentials path (use /etc/letsencrypt/duckdns-credentials)
  • Insufficient propagation time (60 seconds sometimes fails)
  • Typo in domain name
  • Token incorrect in credentials file

Transient DNS Failures

If you see errors like “SERVFAIL” or “DNS timeout”, don’t panic. DNS-01 challenges can occasionally fail due to temporary DNS resolution issues. Simply wait a minute and run the command again - it usually succeeds on retry. This is normal and happens even in production environments.

Quick check: Certificate directory exists in /etc/letsencrypt/live/

Step 7: Install Certificate into Nginx

The certificate is obtained but Nginx doesn’t know about it yet. Certbot can automatically configure Nginx to use the certificate and redirect HTTP to HTTPS.

  1. Install the certificate into Nginx (replace SUBDOMAIN):

    sudo certbot install \
        --nginx \
        --cert-name SUBDOMAIN.duckdns.org \
        --redirect \
        --non-interactive
    
  2. Verify Nginx configuration was updated:

    sudo nginx -t
    

    You should see “syntax is ok” and “test is successful”.

  3. Check the Nginx configuration:

    sudo cat /etc/nginx/sites-available/hellojava
    

    You should see SSL directives and a redirect from port 80 to 443.

Concept Deep Dive

The --redirect flag tells Certbot to configure Nginx to redirect all HTTP traffic to HTTPS. Certbot modifies your existing Nginx configuration to add the SSL certificate paths, enable TLS, and set up secure defaults. This is safer than manually editing the configuration.

Quick check: Nginx configuration test passes and shows SSL settings

Step 8: Set Up Automatic Renewal

Let’s Encrypt certificates expire after 90 days. Certbot automatically renews them, but we need to ensure Nginx reloads when certificates are renewed.

  1. Create the renewal hooks directory:

    sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
    
  2. Create a hook script to reload Nginx:

    sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'EOF'
    #!/bin/bash
    systemctl reload nginx
    EOF
    
  3. Make the script executable:

    sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
    
  4. Test the renewal process (dry run):

    sudo certbot renew --dry-run
    

    You should see “Congratulations, all simulated renewals succeeded!”

Concept Deep Dive

Certbot sets up a systemd timer that runs twice daily to check for certificates needing renewal. It only renews certificates within 30 days of expiry. The deploy hook runs after successful renewal to reload Nginx with the new certificate.

Quick check: Dry run completes successfully

Step 9: Test Your HTTPS Setup

Time to verify that everything works correctly. We’ll test HTTPS access, HTTP redirect, and certificate validity from outside the VM.

  1. Exit the SSH session:

    exit
    
  2. Test HTTPS access (replace SUBDOMAIN):

    curl -s https://SUBDOMAIN.duckdns.org/actuator/health
    

    You should see {"status":"UP"}.

  3. Test HTTP to HTTPS redirect:

    curl -s -o /dev/null -w "%{http_code}" http://SUBDOMAIN.duckdns.org/
    

    You should see 301 (redirect).

  4. Check certificate details:

    echo | openssl s_client -connect SUBDOMAIN.duckdns.org:443 2>/dev/null | openssl x509 -noout -issuer -subject -dates
    

    You should see:

    • Issuer containing “Let’s Encrypt”
    • Subject with your domain
    • Valid dates (90 days from now)
  5. Test in a browser:

    Open https://SUBDOMAIN.duckdns.org/ in your browser.

    You should see:

    • Padlock icon (secure connection)
    • No certificate warnings
    • Your HelloJava application

Success indicators:

  • HTTPS returns 200 with health check response
  • HTTP returns 301 redirect to HTTPS
  • Certificate issued by Let’s Encrypt
  • Browser shows padlock without warnings
  • Application works over HTTPS

Final verification checklist:

  • ☐ Port 443 open in Azure NSG
  • ☐ DuckDNS points to correct IP
  • ☐ Certbot and plugin installed
  • ☐ Credentials file in correct location with proper permissions
  • ☐ Certificate obtained and stored
  • ☐ Nginx configured for SSL
  • ☐ Auto-renewal configured and tested
  • ☐ HTTPS accessible from browser

Optional: Add HSTS Header for Maximum Security

HTTP Strict Transport Security (HSTS) tells browsers to always use HTTPS for your domain, preventing downgrade attacks. This is optional but recommended for production.

  1. SSH into your VM:

    ssh azureuser@YOUR_VM_IP
    
  2. Edit the Nginx configuration:

    sudo nano /etc/nginx/sites-available/hellojava
    
  3. Add the HSTS header inside the server block that listens on port 443 (the SSL block):

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    

    Place it after the ssl_certificate lines.

  4. Test the configuration:

    sudo nginx -t
    
  5. Reload Nginx:

    sudo systemctl reload nginx
    
  6. Verify the header is present:

    curl -sI https://SUBDOMAIN.duckdns.org/ | grep -i strict
    

    You should see Strict-Transport-Security: max-age=31536000; includeSubDomains.

Concept Deep Dive

HSTS prevents SSL stripping attacks where an attacker intercepts HTTP requests before they redirect to HTTPS. Once a browser sees the HSTS header, it will always use HTTPS for that domain - even if the user types http://. The max-age=31536000 means browsers will remember this for one year. Use with caution in testing environments since it’s hard to undo!

Common Issues

If you encounter problems:

Certificate request fails with “unauthorized”:

  • Verify your DuckDNS token is correct in the credentials file
  • Check the credentials file is at /etc/letsencrypt/duckdns-credentials
  • Ensure file permissions are 600
  • Try increasing propagation time to 120 seconds

DNS timeout or SERVFAIL during certificate request:

  • This is often transient - simply wait 1-2 minutes and try again
  • DNS-01 challenges require Let’s Encrypt’s servers to query DuckDNS, and temporary network issues can cause failures
  • Retrying usually succeeds - this is normal, not a configuration error
  • Check that DuckDNS is accessible: curl https://www.duckdns.org
  • If it fails repeatedly, try increasing propagation time to 120 seconds

Nginx won’t start after certificate install:

  • Check syntax: sudo nginx -t
  • View error log: sudo tail /var/log/nginx/error.log
  • Verify certificate files exist in /etc/letsencrypt/live/

Browser still shows “Not Secure”:

  • Clear browser cache
  • Check you’re using https:// not http://
  • Verify the domain matches the certificate

Still stuck? Check Certbot logs: sudo cat /var/log/letsencrypt/letsencrypt.log

Summary

You’ve successfully secured your application with a Let’s Encrypt certificate which:

Key takeaway: Let’s Encrypt with DNS-01 challenge is the gold standard for free, automated SSL certificates. This same pattern works for any domain - not just DuckDNS - as long as you have API access to create TXT records. In production, you’d use this with your own domain and a DNS provider like Cloudflare or Azure DNS.

Going Deeper (Optional)

Want to explore more?

  • Research how ACME protocol works under the hood
  • Try issuing a wildcard certificate for *.SUBDOMAIN.duckdns.org
  • Compare HTTP-01 vs DNS-01 challenge types
  • Investigate Certificate Transparency logs
  • Add HSTS preloading for maximum security

Clean Up

When you’re done with this tutorial, you can either:

Keep the VM: Your HTTPS setup is production-ready!

Delete resources:

az group delete --name hellojava-reverseproxy-rg --yes --no-wait

Note

Deleting the VM removes the certificate, but your DuckDNS subdomain remains. You can reuse it for future projects.

Done! 🎉

Excellent work! You’ve learned how to obtain and install trusted SSL certificates using Let’s Encrypt’s DNS-01 challenge. Your application is now secured with industry-standard encryption and will automatically maintain its certificate. This is the same certificate infrastructure used by millions of websites worldwide!