Wildcard Certificate #

A wildcard certificate is a type of TLS certificate designed to secure the parent domain (apex domain) along with all first-level subdomains beneath it using a single certificate. For example, a certificate for *.example.com automatically applies and is valid for app.example.com, api.example.com, blog.example.com, and other subdomains. This approach is popular because it simplifies TLS management and minimizes the need to request new certificates every time you add a new subdomain to your infrastructure.

However, there’s one fundamental rule in the ACME specification you must understand: wildcard certificates can only be obtained through the DNS-01 challenge verification method. You cannot use the HTTP-01 or TLS-ALPN-01 challenges to get a wildcard certificate. This restriction isn’t a Caddy limitation — it’s a strict security rule set by all global Certificate Authority (CA) providers to ensure the requester truly controls the entire DNS zone of the domain, not just a single web server.


Why Do Wildcards Need DNS-01? #

The technical reason behind the mandatory DNS-01 challenge for wildcard certificates is fundamental to verification security:

The HTTP-01 challenge for *.example.com (✗ CANNOT BE DONE):
  1. The CA asks: "Put a token at http://random-subdomain.example.com/.well-known/acme-challenge/..."
  2. Problem: Because this is a wildcard, that subdomain may not exist yet or may point
     to a different IP. The CA can't guess and HTTP GET every possible subdomain.
  3. HTTP-01 only proves control over a web server at a specific IP, not over the domain as a whole.

The DNS-01 challenge for *.example.com (✓ SUCCESSFUL & CA-APPROVED):
  1. The CA asks: "Create a TXT record named _acme-challenge.example.com in the parent domain's DNS zone."
  2. Caddy creates that TXT record through the DNS provider's API.
  3. The CA queries the DNS TXT record at the parent domain (apex domain).
  4. Because DNS records live at the parent domain zone level, the existence of that record proves
     absolutely that the requester has full administrative access to the entire DNS zone
     of the domain, and therefore has the right to secure all subdomains (*.example.com).

When to Choose a Wildcard Certificate? #

Although wildcard certificates are very practical, they aren’t always the best choice for every scenario. The table below compares when you should use a wildcard and when individual (non-wildcard) certificates are more appropriate:

Use Case ScenarioRecommendationTechnical Reason
Static Multi-Tenant Platform (many self-managed subdomains, e.g., user1.app.com, user2.app.com).Wildcard CertificateSaves Let’s Encrypt rate limit quota because hundreds of subdomains use the same certificate file.
Server with Ports 80/443 Closed (internal servers, LAN, behind strict firewalls).DNS-01 (Wildcard or Individual)Doesn’t require inbound traffic from the internet to the Caddy server.
Few Static Subdomains (only have example.com and api.example.com).Individual Certificates (HTTP-01)Much easier to configure because it doesn’t require compiling DNS plugins or DNS API credentials.
Third-Party Domains (clients using their own domains, e.g., customer.com pointed at your application).On-Demand TLS (ODTLS)Your wildcard certificate (*.example.com) doesn’t apply to clients’ independent domains.

Compiling Caddy with a DNS Provider Plugin #

By default, standard Caddy binary distributions don’t include DNS API modules for various providers — both because of the huge variety of DNS providers worldwide and to keep the binary size small. Therefore, you must do a custom Caddy compilation using the xcaddy tool.

Compilation Steps: #

1. Install xcaddy #

Make sure you have the Go programming language installed on your system, then run this command to download xcaddy:

# Install xcaddy into your Go bin folder
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

2. Build Caddy with the DNS Plugin #

Choose the DNS plugin that matches your domain provider (for example, Cloudflare):

# Compile a custom Caddy with the Cloudflare DNS module
xcaddy build --with github.com/caddy-dns/cloudflare

If your infrastructure uses several different DNS providers, you can include multiple plugins in one compilation command:

# Compile with multiple DNS plugins at once
xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/caddy-dns/route53 \
    --with github.com/caddy-dns/digitalocean

3. Verify Compilation Success #

Check whether the DNS module you want is registered in your new Caddy binary:

# Check installed modules
./caddy list-modules | grep dns
# Success output:
# dns.providers.cloudflare
# dns.providers.digitalocean
# dns.providers.route53

4. Install the Custom Binary to the System (Linux Systemd) #

