DayOne
Journal
Today I Learned

Day 21 - DNS-01 Challenge

Using the DNS-01 challenge with Certbot and Cloudflare is an excellent approach to obtain an SSL certificate without opening port 80, as it verifies domain ownership via DNS TXT records rather than HTTP. This is particularly useful for your single-node Kubernetes setup on a VPS without a load balancer, as it avoids the need to expose ports like 80 or 443 during certificate issuance. Below, I’ll explain how to use Certbot with the certbot-dns-cloudflare plugin to obtain a certificate using the DNS-01 challenge, based on the provided documentation and tailored to your Kubernetes environment.

Why DNS-01 Challenge?

  • No Port 80 Required: Unlike the HTTP-01 challenge, DNS-01 doesn’t require a web server or open ports (80 or 443). It proves domain ownership by adding a TXT record to your DNS.
  • Wildcard Support: DNS-01 is required for wildcard certificates (e.g., *.example.com).
  • Security: It’s ideal for internal or firewalled setups, as no external access to your server is needed during validation.[](https://letsencrypt.org/docs/challenge-types/)

Prerequisites

  • A domain managed by Cloudflare with DNS records configured.
  • A Cloudflare API token with DNS edit permissions (preferred over Global API Key for security).
  • Certbot and the certbot-dns-cloudflare plugin installed on your VPS.
  • kubectl access to your single-node Kubernetes cluster.
  • Your application running in Kubernetes with a NodePort or ClusterIP service.

Step-by-Step Guide

1. Install Certbot and the Cloudflare Plugin

Run these commands on your VPS (assuming a Debian/Ubuntu-based system):

sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare

For other distributions, refer to the Certbot instructions or install via pip:

sudo pip3 install certbot certbot-dns-cloudflare

Verify installation:

certbot --version

2. Create a Cloudflare API Token

Following the Certbot DNS Cloudflare documentation, create an API token:

  1. Log into your Cloudflare dashboard.
  2. Go to My Profile > API Tokens.
  3. Click Create Token and use the Edit zone DNS template.
  4. Set permissions:
  5. Zone: DNS - Edit (for all zones or select your specific domain).
  6. Save and copy the generated token.

    Alternatively, you can use a Global API Key (less secure):

    1. Go to My Profile > API Keys.
    2. Copy the Global API Key.

    3. Configure Cloudflare Credentials

    Create a credentials file to store your Cloudflare API token or key, as specified in the documentation:

    sudo mkdir -p /root/.secrets
    sudo touch /root/.secrets/cloudflare.ini
    sudo chmod 600 /root/.secrets/cloudflare.ini

    Edit the file:

    sudo nano /root/.secrets/cloudflare.ini

    Add one of the following, depending on your choice:

    dns_cloudflare_api_token = YOUR_API_TOKEN_HERE
    • Using API Token (recommended):
    dns_cloudflare_email = your-cloudflare-email@example.com
    dns_cloudflare_api_key = YOUR_GLOBAL_API_KEY_HERE
    • Using Global API Key:

    Save and exit. The documentation emphasizes securing this file, so ensure it’s only readable by root.

    4. Run Certbot with DNS-01 Challenge

    Use Certbot to request a certificate for your domain. Replace example.com with your domain. If you want a wildcard certificate, include *.example.com.

    sudo certbot certonly \
      --dns-cloudflare \
      --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
      --preferred-challenges dns-01 \
      -d example.com \
      -d *.example.com

    What happens:

    • Certbot contacts Let’s Encrypt, which provides a token.
    • The certbot-dns-cloudflare plugin uses your API token to create a TXT record at _acme-challenge.example.com.
    • Let’s Encrypt verifies the TXT record.
    • The plugin removes the TXT record after validation.
    • The certificate is saved to /etc/letsencrypt/live/example.com/.

    Key Notes:

    • No port 80 needed: The DNS-01 challenge relies entirely on DNS, so port 80 can remain closed.[](https://mytechiethoughts.com/linux/free-ssl-certificates-using-cloudflare-dns-validation-and-certbot/)[](https://letsencrypt.org/docs/challenge-types/)
    • Propagation delay: The --dns-cloudflare-propagation-seconds option can be added (e.g., --dns-cloudflare-propagation-seconds 30) if DNS updates are slow.[](https://mytechiethoughts.com/linux/free-ssl-certificates-using-cloudflare-dns-validation-and-certbot/)
    • Wildcard certificates: Including *.example.com requires DNS-01, and this command supports it.[](https://labzilla.io/blog/cloudflare-certbot)

    If you encounter errors (e.g., “Incorrect TXT record” or “Invalid request headers”), check:

    • The API token has Edit permissions for DNS.[](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/3063)
    • The domain is correctly configured in Cloudflare.
    • The credentials file is correctly formatted and readable.

    5. Integrate the Certificate into Kubernetes

    As described in the previous response, you can use the certificate in your Kubernetes application or with a reverse proxy like Nginx.

    kubectl create secret tls my-app-tls \
        --cert=/etc/letsencrypt/live/example.com/fullchain.pem \
        --key=/etc/letsencrypt/live/example.com/privkey.pem
    1. Create a Kubernetes Secret: Copy the certificate files to a secure location and create a secret:
    apiVersion: v1
    kind: Service
    metadata:
        name: my-app-service
    spec:
        type: NodePort
        ports:
        - port: 443
            targetPort: 8443
            nodePort: 30443
        selector:
        app: my-app
    1. Option 1: Application-Level SSL: Mount the secret in your application pod and configure your app to use HTTPS (e.g., on port 8443). Update your NodePort service to expose the HTTPS port:

    Update your deployment to mount the secret:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
        name: my-app
    spec:
        selector:
        matchLabels:
            app: my-app
        template:
        metadata:
            labels:
            app: my-app
        spec:
            containers:
            - name: my-app
            image: my-app-image
            ports:
            - containerPort: 8443
            volumeMounts:
            - name: tls
                mountPath: /etc/tls
                readOnly: true
            volumes:
            - name: tls
            secret:
                secretName: my-app-tls
    1. Option 2: Nginx Reverse Proxy:

    Deploy an Nginx pod to terminate SSL and proxy to your app. Use a ConfigMap for Nginx configuration and mount the TLS secret, as shown in the previous response.

    Example Nginx ConfigMap:

    apiVersion: v1
    kind: ConfigMap
    metadata:
        name: nginx-config
    data:
        nginx.conf: |
        server {
            listen 443 ssl;
            server_name example.com;
            ssl_certificate /etc/nginx/certs/tls.crt;
            ssl_certificate_key /etc/nginx/certs/tls.key;
            location / {
            proxy_pass http://my-app-service:80;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            }
        }

    Deploy Nginx and expose it via NodePort, as detailed previously.

    6. Automate Certificate Renewal

    Certificates from Let’s Encrypt expire every 90 days. The certbot-dns-cloudflare plugin supports automatic renewal.[](https://certbot.eff.org/faq)

    sudo certbot renew --dry-run
    1. Test renewal:
    1. Schedule renewal:

    Certbot typically sets up a cron job or systemd timer to run certbot renew twice daily. Verify it:

    sudo systemctl status certbot.timer

    If not enabled, create a cron job:

    sudo crontab -e

    Add:

    0 0 * * * certbot renew --quiet && kubectl create secret tls my-app-tls --cert=/etc/letsencrypt/live/example.com/fullchain.pem --key=/etc/letsencrypt/live/example.com/privkey.pem --dry-run=client -o yaml | kubectl apply -f -

    This renews the certificate and updates the Kubernetes secret. Restart your pods if needed:

    kubectl rollout restart deployment my-app
    kubectl rollout restart deployment nginx

    7. Map Domain to NodePort (Optional)

    To access your app at https://example.com without specifying the NodePort (e.g., 30443), set up a reverse proxy on the VPS host:

    sudo apt install nginx
    1. Install Nginx:
    server {
        listen 443 ssl;
        server_name example.com;
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        location / {
            proxy_pass http://<VPS_PUBLIC_IP>:30443;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
    1. Configure Nginx to forward traffic from port 443 to the NodePort:

    Save to /etc/nginx/sites-available/example.com, enable, and restart Nginx:

    sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
    sudo nginx -t
    sudo systemctl restart nginx
    1. Ensure your domain’s A record points to the VPS’s public IP.

    Verification

    openssl s_client -connect example.com:443
    • Access your app at https://example.com or https://<VPS_PUBLIC_IP>:30443.
    • Check the certificate with:
    • Verify DNS TXT records are cleaned up after issuance (they should be removed automatically by the plugin).[](https://labzilla.io/blog/cloudflare-certbot)

    Answers to Your Questions

    Yes, the DNS-01 challenge does not require port 80 or 443 to be open. It uses DNS TXT records for validation, making it ideal for your setup where you may not want to expose these ports. The certbot-dns-cloudflare plugin handles all DNS operations via Cloudflare’s API.[](https://mytechiethoughts.com/linux/free-ssl-certificates-using-cloudflare-dns-validation-and-certbot/)[](https://www.reddit.com/r/selfhosted/comments/tvwr2u/howtodealwithopenport80torenew/)
    • Can I run it without opening port 80?
    The Certbot DNS Cloudflare documentation is accurate and supports this process. It details:
    • Using the provided documentation:
    • Setting up credentials (cloudflare.ini).
    • Using --dns-cloudflare and --preferred-challenges dns-01.
    • Automatic TXT record creation and cleanup.
    • Renewal automation with the plugin.

    Additional Notes

    Security: Keep the cloudflare.ini file secure (permissions 600). Use API tokens instead of Global API Keys for better scoping.[](https://www.nodinrogers.com/post/2022-03-10-certbot-cloudflare-docker/)

    Wildcard Certificates: If you only need specific subdomains, list them explicitly (e.g., -d app.example.com -d api.example.com) to reduce the attack surface compared to a wildcard.[](https://www.reddit.com/r/selfhosted/comments/16og5nx/letsencryptdns01challengeorrootca/)

    Troubleshooting:

    • If Certbot fails with “unauthorized” or “incorrect TXT record,” verify your API token permissions and DNS zone settings.[](https://community.letsencrypt.org/t/unable-to-generate-a-wildcard-certificate-using-cloudflare-dns-challenge/205336)
    • Check DNS propagation with dig _acme-challenge.example.com TXT.
    • Use --dry-run to test without hitting Let’s Encrypt rate limits.

    Cloudflare Proxy: If Cloudflare’s proxy is enabled, ensure SSL/TLS is set to “Full (strict)” to avoid issues with Let’s Encrypt validation.[](https://community.letsencrypt.org/t/will-cloudflare-proxy-block-certbot-challenge/191879)

    Example Workflow Summary

    1. Install Certbot and certbot-dns-cloudflare.
    2. Create a Cloudflare API token and store it in /root/.secrets/cloudflare.ini.
    3. Run Certbot with the DNS-01 challenge to obtain a certificate.
    4. Create a Kubernetes Secret and configure your app or Nginx to use the certificate.
    5. Expose the app via NodePort (e.g., 30443 for HTTPS).
    6. Set up a host-level Nginx proxy to map example.com:443 to the NodePort.
    7. Automate renewal with a cron job.

    This approach ensures you can secure your Kubernetes app with SSL without opening port 80, fully leveraging the DNS-01 challenge and Cloudflare’s API. If you need help with specific errors or configurations, let me know!

    source