Caddy 2 for Processwire

1. Caddy version 2:

2. How I run Caddy: Caddy is run in docker, the official version, alpine

a. System environment:

Debian 10, LXC with overlayfs, docker with docker-compose

b. Command:

I simply run the docker-compose.yml with no big modifications, the corresponding code is below:

services:
  caddy:
    image: caddy/caddy:alpine
    volumes:
      - /opt/webhosting/config/caddy/Caddyfile:/etc/caddy/Caddyfile
      - /opt/webhosting/config/caddy/shared:/etc/caddy/shared
      - /opt/webhosting/data/caddy:/data
      - /opt/webhosting/logs/caddy:/var/log
      - /opt/webhosting/www:/srv
    ports:
      - '80:80'
      - '443:443'
    networks:
      - frontend
      - backend
    restart: always

c. Service/unit/compose file:

see above

d. My complete Caddyfile or JSON config:

Caddyfile
xxxxxxx.com {
  root * /srv/xxxxxxx.com
  import shared/processwire.conf
  php_fastcgi php73:9000
  file_server
}
imported processwire.conf
encode zstd gzip

#rewrite /\. /forbidden
respond /(COPYRIGHT|LICENSE|README|htaccess)\.txt 404
#rewrite /site(-[^/]+)?/assets/(.*\.php|backups|cache|config|install|logs|sessions) /forbidden
#rewrite /site(-[^/]+)?/install /forbidden
#rewrite /(site(-[^/]+)?|wire)/(config(-dev)?|index\.config)\.php /forbidden
#rewrite /((site(-[^/]+)?|wire)/modules|wire/core)/.*\.(inc|module|php|tpl) /forbidden
#rewrite /(site(-[^/]+)?|wire)/templates(-admin)?/.*\.(inc|html?|php|tpl) /forbidden

# Global
try_files {path} {path}/ /index.php?it={path}&{query}

# Log Files
log {
  output file /var/log/access.log {
    roll_size      50MiB
    roll_keep      5
    roll_keep_for  168h
  }
}

3. The problem I’m having:

I was trying to migrate from version 1 to version 2 conf file. I was able to achieve php parsing and static file serving. The only thing I am missing and I am unable to achieve is to make use of the previously used “internel” direction to make some parts of site unacessible, specially direct downloads.

I tried tot used respond <matcher> <return code> but it still allowes the files to be downloaded. Then I found something in file_server directive which also accepts matchers but not matchers for files to be hidden.

4. Error messages and/or full log output:

There are not errors shown.

5. What I already tried:

I tried the above. At first I run through all the documentation tried “respond” and “file_server” directives. Did not however find anything in the forum so I am stuck now.

6. Links to relevant resources:

Please read the matcher docs:

In the Caddyfile, a matcher token immediately following the directive can limit that directive’s scope. The matcher token can be one of these forms:

  1. * to match all requests (wildcard; default).
  2. /path start with a forward slash to match a request path.
  3. @name to specify a named matcher.

You can’t use a regexp matcher inline, you must use a named matcher for anything other than a simple path matcher.

Thank you @francislavoie. I tried several things but obviously did not reach the goal here. There are simply too few examples to flawlessly more from v1 to v2.

Anyway, I though I should use the directive file_server and it’s hide attribute.

@restricted {
  hide /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
}
file_server @restricted

I also though I could try with respond so tried this:

@restricted {
  /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
}
respond @restricted 404

Obviously, neither one was a way to go. I will be greatful for some help here how to restrict access to certain (group of) files in an invisible way (not with 403 that could likely reveal the system) but as if no such files were there at all, like file_server should allow.

Close!

@restricted {
  path_regexp /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
}
respond @restricted 404

Thanks a lot. :slight_smile:
One more thing though. How does it play along try_files? In order to serve static files (js/css) I am using this directive: try_files {path} /index.php?it={path}&{query} Even though the respond @restricted 404 is above it, the try_files seems to omit it and simple serves the files anyway.

Is there a way around this? try_files does not seem to accept and matchers.

Ah, you’ll want to force the directive ordering to happen in a different order than default using a route block:

@restricted {
	path_regexp /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
}

route {
	respond @restricted 404
	try_files {path} /index.php?it={path}&{query}
}

Unfortunately you can’t define matchers inside a route block so you need to pull it to the outside of it.

You can see the directive order here:

What I am after seems to be too complicated in caddy, I guess. It was a little simpler in v1.
As I know already, everything within a named matcher is “AND”.

The essential part of the import file is this, this was the initial idea:

