Tweaking Frakenphp Caddyfile for Laravel Octane

Hey there!

What I’m up to:

I’m attempting to tweak the Caddyfile used with FrankenPHP + Laravel Octane. This is what’s being used:

Is there a way for me to get the final output from the running server?

{
	{$CADDY_GLOBAL_OPTIONS}

	admin localhost:{$CADDY_SERVER_ADMIN_PORT}

	frankenphp {
		worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
	}
}

{$CADDY_SERVER_SERVER_NAME} {
	log {
		level {$CADDY_SERVER_LOG_LEVEL}

		# Redact the authorization query parameter that can be set by Mercure...
		format filter {
			wrap {$CADDY_SERVER_LOGGER}
			fields {
				uri query {
					replace authorization REDACTED
				}
			}
		}
	}

	route {
		root * "{$APP_PUBLIC_PATH}"
		encode zstd br gzip

		# Mercure configuration is injected here...
		{$CADDY_SERVER_EXTRA_DIRECTIVES}

		php_server {
			index frankenphp-worker.php
			# Required for the public/storage/ directory...
			resolve_root_symlink
		}
	}
}

The Goals

What I’d like to do is add some protections/cache headers for static assets:

  1. Returning 404 or 401 for dot files and files of specific extensions
  2. Adding cache headers for static files ending in specific extensions

What I’ve Tried

I’m still working my way around the Caddyfile and would love any pointers - here’s what I’ve tried (in diff syntax):

{
	{$CADDY_GLOBAL_OPTIONS}

	admin localhost:{$CADDY_SERVER_ADMIN_PORT}

	frankenphp {
		worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
	}
}

{$CADDY_SERVER_SERVER_NAME} {
	log {
		level {$CADDY_SERVER_LOG_LEVEL}

		# Redact the authorization query parameter that can be set by Mercure...
		format filter {
			wrap {$CADDY_SERVER_LOGGER}
			fields {
				uri query {
					replace authorization REDACTED
				}
			}
		}
	}

	route {
		root * "{$APP_PUBLIC_PATH}"
		encode zstd br gzip

		# Mercure configuration is injected here...
		{$CADDY_SERVER_EXTRA_DIRECTIVES}

		php_server {
			index frankenphp-worker.php
			# Required for the public/storage/ directory...
			resolve_root_symlink
		}

+		file_server {
+			hide *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.*
+		}
	}
}

However, after restarting Octane, I can still get “protected” files:

Starting a server:

php -d variables_order=EGPCS artisan octane:start \
    --server=frankenphp --host=0.0.0.0 --admin-port=2019 \
    --port=8080 --caddyfile=./Caddyfile

And sending requests:

curl -i http://0.0.0.0:8080/.env
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 4
Etag: "sdya1l4"
Last-Modified: Thu, 23 May 2024 18:00:57 GMT
Server: Caddy
Date: Thu, 23 May 2024 18:11:05 GMT

foo

---

curl -i http://0.0.0.0:8080/backup.bak
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 9
Etag: "sdyai99"
Last-Modified: Thu, 23 May 2024 18:10:57 GMT
Server: Caddy
Date: Thu, 23 May 2024 18:11:10 GMT

backups!

3. Caddy version:

./frankenphp --version
# FrankenPHP 1.1.5 PHP  Caddy v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

# Laravel setup w/ Octane
composer create-project laravel/laravel frankentest
cd frankentest

composer require laravel/octane
php artisan octane:install --server=frankenphp

# Copy the Caddyfile stub so we can play with it
cp vendor/laravel/octane/src/Commands/stubs/Caddyfile ./Caddyfile

# Start Frankenphp with the local Caddyfile
php -d variables_order=EGPCS artisan octane:start \
    --server=frankenphp --host=0.0.0.0 --admin-port=2019 \
    --port=8080 --caddyfile=./Caddyfile

a. System environment:

MacOS, M3 (ARM) - This is not in Docker but occurs within Docker as well

Whoops, okay - directive order matters. I swapped file_server to be BEFORE php_server and it’s doing the needful.

# stuff omitted
{

	route {
		root * "{$APP_PUBLIC_PATH}"
		encode zstd br gzip

		# Mercure configuration is injected here...
		{$CADDY_SERVER_EXTRA_DIRECTIVES}

+       file_server {
+			hide *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.*
+		}

		php_server {
			index frankenphp-worker.php
			# Required for the public/storage/ directory...
			resolve_root_symlink
		}


-       file_server {
-			hide *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.*
-		}
	}
}

