Skip to content

default_sni + fallback_sni global settings are flimsy #6979

@polarathene

Description

@polarathene

These two global settings don't seem to play well once wildcard cert preference is involved, neither worked when querying Caddy without SNI (expected default_sni) or with an invalid one (expected fallback_sni).

$ step certificate inspect --insecure --servername invalid-value https://172.18.0.2 | grep DNS
failed to connect: remote error: tls: internal error

$ step certificate inspect --insecure https://172.18.0.2 | grep DNS
failed to connect: remote error: tls: internal error

This will occur when a wildcard certificate is available that the configured SNI setting would match. Instead for it to actually work, the SNI setting would need to be set to *.example.internal (instead of say default.example.internal).

NOTE: Unrelated to wildcard cert preference. These two settings fail in the same manner (both on Caddy 2.9 + 2.10, probably earlier versions too) when tls internal (or similar) is not set somewhere (does not need to be related to the configured SNI values), or for Caddy 2.9, setting auto_https prefer_wildcard also triggers similar logic to restore SNI default/fallback functionality.

Additionally:

  • It would be nice for {env.ENV_NAME} support?
  • Caddy L4 doesn't seem to be compatible (presumably these settings are only relevant to the HTTP app?)
  • docs: default_sni should perhaps also mention the expectation of certificate to be provisioned (like fallback_sni)?

Reference

This may only be relevant to locally managed certs but I noticed how default_sni and fallback_sni global settings are affected by wildcard cert preference, along with a requirement for tls directive (at least with internal or externally loading a cert). I've not explored if this affects typical deployments with ACME provisioned certs, or if other variants of the tls directive likewise provide a workaround.

