# To use this file simply install caddy and run the command below from the root of your Grav site # Once running it will redirect http://localhost to https://localhost (new default for Caddy2) # More information here: https://caddyserver.com/docs/ # # $ caddy run --config webserver-configs/Caddyfile localhost encode gzip root * . # Begin - Security # # Two things to know before editing this block: # # 1. Caddy's `path` matcher is literal — it only understands `*` wildcards, it # is NOT a regex. Any rule needing alternation, character classes or anchors # has to use a named `path_regexp` matcher. A bare # `rewrite /(cache|logs)/.* /403` matches nothing at all, which silently # turns the rule off. # # 2. Outside a `route` block Caddy applies its own directive order rather than # the order written in this file, and the global `try_files` rewrite runs # before `respond`. That silently disables every rule below for any path # Caddy can't resolve to a file on disk. Everything is therefore wrapped in # a `route` block, which executes top to bottom as written. # deny all direct access for these folders @denied_dirs path_regexp (?i)^/(\.git|cache|bin|logs|backups?|tmp|webserver-configs|tests)/ # deny all direct access to these sensitive user folders, whatever the file type @denied_user_config path_regexp (?i)^/user/(env/[^/]+/)?config/ # block user/accounts too, but allow avatar images to be served directly, whether # stored at user/accounts/avatars/ (flatfile accounts) or user/accounts/ # / (Flex folder storage). SVG is excluded as a stored-XSS vector. # (Go's RE2 has no lookbehind, so this uses a negated matcher.) @denied_user_accounts { path_regexp (?i)^/user/(env/[^/]+/)?accounts/ not path_regexp (?i)^/user/(env/[^/]+/)?accounts/[^/]+/[^/]+\.(jpe?g|png|gif|webp|avif|bmp|ico)$ } # block user/data too, but allow public media uploads (e.g. Flex Object images) # to be served directly. SVG is intentionally excluded as a stored-XSS vector. # (Go's RE2 has no lookbehind, so this uses a negated matcher.) @denied_user_data { path_regexp (?i)^/user/(env/[^/]+/)?data/ not path_regexp (?i)\.(jpe?g|png|gif|webp|avif|bmp|ico|mp4|webm|ogg|ogv|mov|mp3|wav|m4a|flac|pdf)$ } # Serve page media only through the page route, so plugins can apply the page's # `access` rules to it. DO NOT uncomment this matcher and its `respond` line # below without first setting `pages.media_route_urls: true` in # user/config/system.yaml, or every media URL on the site becomes a 403. # @denied_user_pages { # path_regexp (?i)^/user/pages/ # } # deny running scripts in the public cache folders (image derivatives and combined assets) @denied_cache_scripts path_regexp (?i)^/(images|assets)/.*\.(php|php2|php3|php4|php5|php7|php8|phar|phtml|pht|phtm|phps|pl|py|cgi|sh|bat)$ # deny running scripts inside core system folders @denied_system path_regexp (?i)^/(system|vendor)/.*\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ # deny running scripts inside user folder @denied_user_scripts path_regexp (?i)^/user/.*\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ # deny access to specific files in the root folder @denied_root_files path_regexp (?i)^/(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt)$ # deny Markdown source files such as README.md and CHANGELOG.md; a page route # ending in .md is not a file, so it still reaches Grav's Markdown output @denied_markdown_files { path_regexp (?i)\.md$ file } # deny hidden files and directories at any depth (.htaccess, .env, .env.local, # .migrating, nested .git repositories), except /.well-known/ files whose own # names don't start with a dot (ACME challenges, security.txt). # Caddy matchers use RE2, which has no lookahead, so .well-known is excluded # with a separate `not` rather than inline in the expression. @denied_dotfiles { path_regexp (?i)(^|/)\. not path_regexp ^/\.well-known(/[^./][^/]*)+$ } ## End - Security @immutable path_regexp (?i)/_app/immutable/ route { respond @denied_dirs 403 respond @denied_user_config 403 respond @denied_user_accounts 403 respond @denied_user_data 403 # respond @denied_user_pages 403 respond @denied_cache_scripts 403 respond @denied_system 403 respond @denied_user_scripts 403 respond @denied_root_files 403 respond @denied_markdown_files 403 respond @denied_dotfiles 403 # global rewrite should come last. try_files {path} {path}/ /index.php?_url={uri}&{query} # Content-hashed bundles (the Admin panel and other SvelteKit-based plugins) # can be cached for a year: the name changes whenever the file does. After # try_files on purpose, so a missing chunk has been rewritten to index.php # by now and Grav's 404 doesn't match. header @immutable Cache-Control "public, max-age=31536000, immutable" php_fastcgi 127.0.0.1:9000 file_server }