Replace the system’s built-in Caddy binary with your newly built custom one:

# Stop the running Caddy service
sudo systemctl stop caddy

# Copy the new binary to the system binary directory
sudo cp ./caddy /usr/bin/caddy

# Restart the Caddy service
sudo systemctl start caddy

Wildcard Configuration Using Cloudflare #

Cloudflare is one of the most popular DNS providers and has excellent integration with Caddy.

1. Creating a Cloudflare API Token Correctly #

[!CAUTION] Never use your Cloudflare account’s Global API Key for Caddy configuration. The Global API Key has full access to make any change (including deleting domains or changing the account email). If this key leaks, your entire Cloudflare infrastructure is at risk. Use an API Token with least privilege restrictions.

Steps to create an API Token in Cloudflare:

  1. Log in to the Cloudflare dashboard, go to My Profile -> API Tokens.
  2. Click Create Token, then choose Create Custom Token.
  3. Set the token parameters as follows:
    • Token Name: Caddy DNS-01 Challenge
    • Permissions:
      • Zone -> DNS -> Edit
      • Zone -> Zone -> Read
    • Zone Resources:
      • Include -> Specific zone -> Select your domain name (e.g., example.com)
  4. Click Continue to summary then Create Token. Copy the token shown.

2. Writing the Caddyfile for the Wildcard and Apex Domain #

The *.example.com wildcard certificate does not cover the example.com parent domain itself. Therefore, you must configure Caddy to secure both domains. You can combine them in one site block in the Caddyfile:

# Secure the apex domain and all first-level subdomains
example.com, *.example.com {
    tls {
        # Use the cloudflare plugin, injecting the token from env
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    
    # Route traffic based on hostname using matchers
    @apex host example.com
    @app host app.example.com
    @api host api.example.com
    
    # Route traffic to the appropriate backend
    handle @apex {
        root * /var/www/html
        file_server
    }
    
    handle @app {
        reverse_proxy localhost:3000
    }
    
    handle @api {
        reverse_proxy localhost:8080
    }
    
    # Fallback handling if the subdomain isn't recognized
    handle {
        error "Subdomain not registered" 404
    }
}

Wildcard Configuration Using AWS Route 53 #

If your domain is managed with AWS Route 53, you must create a special IAM policy (IAM Policy) in the AWS console granting the minimal permissions Caddy needs to manage TXT records.

1. Minimal IAM Policy (Least Privilege Policy) #

Create a new IAM policy in the AWS console with the following JSON format (replace HOSTED_ZONE_ID with your Route 53 zone ID):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "route53:GetChange",
                "route53:ChangeResourceRecordSets",
                "route53:ListResourceRecordSets"
            ],
            "Resource": [
                "arn:aws:route53:::hostedzone/HOSTED_ZONE_ID",
                "arn:aws:route53:::change/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "route53:ListHostedZones",
                "route53:ListHostedZonesByName"
            ],
            "Resource": "*"
        }
    ]
}

2. AWS Route 53 Caddyfile #

If Caddy runs on an AWS EC2 instance with an IAM Role attached carrying the policy above, Caddy automatically detects the AWS credentials without you writing access keys in the Caddyfile:

# Caddyfile for AWS Route 53 (Using IAM Role)
example.com, *.example.com {
    tls {
        dns route53 {
            max_retries 10
        }
    }
    
    reverse_proxy localhost:8080
}

On-Demand TLS for Multi-Tenant Platforms #

A wildcard certificate is great when all subdomains are under your domain (*.example.com). However, if you’re building a SaaS or multi-tenant platform (like Shopify or WordPress) where customers can point their own custom domains (client-domain.com) to your server via CNAME, your wildcard certificate can’t secure their custom domains.

The best solution for this problem is Caddy’s built-in On-Demand TLS (ODTLS) feature. ODTLS lets Caddy dynamically request TLS certificates during the first TLS handshake when that domain is accessed by a visitor.

flowchart TD
    A["Visitor Accesses https://client-domain.com"] --> B["Caddy Receives New TLS Connection"]
    B --> C{"Is the client-domain.com certificate in storage?"}
    C -- Yes --> D["Use Certificate for Handshake & Done"]
    C -- No --> E["Send GET Request to 'Ask' Endpoint"]
    E --> F{"Does the 'Ask' Endpoint Reply with HTTP 200?"}
    F -- Yes --> G["Trigger ACME Process to Get New Certificate"]
    F -- No --> H["Reject TLS Handshake (Connection Terminated)"]
    G --> I["Store Certificate & Complete Handshake"]

Why Is the ‘Ask’ Endpoint Critical? #

[!WARNING] Enabling On-Demand TLS without defining the ask endpoint is a critical security hole (DDoS/Resource Exhaustion). Without validation, attackers can make millions of TLS connections to your server using random domains they point at your server’s IP. This forces Caddy to request new certificates for each junk domain from Let’s Encrypt, causing the server to run out of memory, fill the disk, and get your ACME account blocked for exceeding rate limits.

Example Caddyfile Configuration with On-Demand TLS: #

# Secure On-Demand TLS configuration
{
    email [email protected]
    
    on_demand_tls {
        # Your internal endpoint for domain name validation
        ask http://localhost:5000/api/v1/validate-domain
        
        # New certificate creation limits to prevent exploitation
        interval 2m
        burst    5
    }
}

# Special site block to handle dynamic port 443 traffic
:443 {
    tls {
        # Enable on-demand certificate creation when accessed
        on_demand
    }
    
    reverse_proxy localhost:8080
}

Example ‘Ask’ Validation Endpoint Implementation (Python/FastAPI) #

The ask endpoint receives the ?domain=client-domain.com query from Caddy and must return HTTP status code 200 if the domain is registered as an active customer in your database, or HTTP 403 if the domain isn’t recognized:

from fastapi import FastAPI, Query, HTTPException, status
import database_helper # Example of our internal database module

app = FastAPI()

@app.get("/api/v1/validate-domain")
async def validate_domain(domain: str = Query(..., description="Domain name requesting TLS")):
    # Clean the input for security
    clean_domain = domain.strip().lower()
    
    # Check whether the domain is registered and active in our SaaS system
    is_active_tenant = await database_helper.check_tenant_domain_exists(clean_domain)
    
    if is_active_tenant:
        # Return HTTP 200 to approve TLS certificate issuance
        return {"status": "approved", "domain": clean_domain}
    else:
        # Return HTTP 403 to reject the TLS handshake
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Domain not registered on our SaaS platform"
        )

Wildcard & ODTLS Troubleshooting #

1. “Unrecognized Module” Error When Running Caddy #

  • Cause: The Caddyfile configuration uses a DNS provider (like dns cloudflare), but the Caddy binary running is the standard binary not compiled with the DNS plugin.
  • Solution: Re-run the compilation steps with xcaddy and make sure the custom compiled binary has replaced the standard binary at /usr/bin/caddy.

2. Verification Failure Due to DNS Propagation Delay #

  • Log Symptoms: The Caddy log shows DNS record validation failed or txt record not found errors.
  • Cause: Caddy asks Let’s Encrypt to verify the TXT record too quickly, while your provider’s DNS nameservers haven’t finished propagating the new record.
  • Solution: Increase the propagation wait time in the tls Caddyfile configuration so Caddy waits longer before signaling verification to the CA:
example.com, *.example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
        # Wait up to 5 minutes for DNS propagation (default: 2 minutes)
        propagation_timeout 5m
        # Add a static delay before starting to check the record status
        propagation_delay 30s
    }
    reverse_proxy localhost:8080
}

Summary #

  • Wildcard Certificate — A TLS certificate (*.domain.com) securing the parent domain and all first-level subdomains with one certificate.
  • DNS-01 Requirement — The DNS-01 verification challenge is the only method the ACME protocol allows for obtaining wildcard certificates.
  • xcaddy Compilation — You must use the xcaddy tool to compile a custom Caddy binary including your DNS provider’s API plugin.
  • Separate Apex Domain — The *.domain.com wildcard certificate doesn’t cover the domain.com parent domain; both must be listed together in the Caddyfile configuration.
  • On-Demand TLS (ODTLS) — The dynamic solution for multi-tenant platforms to automatically obtain TLS certificates for customers’ custom domains on first access.
  • Critical ‘Ask’ Endpoint — Must be configured when using ODTLS to validate domains in your internal database and prevent DDoS attacks that drain the ACME quota.

← Previous: Self-Signed & Internal CA   Next: DNS Challenge →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact