Automating Wildcard SSL Certificate Generation Using DNS Challenge with Secondary Domain

The Challenge

If you’ve ever hosted a website with a provider that doesn’t offer API access for DNS management, you’ve likely encountered challenges with automating SSL certificate generation - particularly for wildcard certificates. This is a common issue when you need to secure multiple subdomains but can’t automatically verify domain ownership through DNS challenges.

The Solution: Using a Secondary Domain

A clever workaround for this limitation is to leverage a secondary domain hosted with a provider that does support API access. Here’s how it works:

  1. Purchase a secondary domain from a provider with API support (like Hetzner)
  2. Set up DNS CNAME records to redirect the ACME challenge verification
  3. Use acme.sh to automate certificate generation

How It Works

When requesting a wildcard certificate for *.primary-domain.com, Let’s Encrypt requires verification through a TXT record at _acme-challenge.primary-domain.com. Instead of placing the TXT record directly there, we:

  1. Create a CNAME record on the primary domain:

    _acme-challenge.primary-domain.com IN CNAME _acme-challenge.secondary-domain.com
    
  2. Let the automation script place the TXT record on the secondary domain where we have API access

  3. When Let’s Encrypt checks the challenge, it follows the CNAME and finds the verification record

Automated Implementation

Here’s a comprehensive script that automates the entire process:

#!/bin/bash

# Configuration
PRIMARY_DOMAIN="myblog.technology"
SECONDARY_DOMAIN="myblog-alternative.technology"
ADMIN_EMAIL="admin@myblog.technology"
CERT_DIR="/etc/ssl/private"
SERVICE_RESTART_CMD="systemctl restart nginx"

# Function to check if acme.sh is installed
check_acme_sh() {
    if ! command -v acme.sh &> /dev/null; then
        echo "acme.sh not found. Installing..."
        curl https://get.acme.sh | sh
        source ~/.bashrc
    fi
}

# Function to validate environment
validate_environment() {
    if [ -z "$HETZNER_TOKEN" ]; then
        echo "Error: HETZNER_TOKEN environment variable not set"
        echo "Please set it using: export HETZNER_TOKEN=your_token_here"
        exit 1
    fi
}

# Function to generate and deploy certificate
generate_certificate() {
    local domain="*.$PRIMARY_DOMAIN"
    local challenge_domain="$SECONDARY_DOMAIN"
    
    echo "Generating certificate for $domain using $challenge_domain for verification..."
    
    acme.sh --issue \
        --server letsencrypt \
        -d "$domain" \
        --dns dns_hetzner \
        --challenge-alias "$challenge_domain" \
        -m "$ADMIN_EMAIL" \
        --renew-hook "$0 deploy" || {
            echo "Certificate generation failed"
            exit 1
        }
}

# Function to deploy certificate
deploy_certificate() {
    local cert_path="$HOME/.acme.sh/*.$PRIMARY_DOMAIN"
    
    echo "Deploying certificates to $CERT_DIR..."
    
    # Create directory if it doesn't exist
    mkdir -p "$CERT_DIR"
    
    # Copy certificates
    cp "$cert_path/fullchain.cer" "$CERT_DIR/fullchain.pem"
    cp "$cert_path/*.tech-tales.blog.key" "$CERT_DIR/privkey.pem"
    
    # Set permissions
    chmod 644 "$CERT_DIR/fullchain.pem"
    chmod 600 "$CERT_DIR/privkey.pem"
    
    # Restart services
    echo "Restarting services..."
    $SERVICE_RESTART_CMD
}

# Main execution
main() {
    case "$1" in
        "generate")
            check_acme_sh
            validate_environment
            generate_certificate
            ;;
        "deploy")
            deploy_certificate
            ;;
        *)
            echo "Usage: $0 {generate|deploy}"
            echo "  generate: Generate new certificate"
            echo "  deploy: Deploy existing certificate"
            exit 1
            ;;
    esac
}

main "$@"

Prerequisites

  1. A primary domain where you want to use the wildcard certificate
  2. A secondary domain with API access for DNS management (e.g., Hetzner)
  3. acme.sh installed on your system
  4. Hetzner API token (for this example)

Setting Up

  1. First, set up your Hetzner API token:

    export HETZNER_TOKEN=your_token_here
    
  2. Add the CNAME record to your primary domain’s DNS:

    _acme-challenge.tech-tales.blog IN CNAME _acme-challenge.tech-tales-alternative.blog
    
  3. Save the script above as ssl-cert-manager.sh and make it executable:

    chmod +x ssl-cert-manager.sh
    

Usage

To generate a new certificate:

./ssl-cert-manager.sh generate

The script will handle:

  • Checking for acme.sh installation
  • Validating the environment
  • Generating the certificate using DNS challenge
  • Deploying the certificate to the specified location
  • Restarting necessary services

Automation Tips

  1. Add the script to cron for automatic renewal:

    0 0 1 * * /path/to/ssl-cert-manager.sh generate
    
  2. Store the Hetzner token in a secure environment file:

    echo "export HETZNER_TOKEN=your_token_here" > ~/.ssl-cert-env
    chmod 600 ~/.ssl-cert-env
    
  3. Source the environment file in your cron job:

    0 0 1 * * source ~/.ssl-cert-env && /path/to/ssl-cert-manager.sh generate
    

Troubleshooting

If you encounter issues:

  1. Check the Let’s Encrypt logs in ~/.acme.sh/acme.sh.log
  2. Verify DNS propagation using dig or nslookup
  3. Ensure the HETZNER_TOKEN has sufficient permissions
  4. Test with Let’s Encrypt staging environment first by modifying the --server parameter

Security Considerations

  1. Keep your API token secure and regularly rotate it
  2. Use restricted permissions for certificate files
  3. Monitor certificate expiration dates
  4. Maintain backup copies of certificates

Remember to replace the example domains, email addresses, and paths with your actual values before using the script.

This solution provides a robust way to automate wildcard certificate generation even when your primary domain host doesn’t provide API access for DNS management.