Serve Private Files via Caddy Without Opening Permissions — A 4-Line Bind Mount Trick
I needed Caddy to serve private files without world-readable permissions. The fix: a 4-line fstab entry.
The Challenge: Caddy Can't Read chmod 750 Directories
Caddy runs as uid 995 on our Tacavar infrastructure. It needs to serve video files from /home/tacavar/Desktop/ — a directory with chmod 750 permissions. That means only the owner and group can read or traverse it. Caddy, being outside that group, hits a permission denied on the parent directory. The naive fix is to chmod the home directory to 755, but that exposes every file in /home/tacavar/ to any process on the system. Running Caddy as root is worse — it violates the principle of least privilege and opens attack surface. Copying files to a public directory duplicates data, wastes disk, and introduces sync lag. We needed a way to serve files from a private location without compromising security or duplicating data.
Why Symlinks Don't Work
Symlinks seem like an obvious solution: create a symlink from /var/www/videos to /home/tacavar/Desktop/. But Caddy doesn't follow symlinks blindly — it resolves the full path. When Caddy tries to access /var/www/videos, it follows the symlink to /home/tacavar/Desktop/, then attempts to traverse /home/, /home/tacavar/, and finally /home/tacavar/Desktop/. Each parent directory is checked for execute permission. Since /home/tacavar/ is chmod 750 and Caddy isn't the owner or in the group, it fails at the first step. Symlinks are just pointers; they don't bypass the Linux filesystem's permission model. The kernel enforces permissions on the real path, not the symlink path. So symlinks are useless here.
The Bind Mount Solution: /etc/fstab to the Rescue
Bind mounts solve this by creating an alternative path to the same inodes, but with a different parent hierarchy. The trick is a 4-line entry in /etc/fstab:
/home/tacavar/Desktop /var/www/videos none bind 0 0
After adding that, run mount -a to activate it. Now /var/www/videos points to the exact same files as /home/tacavar/Desktop/ — same inodes, same data, zero copy. But the path Caddy traverses is /var/www/ → /var/www/videos, and /var/www/ has permissions that allow Caddy (uid 995) to read it. The bind mount effectively creates a new entry in the filesystem tree, so the permission check starts from the mount point, not the original home directory. Caddy can now serve files from /var/www/videos without ever needing access to /home/tacavar/. The original directory remains private. This is a pure Linux filesystem feature — no symlink, no copy, no permission change.
Security Without Data Duplication
This approach keeps the home directory locked down at chmod 750. No other process can read Tacavar's private files. Caddy only sees what's exposed through the bind mount. If a vulnerability in Caddy allows path traversal, the attacker is confined to /var/www/videos — they can't escape to the home directory because the bind mount is a separate mount point. There's no data duplication: files written by Hermes (our content pipeline) to /home/tacavar/Desktop/ appear instantly at hub.tacavar.com/videos/. No sync scripts, no cron jobs, no wasted disk. The bind mount respects the original file permissions within the mounted directory — if a file is chmod 600, Caddy still can't read it unless its uid matches. But the directory itself is accessible. This is a clean, minimal-infrastructure pattern that any DevOps practitioner can implement in minutes.
Production Lessons for File Serving Patterns
We've learned several lessons from this pattern. First, always audit your Caddyfile to ensure the served path matches the bind mount target. Second, use mount --bind in your deployment scripts to make the setup repeatable. Third, document the fstab entry so future operators understand why /var/www/videos exists. Fourth, consider using bind mounts for any scenario where a service needs access to a subset of files from a restricted directory — not just Caddy, but also Nginx, Apache, or any application server. This is a core DevOps pattern for secure file serving. At Tacavar, we apply this across multiple services to maintain strict file permission boundaries without sacrificing performance or simplicity. The bind mount trick eliminates the trade-off between security and convenience.
Explore Tacavar's devops patterns for secure, minimal-infrastructure deployments at tacavar.com.