And for completeness, this is working as well for static asset caching (so far, got more tweaks to do!)

route {
		root * "{$APP_PUBLIC_PATH}"
		encode zstd br gzip

		# Mercure configuration is injected here...
		{$CADDY_SERVER_EXTRA_DIRECTIVES}

+        @static {
+            file
+            path *.js *.css
+        }
+
+        # 1 year, similar to h5bp
+        header @static Cache-Control max-age=31536000

        file_server {
			hide *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.*
		}

		php_server {
			index frankenphp-worker.php
			# Required for the public/storage/ directory...
			resolve_root_symlink
		}
	}

FYI, the php_server directive from FrankenPHP is a shortcut for a longer config. See the docs FrankenPHP: the modern PHP app server it shows the long form. I suggest that you use the long-form so that you can configure the file_server that’s built into that directive instead of adding another in front.

Also, your config looks somewhat outdated compared to the current recommendations – using route forces order inside of your config to matter, whereas if you use the order global option, the Caddyfile’s directive sorting will deal with it. Lets you un-indent your config one step too.

An alternate option is to use a matcher+error to reject those files, like this:

@rejected path *.bak *.conf *.dist *.fla *.ini *.inc *.inci *.log *.orig *.psd *.sh *.sql *.swo *.swp *.swop */.*
error @rejected 404

You can present a custom error page with handle_errors if you need to.

Thank you for the pointers, much appreciated!

I may leave the order and route alone for this exact moment, I suspect that’ll be an upstream change (I’ll bring it up to the Laravel team - I’m not yet sure if this configuration I grabbed was created by the Octane team or Frankenphp team).

What’s the best way to reject those files but NOT */.well-known/* ?

Kinda tricky. Could either switch to using a complicated path_regexp (I’ll leave that to you to write if you want), or you could use an expression matcher which allows you to use boolean logic.

@rejected `path('*.bak', '*.conf', '*.dist', '*.fla', '*.ini', '*.inc', '*.inci', '*.log', '*.orig', '*.psd', '*.sh', '*.sql', '*.swo', '*.swp', '*.swop', '*/.*') && !path(' `*/.well-known/*`')`
error @rejected 404

Lovely, thank you!

For anyone watching / the future / my future self, here’s the final thing for now:

{
	{$CADDY_GLOBAL_OPTIONS}

	admin localhost:{$CADDY_SERVER_ADMIN_PORT}

	frankenphp {
		worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
	}
}

{$CADDY_SERVER_SERVER_NAME} {
	log {
		level {$CADDY_SERVER_LOG_LEVEL}

		# Redact the authorization query parameter that can be set by Mercure...
		format filter {
			wrap {$CADDY_SERVER_LOGGER}
			fields {
				uri query {
					replace authorization REDACTED
				}
			}
		}
	}

	route {
		root * "{$APP_PUBLIC_PATH}"
		encode zstd br gzip

		# Mercure configuration is injected here...
		{$CADDY_SERVER_EXTRA_DIRECTIVES}

		@static {
			file
			path *.js *.css *.jpg *.jpeg *.gif *.png *.ico *.cur *.gz *.svg *.svgz *.mp4 *.mp3 *.ogg *.ogv *.webm *.htc *.woff2 *.woff
		}

		@staticshort {
			file
			path *.json *.xml *.rss
		}

		# 1 year, similar to h5bp nginx config
		header @static Cache-Control "public, immutable, stale-while-revalidate, max-age=31536000"

		# 1 hour max, gets validated with the origin server
		# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#no-cache
		header @staticshort Cache-Control "no-cache, max-age=3600"

		# Restrict access to dot files and certain file extensions
		@rejected `path('*.bak', '*.conf', '*.dist', '*.fla', '*.ini', '*.inc', '*.inci', '*.log', '*.orig', '*.psd', '*.sh', '*.sql', '*.swo', '*.swp', '*.swop', '*/.*') && !path('*/.well-known/')`
		error @rejected 401

		php_server {
			index frankenphp-worker.php
			# Required for the public/storage/ directory...
			resolve_root_symlink
		}
	}
}

This gets (not totally but pretty close) on par to caching + secure file access to H5BP’s nginx configuration.

3 Likes

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