Skip to content

feat: add rootless image#428

Open
i5okie wants to merge 8 commits intocaddyserver:masterfrom
i5okie:feat/add-rootless-images
Open

feat: add rootless image#428
i5okie wants to merge 8 commits intocaddyserver:masterfrom
i5okie:feat/add-rootless-images

Conversation

@i5okie
Copy link

@i5okie i5okie commented Nov 19, 2025

Add rootless Docker images for enhanced security and Kubernetes/OpenShift compatibility

Purpose

This PR adds rootless variant of the Caddy Docker images that run as a non-root user (UID 1001), making them suitable for security-constrained environments like Kubernetes and OpenShift, while remaining a drop-in replacement for the alpine images.

Why rootless?

Security best practices: Running containers as root is discouraged in production environments. Non-root containers provide defense-in-depth by limiting the impact of potential container breakouts or exploits.

Kubernetes/OpenShift requirements: Many Kubernetes clusters enforce Pod Security Standards that prohibit root containers. OpenShift, in particular, assigns arbitrary UIDs to containers by default and requires image to support this pattern.

Port restrictions: Non-root users cannot bind to privileged ports (< 1024). Using ports 80 and 443 in Kubernetes is problematic anyway since:

  • Services and Ingress controllers handle external traffic routing
  • Internal container ports are mapped through service definitions
  • Non-privileged ports (8080, 8443) are the standard convention

What's included

New image variant:

  • caddy:rootless - Rootless runtime image (ports 8080, 8443, 2019)

Key features:

  • Runs as UID 1001 with GID 0 (root group) for OpenShift compatibility
  • Automatically uses ports 8080/8443 via environment variables
  • Drop-in replacement for the standard image - just change the tag
  • All directories writable by group 0 for arbitrary UID support
  • Uses the standard Caddyfile with automatic port substitution

Implementation details:
The rootless templates are based on the standard alpine templates with these modifications:

  • Removed setcap capability (not needed for non-privileged ports)
  • Added non-root user creation with adduser -D -u 1001 -g 0
  • Set proper ownership (chown 1001:0) and group permissions (chmod g+w) on all Caddy directories
  • Downloads the standard Caddyfile and uses sed to replace :80 with :{$CADDY_HTTP_PORT:8080}
  • Sets CADDY_HTTP_PORT=8080 and CADDY_HTTPS_PORT=8443 environment variables
  • Changed exposed ports from 80/443 to 8080/8443
  • Added USER 1001 directive to run as non-root
  • Users can mount custom Caddyfiles that reference these env vars or hardcode ports

Testing

Built and tested locally - serves the welcome page on port 8080 as expected. The image runs without root privileges and properly serves static content.

@i5okie
Copy link
Author

i5okie commented Nov 24, 2025

Is anyone available to take a look at this and offer feedback? There are several issues discussing the need for a rootless image.

Our team is working on several Hyperledger and OpenWallet Foundation projects utilizing Caddy server as a reverse proxy.
I've discovered that most of our deployments use older Caddy images and are working properly in a government maintained OpenShift platform. I'm working on creating helm charts for some of these projects. When attempting to use the latest caddy docker images, the deployments fail due to strict SCC rules in OpenShift. When developing helm charts which are meant to be used by the wider community, and be compatible with the government platform, using an initContainer to copy the binary into an arbitrary directory to bypass the issues, is not ideal. It does not look like a professional and robust solution to the problem. This PR should solve this issue. Not just for our projects, but for the wider community overall. And hopefully boost Caddy Server's use across the community :).

@francislavoie
Copy link
Member

Sorry for the wait. Thanks for this, I think it's the way we should go (separate tagged variant for rootless), rather than try to force the main one to support rootless (which is what prior discussion has always suggested).

There's still some things I think won't work well though, like the pki app trying to install its own root cert into /etc/ssl/certs which would require root. So that means if trying to use the rootless Caddy instance as an ACME server for mTLS, it wouldn't work well since it wouldn't trust connections made to servers which use a cert signed by Caddy's internal CA.

I want to get @hairyhenderson's thoughts on this before moving ahead with it though.

@hairyhenderson
Copy link
Contributor

Overall I'm supportive of this - @francislavoie and I have chatted about this and I'll let him make a few comments and get this merged. Thanks for your patience, @i5okie!

@i5okie i5okie changed the title feat: add rootless images feat: add rootless image Jan 13, 2026
@i5okie i5okie force-pushed the feat/add-rootless-images branch from caf8dc1 to d9685cb Compare January 15, 2026 18:50
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.

Thanks! Looks good to me. We'll need to generate the 2.11 ones as well (but I can do that after merge, that's fine)

Just as a last sanity check, @tianon do you have any objections to us adding another image variant for rootless usage? It's tricky with a webserver cause low ports as well as filesystem permissions, so I think this is the best of both worlds instead of trying to support both root and rootless in one image.

i5okie and others added 7 commits January 16, 2026 07:24
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
Signed-off-by: Ivan P <2119240+i5okie@users.noreply.github.com>
@francislavoie francislavoie force-pushed the feat/add-rootless-images branch from d9685cb to 7e724d4 Compare January 16, 2026 12:26
@francislavoie
Copy link
Member

I rebased on master, added 2.11, and added curl as a dep, and normalized the ENV syntax

@tianon
Copy link
Contributor

tianon commented Jan 23, 2026

My knee-jerk reaction is definitely that this is weird (both it being a whole separate variant and it being a complete copy not just a few configuration/metadata tweaks) 😅

However, I clearly need to dig in to the details more to make sure I understand how we ended up here so I can provide more meaningful thoughts 🙇 ❤️

@tianon
Copy link
Contributor

tianon commented Jan 24, 2026

Heh, moby/moby#8460 -- this issue haunts me forever 😂

The privileged ports problem in Docker itself was fixed in moby/moby#41030, way back in Docker 20.10 😅

See also kubernetes/kubernetes#102612 for where Kubernetes has spent a lot of time discussing this same change.

More relevantly, containerd enabled this by default in containerd/containerd#9348, which is containerd 2.0+ (and I believe that applies transitively to Kubernetes deployments using containerd also).

Regarding the filesystem access, saying that users who want to run as non-root need to deal with filesystem permissions is really reasonable, but pre-seeding with directories at mode 1777 is extremely reasonable, and I think mostly solves the problem in a way that's reasonably safe (as I noted over in #287 (comment) 😅).

Just to illustrate, here's an example of me running caddy:latest as UID 1234 and GID 5678 without really doing anything extra (and adding --security-opt no-new-privileges to make sure we're completely locked down and can't possibly be hitting setcap or anything):

$ docker run -it --rm --pull=always --user 1234:5678 --security-opt no-new-privileges --tmpfs /data:mode=1777 --tmpfs /config:mode=1777 caddy
latest: Pulling from library/caddy
Digest: sha256:2adb640cdc0ce1d8870887c30af1e21edfb3cdfd8433431b2a15f40119a7d654
Status: Image is up to date for caddy:latest
2026/01/23 23:43:19.569	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/01/23 23:43:19.569	INFO	GOMEMLIMIT is updated	{"package": "github.com/KimMachineGun/automemlimit/memlimit", "GOMEMLIMIT": 60335316172, "previous": 9223372036854775807}
2026/01/23 23:43:19.569	INFO	using config from file	{"file": "/etc/caddy/Caddyfile"}
2026/01/23 23:43:19.570	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/01/23 23:43:19.570	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//[::1]:2019", "//127.0.0.1:2019", "//localhost:2019"]}
2026/01/23 23:43:19.570	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/01/23 23:43:19.571	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/01/23 23:43:19.571	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/01/23 23:43:19.571	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/01/23 23:43:19.571	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0xc0003f5480"}
2026/01/23 23:43:19.571	INFO	autosaved config (load with --resume flag)	{"file": "/config/caddy/autosave.json"}
2026/01/23 23:43:19.571	INFO	serving initial configuration
2026/01/23 23:43:19.571	INFO	tls	cleaning storage unit	{"storage": "FileStorage:/data/caddy"}
2026/01/23 23:43:19.571	INFO	tls	finished cleaning storage units

@crntnvdl
Copy link

I think I got bitten by this one docker-library/php#1142 ?
Caddy runs without issues but for some reason the files created by caddy are 0600 instead of the expected 0644(I think?), when using docker exec -u 20669 ... sh (the same user the container is running with) and creating files from there the files have the correct permissions 0644

/data/caddy $ ls -la
total 28
drwx------    5 20669    root          4096 Jan 26 17:39 .
drwxrwxr-x    3 root     root          4096 Jan 26 17:35 ..
drwx------    3 20669    root          4096 Jan 26 17:26 certificates
-rw-------    1 20669    root            36 Jan 26 17:26 instance.uuid
-rw-------    1 20669    root           107 Jan 26 17:26 last_clean.json
drwx------    2 20669    root          4096 Jan 26 17:38 locks
drwx------    3 20669    root          4096 Jan 26 17:26 pki
/data/caddy $ touch test
/data/caddy $ ls -la
total 28
drwx------    5 20669    root          4096 Jan 26 17:39 .
drwxrwxr-x    3 root     root          4096 Jan 26 17:35 ..
drwx------    3 20669    root          4096 Jan 26 17:26 certificates
-rw-------    1 20669    root            36 Jan 26 17:26 instance.uuid
-rw-------    1 20669    root           107 Jan 26 17:26 last_clean.json
drwx------    2 20669    root          4096 Jan 26 17:38 locks
drwx------    3 20669    root          4096 Jan 26 17:26 pki
-rw-r--r--    1 20669    root             0 Jan 26 17:39 test
/data/caddy $ id
uid=20669 gid=0(root) groups=0(root)
/data/caddy $ umask
0022

Is this expected behavior ? I am still struggling wrapping my head around fs permissions with containers...

@i5okie
Copy link
Author

i5okie commented Jan 26, 2026

Thanks for digging into this @tianon.

The challenge with OpenShift is it assigns arbitrary UIDs at runtime, always with GID 0.
Your --tmpfs workaround works great for Docker, but in Kubernetes you'd need emptyDir volumes plus init containers or security contexts to set permissions. For helm charts meant to work across different platforms (including government OpenShift with strict SCCs), that adds complexity.

The rootless variant pre-configures group-writable directories for GID 0, similar to nginx-unprivileged, redis, etc. It just works out of the box.

@tianon
Copy link
Contributor

tianon commented Feb 4, 2026

@crntnvdl the answer to your umask question is probably best answered by the code in Caddy itself:

https://github.com/caddyserver/caddy/blob/e0f8d9b2047af417d8faf354b675941f3dac9891/caddy.go#L926

It sets mode explicitly to 0600 when creating at least instance.uuid, and likely for other files too, and umask is only subtractive from that, never additive (it can turn something like 0666 into 0600 but it will never turn 0600 into 0660, for example). 👍

@i5okie isn't managing deployment complexity the entire point of Helm charts in the first place? perhaps I'm misunderstanding, but using an initContainer, etc is a more secure practice / leads to a more secure deployment in all cases, not just in OpenShift/regulated environments 🙇 😅

That being said, some of that pain is mitigated by baking those permissions into the image itself, so I've suggested exactly that in #441 (which allows running Caddy as an arbitrary user without any custom mounts or binds).

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.

5 participants