Open polarathene opened 2 weeks ago
Sharing for reference to other users landing here that might be interested in technical details 😅 (not that relevant to resolving the slice request)
The lower-level useradd
command is already available via the passwd_bins
slice (13MB layer weight).
adduser
is dependent upon perl
to provide a more user-friendly frontend wrapper to useradd
. Thus this slice will add more weight.adduser
+ useradd
do differ in defaults (EDIT: Covered more thoroughly below)useradd
command package in other distros varies in package name (passwd
for Debian/Ubuntu, shadow
for Alpine, shadow-utils
for Fedora). Caveats:
adduser
by default via the busybox
package. This adduser
command differs in behaviour from the non-busybox version.adduser
command is a symlink to useradd
, thus it's misleading and expects useradd
args/flags.If choosing to symlink to busybox without ln
(coreutils_bins
slice), or a custom slice config, I have documented various ways that a Dockerfile
can approach this to minimize adding unnecessary image weight to peform a symlink.
EDIT: Busybox symlinks can be provisioned via busybox --install -s
, alternatively just prefix the command with busybox like busybox ls /
. Caution should be taken with busybox commands, while lightweight compared to coreutils they often have some differences like demonstrated with adduser
below, so keep that caveat in mind (date
and find
IIRC are known to have some drawbacks in functionality).
EDIT2: adduser
+ addgroup
are busybox commands that are not available from the busybox_bins
slice. The package is presumably not built with these applets trying to use them outputs the error adduser: applet not found
.
NOTE: Both useradd
and adduser
(including the BusyBox variant) will output an error when there is a conflict in user/group name or UID/GID assignment, but each vary in text description.
useradd
and both adduser
variantsadduser
on Fedora is actually useradd
adduser --group
and useradd --user-group
should be equivalent to infer the group name from the username provided, while useradd --group
instead expects input for an existing group.
This is not the case for Fedora which symlinks adduser
to useradd
:
# Fails because `adduser` is actually `useradd` on Fedora:
$ adduser --system --uid 500 --group --no-create-home clamav
useradd: group '--no-create-home' does not exist
useradd --shell /usr/sbin/nologin
with Fedora / chisel
Fedora useradd
emits a warning due to the shell choice below, despite it's intent and usage already existing in /etc/passwd
.
As chisel
has no slice to provide /usr/sbin/nologin
(approx 15KB in size), you would also get this warning but it is only for UX, so nothing to be concerned about.
$ useradd --system --uid 500 --user-group --no-create-home --shell /usr/sbin/nologin clamav
useradd: Warning: missing or non-executable shell '/usr/sbin/nologin'
# Fedora doesn't include the executable:
# Symlinks: `/sbin/` -> `/usr/sbin/` -> `/bin/` -> `/usr/bin/`
$ docker run --rm -it fedora ls -l /usr/sbin/nologin
ls: cannot access '/usr/sbin/nologin': No such file or directory
# Other distros do:
# Alpine (_configures for `/sbin/nologin` instead_):
# NOTE: `/usr/sbin/` exists but is distinct, no symlink
$ docker run --rm -it alpine /sbin/nologin
This account is not available
# Debian:
# Symlinks: `/sbin` -> `/usr/sbin/` (distinct from `/bin` -> `/usr/bin/`)
$ docker run --rm -it debian:12-slim /usr/sbin/nologin
nologin: Attempted login by UNKNOWN (UID: 0) on /dev/pts/0
This account is currently not available.
NOTE: Unlike adduser --system
, --shell /usr/sbin/nologin
(or equivalent) is not inferred with the useradd
command. useradd
must instead explicitly opt-out as shown above with --shell
.
The default login shell to assign when not using --system
or --disabled-login
(not available for busybox adduser
) will differ too (/bin/bash
for Debian, /bin/sh
for Alpine).
There are some relevant configs related to shell default:
/etc/shells
lists valid login shells./etc/default/useradd
on Fedora configures SHELL=/bin/bash
while Debian configures SHELL=/bin/sh
/etc/adduser.conf
for configuring a default shell override. It's unclear where the actual shell default is set 😓adduser
default output verbosity vs useradd
adduser
will output a bit more information unless using --quiet
.
# NOTE: `/nonexistent` is the default home path, add `--home /path-here` to change.
# All of this output is omitted if using `--quiet`:
$ adduser --system --uid 500 --group --no-create-home clamav
Adding system user `clamav' (UID 500) ...
Adding new group `clamav' (GID 500) ...
Adding new user `clamav' (UID 500) with group `clamav' ...
Not creating `/nonexistent'.
Even without --quiet
, you will not always get informed about failure to create an account. Such as when creating without --system
:
# This only creates a group, the lack of other output shown earlier indicates no user created:
$ adduser --uid 500 --group --no-create-home clamav
Adding group `clamav' (GID 1000) ...
Done.
Instead you would need to swap --group
for --ingroup clamav
or --gid 500
, with that group already created (addgroup --gid 500 clamav
).
Additionally to avoid interactive prompts for input:
--disabled-password
, or via --disabled-login
to infer /usr/sbin/nologin
.--comment
or --gecos
When not using --system
, it is implied a non-system user/group will have UID/GID at 1000
or above (may not be enforced, but a check may emit a warning when omitting the --system
flag, likewise for non-system UID/GID range when using the --system
flag):
# Without `--system`:
# - A home directory at `/home/<username>` is created.
# - A warning is emitted from the `useradd` wrapped call due to using system UID/GID range (<1000).
# Without `--gid` or `--ingroup`, the `clamav` group would be created implicitly for a GID equivalent to UID provided
$ addgroup --quiet --gid 500 clamav
$ adduser --quiet --uid 500 --gid 500 --disabled-password --comment 'ClamAV User' clamav
useradd warning: clamav's uid 500 outside of the UID_MIN 1000 and UID_MAX 60000 range.
# Busybox `adduser` equivalent:
# (no `--quiet`, `--gid` or `--comment`, use `--ingroup` / `-G` and `--gecos`)
$ addgroup --quiet --gid 500 clamav
$ adduser --uid 500 --ingroup clamav --disabled-password --gecos 'ClamAV User' clamav
The comment / gecos field value stored in /etc/passwd
will differ between the two adduser
commands.
,,,
appendedadduser
option --comment
/ --gecos
(both function the same) can provide a value using the ,
delimiters, which adds content between the extra ,
that were otherwise tacked on.For --system
users:
adduser
=> /nonexistent
(only the non-busybox variant)useradd
=> /home/<username>
useradd
always defaults to /home/<username>
, even for system accounts.
/home
portion is configured at /etc/default/useradd
. adduser
has an equivalent DHOME
at /etc/adduser.conf
(probably not relevant to busybox variant?)./var/lib/<username>
, or as Debian does with /nonexistent
when paired with /usr/sbin/nologin
for the login shell.Debian adduser
/nonexistent
path config locations:
/etc/login.defs
does have a NONEXISTENT
setting which in Debian is configured for /nonexistent
.adduser
home location fallback value is hard-coded into the adduser
(and addgroup
) script itself (/usr/sbin/adduser
).adduser
with --system
implies --no-create-home
+ --home /nonexistent
(unless you explicitly set a different home path, which will then assume you want that home created).
--system
will also default the group to nogroup
(65534
). Hence --group
flag being useful with --system
.--system
, setting the group name to the user name will be implicit, thus --group
provides no benefit.adduser
differs, no --group
option, and --system
does not imply --no-create-home
, nor is the default home set to /nonexistent
.Debian and Fedora differ by useradd
behaviour slightly too.
--system
, Fedora and Debian both do not create a home directory (even when one is set explicitly and is non-default)--system
, Fedora does create a home directly implicitly by default. Debian does not. You must manually opt-in via --create-home
.0700
, while Debian creates with 0755
(standard umask). Unclear where the 0700
is configured./var/spool/mail/<username>
due to CREATE_MAIL_SPOOL=yes
in /etc/default/useradd
(Debian instead leaves these at defaults except for setting SHELL=/bin/sh
).adduser
vs useradd
useradd
with --user-group
will create a group with the equivalent user name if one doesn't already exist (otherwise fails with error).
adduser
would instead fail with an error when the equivalent GID is already in use (using equivalent --group
, or omission when not using --system
).useradd
without --system
will still imply --user-group
, unlike adduser
that instead defaults to nogroup
/ (65534
on Debian, 65533
on Alpine).adduser
BusyBox variant differencesThe Alpine / BusyBox adduser
command is more limited in supported options/flags:
--disabled-login
. You'll instead need to be explicit with --shell /sbin/nologin
or use --system
(that implies this nologin
"shell")--group
for implicit creation. Instead you need a group to exist already, and to reference that via --ingroup
/ -G
:
addgroup --gid 500 clamav
adduser --system --uid 500 --ingroup clamav --no-create-home clamav
Alternatively: You can omit --ingroup
and it will expect to create a group with the same user name and a GID matching the UID.
--system
, then it will default to nogroup
/65534
like it's non-busybox variant.adduser
, but the error message will incorrectly state the uid
is in use, not the gid
.Compared to the adduser
(non-BusyBox variant) when using --system
:
/home/<username>
(like with useradd
), not /nonexistent
.--no-create-home
.chmod 2775
/ chmod g+s
) when creating a home directory.There may be other differences in support/behaviour. Thus it's usually better to more explicit than relying on implicit functionality.
The following should work across both adduser
variants:
addgroup --gid 500 clamav
# `--no-create-home` needs to be explicit for BusyBox, otherwise implied by `--system`:
adduser --system --uid 500 --ingroup clamav --no-create-home clamav
# Equivalent without `--system`?:
# `--disabled-password` + `--gecos` to avoid prompting when used without `--system`:
# `--shell` default varies, for no login shell is `/sbin/nologin` or `/user/sbin/nologin`,
# but as it is to disable login, an invalid path works fine (without user friendly error message)
adduser --disabled-password --shell /sbin/nologin --gecos 'ClamAV User' --uid 500 --ingroup clamav --no-create-home clamav
NOTE: The help output from the adduser
command on Alpine does not show supported long option names, only short. Long option names are supported (-G
vs --ingroup
).
/etc/{group,passwd,shadow}
filesWhile the useradd
and two adduser
commands can be easy to get muddled up in behaviour, they all respect the same format for these config files. For a container you could just add these files yourself or append your own users/groups as needed directly.
# clamav:clamav (user:group => 500:500)
echo 'clamav:!::0:::::' >> /etc/shadow
echo 'clamav:x:500:clamav' >> /etc/group
echo 'clamav:x:500:500:ClamAV:/var/lib/clamav:/sbin/nologin' >> /etc/passwd
# Optional (present on Debian and Fedora, not Alpine):
echo 'clamav:*::' >> /etc/gshadow
# Optionally create a home directory like the `adduser` command would:
# NOTE: Only busybox `adduser` sets SGID, otherwise is `--mode 0775`
# NOTE: On Fedora with `useradd` permissions are instead `0700`
install --directory --mode 2775 --owner clamav --group clamav /var/lib/clamav
That additionally avoids creating the backup files the commands create (with a -
suffix).
Here is a more verbose version with some variables for those unfamiliar with the format of each entry in those files:
# syntax=docker/dockerfile:1
FROM alpine
RUN <<HEREDOC
export \
WITH_USER=clamav \
WITH_GROUP=clamav \
WITH_UID=500 \
WITH_GID=500 \
WITH_GECOS=ClamAV \
WITH_HOME=/var/lib/clamav \
WITH_SHELL=/sbin/nologin
# The files that would get populated for a system user (no login or other attribtues)
echo "${WITH_USER}:!::0:::::" >> /etc/shadow
echo "${WITH_GROUP}:x:${WITH_GID}:${WITH_USER}" >> /etc/group
echo "${WITH_USER}:x:${WITH_UID}:${WITH_GID}:${WITH_GECOS}:${WITH_HOME}:${WITH_SHELL}" >> /etc/passwd
# Optionally create a home directory like the `adduser` command would:
# NOTE: Only busybox `adduser` sets SGID, otherwise is `-m 0775`
install -d -m 2775 -o "${WITH_USER}" -g "${WITH_GROUP}" "${WITH_HOME}"
HEREDOC
Both echo
(would also be available via a shell built-in) and install
could technically be used via the busybox_bins
slice (5.7MB layer weight).
The equivalent install
command (_available via the coreutils_bins
slice, 12MB layer weight_) is a simpler way to create the directory with ownership and setgid-bit (aka SGID, files created in that directory will default to matching the group ownership of the directory, not the user invoking the creation command which user ownership will be assigned to):
mkdir -p "${WITH_HOME}"
chmod 2775 "${WITH_HOME}"
chown "${WITH_USER}:${WITH_GROUP}" "${WITH_HOME}"
The equivalent with busybox adduser
commands:
# syntax=docker/dockerfile:1
FROM alpine
RUN <<HEREDOC
addgroup --gid 500 clamav
adduser --system --gecos clamav --home /var/lib/clamav --ingroup clamav --uid 500 clamav
# Optional - Cleanup /etc/shadow entry by stripping away redundant metadata:
# (minimize the entry similar to better match format of pre-existing system users)
apk add --no-cache shadow
chage --lastday -1 --maxdays -1 --warndays -1 clamav
apk del shadow
# Optional - Cleanup backup copies created:
rm /etc/group- /etc/passwd- /etc/shadow-
HEREDOC
NOTE: The /etc/shadow
management doesn't seem relevant for containers:
chage
slice is available via the passwd_bins
slice, along with useradd
+ groupadd
commands./etc/group
+ /etc/passwd
are provided via base-passwd_data
slice.
/etc/shadow
/ /etc/gshadow
, but you shouldn't need those either in a typical container 🤷♂gcr.io/distroless/static-debian12
image from Google Distroless also only populates /etc/group
+ /etc/passwd
, no /etc/{gshadow,shadow}
files 👍If you have a minimal image without slices already added (_such as libc6_libs
that provides 4.9MB of the layer weight from mentioned slices_), then adding the slices for commands to add a new user may be something you want to avoid.
You could manage via a separate image stage and use COPY
, or you can use COPY
with inline file content (if you don't want to COPY
from an external file or from an earlier stage):
# Add user:group => clamav(500):clamav(500)
COPY <<HEREDOC /etc/passwd
clamav:x:500:500:ClamAV:/var/lib/clamav:/sbin/nologin
HEREDOC
# Technically only required if you actually need group ownership,
# You could instead have `/etc/passwd` map to an existing GID of /etc/group
COPY <<HEREDOC /etc/group
clamav:x:500:clamav
HEREDOC
# Not needed - See notes below
# NOTE: This file ownership and permissions vary by distro
# Debian `root:shadow` + `0640`, Fedora `root:root` + `000`, Alpine `root:shadow` + `0640`
COPY <<HEREDOC /etc/shadow
clamav:!::0:::::
HEREDOC
# Optionally create a home directory:
# (USER will set the ownership, permissions are 0755)
USER 500:500
WORKDIR /var/lib/clamav
# Reset back USER and WORKDIR:
USER 0:0
WORKDIR /
When using that approach, you'll want to include the base users/groups like root
+ nobody
/nogroup
in the file content too.
# NOTE: You can use build args within the file content if you need extra flexibility:
ARG NEW_USER=clamav
COPY <<HEREDOC /etc/passwd
root:x:0:0:root:/root:/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
${NEW_USER}:x:500:500:ClamAV:/var/lib/${NEW_USER}:/sbin/nologin
HEREDOC
COPY <<HEREDOC /etc/group
root:x:0:
nobody:x:65534:
tty:x:5:
${NEW_USER}:x:500:
HEREDOC
# Optionally create home dir:
RUN --mount=type=bind,from=busybox:latest,source=/bin,target=/bin \
install --directory --mode 2775 --owner clamav --group clamav /var/lib/clamav
You could use that RUN
approach for other busybox commands like adduser
/addgroup
. The busybox
image does differ a little from the Alpine busybox build IIRC, it's also dynamically linked to libc, so just mounting /bin
like above may not always be compatible.
A better approach may be to just have a separate stage and COPY
the changes over (assumes an official chisel
image has been published):
FROM chisel AS base
RUN --mount=type=cache,target=/.cache/chisel,id="chisel-cache",sharing=locked <<HEREDOC
chisel cut --release ubuntu-24.04 --root / passwd_bins
useradd --system --uid 500 --user-group --create-home --home /var/lib/clamav --shell /usr/sbin/nologin clamav
HEREDOC
FROM scratch
COPY --link --from=base /etc/group /etc/group
COPY --link --from=base /etc/passwd /etc/passwd
COPY --link --from=base /var/lib/clamav /var/lib/clamav
That won't work with chisel
until https://github.com/canonical/chisel/issues/221 is resolved. You could do the same with Alpine though:
FROM alpine AS base
COPY --link --from=gcr.io/distroless/static-debian12 /etc/passwd /etc/passwd
COPY --link --from=gcr.io/distroless/static-debian12 /etc/group /etc/group
RUN apk add shadow \
&& useradd --system --uid 500 --user-group --create-home --home /var/lib/clamav --shell /usr/sbin/nologin clamav
# Use `chisel` here:
FROM scratch
COPY --link --from=base /etc/group /etc/group
COPY --link --from=base /etc/passwd /etc/passwd
# `--chown` seems required (even without --link) it fails to preserve the ownership otherwise?
# `--chmod` is not applying any change and defaulting to 755, may be a bug/regression with `docker build`?
COPY --link --from=base --chown=500:500 --chmod=700 /var/lib/clamav /var/lib/clamav
For gcr.io/distroless/static-debian12
the image, these are the /etc/passwd
and /etc/group
contents (configured here for bazel):
# /etc/passwd
root:x:0:0:root:/root:/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
nonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin
# /etc/group
root:x:0:
nobody:x:65534:
tty:x:5:
staff:x:50:
nonroot:x:65532:
These extra groups tty
and staff
are included due to filesystem assignment from the installed base-files
packages that provides their image base layer (staff
=> /var/local
(empty), tty
=> /dev/console
/dev/pts/0
).
/var/local
created from the package base-files
is not part of the base-files_var
slice_), so no need for it.chisel
and the Google distroless image, the /dev
dir is populated when shelling into the container. chisel
with just this slice on a scratch base won't have /dev
in the image at all, while it's an empty dir for Google distroless.The nonroot
user technically shouldn't be there, but is to keep their build process simple. Sidenote: /sbin/nologin
is used instead of /usr/sbin/nologin
that the official Debian image uses, no binary provided.
You generally want the root
+ nobody
users/groups at a minimum (Official Debian has a nogroup
instead of nobody
group).
For reference this is what the base-passwd_data
slice (2.1KB layer weight) installs for the equivalent files:
EDIT: Looks like this will be resolved by: https://github.com/canonical/chisel-releases/pull/305
For reference, the
busybox
slice should technically also be able to provideadduser
. Alpine symlinks/usr/sbin/adduser
to/bin/busybox
for example.Similar to https://github.com/canonical/chisel-releases/issues/533 this slice had been contributed initially as part of the PR for another slice but was later removed.