Docker - Caddy doesn't recognize its own generated certificate

1. The problem I’m having:

I have a react spa that I serve using Caddy both the react app and caddy installation are in the same docker container, I’ve been working just fine with the Caddyfile at the end of this op until today, today I started to experience an issue, it seems that Caddy is not recognizing its own automatic generated certificate.

If I request: curl -vL https://localhost:443, I get:

* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to localhost (::1) 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: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Which is rare because I didn’t change anything in my Dockerfile:

FROM node:alpine3.18

WORKDIR /app

RUN apk add --no-cache caddy

COPY . /app

RUN npm install --include=dev
RUN npm run build

CMD [ "npm", "run", "start" ]

EXPOSE 443

2. Error messages and/or full log output:

When I request curl -vL https://localhost:443 I get these 5 log blocks

{
   "level":"debug",
   "ts":1718745835.9707325,
   "logger":"events",
   "msg":"event",
   "name":"tls_get_certificate",
   "id":"719222c5-ecb1-4018-b0b9-e28909b5ef37",
   "origin":"tls",
   "data":{
      "client_hello":{
         "CipherSuites":[4865,4867,4866,49195,49199,52393,52392,49196,49200,49162,49161,49171,49172,156,157,47,53],
         "ServerName":"localhost",
         "SupportedCurves":[29,23,24,25,256,257],
         "SupportedPoints":"AA==",
         "SignatureSchemes":[1027,1283,1539,2052,2053,2054,1025,1281,1537,515,513],
         "SupportedProtos":["h2","http/1.1"],
         "SupportedVersions":[772,771],
         "Conn":{
            
         }
      }
   }
}

{
   "level":"debug",
   "ts":1718745835.9708292,
   "logger":"tls.handshake",
   "msg":"choosing certificate",
   "identifier":"localhost",
   "num_choices":1
}

{
   "level":"debug",
   "ts":1718745835.9708395,
   "logger":"tls.handshake",
   "msg":"default certificate selection results",
   "identifier":"localhost",
   "subjects":[
      "localhost"
   ],
   "managed":true,
   "issuer_key":"local",
   "hash":"35948d05e73f33cffc6e5fd336eb7c24bf6e86d4c6b83c647a89ee43edc6ba5a"
}

{
   "level":"debug",
   "ts":1718745835.9708445,
   "logger":"tls.handshake",
   "msg":"matched certificate in cache",
   "remote_ip":"172.18.0.1",
   "remote_port":"44692",
   "subjects":[
      "localhost"
   ],
   "managed":true,
   "expiration":1718789021,
   "hash":"35948d05e73f33cffc6e5fd336eb7c24bf6e86d4c6b83c647a89ee43edc6ba5a"
}

{
   "level":"debug",
   "ts":1718745835.997725,
   "logger":"http.stdlib",
   "msg":"http: TLS handshake error from 172.18.0.1:44692: remote error: tls: bad certificate"
}

3. Caddy version:

2.6.4-r7

The base image for my docker contianer is linux 3.18 and the caddy package for that version of alpine is 2.6.4-r7

4. How I installed and ran Caddy:

RUN apk add --no-cache caddy inside my Dockerfile

a. System environment:

Docker custom image based on alpine linux 3.18, host OS is Arch linux

b. Command:

caddy run

c. Service/unit/compose file:

services:
  client:
    image: my_client_image
    ports:
      - 443:443
    env_file: .env
    networks: 
      - my_network

d. My complete Caddy config:

{
	admin off
	persist_config off	
	log {
		format json  
        output file /var/log/caddy/global-log.json
	}	
	servers {
		trusted_proxies static private_ranges
	}
    debug
}

localhost {	
	log {
		format json 
        output file /var/log/caddy/localhost-log.json
	}
	respond /health 200
	root * build
	encode gzip
	file_server
	try_files {path} /index.html
}

Please use the latest version of Caddy, v2.8.4.

Don’t do that, it gives you an old version of Caddy from the Alpine package manager. Instead, use the Caddy docker image https://hub.docker.com/_/caddy

You can use NPM to build your frontend bundle, and then copy it into your Caddy container (multi-stage build), then run the Caddy container to serve the files.

That just looks like your system not trusting Caddy’s root CA cert.

Since you’re not using the official Caddy Docker image, you’re probably not persisting Caddy’s certs (which is normally done by setting up a volume for /data), so if you did something to recreate your container, the root CA cert would have been regenerated and whatever was installed on your system is now no longer valid.

1 Like

Thank you for putting me on the right path about running caddy on docker. Now I’m using the official caddy image as the second stage of my build, also I set a named volume caddy_data for the /data directory, however the issue persists.

This is my Dockerfile:

FROM node:alpine3.18 as base

WORKDIR /app

COPY . .

RUN npm install --include=dev
RUN npm run build

FROM caddy

COPY --from=base /app/build/* /usr/share/caddy/
COPY --from=base /app/Caddyfile /etc/caddy

EXPOSE 443

and my docker-compose.yarml

services:
  client:
    image: my_image
    ports:
      - 443:443
    volumes:
      - caddy_data:/data
    env_file: ./client/.env
    networks: 
      - my_network

volumes:
  caddy_data:
    external: true

networks:
  my_network:
    name: my_network

Please show evidence of the “issue”. How did you grab Caddy’s root CA cert? Show an example request with curl -v, show your current Caddyfile, show your logs.

Sorry for the late response.

This is my Caddyfile:

{
	admin off
	persist_config off	
	log {
		format json  
        output file /var/log/caddy/global-log.json
	}	
	servers {
		trusted_proxies static private_ranges
	}
    debug
}

localhost {	
	log {
		format json 
        output file /var/log/caddy/localhost-log.json
	}
	respond /health 200
	root * build
	encode gzip
    file_server
	try_files {path} /index.html
}

And this is the curl request:

curl -v https://localhost:443
* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to localhost (::1) 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: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

and the logs from Caddy when I make such request:

{
  "level": "debug",
  "ts": 1719242142.9768884,
  "logger": "events",
  "msg": "event",
  "name": "tls_get_certificate",
  "id": "d25df6ab-8f88-4cea-a73e-8ac88d5136a1",
  "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": "localhost",
      "SupportedCurves": [
        29,
        23,
        30,
        25,
        24,
        256,
        257,
        258,
        259,
        260
      ],
      "SupportedPoints": "AAEC",
      "SignatureSchemes": [
        1027,
        1283,
        1539,
        2055,
        2056,
        2074,
        2075,
        2076,
        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.18.0.1",
        "Port": 55364,
        "Zone": ""
      },
      "LocalAddr": {
        "IP": "172.18.0.3",
        "Port": 443,
        "Zone": ""
      }
    }
  }
}
{
  "level": "debug",
  "ts": 1719242142.9769263,
  "logger": "tls.handshake",
  "msg": "choosing certificate",
  "identifier": "localhost",
  "num_choices": 1
}
{
  "level": "debug",
  "ts": 1719242142.9769363,
  "logger": "tls.handshake",
  "msg": "default certificate selection results",
  "identifier": "localhost",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "issuer_key": "local",
  "hash": "62fd4c62c65e0b9f58a33abd573ea292a0341166a89a80e75a240f869211d6f2"
}
{
  "level": "debug",
  "ts": 1719242142.9769413,
  "logger": "tls.handshake",
  "msg": "matched certificate in cache",
  "remote_ip": "172.18.0.1",
  "remote_port": "55364",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "expiration": 1719282278,
  "hash": "62fd4c62c65e0b9f58a33abd573ea292a0341166a89a80e75a240f869211d6f2"
}
{
  "level": "debug",
  "ts": 1719242142.9839675,
  "logger": "http.stdlib",
  "msg": "http: TLS handshake error from 172.18.0.1:55364: local error: tls: bad record MAC"
}

Also these are the logs when I just start the server:

{
  "level": "warn",
  "ts": 1719241923.6342556,
  "logger": "admin",
  "msg": "admin endpoint disabled"
}
{
  "level": "info",
  "ts": 1719241923.6345546,
  "logger": "http.auto_https",
  "msg": "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
}
{
  "level": "info",
  "ts": 1719241923.6345658,
  "logger": "http.auto_https",
  "msg": "enabling automatic HTTP->HTTPS redirects",
  "server_name": "srv0"
}
{
  "level": "info",
  "ts": 1719241923.6346147,
  "logger": "tls.cache.maintenance",
  "msg": "started background certificate maintenance",
  "cache": "0xc000522a00"
}
{
  "level": "debug",
  "ts": 1719241923.6348002,
  "logger": "http.auto_https",
  "msg": "adjusted config",
  "tls": {
    "automation": {
      "policies": [
        {
          "subjects": [
            "localhost"
          ]
        },
        {}
      ]
    }
  },
  "http": {
    "servers": {
      "remaining_auto_https_redirects": {
        "listen": [
          ":80"
        ],
        "routes": [
          {},
          {}
        ],
        "logs": {
          "logger_names": {
            "localhost": [
              "log0"
            ]
          }
        }
      },
      "srv0": {
        "listen": [
          ":443"
        ],
        "routes": [
          {
            "handle": [
              {
                "handler": "subroute",
                "routes": [
                  {
                    "handle": [
                      {
                        "handler": "vars",
                        "root": "build"
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "rewrite",
                        "uri": "{http.matchers.file.relative}"
                      }
                    ],
                    "match": [
                      {
                        "file": {
                          "try_files": [
                            "{http.request.uri.path}",
                            "/index.html"
                          ]
                        }
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "encodings": {
                          "gzip": {}
                        },
                        "handler": "encode",
                        "prefer": [
                          "gzip"
                        ]
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "static_response",
                        "status_code": 200
                      }
                    ],
                    "match": [
                      {
                        "path": [
                          "/health"
                        ]
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "file_server",
                        "hide": [
                          "/etc/caddy/Caddyfile"
                        ]
                      }
                    ]
                  }
                ]
              }
            ],
            "terminal": true
          }
        ],
        "tls_connection_policies": [
          {}
        ],
        "automatic_https": {},
        "trusted_proxies": {
          "ranges": [
            "192.168.0.0/16",
            "172.16.0.0/12",
            "10.0.0.0/8",
            "127.0.0.1/8",
            "fd00::/8",
            "::1"
          ],
          "source": "static"
        },
        "logs": {
          "logger_names": {
            "localhost": [
              "log0"
            ]
          }
        }
      }
    }
  }
}
{
  "level": "warn",
  "ts": 1719241923.6351461,
  "logger": "pki.ca.local",
  "msg": "installing root certificate (you might be prompted for password)",
  "path": "storage:pki/authorities/local/root.crt"
}
{
  "level": "info",
  "ts": 1719241923.6710618,
  "logger": "http",
  "msg": "enabling HTTP/3 listener",
  "addr": ":443"
}
{
  "level": "debug",
  "ts": 1719241923.6712677,
  "logger": "http",
  "msg": "starting server loop",
  "address": "[::]:443",
  "tls": true,
  "http3": true
}
{
  "level": "info",
  "ts": 1719241923.6712794,
  "logger": "http.log",
  "msg": "server running",
  "name": "srv0",
  "protocols": [
    "h1",
    "h2",
    "h3"
  ]
}
{
  "level": "debug",
  "ts": 1719241923.6713119,
  "logger": "http",
  "msg": "starting server loop",
  "address": "[::]:80",
  "tls": false,
  "http3": false
}
{
  "level": "info",
  "ts": 1719241923.6713185,
  "logger": "http.log",
  "msg": "server running",
  "name": "remaining_auto_https_redirects",
  "protocols": [
    "h1",
    "h2",
    "h3"
  ]
}
{
  "level": "info",
  "ts": 1719241923.6713228,
  "logger": "http",
  "msg": "enabling automatic TLS certificate management",
  "domains": [
    "localhost"
  ]
}
{
  "level": "warn",
  "ts": 1719241923.6715567,
  "logger": "tls",
  "msg": "stapling OCSP",
  "error": "no OCSP stapling for [localhost]: no OCSP server specified in certificate",
  "identifiers": [
    "localhost"
  ]
}
{
  "level": "debug",
  "ts": 1719241923.6715689,
  "logger": "tls.cache",
  "msg": "added certificate to cache",
  "subjects": [
    "localhost"
  ],
  "expiration": 1719282278,
  "managed": true,
  "issuer_key": "local",
  "hash": "62fd4c62c65e0b9f58a33abd573ea292a0341166a89a80e75a240f869211d6f2",
  "cache_size": 1,
  "cache_capacity": 10000
}
{
  "level": "debug",
  "ts": 1719241923.6715894,
  "logger": "events",
  "msg": "event",
  "name": "cached_managed_cert",
  "id": "23a526f9-ba4b-4524-9091-183c2325066b",
  "origin": "tls",
  "data": {
    "sans": [
      "localhost"
    ]
  }
}
{
  "level": "info",
  "ts": 1719241923.671606,
  "msg": "serving initial configuration"
}
{
  "level": "info",
  "ts": 1719241923.6883938,
  "logger": "tls",
  "msg": "storage cleaning happened too recently; skipping for now",
  "storage": "FileStorage:/data/caddy",
  "instance": "5d261392-cee3-4b51-bbd5-ebabdceba0df",
  "try_again": 1719328323.688392,
  "try_again_in": 86399.999999631
}
{
  "level": "info",
  "ts": 1719241923.7042854,
  "logger": "tls",
  "msg": "finished cleaning storage units"
}

Sorry for the delay, had a busy week.

Are you sure you installed Caddy’s root CA cert in your system’s trust store, so that curl would pick it up? What steps did you take to install it?

1 Like

It’s ok, sorry for the late response too :sweat_smile:

To be honest I didn’t think about that, I though that because I was running caddy on docker I didn’t have to install certificates on my system, but after reading a bit more on the subject it made sense to me because I’m using curl from my system and not from the docker container.

So I had to install manually the caddy certificates in my system, to do so I copied the root and intermediate certificates (the .crt files) from inside the container in /data/caddy/pki/authorities/local to this directory on my system /etc/ca-certificates/trust-source/anchors

Then I tried the curl request again and I got this response:

curl -v https://localhost:443

* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to localhost (::1) 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: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jul  5 16:07:43 2024 GMT
*  expire date: Jul  6 04:07:43 2024 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://localhost:443/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: localhost]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.8.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: localhost
> User-Agent: curl/8.8.0
> Accept: */*
>
* Request completely sent off
< HTTP/2 404
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Fri, 05 Jul 2024 16:38:58 GMT
<
* Connection #0 to host localhost left intact

These are the server logs just for the startup:

{
  "level": "warn",
  "ts": 1720197908.2813835,
  "logger": "tls",
  "msg": "stapling OCSP",
  "error": "no OCSP stapling for [localhost]: no OCSP server specified in certificate",
  "identifiers": [
    "localhost"
  ]
}
{
  "level": "debug",
  "ts": 1720197908.281402,
  "logger": "tls.cache",
  "msg": "added certificate to cache",
  "subjects": [
    "localhost"
  ],
  "expiration": 1720238864,
  "managed": true,
  "issuer_key": "local",
  "hash": "43e816e378b67ba5b965ace5b6d8b527d44a447dfd5bcad2f32265cec9071eda",
  "cache_size": 1,
  "cache_capacity": 10000
}
{
  "level": "debug",
  "ts": 1720197908.2814224,
  "logger": "events",
  "msg": "event",
  "name": "cached_managed_cert",
  "id": "f658b288-3938-4367-a79f-9eb598943f78",
  "origin": "tls",
  "data": {
    "sans": [
      "localhost"
    ]
  }
}
{
  "level": "info",
  "ts": 1720197908.2814384,
  "msg": "serving initial configuration"
}

And these are the logs for when I make the curl request:

{
  "level": "debug",
  "ts": 1720198012.9832602,
  "logger": "events",
  "msg": "event",
  "name": "tls_get_certificate",
  "id": "db29ebb1-a87d-46f6-a5fc-235fde30c812",
  "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": "localhost",
      "SupportedCurves": [
        29,
        23,
        30,
        25,
        24,
        256,
        257,
        258,
        259,
        260
      ],
      "SupportedPoints": "AAEC",
      "SignatureSchemes": [
        1027,
        1283,
        1539,
        2055,
        2056,
        2074,
        2075,
        2076,
        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.18.0.1",
        "Port": 44730,
        "Zone": ""
      },
      "LocalAddr": {
        "IP": "172.18.0.3",
        "Port": 443,
        "Zone": ""
      }
    }
  }
}
{
  "level": "debug",
  "ts": 1720198012.9833813,
  "logger": "tls.handshake",
  "msg": "choosing certificate",
  "identifier": "localhost",
  "num_choices": 1
}
{
  "level": "debug",
  "ts": 1720198012.9834154,
  "logger": "tls.handshake",
  "msg": "default certificate selection results",
  "identifier": "localhost",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "issuer_key": "local",
  "hash": "43e816e378b67ba5b965ace5b6d8b527d44a447dfd5bcad2f32265cec9071eda"
}
{
  "level": "debug",
  "ts": 1720198012.9834342,
  "logger": "tls.handshake",
  "msg": "matched certificate in cache",
  "remote_ip": "172.18.0.1",
  "remote_port": "44730",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "expiration": 1720238864,
  "hash": "43e816e378b67ba5b965ace5b6d8b527d44a447dfd5bcad2f32265cec9071eda"
}
{
  "level": "debug",
  "ts": 1720198013.0051136,
  "logger": "http.handlers.file_server",
  "msg": "sanitized path join",
  "site_root": "build",
  "fs": "",
  "request_path": "/",
  "result": "build"
}
{
  "level": "debug",
  "ts": 1720198013.0051863,
  "logger": "http.log.error.log0",
  "msg": "{id=vm8nw9am1} fileserver.(*FileServer).notFound (staticfiles.go:651): HTTP 404",
  "request": {
    "remote_ip": "172.18.0.1",
    "remote_port": "44730",
    "client_ip": "172.18.0.1",
    "proto": "HTTP/2.0",
    "method": "GET",
    "host": "localhost",
    "uri": "/",
    "headers": {
      "User-Agent": [
        "curl/8.8.0"
      ],
      "Accept": [
        "*/*"
      ]
    },
    "tls": {
      "resumed": false,
      "version": 772,
      "cipher_suite": 4865,
      "proto": "h2",
      "server_name": "localhost"
    }
  },
  "duration": 0.000171828,
  "status": 404,
  "err_id": "vm8nw9am1",
  "err_trace": "fileserver.(*FileServer).notFound (staticfiles.go:651)"
}

Okay, TLS works fine now, that’s great.

Looks like you’re just getting 404s from file_server now. Are you sure the files exist? Is root correct? Do you actually have a folder build relative to where Caddy is running? What does your file structure look like?

2 Likes

Right, I forgot to update the root path build, since I started using the official caddy image I should have updated the root to /srv since there is where I’m copying my build files to at the end of my Dockerfile.

My file structure is

srv
├── index.html
├── asset-manifest.json
├── favicon.ico
├── manifest.json
├── robots.txt
├── service-worker.js
├── service-worker.js.LICENSE.txt
└── static
    ├── css
    │   └── bunch of css files
    └── js
        └── bunch of js files

This is my updated Dockerfile:

FROM node:alpine3.18 as base

WORKDIR /app

COPY . .

RUN npm install --include=dev
RUN npm run build

FROM caddy

COPY --from=base /app/build/ /srv
COPY --from=base /app/Caddyfile /etc/caddy/Caddyfile

EXPOSE 443

This is my updated Caddyfile:

{
	admin off
	persist_config off
	log {
		format json 
        output file /var/log/caddy/global-log.json
	}	
    debug
}

localhost {
	log {
		format json
        output file /var/log/caddy/localhost-log.json
	}

	respond /health 200
	root * /srv
	encode gzip
	file_server
	try_files {path} index.html
}