I have verified this issue persists across both versions of Caddy (and perhaps earlier versions I've not tested). Below is a reproduction compose.yaml for use with Docker Compose. Otherwise you just need Caddy with a Caddyfile and Smallstep step CLI for verifying the SNI functionality for these two global settings.

I've inlined commentary in this config below related to my findings (apologies for lacking time to better format for this bug report). The reproduction commands gives an idea of how to verify, but you'll need to go over my Caddyfile config notes below for caveats of what works/doesn't and other related observations that should help pinpoint the cause. (EDIT: Briefly highlighted caveats in report above)

Reproduction

services:
  reverse-proxy:
    container_name: caddy
    image: caddy:2.10.0
    #image: caddy:2.9.1
    environment:
      APEX_DOMAIN: example.internal
    # Configure containers on the same network to resolve `bug.example.internal` to the Caddy container IP:
    networks:
      default:
        aliases:
          - bug.example.internal
    configs:
      - source: caddy-config
        target: /etc/caddy/Caddyfile
      - source: snippet-global-sni
        target: /srv/globals/sni
      # Optional for related Caddy L4 compatibility issue (requires custom Caddy built with L4 module):
      - source: snippet-global-l4
        target: /srv/globals/l4
      # Toggle this to add/remove the wildcard snippet:
      - source: snippet-wildcard
        target: /srv/sites/wildcard

  debug:
    scale: 0 # Prevent this container starting with `docker compose up`
    image: localhost/debug
    build:
      dockerfile_inline: |
        FROM alpine
        RUN apk add curl step-cli

configs:
  caddy-config:
    content: |
      # Global Settings:
      {
        # Not entirely compatible with global SNI settings (due to need for manual workaround via a `tls` directive):
        local_certs

        # Caddy 2.9.x only (Default functionality in Caddy 2.10.0):
        #auto_https prefer_wildcard

        import /srv/globals/*
      }

      # WORKAROUND NOTE:
      # - `tls internal` must be used for `default_sni` / `fallback_sni` to work?
      #   `tls <cert> <key>` also works, thus may be something else related to this directive?
      #   However the directive does not need to be inside a site-block related to the SNI values.
      #   Nor is the directive required for Caddy 2.9.x when setting `auto_https prefer_wildcard`.
      # - Caddy 2.10.0 roughly enables equivalent `auto_https prefer_wildcard` functionality by default,
      #   but when a wildcard cert is provisioned/loaded, the global SNI settings are only compatible
      #   with wildcard SNI value and additionally requires the `tls` directive workaround
      #   (unlike Caddy 2.9.x with `auto_https prefer_wildcard`).
      # - Without either config workaround, `step certificate inspect` will fail with error:
      #   failed to connect: remote error: tls: internal error
      hello-world.localhost {
        tls internal
        abort
      }

      # This does not interfere despite provisioning a wildcard cert
      # The wildcard cert caveat presumably only affects SNI settings from working when
      # there is a valid wildcard cert match that gets preferred but doesn't match the SNI value?
      *.localhost {
        abort
      }

      # default_sni: step certificate inspect --insecure https://172.18.0.2 | grep DNS
      # fallback_sni: step certificate inspect --insecure --servername invalid-value https://172.18.0.2 | grep DNS
      bug.{env.APEX_DOMAIN}, default.{env.APEX_DOMAIN}, fallback.{env.APEX_DOMAIN} {
        respond <<HEREDOC
          Hello from subdomain: {labels.2}

          HEREDOC
      }

      # Adds the wildcard site snippet when provided to the container:
      import /srv/sites/*

  snippet-wildcard:
    content: |
      *.{env.APEX_DOMAIN} {
        respond "wildcard cert"
      }

  snippet-global-sni:
    content: |
        # Placeholders are incompatible, must be an exact match to SAN?
        #default_sni hello.{env.APEX_DOMAIN}
        #fallback_sni bye.{env.APEX_DOMAIN}

        # Returns wildcard cert (provided one is provisioned/loaded):
        # Valid for Caddy 2.9.1 (even when `auto_https prefer_wildcard`)
        # Valid for Caddy 2.10.0 (requires `tls` directive workaround + wildcard cert provisioned/loaded)
        #default_sni *.example.internal
        #fallback_sni *.example.internal

        # This requires the `tls internal` directive workaround
        # Alternative workaround (Caddy 2.9.x) via `auto_https prefer_wildcard` (but only _without_ a wildcard cert available)
        # Similar to the alternative workaround, for Caddy 2.10.0 when a wildcard cert is available this SNI config will fail.
        default_sni default.example.internal
        fallback_sni fallback.example.internal

  # Related - These always return `failed to connect: EOF` - Global SNI not supported by Caddy L4?:
  # default_sni: step certificate inspect --insecure tcp://172.18.0.2:4443 | grep DNS
  # fallback_sni: step certificate inspect --insecure --servername invalid-value tcp://172.18.0.2:4443 | grep DNS
  # Direct queries are valid (they will return their direct certificate or when preferring wildcard, the wildcard cert):
  # step certificate inspect --insecure --servername bug.example.internal tcp://172.18.0.2:4443 | grep DNS
  snippet-global-l4:
    content: |
      layer4 {
        :4443 {
          @host-any tls sni_regexp .*\.example\.internal
          route @host-any {
            proxy caddy:443
          }
        }
      }

To run the example:

# Start Caddy container:
docker compose up -d --force-recreate
# Start the debug container and shell into it to run `step` commands:
docker compose run --rm -it debug ash

# Depending on success/failure, you'll get output from the below commands similar to:
# Success: `DNS:default.example.internal`
# Fail: `failed to connect: remote error: tls: internal error`
# Tip: For getting the IP you could use something like `ping bug.example.internal` within the `debug` container

# default_sni:
step certificate inspect --insecure https://172.18.0.2 | grep DNS
# fallback_sni:
step certificate inspect --insecure --servername invalid-value https://172.18.0.2 | grep DNS

# Optional: Caddy L4 (presently not compatible at all with the global SNI settings)
# default_sni:
step certificate inspect --insecure tcp://172.18.0.2:4443 | grep DNS
# fallback_sni:
step certificate inspect --insecure --servername invalid-value tcp://172.18.0.2:4443 | grep DNS

# Optional: You can verify a connection with curl by setting SNI this way:
curl --insecure --resolve bug.example.internal:443:172.18.0.2 https://bug.example.internal
# Caddy L4 equivalent (proxies port 4443 to 443 when matched successfully):
curl --insecure --resolve bug.example.internal:4443:172.18.0.2 https://bug.example.internal:4443

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussion 💬The right solution needs to be found

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions