SSL Connection fails (caddy inside docker, local network)

1. The problem I’m having:

Caddy is running inside docker as a file_server. When connecting to it (e.g. using curl), I get an SSL error:

$ curl -vL https://172.30.96.152/
* Uses proxy env variable no_proxy == 'localhost,127.0.0.1,::1,.some-host.local,.some-host.de,172.16.0.0/12'
*   Trying 172.30.96.152:443...
* Connected to 172.30.96.152 (172.30.96.152) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS alert, internal error (592):
* OpenSSL/3.0.13: error:0A000438:SSL routines::tlsv1 alert internal error
* Closing connection
curl: (35) OpenSSL/3.0.13: error:0A000438:SSL routines::tlsv1 alert internal error
  • The command is executed from the docker host.
  • 172.30.96.152 is the ip of this machine in the local network (not the docker network), I’m using this ip instead of localhost, since the certificate was issued for it and I want to test that.
  • I installed the root certificate on this machine:
    sudo docker compose cp php:/data/caddy/pki/authorities/local/root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates
  • I also verified, that /etc/ssl/certs/ca-certificates.crt contains this certificat.

I guess this has something to do with caddy running inside docker, since in the logs there is something like no certificate available for '10.20.0.2' and 10.20.0.2 is the ip of the container in the docker network. However I want to have the certificate for the hosts ip, since that is of course what the clients in my local network will use. I guess my understanding of how this should work with Caddy is just not sufficient.

Note: The docker setup originally stems from https://github.com/dunglas/symfony-docker which uses frankenphp under the hood, which is a caddy configuration for php, as much as I understand it. The current configuration however just uses caddy as fileserver.

2. Error messages and/or full log output:

Waiting for database to be ready...
The database is now ready and reachable

                                                                                
 [OK] Already at the latest version ("DoctrineMigrations\Version20241115093811")
                                                                                

2025/02/06 09:46:45.708	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2025/02/06 09:46:45.710	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2025/02/06 09:46:45.710	WARN	Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies	{"adapter": "caddyfile", "file": "/etc/caddy/Caddyfile", "line": 2}
2025/02/06 09:46:45.711	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//[::1]:2019", "//127.0.0.1:2019", "//localhost:2019"]}
2025/02/06 09:46:45.712	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc0007d1380"}
2025/02/06 09:46:45.712	INFO	http.auto_https	server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS	{"server_name": "srv0", "https_port": 443}
2025/02/06 09:46:45.712	INFO	http.auto_https	enabling automatic HTTP->HTTPS redirects	{"server_name": "srv0"}
2025/02/06 09:46:45.712	DEBUG	http.auto_https	adjusted config	{"tls": {"automation":{"policies":[{"subjects":["172.30.96.152"]},{}]}}, "http": {"servers":{"remaining_auto_https_redirects":{"listen":[":80"],"routes":[{},{}]},"srv0":{"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"vars","root":"/app/frontend/WebContent"},{"encodings":{"br":{},"gzip":{},"zstd":{}},"handler":"encode","prefer":["zstd","br","gzip"]},{"handler":"file_server","hide":["/etc/caddy/Caddyfile"]}]}]}],"terminal":true}],"tls_connection_policies":[{}],"automatic_https":{}}}}}
2025/02/06 09:46:45.713	WARN	pki.ca.local	installing root certificate (you might be prompted for password)	{"path": "storage:pki/authorities/local/root.crt"}
2025/02/06 09:46:45.714	INFO	warning: "certutil" is not available, install "certutil" with "apt install libnss3-tools" or "yum install nss-tools" and try again
2025/02/06 09:46:45.714	INFO	define JAVA_HOME environment variable to use the Java trust
2025/02/06 09:46:46.343	INFO	certificate installed properly in linux trusts
2025/02/06 09:46:46.349	INFO	tls	storage cleaning happened too recently; skipping for now	{"storage": "FileStorage:/data/caddy", "instance": "3c8bc581-0b32-4eae-b0c6-2a42a9b7ab49", "try_again": "2025/02/07 09:46:46.349", "try_again_in": 86399.999999088}
2025/02/06 09:46:46.349	INFO	tls	finished cleaning storage units
2025/02/06 09:46:46.360	DEBUG	setHandler
2025/02/06 09:46:46.360	DEBUG	setHandler
2025/02/06 09:46:46.360	DEBUG	setHandler
2025/02/06 09:46:46.360	DEBUG	setHandler
2025/02/06 09:46:46.360	INFO	FrankenPHP started 🐘	{"php_version": "8.3.16", "num_threads": 4}
2025/02/06 09:46:46.360	DEBUG	http	starting server loop	{"address": "[::]:443", "tls": true, "http3": false}
2025/02/06 09:46:46.360	INFO	http	enabling HTTP/3 listener	{"addr": ":443"}
2025/02/06 09:46:46.361	INFO	failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details.
2025/02/06 09:46:46.361	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2025/02/06 09:46:46.361	DEBUG	http	starting server loop	{"address": "[::]:80", "tls": false, "http3": false}
2025/02/06 09:46:46.361	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2025/02/06 09:46:46.361	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2025/02/06 09:46:46.361	INFO	http.log	server running	{"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2025/02/06 09:46:46.361	INFO	http	enabling automatic TLS certificate management	{"domains": ["172.30.96.152"]}
2025/02/06 09:46:46.361	DEBUG	tls.cache	added certificate to cache	{"subjects": ["172.30.96.152"], "expiration": "2025/02/06 20:19:02.000", "managed": true, "issuer_key": "local", "hash": "045c85a0f6904604cf5f59c8a77f74522818f104ca5ebe78a0967f7458096742", "cache_size": 1, "cache_capacity": 10000}
2025/02/06 09:46:46.361	DEBUG	events	event	{"name": "cached_managed_cert", "id": "f0e83406-2f7b-4992-aec0-72e9caf72681", "origin": "tls", "data": {"sans":["172.30.96.152"]}}
2025/02/06 09:46:46.361	INFO	autosaved config (load with --resume flag)	{"file": "/config/caddy/autosave.json"}
2025/02/06 09:46:46.362	INFO	serving initial configuration
2025/02/06 09:46:46.362	INFO	watcher	watching config file for changes	{"config_file": "/etc/caddy/Caddyfile"}
2025/02/06 09:46:50.295	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "48950", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:47:20.345	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "43244", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:47:50.395	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "47730", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:48:20.449	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "45342", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:48:50.503	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "47420", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:49:20.552	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "56206", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:49:50.609	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "49260", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:50:20.667	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "45410", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:50:50.722	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "36446", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:51:20.774	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "33640", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:51:50.825	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "33390", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:52:20.883	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "50692", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:52:50.941	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "59746", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:53:20.998	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "58630", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:53:51.055	DEBUG	admin.api	received request	{"method": "GET", "host": "localhost:2019", "uri": "/metrics", "remote_ip": "127.0.0.1", "remote_port": "51788", "headers": {"Accept":["*/*"],"User-Agent":["curl/7.88.1"]}}
2025/02/06 09:53:56.641	DEBUG	events	event	{"name": "tls_get_certificate", "id": "822f4bf5-0e3b-487b-ad1f-2404c5144051", "origin": "tls", "data": {"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"172.30.96.152","Port":38834,"Zone":""},"LocalAddr":{"IP":"10.20.0.2","Port":443,"Zone":""}}}}
2025/02/06 09:53:56.641	DEBUG	tls.handshake	no matching certificates and no custom selection logic	{"identifier": "10.20.0.2"}
2025/02/06 09:53:56.641	DEBUG	tls.handshake	no certificate matching TLS ClientHello	{"remote_ip": "172.30.96.152", "remote_port": "38834", "server_name": "", "remote": "172.30.96.152:38834", "identifier": "10.20.0.2", "cipher_suites": [4866, 4867, 4865, 49196, 49200, 159, 52393, 52392, 52394, 49195, 49199, 158, 49188, 49192, 107, 49187, 49191, 103, 49162, 49172, 57, 49161, 49171, 51, 157, 156, 61, 60, 53, 47, 255], "cert_cache_fill": 0.0001, "load_or_obtain_if_necessary": true, "on_demand": false}
2025/02/06 09:53:56.641	DEBUG	http.stdlib	http: TLS handshake error from 172.30.96.152:38834: no certificate available for '10.20.0.2'

3. Caddy version:

I didn’t find out, where caddy was installed in the docker container. However the output of frankenphp --version is:
FrankenPHP v1.4.1 PHP 8.3.16 Caddy v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=

4. How I installed and ran Caddy:

a. System environment:

Inside docker

# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

On the host

# $ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.1 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

b. Command:

I don’t know how frankenphp works and how it starts caddy. If necessary I will dig deeper.

c. Service/unit/compose file:

services:
  php:
    container_name: $DASHBOARD_PHP_APP_CONTAINER_NAME
    image: $DOCKER_IMAGE_REPOSITORY/php:$APP_VERSION
    restart: unless-stopped
    environment:
      SERVER_NAME: ${SERVER_NAME:-localhost}, php:80
      MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      # Run "composer require symfony/orm-pack" to install and configure Doctrine ORM
      DATABASE_URL: $MARIADB_URL
      # Run "composer require symfony/mercure-bundle" to install and configure the Mercure integration
      MERCURE_URL: ${CADDY_MERCURE_URL:-http://php/.well-known/mercure}
      MERCURE_PUBLIC_URL: ${CADDY_MERCURE_PUBLIC_URL:-https://${SERVER_NAME:-localhost}/.well-known/mercure}
      MERCURE_JWT_SECRET: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!}
      # The two next lines can be removed after initial installation
      SYMFONY_VERSION: ${SYMFONY_VERSION:-}
      STABILITY: ${STABILITY:-stable}
    volumes:
      #- ./volumes/caddy/data:/data
      #- ./volumes/caddy/config:/config
      - caddy_data:/data
      - caddy_config:/config
      #- ./volumes/var:/app/var
      - app_var:/app/var
      - ./frankenphp/docker-entrypoint.sh:/usr/local/bin/docker-entrypoint
      - vendor:/app/vendor
    ports:
      # HTTP
      - target: 80
        published: ${HTTP_PORT:-80}
        protocol: tcp
      # HTTPS
      - target: 443
        published: ${HTTPS_PORT:-443}
        protocol: tcp
      # HTTP/3
      - target: 443
        published: ${HTTP3_PORT:-443}
        protocol: udp
      # Websocket
      - "3001:3001"

# Mercure is installed as a Caddy module, prevent the Flex recipe from installing another service
###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###

###> doctrine/doctrine-bundle ###
  database:
    image: mariadb:$MARIADB_IMAGE
    container_name: $DATABASE_CONTAINER_NAME
    environment:
      MARIADB_DATABASE: $MARIADB_DATABASE
      MARIADB_ROOT_PASSWORD: $MARIADB_ROOT_PASSWORD
      MARIADB_USER: $MARIADB_USER
      MARIADB_PASSWORD: $MARIADB_PASSWORD
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 5s
      retries: 10
    ports:
      - "3306:3306"
    volumes:
      - database_data:/var/lib/mysql/data:rw
      - ./migrations/initial-sql/:/docker-entrypoint-initdb.d/
###< doctrine/doctrine-bundle ###


volumes:
  caddy_data:
  caddy_config:
  database_data:
  app_log:
  app_var:
  vendor:

compose.override.yaml

services:
  php:
    build:
      context: .
      target: frankenphp_dev
    volumes:
      - ./:/app
      - ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro
      - ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro
      # If you develop on Mac or Windows you can remove the vendor/ directory
      #  from the bind-mount for better performance by enabling the next line:
      #- /app/vendor
    environment:
      MERCURE_EXTRA_DIRECTIVES: demo
      # See https://xdebug.org/docs/all_settings#mode
      XDEBUG_MODE: "${XDEBUG_MODE:-off}"
    extra_hosts:
      # Ensure that host.docker.internal is correctly defined on Linux
      - host.docker.internal:host-gateway
    tty: true

###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ###

###> doctrine/doctrine-bundle ###
  database:
    ports:
      - "5432"
###< doctrine/doctrine-bundle ###

d. My complete Caddy config:

{
    debug

	frankenphp {
		{$FRANKENPHP_CONFIG}
	}
}

{$CADDY_EXTRA_CONFIG}

(symfony) {
    root * /app/public
    encode zstd br gzip

    mercure {
        # Transport to use (default to Bolt)
        transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
        # Publisher JWT key
        publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
        # Subscriber JWT key
        subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
        # Allow anonymous subscribers (double-check that it's what you want)
        anonymous
        # Enable the subscription API (double-check that it's what you want)
        subscriptions
        # Extra directives
        {$MERCURE_EXTRA_DIRECTIVES}
    }

    vulcain

    {$CADDY_SERVER_EXTRA_DIRECTIVES}

    # Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
    header ?Permissions-Policy "browsing-topics=()"

    php_server
}

https://172.30.96.152:443 {
    tls internal

    root /app/frontend/WebContent
    encode zstd br gzip
    file_server
}

5. Links to relevant resources:

Original docker setup for symfony + frankenphp on github

I needed some help to figure it out, but…

2025/02/06 09:53:56.641	DEBUG	events	event	{"name": "tls_get_certificate", "id": "822f4bf5-0e3b-487b-ad1f-2404c5144051", "origin": "tls", "data": {"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"","SupportedCurves":[29,23,30,25,24,256,257,258,259,260],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"172.30.96.152","Port":38834,"Zone":""},"LocalAddr":{"IP":"10.20.0.2","Port":443,"Zone":""}}}}

Caddy has obtained a certificate for 172.30.96.152 (which is good), but when a connection comes in on 172.30.96.152, it tries to use the internal Docker IP (10.20.0.2) during the TLS handshake.

2025/02/06 09:53:56.641	DEBUG	tls.handshake	no matching certificates and no custom selection logic	{"identifier": "10.20.0.2"}

This causes the client (curl) to receive a certificate for the wrong IP, resulting in the SSL routines::tlsv1 alert internal error. The client expects a certificate for 172.30.96.152, but it gets one for 10.20.0.2, hence the mismatch and the error. Caddy is attempting to manage certificates based on the incoming request’s IP, which is the internal Docker IP when the request hits the container.

You need to explicitly tell Caddy to use the certificate for 172.30.96.152 when serving requests to that IP, but I’m not quite sure how to accomplish that.

You could try replacing your site block:

https://172.30.96.152:443 {
    tls internal
   ...

with the hostname associated with that IP. You’ll need to check /etc/hosts to find the hostname associated with the IP.

Using a local domain (hostname) is a good idea if You have one. I don’t know if linux hostnames are communicated in the network and also accepted by the other machines (which are partly windows machines). It would be worth a try, but I doubt it will work out of the box.

The solution I’m using now is to generate an own certificate with openssl which is valid for the docker hosts ip and for the ip of the container in the docker network. I wouldn’t have guessed that this was possible, but I had an AI guiding me to the process :wink:. This certificate must of course be copied to caddy and activated in the Caddyfile. Browsers then will of course still complain, that the certificate is untrusted, but at least one can make an exception. And to be completely secured, one just needs to copy the certificate to the respective machines.

Another possibility the ai suggested, which I didn’t try, because it was unusable for my case, would be to set the docker network to be the same as the hosts network (setting network_mode: host for the service in compose.yaml).

Unfortunatly I’m now currently working on another project, but I will update this post with details, as soon as I switch back.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.