Skip to content

Allow the image to be run as any arbitrary UID/GID#441

Merged
francislavoie merged 1 commit intocaddyserver:masterfrom
tianon:chmod-1777
Feb 4, 2026
Merged

Allow the image to be run as any arbitrary UID/GID#441
francislavoie merged 1 commit intocaddyserver:masterfrom
tianon:chmod-1777

Conversation

@tianon
Copy link
Contributor

@tianon tianon commented Feb 4, 2026

Namely, this sets the runtime-mutable directory permissions to 1777 (matching the notable/venerable /tmp) -- this allows them to be writable by anyone, but files created are owned by the user who creates them.

In specific, the two directories this changes are /config/caddy and /data/caddy because those appear to be the only two that Caddy tries to write to at runtime by default (and thus needs access to create files/folders it can then own within).

Prior to this change, I could run Caddy as an arbitrary user by getting "clever" with --tmpfs mounts in the container, but with this change, I can run it much more simply.

Failing without mounts/permissions:

$ docker run -it --rm --user 1234:5678 --security-opt no-new-privileges caddy
2026/02/04 18:49:39.972	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/02/04 18:49:39.972	INFO	GOMEMLIMIT is updated	{"package": "github.com/KimMachineGun/automemlimit/memlimit", "GOMEMLIMIT": 60335312486, "previous": 9223372036854775807}
2026/02/04 18:49:39.972	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/02/04 18:49:39.973	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/02/04 18:49:39.973	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/02/04 18:49:39.974	WARN	http.auto_https	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv0", "http_port": 80}
2026/02/04 18:49:39.974	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:49:39.974	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:49:39.974	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/02/04 18:49:39.974	ERROR	unable to autosave config	{"file": "/config/caddy/autosave.json", "error": "open /config/caddy/autosave.json: permission denied"}
2026/02/04 18:49:39.974	INFO	serving initial configuration
2026/02/04 18:49:39.974	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc000183480"}
2026/02/04 18:49:39.974	WARN	tls	unable to get instance ID; storage clean stamps will be incomplete	{"error": "open /data/caddy/instance.uuid: permission denied"}
2026/02/04 18:49:39.974	ERROR	tls	could not clean default/global storage	{"error": "unable to acquire storage_clean lock: creating lock file: open /data/caddy/locks/storage_clean.lock: no such file or directory"}
2026/02/04 18:49:39.974	INFO	tls	finished cleaning storage units

Succeeding with permissions (this change):

$ docker run -it --rm --user 1234:5678 --security-opt no-new-privileges sha256:359f2a56ad454e5516c5473a9b9203c70b8801a9e2405d5e883e3c8e1c55da58
2026/02/04 18:50:28.491	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/02/04 18:50:28.491	INFO	GOMEMLIMIT is updated	{"GOMEMLIMIT": 60335312486, "previous": 9223372036854775807}
2026/02/04 18:50:28.491	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/02/04 18:50:28.491	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/02/04 18:50:28.492	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/02/04 18:50:28.492	WARN	http.auto_https	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv0", "http_port": 80}
2026/02/04 18:50:28.492	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:50:28.492	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:50:28.492	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/02/04 18:50:28.492	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc00050fb00"}
2026/02/04 18:50:28.497	INFO	autosaved config (load with --resume flag)	{"file": "/config/caddy/autosave.json"}
2026/02/04 18:50:28.497	INFO	serving initial configuration
2026/02/04 18:50:28.501	INFO	tls	cleaning storage unit	{"storage": "FileStorage:/data/caddy"}
2026/02/04 18:50:28.503	INFO	tls	finished cleaning storage units

Alternative to #428 (see especially #428 (comment))

Namely, this sets the runtime-mutable directory permissions to 1777 (matching the notable/venerable `/tmp`) -- this allows them to be writable by anyone, but files created are owned by the user who creates them.