So with these changes I would think that it will work and it does but in a weird way, this is the curl response:

curl -v https://localhost:443

* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* Connected to localhost (::1) 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: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: [NONE]
*  start date: Jul  6 13:35:12 2024 GMT
*  expire date: Jul  7 01:35:12 2024 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: CN=Caddy Local Authority - ECC Intermediate
*  SSL certificate verify ok.
*   Certificate level 0: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
*   Certificate level 1: Public key type EC/prime256v1 (256/128 Bits/secBits), signed using ecdsa-with-SHA256
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://localhost:443/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: localhost]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.8.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: localhost
> User-Agent: curl/8.8.0
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< accept-ranges: bytes
< alt-svc: h3=":443"; ma=2592000
< content-type: text/html; charset=utf-8
< etag: "d2ihuo5t5zk5r9"
< last-modified: Sat, 06 Jul 2024 13:35:08 GMT
< server: Caddy
< vary: Accept-Encoding
< content-length: 981
< date: Sat, 06 Jul 2024 14:18:42 GMT
<
* Connection #0 to host localhost left intact
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="my-app"/><base href="/"><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>My App</title><script defer="defer" src="./static/js/main.376a5fe7.js"></script><link href="./static/css/main.0a85a964.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><script>"serviceWorker"in navigator&&navigator.serviceWorker.register("service-worker.js").catch((function(e){console.warn(e),console.warn("(This warning can be safely ignored outside of the production build.)")}))</script><div id="root"></div><div id="someId"></div><div id="someId"></div><div id="someId"></div></body></html>

