canonical / chisel-releases

42 stars 58 forks source link

Add `adduser` slice #549

Open polarathene opened 2 weeks ago

polarathene commented 2 weeks ago

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 provide adduser. 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.

package: adduser

essential:
  - adduser_copyright

slices:
  bins:
    essential:
      - adduser_config
      - adduser_modules
      - passwd_bins
    contents:
      /usr/sbin/addgroup:
      /usr/sbin/adduser:
      /usr/sbin/delgroup:
      /usr/sbin/deluser:

  modules:
    contents:
      /usr/share/perl5/Debian/AdduserCommon.pm:
      /usr/share/perl5/Debian/AdduserLogging.pm:
      /usr/share/perl5/Debian/AdduserRetvalues.pm:

  config:
    contents:
      /etc/adduser.conf:
      /etc/deluser.conf:

  copyright:
    contents:
      /usr/share/doc/adduser/copyright:
polarathene commented 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).

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.


Reference - Differences between useradd and both adduser variants

adduser 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:

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:

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.

Default home

For --system users:

useradd always defaults to /home/<username>, even for system accounts.

Debian adduser /nonexistent path config locations:

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).

Debian and Fedora differ by useradd behaviour slightly too.

Implicit group creation - 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 BusyBox variant differences

The Alpine / BusyBox adduser command is more limited in supported options/flags:

Compared to the adduser (non-BusyBox variant) when using --system:

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).


Alternative - Add directly to /etc/{group,passwd,shadow} files

While 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:


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).

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:

/etc/passwd + /etc/group (click to view) ``` # /etc/passwd root:*:0:0:root:/root:/bin/bash daemon:*:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:*:2:2:bin:/bin:/usr/sbin/nologin sys:*:3:3:sys:/dev:/usr/sbin/nologin sync:*:4:65534:sync:/bin:/bin/sync games:*:5:60:games:/usr/games:/usr/sbin/nologin man:*:6:12:man:/var/cache/man:/usr/sbin/nologin lp:*:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:*:8:8:mail:/var/mail:/usr/sbin/nologin news:*:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:*:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:*:13:13:proxy:/bin:/usr/sbin/nologin www-data:*:33:33:www-data:/var/www:/usr/sbin/nologin backup:*:34:34:backup:/var/backups:/usr/sbin/nologin list:*:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:*:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:*:42:65534::/nonexistent:/usr/sbin/nologin nobody:*:65534:65534:nobody:/nonexistent:/usr/sbin/nologin # /etc/group root:*:0: daemon:*:1: bin:*:2: sys:*:3: adm:*:4: tty:*:5: disk:*:6: lp:*:7: mail:*:8: news:*:9: uucp:*:10: man:*:12: proxy:*:13: kmem:*:15: dialout:*:20: fax:*:21: voice:*:22: cdrom:*:24: floppy:*:25: tape:*:26: sudo:*:27: audio:*:29: dip:*:30: www-data:*:33: backup:*:34: operator:*:37: list:*:38: irc:*:39: src:*:40: shadow:*:42: utmp:*:43: video:*:44: sasl:*:45: plugdev:*:46: staff:*:50: games:*:60: users:*:100: nogroup:*:65534: ```