How setup "Basic authentication" (for preprod only) but keep "/healthcheck" accessible?

Hi,

Everything is in the title, how setup “Basic authentication” (for preprod only, so keep prod intact and enable it if there is some specific env variable) but keep “/healthcheck” accessible?

I’m using FrankenPHP but I guess there is nothing specific there.
The Caddyfile:

{
	skip_install_trust

	{$CADDY_GLOBAL_OPTIONS}

	frankenphp {
		{$FRANKENPHP_CONFIG}

		worker {
			file ./public/index.php
			{$FRANKENPHP_WORKER_CONFIG}
		}
	}
}

{$CADDY_EXTRA_CONFIG}

{$SERVER_NAME:localhost} {
	log {
		{$CADDY_SERVER_LOG_OPTIONS}
		# Redact the authorization query parameter that can be set by Mercure
		format filter {
			request>uri query {
				replace authorization REDACTED
			}
		}
	}

	root /app/public
	encode zstd br gzip

	{$CADDY_SERVER_EXTRA_DIRECTIVES}

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

	@phpRoute {
		not path /.well-known/mercure*
		not file {path}
	}
	rewrite @phpRoute index.php

	@frontController path index.php
	php @frontController

	file_server {
		hide *.php
	}
}

(src)

Can you help me? I tried many thing, even let claude try for many minutes/hours… :sweat_smile:
Thanks

You can probably configure your CADDY_SERVER_EXTRA_DIRECTIVES variable for your PRE-PROD like this:

	@preprod_auth {
		not path /healthcheck
	}

	handle @preprod_auth {
		basic_auth {
			Bob JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
		} 
	}

For PROD, just leave it empty.

Here’s a quick demo:

:80 {

	{$CADDY_SERVER_EXTRA_DIRECTIVES}

	respond "My PHP code"
}

PRE-PROD:

$ export CADDY_SERVER_EXTRA_DIRECTIVES="$(cat <<'EOF'
	@preprod_auth {
		not path /healthcheck
	}

	handle @preprod_auth {
		basic_auth {
			Bob JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
		} 
	}
	EOF
)"

$ caddy run
$ curl http://localhost/healthcheck
My PHP code

$ curl http://localhost/ -I
HTTP/1.1 401 Unauthorized
Server: Caddy
Www-Authenticate: Basic realm="restricted"
Date: Thu, 18 Dec 2025 00:05:04 GMT

$ curl http://localhost/ -u Bob:hiccup
My PHP code

PROD:

$ export CADDY_SERVER_EXTRA_DIRECTIVES=""
$ caddy run
$ curl http://localhost/healthcheck
My PHP code

$ curl http://localhost/
My PHP code

This might help for setting the env variable value in a container deployment:

1 Like

Hi @timelordx, thanks for your help!

I tried, but this don’t allow /heathcheck, I still have an http basic auth :melting_face:

$ curl -v -k https://localhost/healthcheck
* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL Trust: peer verification disabled
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* 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 / X25519MLKEM768 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*   subject: 
*   start date: Dec 18 10:03:10 2025 GMT
*   expire date: Dec 18 22:03:10 2025 GMT
*   issuer: CN=Caddy Local Authority - ECC Intermediate
*   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
* SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
*  SSL certificate verification failed, continuing anyway!
* Established connection to localhost (::1 port 443) from ::1 port 39568 
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://localhost/healthcheck
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: localhost]
* [HTTP/2] [1] [:path: /healthcheck]
* [HTTP/2] [1] [user-agent: curl/8.17.0]
* [HTTP/2] [1] [accept: */*]
> GET /healthcheck HTTP/2
> Host: localhost
> User-Agent: curl/8.17.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Request completely sent off
< HTTP/2 401 
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< www-authenticate: Basic realm="restricted"
< content-length: 0
< date: Thu, 18 Dec 2025 10:44:25 GMT
< 
* Connection #0 to host localhost:443 left intact

Here is the result of curl “http://localhost:2019/config/” :

{
  "apps": {
    "frankenphp": {
      "workers": [
        {
          "env": {
            "APP_RUNTIME": "Runtime\\FrankenPhpSymfony\\Runtime"
          },
          "file_name": "./public/index.php",
          "watch": [
            "./**/*.{php,yaml,yml,twig,env}"
          ]
        }
      ]
    },
    "http": {
      "metrics": {},
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "logs": {
            "logger_names": {
              "localhost": [
                "log0"
              ]
            }
          },
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "vars",
                          "root": "/app/public"
                        },
                        {
                          "handler": "headers",
                          "response": {
                            "require": {
                              "headers": {
                                "Permissions-Policy": null
                              }
                            },
                            "set": {
                              "Permissions-Policy": [
                                "browsing-topics=()"
                              ]
                            }
                          }
                        }
                      ]
                    },
                    {
                      "group": "group1",
                      "handle": [
                        {
                          "handler": "rewrite",
                          "uri": "index.php"
                        }
                      ],
                      "match": [
                        {
                          "not": [
                            {
                              "path": [
                                "/metrics"
                              ]
                            },
                            {
                              "file": {
                                "try_files": [
                                  "{http.request.uri.path}"
                                ]
                              }
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "encodings": {
                            "br": {},
                            "gzip": {},
                            "zstd": {}
                          },
                          "handler": "encode",
                          "prefer": [
                            "zstd",
                            "br",
                            "gzip"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "authentication",
                                  "providers": {
                                    "http_basic": {
                                      "accounts": [
                                        {
                                          "password": "$2a$14$a3L/wRWU9mvnYoO2AOuS4u8GztSKf82tFLqi36VSbBsD3hvsDbJ.G",
                                          "username": "foo"
                                        }
                                      ],
                                      "hash": {
                                        "algorithm": "bcrypt"
                                      },
                                      "hash_cache": {}
                                    }
                                  }
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "not": [
                            {
                              "path": [
                                "/healthcheck"
                              ]
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "metrics"
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/metrics"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "php"
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "index.php"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "file_server",
                          "hide": [
                            "*.php",
                            "/etc/frankenphp/Caddyfile"
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "localhost"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        },
        "srv1": {
          "listen": [
            ":80"
          ],
          "logs": {
            "logger_names": {
              "php": [
                "log0"
              ]
            }
          },
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "vars",
                          "root": "/app/public"
                        },
                        {
                          "handler": "headers",
                          "response": {
                            "require": {
                              "headers": {
                                "Permissions-Policy": null
                              }
                            },
                            "set": {
                              "Permissions-Policy": [
                                "browsing-topics=()"
                              ]
                            }
                          }
                        }
                      ]
                    },
                    {
                      "group": "group2",
                      "handle": [
                        {
                          "handler": "rewrite",
                          "uri": "index.php"
                        }
                      ],
                      "match": [
                        {
                          "not": [
                            {
                              "path": [
                                "/metrics"
                              ]
                            },
                            {
                              "file": {
                                "try_files": [
                                  "{http.request.uri.path}"
                                ]
                              }
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "encodings": {
                            "br": {},
                            "gzip": {},
                            "zstd": {}
                          },
                          "handler": "encode",
                          "prefer": [
                            "zstd",
                            "br",
                            "gzip"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "subroute",
                          "routes": [
                            {
                              "handle": [
                                {
                                  "handler": "authentication",
                                  "providers": {
                                    "http_basic": {
                                      "accounts": [
                                        {
                                          "password": "$2a$14$a3L/wRWU9mvnYoO2AOuS4u8GztSKf82tFLqi36VSbBsD3hvsDbJ.G",
                                          "username": "foo"
                                        }
                                      ],
                                      "hash": {
                                        "algorithm": "bcrypt"
                                      },
                                      "hash_cache": {}
                                    }
                                  }
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "match": [
                        {
                          "not": [
                            {
                              "path": [
                                "/healthcheck"
                              ]
                            }
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "metrics"
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "/metrics"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "php"
                        }
                      ],
                      "match": [
                        {
                          "path": [
                            "index.php"
                          ]
                        }
                      ]
                    },
                    {
                      "handle": [
                        {
                          "handler": "file_server",
                          "hide": [
                            "*.php",
                            "/etc/frankenphp/Caddyfile"
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "php"
                  ]
                }
              ],
              "terminal": true
            }
          ]
        }
      }
    },
    "pki": {
      "certificate_authorities": {
        "local": {
          "install_trust": false
        }
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "exclude": [
          "http.log.access.log0"
        ]
      },
      "log0": {
        "encoder": {
          "fields": {
            "request>uri": {
              "actions": [
                {
                  "parameter": "authorization",
                  "type": "replace",
                  "value": "REDACTED"
                }
              ],
              "filter": "query"
            }
          },
          "format": "filter"
        },
        "include": [
          "http.log.access.log0"
        ]
      }
    }
  }
}

Here is my docker entrypoint :

...


# HTTP Basic Auth configuration
if [ -n "$HTTP_BASIC_USER" ] && [ -n "$HTTP_BASIC_PASSWORD" ]; then
    HASHED_PASSWORD=$(frankenphp hash-password --plaintext "$HTTP_BASIC_PASSWORD")
    unset HTTP_BASIC_PASSWORD

    CADDY_SERVER_EXTRA_DIRECTIVES=$(cat <<EOF
@protected {
    not path /healthcheck
}

handle @protected {
    basic_auth {
        ${HTTP_BASIC_USER} ${HASHED_PASSWORD}
    }
}
${CADDY_SERVER_EXTRA_DIRECTIVES:-}
EOF
)
    export CADDY_SERVER_EXTRA_DIRECTIVES
fi

exec docker-php-entrypoint "$@"

(frankenphp = caddy with php)

Any idea of what is wrong?

I’m pretty sure it’s because this rewrite runs before the handle, as per Caddy’s directive order:

	@phpRoute {
		not path /.well-known/mercure*
		not file {path}
	}
	rewrite @phpRoute index.php

TBH, I’m not at all a fan of FrankenPHP’s approach of recommending to use env vars for extra config. Makes no sense, honestly, way too little control.

Instead, just take the full Caddyfile and make the actual change you need directly in it, mount the config into your container. Don’t use env vars for config.

It would probably look like this, using a route to force directive order within it such that basic_auth goes before rewrite:

{
	skip_install_trust
	frankenphp {
		worker {
			file ./public/index.php
		}
	}
}

{$SERVER_NAME:localhost} {
	log {
		# Redact the authorization query parameter that can be set by Mercure
		format filter {
			request>uri query {
				replace authorization REDACTED
			}
		}
	}

	root /app/public
	encode zstd br gzip

	route {
		@protected not path /healthcheck
		basic_auth @protected {
        	${HTTP_BASIC_USER} ${HASHED_PASSWORD}
    	}

		@phpRoute {
			not path /.well-known/mercure*
			not file {path}
		}
		rewrite @phpRoute index.php
	}

	@frontController path index.php
	php @frontController

	file_server {
		hide *.php
	}
}

Also of note, I recommend hashing your password once and storing that in your vars, not every time you run the container. Hashing is a slow operation, it’s not meant to be done repeatedly unless the password changes.

1 Like

I agree with this. That was going to be my next advice too.

For @mykiwi: just keep in mind, that systemt env ${HTTP_BASIC_USER} will become {$HTTP_BASIC_USER} in Caddyfile. Also, there will need to be a couple of extra changes needed to make sure PROD doesn’t get auth.

So, if @mykiwi wants to keep the same config for both PROD and PRE-PROD, this block:

basic_auth @protected {
		${HTTP_BASIC_USER} ${HASHED_PASSWORD}
}

may still need to be moved into an env variable, let’s say PREPROD_AUTH, since the Caddyfile doesn’t support templating directly.

The resulting Caddyfile (borrowing from @francislavoie) would then look like this:

{
	skip_install_trust
	frankenphp {
		worker {
			file ./public/index.php
		}
	}
}

{$SERVER_NAME:localhost} {
	log {
		# Redact the authorization query parameter that can be set by Mercure
		format filter {
			request>uri query {
				replace authorization REDACTED
			}
		}
	}

	root /app/public
	encode zstd br gzip

	route {
		@protected not path /healthcheck
		{$PREPROD_AUTH}

		@phpRoute {
			not path /.well-known/mercure*
			not file {path}
		}
		rewrite @phpRoute index.php
	}

	@frontController path index.php
	php @frontController

	file_server {
		hide *.php
	}
}