@restricted {
  path_regexp /\.
  path_regexp (CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
  path_regexp /site(-[^/]+)?/assets/(.*\.php|backups|cache|config|install|logs|sessions)
  path_regexp /site(-[^/]+)?/install
  path_regexp /(site(-[^/]+)?|wire)/(config(-dev)?|index\.config)\.php
  path_regexp /((site(-[^/]+)?|wire)/modules|wire/core)/.*\.(inc|module|php|tpl)
  path_regexp /(site(-[^/]+)?|wire)/templates(-admin)?/.*\.(inc|html?|php|tpl)
}
respond @restricted 404

try_files {path} /index.php?it={path}&{query}

As within a matcher all lines are AND, not OR, I can either use a named matcher for each line, or I also found a negative matching, which could work like this:

@not_restricted {
  not path_regexp /\.
  not path_regexp (CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
  not path_regexp /site(-[^/]+)?/assets/(.*\.php|backups|cache|config|install|logs|sessions)
  not path_regexp /site(-[^/]+)?/install
  not path_regexp /(site(-[^/]+)?|wire)/(config(-dev)?|index\.config)\.php
  not path_regexp /((site(-[^/]+)?|wire)/modules|wire/core)/.*\.(inc|module|php|tpl)
  not path_regexp /(site(-[^/]+)?|wire)/templates(-admin)?/.*\.(inc|html?|php|tpl)
}

This would allow to match every path that actually passes the filter. I tried to use it with file_server @not_restricted as I need these only to match static files. I am of course using fastcgi:

xxx.com {
  root * /srv/xxx.com
  import shared/processwire.conf
  php_fastcgi php73:9000
  file_server @not_restricted
}

Sorry for such long questions but I am just trying to work it out in v2 to migrate. But it’s not just that easy to grasp.

The issue with that approach is that due to the directive order, php_fastcgi is handled before file_server, so if you let anything fall through, then you’ll just get empty HTTP 200 responses.

I’m pretty sure you could do this sort of thing as well:

@restricted {
	not {
		not path_regexp ...
		not path_regexp ...
	}
}

The double negative would let you ultimately get OR’d paths to exclude, which you can use with respond.

Another option which I don’t think is as good, but you could wrap file_server in a route along with a respond as a fallthrough case:

route {
	php_fastcgi php73:9000
	file_server @not_restricted
	respond 404
}

So for anything that doesn’t get handled by PHP and anything that isn’t “not restricted” will get a 404 response.

I have to say though, I don’t think this is a problem with Caddy at this point, it’s just that you might have some pretty wacky requirements. :stuck_out_tongue:

1 Like

Okay, thanks for advices. I ended up doing it like this. I am not really sure this is the most elegant way to achieve but…

xxxxxx.com {
  root * /srv/xxxxxx.com
  import shared/processwire.conf
  php_fastcgi php73:9000
  file_server
}

and shared/processwire.conf

# encoding
encode zstd gzip

# matchers
@restrict_hidden    {
  path_regexp /\.
}

@restrict_root      {
  path_regexp /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
}

@restrict_assets    {
  path_regexp /site(-[^/]+)?/assets/(.*\.php|backups|cache|config|install|logs|sessions)
}

@restrict_install   {
  path_regexp /site(-[^/]+)?/install
}

@restrict_config    {
  path_regexp /(site(-[^/]+)?|wire)/(config(-dev)?|index\.config)\.php
}

@restrict_modules   {
  path_regexp /((site(-[^/]+)?|wire)/modules|wire/core)/.*\.(inc|module|php|tpl)
}

@restrict_templates {
  path_regexp /(site(-[^/]+)?|wire)/templates(-admin)?/.*\.(inc|html?|php|tpl)
}

# Do not allow access to some files directly
respond @restrict_hidden    404
respond @restrict_root      404
respond @restrict_assets    404
respond @restrict_install   404
respond @restrict_config    404
respond @restrict_modules   404
respond @restrict_templates 404

# global rule
try_files {path} {path}/ /index.php?it={path}&{query}

# log
log {
  output file /var/log/access.log {
    roll_size      50MiB
    roll_keep      5
    roll_keep_for  168h
  }
}

Thanks in advance if it can be made any easier, otherwise feel free to close this. :slight_smile:

1 Like

If that works, then :+1:

FYI, in Caddy v2.1 you’ll be able to shorten that plenty by making your named matchers one-liners:

@restrict_hidden path_regexp /\.
@restrict_root path_regexp /(CONTRIBUTING|COPYRIGHT|LICENSE|README|htaccess)\.(txt|md)
...

Great! Looking forward because this is hard to read as it is.

I disagree that it’s so hard to read – what config were you using before?

Hello @matt, no offense at all. And you are right that caddy config files are way better than anything like Nginx or Apache. I used to use Nginx and switched to Caddy. Great piece of software.

What I meant by “hard to read” was that each path_regex needs 3 lines at least; what @francislavoie outlined, that it will be able to be a one-liner, is great. But of course, each config file is way smaller than with any other server.

1 Like

Just to clarify, do you mean that it’s hard to read (i.e. difficult to parse and understand what it’s doing) or just annoying to read (i.e. aesthetically displeasing)?

Big agree on the latter, and I love @francislavoie’s one-liner matchers - looking forward to 2.1 release for that reason (among a bunch of other reasons too).

That would be #2 of course. I believe the documentation is pretty clear about what it is doing. I find one-liners quite a handy thing for simple and short commands, or matches in this case.

1 Like

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