Skip to content

feat: add adduser slices #549

Open
Open

Description

@polarathene

EDIT: Looks like this will be resolved by: #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 #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:

Activity

  1. polarathene commented on Apr 27, 2025

    @polarathene
    Author

    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)
    • The useradd command package in other distros varies in package name (passwd for Debian/Ubuntu, shadow for Alpine, shadow-utils for Fedora). Caveats:
      • Alpine provides adduser by default via the busybox package. This adduser command differs in behaviour from the non-busybox version.
      • Fedora 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.


    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:

    • /etc/shells lists valid login shells.
    • /etc/default/useradd on Fedora configures SHELL=/bin/bash while Debian configures SHELL=/bin/sh
    • Debian also has a /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:

    • Password prompt: --disabled-password, or via --disabled-login to infer /usr/sbin/nologin.
    • User comment/info: --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.

    • The Busybox variant won't have the extra ,,, appended
    • Debian adduser 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.

    Default home

    For --system users:

    • adduser => /nonexistent (only the non-busybox variant)
    • useradd => /home/<username>

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

    • The /home portion is configured at /etc/default/useradd. adduser has an equivalent DHOME at /etc/adduser.conf (probably not relevant to busybox variant?).
    • For system users you may instead want to adjust the home directory to something like /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.
    • However the actual 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.
    • Without --system, setting the group name to the user name will be implicit, thus --group provides no benefit.
    • NOTE: The busybox variant of 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.

    • With --system, Fedora and Debian both do not create a home directory (even when one is set explicitly and is non-default)
    • Without --system, Fedora does create a home directly implicitly by default. Debian does not. You must manually opt-in via --create-home.
    • Fedora creates the home directory with permissions 0700, while Debian creates with 0755 (standard umask). Unclear where the 0700 is configured.
    • Fedora will additionally create /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).

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

    • When a group already exists with a GID equivalent to the UID, provided the group name is not the expected name, it'll implicitly create a group with an alternative GID value.
    • 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 differences

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

      • Unless using --system, then it will default to nogroup/65534 like it's non-busybox variant.
      • If the group already exists for the GID equivalent to the UID, it will fail like Debian 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:

    • The default home path is /home/<username> (like with useradd), not /nonexistent.
    • Does not imply --no-create-home.
    • Additionally sets SGID (aka 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).


    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:

    • chage slice is available via the passwd_bins slice, along with useradd + groupadd commands.
    • Actual /etc/group + /etc/passwd are provided via base-passwd_data slice.
      • I don't think there's an equivalent /etc/shadow / /etc/gshadow, but you shouldn't need those either in a typical container 🤷‍♂
      • The 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 canonical/chisel#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.
    • In both 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:

    /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:
    
  2. changed the title [-]Add `adduser` slice[/-] [+]feat: add `adduser` slices[/+] on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions