From 949ba4c263e1f9908b0d4fec72a9048064d2b994 Mon Sep 17 00:00:00 2001 From: Bailey Jayes Date: Wed, 2 Sep 2026 17:23:51 +0000 Subject: [PATCH] mkctr: add --user flag Images built with `mkctr` run as the user configured in the base image used for the build, which for typical bases such as alpine is root. Add a --user flag that sets User in the OCI image config ("uid" or "uid:gid"), so callers can more easily build images that run as non-root users. Signed-off-by: Bailey Jayes --- README.md | 8 ++++++++ mkctr.go | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/README.md b/README.md index d1573c3..56df622 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,18 @@ mkctr \ --repos="tailscale/tailscale" \ [--files=foo.txt:/var/lib/foo.txt,bar.txt:/var/lib/bar.txt] \ [--target=] \ # e.g. flyio, local + [--user=1000:1000] \ # user (uid[:gid]) to run the container as [--push] \ [--] [...] ``` +By default the container runs as the base image's user (for most base images, +root). Use `--user` to set the `User` in the image config, e.g. +`--user=1000:1000` to run as UID 1000, GID 1000. Note that the image's +filesystem is not otherwise modified: files and directories added by `mkctr` +are owned by root, so a non-root `--user` can only write to world-writable +paths (such as `/tmp`) or volumes mounted at runtime. + `mkctr` auto discovers `GOOS`/`GOARCH` from the specified base image. If the base image supports multiple platforms, binaries are compiled for each platform as long as it's one of `linux/amd64`, `linux/386`, `linux/arm`, `linux/arm64`. Multi-arch base image must be either an [OCI image index](https://github.com/opencontainers/image-spec/blob/main/image-index.md) or [Docker manifest list](https://github.com/openshift/docker-distribution/blob/master/docs/spec/manifest-v2-2.md#manifest-list). `mkctr` produces image of the same media type as the base image and uses the media type of the base image, or of the individual image references in case of a multi-arch image, to determine the media type of the layer it builds. diff --git a/mkctr.go b/mkctr.go index 6e9d377..86a3882 100644 --- a/mkctr.go +++ b/mkctr.go @@ -97,6 +97,7 @@ type buildParams struct { annotations map[string]string // OCI image annotations volumes map[string]struct{} envVars []string // Environment variables to add to image config + user string // User ("uid" or "uid:gid") to set in the image config } func main() { @@ -118,6 +119,7 @@ func main() { For an image index (a multi-platform manifest list) annotations will get added to each image manifest as well as the image index. Annotations with empty values are not supported.`) envArg = flag.String("env", "", "comma-separated list of environment variables in KEY=value form to add to the image config") + user = flag.String("user", "", `user to run the container as, in "uid" or "uid:gid" form; sets the image config User. If unset, the base image's user (often root) is retained`) ) flag.Parse() if *tagArg == "" { @@ -173,6 +175,7 @@ func main() { annotations: parseAnnotations(*annotations), volumes: vols, envVars: parseEnv(*envArg), + user: *user, } if err := fetchAndBuild(bp); err != nil { @@ -267,6 +270,10 @@ func fetchAndBuild(bp *buildParams) error { if err != nil { return err } + img, err = applyUser(img, bp.user) + if err != nil { + return err + } if !bp.publish { logf("not pushing") return nil @@ -334,6 +341,11 @@ func fetchAndBuild(bp *buildParams) error { return err } + img, err = applyUser(img, bp.user) + if err != nil { + return err + } + if bp.volumes != nil { img, err = mutateConfig(img, func(c *v1.Config) error { c.Volumes = bp.volumes @@ -720,6 +732,19 @@ func applyEnvVars(img v1.Image, newEnvVars []string) (v1.Image, error) { }) } +// applyUser sets the user (and optionally group) that the container runs as +// in the image config, overriding any user set by the base image. An empty +// user leaves the base image's user untouched. +func applyUser(img v1.Image, user string) (v1.Image, error) { + if user == "" { + return img, nil + } + return mutateConfig(img, func(c *v1.Config) error { + c.User = user + return nil + }) +} + // mutateConfig returns img with its config mutated by f. // // The pointer given to f is a deep copy of the existing config,