From that it may seem, at least to me, that the issue is solved because I’m getting an actual html response with code 200, however in firefox that url doesn’t load the page, instead I get Error code: SEC_ERROR_BAD_SIGNATURE, in chrome the page does load but in the chrome console I get this error An SSL certificate error occurred when fetching the script. and I cannot utilize any function in my app that needs to connect to my api server which is running in a separate container because of CORS errors.

This is weird because in these days I’ve been working with a very basic express server (in the mean time since I’m having issues with caddy) to serve my app’s static files and it worked just fine, without cors errors or so. This leads me to believe that I’m doing something wrong in my caddy configuration.

These are the logs for when the caddy server just starts:

{
  "level": "warn",
  "ts": 1720280535.1809287,
  "logger": "admin",
  "msg": "admin endpoint disabled"
}
{
  "level": "info",
  "ts": 1720280535.1812863,
  "logger": "http.auto_https",
  "msg": "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
}
{
  "level": "info",
  "ts": 1720280535.1812987,
  "logger": "http.auto_https",
  "msg": "enabling automatic HTTP->HTTPS redirects",
  "server_name": "srv0"
}
{
  "level": "info",
  "ts": 1720280535.181343,
  "logger": "tls.cache.maintenance",
  "msg": "started background certificate maintenance",
  "cache": "0xc00064c300"
}
{
  "level": "debug",
  "ts": 1720280535.1816103,
  "logger": "http.auto_https",
  "msg": "adjusted config",
  "tls": {
    "automation": {
      "policies": [
        {
          "subjects": [
            "localhost"
          ]
        },
        {}
      ]
    }
  },
  "http": {
    "servers": {
      "remaining_auto_https_redirects": {
        "listen": [
          ":80"
        ],
        "routes": [
          {},
          {}
        ],
        "logs": {
          "logger_names": {
            "localhost": [
              "log0"
            ]
          }
        }
      },
      "srv0": {
        "listen": [
          ":443"
        ],
        "routes": [
          {
            "handle": [
              {
                "handler": "subroute",
                "routes": [
                  {
                    "handle": [
                      {
                        "handler": "vars",
                        "root": "/srv"
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "rewrite",
                        "uri": "{http.matchers.file.relative}"
                      }
                    ],
                    "match": [
                      {
                        "file": {
                          "try_files": [
                            "{http.request.uri.path}",
                            "index.html"
                          ]
                        }
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "encodings": {
                          "gzip": {}
                        },
                        "handler": "encode",
                        "prefer": [
                          "gzip"
                        ]
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "static_response",
                        "status_code": 200
                      }
                    ],
                    "match": [
                      {
                        "path": [
                          "/health"
                        ]
                      }
                    ]
                  },
                  {
                    "handle": [
                      {
                        "handler": "file_server",
                        "hide": [
                          "/etc/caddy/Caddyfile"
                        ]
                      }
                    ]
                  }
                ]
              }
            ],
            "terminal": true
          }
        ],
        "tls_connection_policies": [
          {}
        ],
        "automatic_https": {},
        "trusted_proxies": {
          "ranges": [
            "192.168.0.0/16",
            "172.16.0.0/12",
            "10.0.0.0/8",
            "127.0.0.1/8",
            "fd00::/8",
            "::1"
          ],
          "source": "static"
        },
        "logs": {
          "logger_names": {
            "localhost": [
              "log0"
            ]
          }
        }
      }
    }
  }
}
{
  "level": "warn",
  "ts": 1720280535.1820352,
  "logger": "pki.ca.local",
  "msg": "installing root certificate (you might be prompted for password)",
  "path": "storage:pki/authorities/local/root.crt"
}
{
  "level": "info",
  "ts": 1720280535.1842167,
  "logger": "tls",
  "msg": "storage cleaning happened too recently; skipping for now",
  "storage": "FileStorage:/data/caddy",
  "instance": "5d261392-cee3-4b51-bbd5-ebabdceba0df",
  "try_again": 1720366935.1842139,
  "try_again_in": 86399.999998899
}
{
  "level": "info",
  "ts": 1720280535.1842873,
  "logger": "tls",
  "msg": "finished cleaning storage units"
}
{
  "level": "info",
  "ts": 1720280535.2093556,
  "logger": "http",
  "msg": "enabling HTTP/3 listener",
  "addr": ":443"
}
{
  "level": "debug",
  "ts": 1720280535.2094836,
  "logger": "http",
  "msg": "starting server loop",
  "address": "[::]:443",
  "tls": true,
  "http3": true
}
{
  "level": "info",
  "ts": 1720280535.209493,
  "logger": "http.log",
  "msg": "server running",
  "name": "srv0",
  "protocols": [
    "h1",
    "h2",
    "h3"
  ]
}
{
  "level": "debug",
  "ts": 1720280535.2095242,
  "logger": "http",
  "msg": "starting server loop",
  "address": "[::]:80",
  "tls": false,
  "http3": false
}
{
  "level": "info",
  "ts": 1720280535.2095325,
  "logger": "http.log",
  "msg": "server running",
  "name": "remaining_auto_https_redirects",
  "protocols": [
    "h1",
    "h2",
    "h3"
  ]
}
{
  "level": "info",
  "ts": 1720280535.2095368,
  "logger": "http",
  "msg": "enabling automatic TLS certificate management",
  "domains": [
    "localhost"
  ]
}
{
  "level": "warn",
  "ts": 1720280535.2097847,
  "logger": "tls",
  "msg": "stapling OCSP",
  "error": "no OCSP stapling for [localhost]: no OCSP server specified in certificate",
  "identifiers": [
    "localhost"
  ]
}
{
  "level": "debug",
  "ts": 1720280535.2097962,
  "logger": "tls.cache",
  "msg": "added certificate to cache",
  "subjects": [
    "localhost"
  ],
  "expiration": 1720316113,
  "managed": true,
  "issuer_key": "local",
  "hash": "a20f02ffe53a4f6d7a993d719b8ab9969b6f81108b752fe2765f28150f8a42e0",
  "cache_size": 1,
  "cache_capacity": 10000
}
{
  "level": "debug",
  "ts": 1720280535.209817,
  "logger": "events",
  "msg": "event",
  "name": "cached_managed_cert",
  "id": "e41c46f1-1dc1-404d-a404-8cc447e6e19e",
  "origin": "tls",
  "data": {
    "sans": [
      "localhost"
    ]
  }
}
{
  "level": "info",
  "ts": 1720280535.2098567,
  "msg": "serving initial configuration"
}

And these are the logs for when I make the curl request https://localhost:443:

{
  "level": "debug",
  "ts": 1720280629.5782323,
  "logger": "events",
  "msg": "event",
  "name": "tls_get_certificate",
  "id": "746a176c-5082-4345-bab2-917dc960c6c8",
  "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": "localhost",
      "SupportedCurves": [
        29,
        23,
        30,
        25,
        24,
        256,
        257,
        258,
        259,
        260
      ],
      "SupportedPoints": "AAEC",
      "SignatureSchemes": [
        1027,
        1283,
        1539,
        2055,
        2056,
        2074,
        2075,
        2076,
        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.18.0.1",
        "Port": 47596,
        "Zone": ""
      },
      "LocalAddr": {
        "IP": "172.18.0.3",
        "Port": 443,
        "Zone": ""
      }
    }
  }
}
{
  "level": "debug",
  "ts": 1720280629.578272,
  "logger": "tls.handshake",
  "msg": "choosing certificate",
  "identifier": "localhost",
  "num_choices": 1
}
{
  "level": "debug",
  "ts": 1720280629.5782814,
  "logger": "tls.handshake",
  "msg": "default certificate selection results",
  "identifier": "localhost",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "issuer_key": "local",
  "hash": "a20f02ffe53a4f6d7a993d719b8ab9969b6f81108b752fe2765f28150f8a42e0"
}
{
  "level": "debug",
  "ts": 1720280629.578286,
  "logger": "tls.handshake",
  "msg": "matched certificate in cache",
  "remote_ip": "172.18.0.1",
  "remote_port": "47596",
  "subjects": [
    "localhost"
  ],
  "managed": true,
  "expiration": 1720316113,
  "hash": "a20f02ffe53a4f6d7a993d719b8ab9969b6f81108b752fe2765f28150f8a42e0"
}
{
  "level": "debug",
  "ts": 1720280629.5884564,
  "logger": "http.handlers.rewrite",
  "msg": "rewrote request",
  "request": {
    "remote_ip": "172.18.0.1",
    "remote_port": "47596",
    "client_ip": "172.18.0.1",
    "proto": "HTTP/2.0",
    "method": "GET",
    "host": "localhost",
    "uri": "/",
    "headers": {
      "User-Agent": [
        "curl/8.8.0"
      ],
      "Accept": [
        "*/*"
      ]
    },
    "tls": {
      "resumed": false,
      "version": 772,
      "cipher_suite": 4865,
      "proto": "h2",
      "server_name": "localhost"
    }
  },
  "method": "GET",
  "uri": "/index.html"
}
{
  "level": "debug",
  "ts": 1720280629.5885112,
  "logger": "http.handlers.file_server",
  "msg": "sanitized path join",
  "site_root": "/srv",
  "fs": "",
  "request_path": "/index.html",
  "result": "/srv/index.html"
}
{
  "level": "debug",
  "ts": 1720280629.5885305,
  "logger": "http.handlers.file_server",
  "msg": "opening file",
  "filename": "/srv/index.html"
}

You need to also install Caddy’s root CA cert on Firefox and Chrome. Modern browsers have their own trust stores now, they don’t use the system trust store, so that they can more rapidly update in case of some problem (e.g. a CA going rogue and needing to be banned) since browser updates are better automated than system updates.

That’s not a Caddy problem. CORS is an application-layer problem. You can set up Caddy to add CORS headers with the header directive, but that’s on you to figure out what you need for your specific usecase.

1 Like

That was it, thanks!

I’m aware that cors is application specific, however I figured out that I’m getting these errors because requests from my client (served with caddy) to my api (an express server) are not including the port number in the origin therefore not passing the cors validation.

I think I’m missing something in the Caddy configuration because if I go back to serve my client with my basic express server and make the same http requests, then the origin does include the port number thus passing the cors validation at my api endpoints.

I’m aware that this is out of the scope of my initial question, but I just wanted to be sure if this is something related to caddy (maybe my configuration is lacking something) or not so I can open a new post.

Anyways thank you very much @francislavoie for taking your time to guide me through all these steps!