Handling wildcard TLS for domain names using Kamal and Caddy

People working on SaaS applications that serve content using customer domain names need a way to issue TLS certificates for the domains their customers provide. This means either updating the list of accepted domains or using a wildcard to issue them. Let's see how this could be solved using a Caddy reverse proxy.

Kamal SSL/TLS support

Kamal 2.7.0 does support automatic SSL/TLS certificates using Let's Encrypt, but doesn't yet support wildcard domains. Since updating the list of domains inside config/deploy.yml isn't usually an option as it requires a redeploy of the whole application, we have to currently look elsewhere for wildcard support.

Caddy

Caddy is a modern easy-to-use proxy with support for issuing wildcard SSL/TLS certificates. It's not the only option we have, but it let us to set up a reverse proxy with minimal configuration and effort. To make this work we essentially need to do three things. Deploy Caddy in front of Kamal Proxy, disable Kamal's SSL, and update application's host checking policy.

Caddyfile

Here's a small Caddyfile that will let us accept any hostname and allows us to specify an URL for a hostnames check which will ensure people won't misuse our automatic cert generation:

{
  email admin@lakyai.com

  on_demand_tls {
    # Caddy will call this URL with ?domain=name to ask
    # whether to issue a cert
    ask https://lakyai.com/internal/allow_domain
  }
}

:80 {
  # Keep ACME http-01 challenges working while redirecting other requests
  redir https://{host}{uri} permanent
}

:443 {
  tls {
    on_demand
  }

  reverse_proxy 64.225.112.80 {
    header_up Host {host}
  }
}

The ask directive specifies this check URL and automatically send the domain argument to it. The reverse_proxy directive will point to our Kamal server IP or to the port of Kamal Proxy if running on the same server. Note that the on_demand directive does need the on_demand_tls counterpart. And that's really it for the Caddy part.

Kamal configuration

We can run Caddy as a Kamal accessory:

# config/deploy.yml
accessories:
  caddy:
    host: 133.59.142.90
    image: caddy:2.10.2
    directories:
      - /etc/caddy:/etc/caddy
    files:
      - config/Caddyfile:/etc/caddy/Caddyfile
    volumes:
      - caddy_data:/data
      - caddy_config:/config
    network: host

By specifying network: host we can run Caddy as if it's running directly on the host. This way it can receive traffic on ports 80 and 443.

If not using a root user, you might need to create the directory on the server beforehand:

sudo mkdir /etc/caddy

To boot Caddy for the first time, we can use kamal accessory boot command:

kamal accessory boot caddy

Kamal

Now that the proxy is running on it's own server, we should disable the SSL:

# config/deploy.yml
proxy:
  ssl: false

If you want to run both Caddy and Kamal Proxy on the same server, you'll need to configure Kamal Proxy to listen on different ports which is supported in the proxy, but not exposed on the Kamal side as of 2.7.0.

Host checking

If your application framework supports host checking, you'll need to ensure that your customer domain names are whitelisted.

Author
Josef Strzibny
I am a full stack web developer with love for Linux. I made and deployed my first commercial web applications during high school in 2008. I have worked for Red Hat on the platform and developer experience teams as a Linux packager.

© Deploy Linux Blog