In specific, the two directories this changes are `/config/caddy` and `/data/caddy` because those appear to be the only two that Caddy tries to write to at runtime by default (and thus needs access to create files/folders it can then own within).

Prior to this change, I could run Caddy as an arbitrary user by getting "clever" with `--tmpfs` mounts in the container, but with this change, I can run it much more simply.

Failing without mounts/permissions:

```console
$ docker run -it --rm --user 1234:5678 --security-opt no-new-privileges caddy
2026/02/04 18:49:39.972	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/02/04 18:49:39.972	INFO	GOMEMLIMIT is updated	{"package": "github.com/KimMachineGun/automemlimit/memlimit", "GOMEMLIMIT": 60335312486, "previous": 9223372036854775807}
2026/02/04 18:49:39.972	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/02/04 18:49:39.973	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/02/04 18:49:39.973	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/02/04 18:49:39.974	WARN	http.auto_https	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv0", "http_port": 80}
2026/02/04 18:49:39.974	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:49:39.974	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:49:39.974	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/02/04 18:49:39.974	ERROR	unable to autosave config	{"file": "/config/caddy/autosave.json", "error": "open /config/caddy/autosave.json: permission denied"}
2026/02/04 18:49:39.974	INFO	serving initial configuration
2026/02/04 18:49:39.974	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc000183480"}
2026/02/04 18:49:39.974	WARN	tls	unable to get instance ID; storage clean stamps will be incomplete	{"error": "open /data/caddy/instance.uuid: permission denied"}
2026/02/04 18:49:39.974	ERROR	tls	could not clean default/global storage	{"error": "unable to acquire storage_clean lock: creating lock file: open /data/caddy/locks/storage_clean.lock: no such file or directory"}
2026/02/04 18:49:39.974	INFO	tls	finished cleaning storage units
```

Succeeding with permissions (this change):

```console
$ docker run -it --rm --user 1234:5678 --security-opt no-new-privileges sha256:359f2a56ad454e5516c5473a9b9203c70b8801a9e2405d5e883e3c8e1c55da58
2026/02/04 18:50:28.491	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/02/04 18:50:28.491	INFO	GOMEMLIMIT is updated	{"GOMEMLIMIT": 60335312486, "previous": 9223372036854775807}
2026/02/04 18:50:28.491	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/02/04 18:50:28.491	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/02/04 18:50:28.492	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/02/04 18:50:28.492	WARN	http.auto_https	server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server	{"server_name": "srv0", "http_port": 80}
2026/02/04 18:50:28.492	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:50:28.492	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/04 18:50:28.492	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/02/04 18:50:28.492	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc00050fb00"}
2026/02/04 18:50:28.497	INFO	autosaved config (load with --resume flag)	{"file": "/config/caddy/autosave.json"}
2026/02/04 18:50:28.497	INFO	serving initial configuration
2026/02/04 18:50:28.501	INFO	tls	cleaning storage unit	{"storage": "FileStorage:/data/caddy"}
2026/02/04 18:50:28.503	INFO	tls	finished cleaning storage units
```
@francislavoie
Copy link
Member

francislavoie commented Feb 4, 2026

Technically /data/caddy and /config/caddy are created by the caddy process on-the-fly, but /data and /config are the actual XDG_DATA_HOME and XDG_CONFIG_HOME respectively (as per https://caddyserver.com/docs/conventions#data-directory). Wouldn't this require a mkdir /data/caddy && mkdir /config/caddy? Would setting /data to 1777 work too or does it have to be /data/caddy ? Edit: derp, we do mkdir those already, my bad.

Copy link
Member

@francislavoie francislavoie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me.

Of course this would only fix fresh deployments, wouldn't fix existing named volumes if the user tries to switch from root to a different UID. But not much we can do about that anyway.

@francislavoie francislavoie merged commit 272e3f8 into caddyserver:master Feb 4, 2026
2 checks passed
@tianon tianon deleted the chmod-1777 branch February 5, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants