Add multilined sniped to the Caddy file with an enviroment env (from docker compose)

I want to add a forbidden block to the FrankenPHP Caddy file.

I would prefer to do it via the environment var prepared for this case, instead of providing a custom Caddyfile because it seems more update and change resistant, it’s also really easy to declare env var’s in a docker compose file.

I’m trying to use the provided $CADDY_SERVER_EXTRA_DIRECTIVES var for it.

I am struggling to get a multi lined snipped, passed along, as env var tho.
Am I just doing it wrong?
Is there a better way of achieving this?

2. Here the FrankenPHP Caddyfile

{
	{$CADDY_GLOBAL_OPTIONS}

	frankenphp {
		#worker /path/to/your/worker.php
		{$FRANKENPHP_CONFIG}
	}

	# https://caddyserver.com/docs/caddyfile/directives#sorting-algorithm
	order mercure after encode
	order vulcain after reverse_proxy
	order php_server before file_server
	order php before file_server
}

{$CADDY_EXTRA_CONFIG}

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

	root * public/
	encode zstd br gzip

	# Uncomment the following lines to enable Mercure and Vulcain modules
	#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}

	php_server
}

3. Here the forbidden block from the official install instructions of DokuWiki

 #Remember to comment the below forbidden block out when you're installing, and uncomment it when done.
    @forbidden path /data/* /conf/* /bin/* /inc/* /install.php
    handle @forbidden {
        respond * 403
    }
    #End of the forbidden block

4. How I installed and ran Caddy:

Oficila FrankenPHP docker image

a. System environment: My Docker compose.yml

version: "3.9"

networks:
  lan20:
    enable_ipv6: true
    name: vlan20
    driver: macvlan
    driver_opts:
      parent: br-lan.20
    ipam:
      config:
        - subnet: 192.168.20.0/24
          gateway: 192.168.20.1
        - subnet: fd19:7219:b304:20::1/60
          gateway: fd19:7219:b304:20::1
        - subnet: 2406:2d40:7238:9a20::1/60
          gateway: 2406:2d40:7238:9a20::1

services:

  dokuwiki:
    image: dunglas/frankenphp
    container_name: dokuwiki
    environment:
#      - TZ=Pacific/Auckland
      - SERVER_NAME=wiki.village
      - CADDY_GLOBAL_OPTIONS=local_certs
      - CADDY_SERVER_EXTRA_DIRECTIVES="@forbidden path /data/* /conf/* /bin/* /inc/* /install.php \n    handle @forbidden { \n        respond * 403 \n    }"
    restart: unless-stopped
#    cap_add:
#      - NET_ADMIN
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./dokuwiki_test:/app/public
#      - ./Caddyfile:/etc/caddy/Caddyfile
      - dokuwiki_caddy_data:/data
      - caddy_config:/config
    tty: true
    dns:
      - "192.168.20.10"
      - "192.168.20.1"
    networks:
      lan20:
        ipv4_address: 192.168.20.20
        ipv6_address: fd19:7219:b304:20::20
#        ipv6_address: 2406:2d40:7238:9a20::20


volumes:
  dokuwiki_caddy_data:
    external: true
  caddy_config:

b. Command:

docker-compose down --remove-orphans; docker-compose up -d

5. Links to relevant resources:

Here the official DokuWiki Caddy install guide.
https://www.dokuwiki.org/install:caddy

I think you need to declare your env vars like this:

services:
  dokuwiki:
    image: dunglas/frankenphp
    container_name: dokuwiki
    environment:
#      TZ: Pacific/Auckland
      SERVER_NAME: wiki.village
      CADDY_GLOBAL_OPTIONS: local_certs
      CADDY_SERVER_EXTRA_DIRECTIVES: |
        @forbidden path /data/* /conf/* /bin/* /inc/* /install.php
        handle @forbidden {
            respond * 403
        }
      restart: unless-stopped

The key is using the | syntax which preserves the newlines in the text that follows on the next few lines (the leading spaces are stripped).

1 Like

Cheers, that seems to work, at least it looks right if I run echo "$CADDY_SERVER_EXTRA_DIRECTIVES" in the container.

But it does not actually block access to any of the files.

Caddy will complain if I just put some garbage in the var, as in unrecognised option on line xx.

I meanwhile also tried:

      CADDY_SERVER_EXTRA_DIRECTIVES: |
 38         @forbidden {
 39               path /data/* /conf/* /bin/* /inc/* /install.php
 40         }
 41         respond @forbidden 403
 42         file_server

but to no avail …

Is the order block on top of the Caddyfile maybe messing up things?
Any ideas on how to troubleshoot this further?
Is there a way to get the Caddyfile with all vars expanded as in the version its actually running on?

PS: Cheers for the awesome help so far, means a lot!

Show the behaviour you’re seeing with curl -v.

I recommend using the error directive instead of respond, then you can use handle_errors to write responses for errors emitted by other handlers.

Cheers,
I’m currently using:

 37       CADDY_SERVER_EXTRA_DIRECTIVES: |
 38         @forbidden {
 39               path /data/* /conf/* /bin/* /inc/* /install.php
 40         }
 41         error @forbidden 403
 42         file_server

curl -v response:

* Host wiki.village:443 was resolved.
* IPv6: (none)
* IPv4: 192.168.20.20
*   Trying 192.168.20.20:443...
* Connected to wiki.village (192.168.20.20) 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.

Well, guess I need to find the certificate and copy it to my local machine.
My break is over, so cant do that now …

If you know where it would be … pls let me know.

My Dockerfile:

FROM dunglas/frankenphp
#no_cache

USER root
## Uncoment the below 2 lines and call the build with: docker-compose -f docker-compose.yml build --build-arg CACHEBUST=$(date +%s)
## to force an uncached build
#ARG CACHEBUST
#RUN echo "$CACHEBUST"
ARG USER=www-data

RUN DEBIAN_FRONTEND="noninteractive" \
	# Use "adduser -D ${USER}" for alpine based distros
	useradd -D ${USER}; \
	# Add additional capability to bind to port 80 and 443
	setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
	# Give write access to /data/caddy and /config/caddy
	mkdir --parents --verbose /app/public; \
	sleep 10s; \
	chown -R ${USER}:${USER} /data/caddy && chown -R ${USER}:${USER} /config/caddy; \
	chown -R ${USER}:${USER} /app/public; \
	# Update pakagemanager repositorry, install Imagemagic and clean up afterwards
	apt-get update; \
	apt-get install libmagickwand-dev --no-install-recommends -y; \
	apt-get clean && \
	rm -rf /var/lib/apt/lists/*;
USER ${USER}

Again, cheers for the awesome help!

Found it!
In a Caddy or FrankenPHP Docker container, the cert is at /data/caddy/pki/authorities/local/root.crt
It can be extracted from the container with docker cp, in my case docker cp dokuwiki:/data/caddy/pki/authorities/local/root.crt caddy_root.crt

I then rsynced it to my laptop … and installed it with sudo trust anchor --store caddy_root.crt

Now to the response of curl -v:
install.php seems to now be successfully blocked; I accidentally had DokuWiki installed in a subfolder in the webroot on my last attempt.

curl -v https://wiki.village/install.php
* Host wiki.village:443 was resolved.
* IPv6: (none)
* IPv4: 192.168.20.20
*   Trying 192.168.20.20:443...
* Connected to wiki.village (192.168.20.20) 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: Apr 18 23:42:03 2024 GMT
*  expire date: Apr 19 11:42:03 2024 GMT
*  subjectAltName: host "wiki.village" matched cert's "wiki.village"
*  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
*   Certificate level 2: 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://wiki.village/install.php
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: wiki.village]
* [HTTP/2] [1] [:path: /install.php]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /install.php HTTP/2
> Host: wiki.village
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 403 
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Fri, 19 Apr 2024 03:32:55 GMT
< 
* Connection #0 to host wiki.village left intact

But /data/pages/wiki/dokuwiki.txt is still accessible … :face_exhaling: :joy:
Edit: got tricked by the browser cache! Curl gets the 403.

Somehow, Firefox gives me a blank page instead of a file not found compared to the instal.php case, but whatever, I’m happy with that.

curl -v https://wiki.village/data/pages/wiki/dokuwiki.txt
* Host wiki.village:443 was resolved.
* IPv6: (none)
* IPv4: 192.168.20.20
*   Trying 192.168.20.20:443...
* Connected to wiki.village (192.168.20.20) 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: Apr 18 23:42:03 2024 GMT
*  expire date: Apr 19 11:42:03 2024 GMT
*  subjectAltName: host "wiki.village" matched cert's "wiki.village"
*  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
*   Certificate level 2: 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://wiki.village/data/pages/wiki/dokuwiki.txt
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: wiki.village]
* [HTTP/2] [1] [:path: /data/pages/wiki/dokuwiki.txt]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /data/pages/wiki/dokuwiki.txt HTTP/2
> Host: wiki.village
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 403 
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Fri, 19 Apr 2024 03:41:01 GMT
< 
* Connection #0 to host wiki.village left intact

So everything seems to work now.
Thank you so much for your awesome help and patience @francislavoie

I just tested, the file_server directive is not needed in the forbidden block.

So the final working best practice solution is:

EDIT: (I commented for this example irrelevant stuff out, but left it in to demonstrate the greater look of the docker-compos.yml file)

33     environment:
34 #      - TZ=Pacific/Auckland
35 #      SERVER_NAME: wiki.village
36 #      CADDY_GLOBAL_OPTIONS: local_certs
37       CADDY_SERVER_EXTRA_DIRECTIVES: |
38         @forbidden {
39               path /data/* /conf/* /bin/* /inc/* /install.php
40         }
41         error @forbidden 403
Note the SOME_VAR: space | (pipe) newline
  Indent once, some instructions
  more instructions
  and even more instructions, syntax
NEXT_VAR:
1 Like

FYI, small thing, you can shorten the config to this (using one-liner matcher syntax):

@forbidden path /data/* /conf/* /bin/* /inc/* /install.php
error @forbidden 403

Glad you figured things out!

1 Like