Caddy requires world-execuable permissions to browse files

1. The problem I’m having:

I am trying to configure Caddy as a simple file server for serving files used in automation, called by wget, Ansible, etc. When viewing the site and attempting to browse files, nothing shows up. It wasn’t until I provided world-executable permissions to directories and files under the root path for the file server that directories and files appeared when browsing. Is this expected behavior?

Additionally, I noticed that even when using the mode option in the Caddyfile, my log file is being created using the default umask permissions (0600) instead of the given mode of 0755, and all entries are being logged at the info level and not debug as directed.

2. Error messages and/or full log output:

Prior to applying world-executable permissions to all files and folders under the root file server path.

{"level":"info","ts":1750436587.2622643,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/","headers":{"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"Connection":["keep-alive"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Cookie":[]}},"user_id":"","duration":0.00345438,"size":14383,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/html; charset=utf-8"]}}
{"level":"info","ts":1750436743.5482035,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/","headers":{"Connection":["keep-alive"],"Cache-Control":["max-age=0"],"Dnt":["1"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Cookie":[]}},"user_id":"","duration":0.002090611,"size":14833,"status":200,"resp_headers":{"Content-Type":["text/html; charset=utf-8"],"Server":["Caddy"]}}
{"level":"info","ts":1750436756.9400399,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/apictl/","headers":{"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Dnt":["1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Referer":["http://mn4s34385.nmdp.org:8080/"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Cookie":[],"Connection":["keep-alive"]}},"user_id":"","duration":0.001431602,"size":13292,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/html; charset=utf-8"]}}

Directory tree and permissions structure:

ls -laR /var/www/html/pub
/var/www/html/pub:
total 132
drwxrwsrw-. 3 caddy    all      72 Jun 20 11:23 .
drwxr-xr-x. 3 root     root     33 Jun 19 16:32 ..
drwxrwsrw-. 2 gmichael all      61 Jun 19 16:40 apictl
-rw-------. 1 caddy    all    2560 Jun 20 11:25 caddy.log
-rwxrwxrw-. 1 caddy    all  124851 Jun 20 11:17 caddy.log.1
-rwxrwxrw-. 1 gmichael all       5 Jun 20 10:19 test.txt

/var/www/html/pub/apictl:
total 12676
drwxrwsrw-. 2 gmichael all       61 Jun 19 16:40 .
drwxrwsrw-. 3 caddy    all       72 Jun 20 11:23 ..
-rwxrwsrw-. 1 caddy    all 12972351 Jun 19 16:35 apictl-4.4.0-linux-amd64.tar.gz
-rw-rw-r--. 1 gmichael all        5 Jun 20 10:19 test.txt

Viewing the website in my browser, I see the default view of the browse interface, 1 directory, 3 files, the filter box, the Name, Size, and Modified columns, and the apictl directory, caddy.log, caddy.log.1, and test.txt file.

Clicking into the apictl directory, I see no files, only the “Go Up” link to return to the parent directory.

After I apply world-execuable permissions to all files and directories under the root, I now see files under the apictl directory.

Logs after adding world-executable permissions:

{"level":"info","ts":1750436587.2622643,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/","headers":{"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"Connection":["keep-alive"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Cookie":[]}},"user_id":"","duration":0.00345438,"size":14383,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/html; charset=utf-8"]}}
{"level":"info","ts":1750436743.5482035,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/","headers":{"Connection":["keep-alive"],"Cache-Control":["max-age=0"],"Dnt":["1"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Cookie":[]}},"user_id":"","duration":0.002090611,"size":14833,"status":200,"resp_headers":{"Content-Type":["text/html; charset=utf-8"],"Server":["Caddy"]}}
{"level":"info","ts":1750436756.9400399,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52090","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/apictl/","headers":{"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Dnt":["1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Referer":["http://mn4s34385.nmdp.org:8080/"],"Accept-Encoding":["gzip, deflate"],"Accept-Language":["en-US,en;q=0.9"],"Cookie":[],"Connection":["keep-alive"]}},"user_id":"","duration":0.001431602,"size":13292,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/html; charset=utf-8"]}}
{"level":"info","ts":1750437370.7442422,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"10.20.147.66","remote_port":"52346","proto":"HTTP/1.1","method":"GET","host":"mn4s34385.nmdp.org:8080","uri":"/apictl/","headers":{"Connection":["keep-alive"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Upgrade-Insecure-Requests":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Accept-Language":["en-US,en;q=0.9"],"Cookie":[]}},"user_id":"","duration":0.004248998,"size":14236,"status":200,"resp_headers":{"Server":["Caddy"],"Content-Type":["text/html; charset=utf-8"]}}

3. Caddy version:

caddy version
v2.6.4

4. How I installed and ran Caddy:

a. System environment:

cat /etc/os-release
NAME="Rocky Linux"
VERSION="9.5 (Blue Onyx)"
ID="rocky"
ID_LIKE="rhel centos fedora"
VERSION_ID="9.5"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Rocky Linux 9.5 (Blue Onyx)"
ANSI_COLOR="0;32"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:rocky:rocky:9::baseos"
HOME_URL="https://rockylinux.org/"
VENDOR_NAME="RESF"
VENDOR_URL="https://resf.org/"
BUG_REPORT_URL="https://bugs.rockylinux.org/"
SUPPORT_END="2032-05-31"
ROCKY_SUPPORT_PRODUCT="Rocky-Linux-9"
ROCKY_SUPPORT_PRODUCT_VERSION="9.5"
REDHAT_SUPPORT_PRODUCT="Rocky Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.5"

uname -a
Linux mn4s34385.nmdp.org 5.14.0-503.33.1.el9_5.x86_64 #1 SMP PREEMPT_DYNAMIC Wed Mar 19 16:23:31 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux

dnf history info 99
Transaction ID : 99
Begin time     : Tue 03 Jun 2025 03:13:32 PM CDT
Begin rpmdb    : 75f720491d3c37c4b3236b46603750cb175c04df51bd8a6d78bf12028b36618d
End time       : Tue 03 Jun 2025 03:13:41 PM CDT (9 seconds)
End rpmdb      : 06392cc2838834306dce61da5712c6fb8ffacd41ee38ff2fddca725820ffc863
User           : Kyle Hakala <khakala>
Return-Code    : Success
Releasever     : 9
Command Line   : install caddy
Comment        :
Packages Altered:
    Install caddy-2.6.4-2.el9.x86_64             @epel
    Install rocky-logos-httpd-90.15-2.el9.noarch @rocky-9-appstream

b. Command:

sudo systemctl start caddy
sudo systemctl restart caddy
sudo caddy reload --config /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo caddy fmt --overwrite /etc/caddy/Caddyfile

c. Service/unit/compose file:

cat /usr/lib/systemd/system/caddy.service
# caddy.service
#
# For using Caddy with a config file.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy web server
Documentation=https://caddyserver.com/docs/
After=network.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStartPre=/usr/bin/caddy validate --config /etc/caddy/Caddyfile
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

d. My complete Caddy config:

cat /etc/caddy/Caddyfile
{
        debug
        admin :2019 {
                # origins http://*.nmdp.org http://10.20.0.0/16 http://10.151.0.0/16 http://localhost
                # enforce_origin
                # ideally I would like to limit sources to IPs and domains within the company
        }
        grace_period 10s
}

:80 {
        # Set this path to your site's directory.
        root * /usr/share/caddy/

        # Enable the static file server.
        file_server

        # Another common task is to set up a reverse proxy:
        # reverse_proxy localhost:8080

        # Or serve a PHP site through php-fpm:
        # php_fastcgi localhost:9000

        # Refer to the directive documentation for more options.
        # https://caddyserver.com/docs/caddyfile/directives
}

:8080 {
        root * /var/www/html/pub/
        file_server browse
        log {
                output file /var/www/html/pub/caddy.log {
                        roll_size 10mb
                        roll_keep 5
                        roll_local_time
                        mode 0755
                        format {
                                console
                                time_local
                                level debug
                        }
                }
        }
}

5. Links to relevant resources:

I have been digging through all the documentation found at the Caddy website: Welcome — Caddy Documentation

This isn’t a Caddy issue. On Linux, assuming SELinux isn’t enforced, a process needs execute (x) permission on a directory to access it.

If the folder is owned by the same user as the process, that user must have execute permission. If it’s owned by a different user, the process must either belong to the group that owns the folder (with execute permission for the group) or rely on execute permission being set for others (world).

In your case, Caddy runs as the caddy user and group, while the folder is owned by user gmichael and group all. Unless the caddy user is a member of the all group, the only option is to ensure execute permission is granted to others.

This is just standard Linux behaviour.

3 Likes

OK, that makes sense. When you sit and pound your head against the same problem for too long you can easily miss the simple stuff. No, we are not using SELinux, so it’s just a matter of forcing the user:group ownership to be caddy:caddy, and all should be right in the caddy world.

Thanks!

1 Like

That also means the files could be overwritten by caddy or something else running as caddy if there was a security exploit. I think it’d be preferable to have the files owned by someone else and either put caddy in the same group, or just make the files world readable. Unless you have local users who shouldn’t be able to read the files for some reason.

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