Debian GNU/Linux · Accounts & authentication

Restrict the su command to an empty group

When it fails: Any user who knows a target password can switch user with su. su bypasses the sudo rules and logs, and lets anyone who guessed or stole a password become that user.

CB-DEB-AUTH-008mediumactiveautomatedautomated fixprofile core

As released in v0.1.0 beta · pre-release. Current state.

Description

su switches to another user, root by default, after asking for that user's password. The pam_wheel module can restrict it to members of one group. Pointing it at an empty group means only root can use su; administrators use sudo instead.

Rationale

Administration through su needs a shared root password and leaves no trace of who ran which command. Restricting it pushes every privileged action through sudo, with per-user rules and logs, and stops password guessing through su.

Impact

Users can no longer run su or su - otheruser. Use sudo -i or sudo -u otheruser -i. Root keeps using su to switch to any account.

Parameters

ParameterCoreServerServer hardenedDesktopDesktop hardenedBounds
su_groupstring. Name of the group allowed to use su. Keep it empty.sugroupsugroupsugroupsugroupsugroup

The value used is your override (tailoring file), else the profile's, else the one of the profile it extends, else the default. Bounds limit what an override may set.

Audit

  1. /etc/pam.d/su must contain an uncommented auth required pam_wheel.so use_uid group={{ su_group }} line.
  2. Run getent group {{ su_group }}. The group must exist and have no members.

Automated: every check must pass. Scanners report each check by its id.

pam-wheel

pam_wheel restricts su to the configured group.

script: |
  grep -Eq "^[[:space:]]*auth[[:space:]]+required[[:space:]]+pam_wheel\.so([[:space:]].*)?[[:space:]]group=$CB_SU_GROUP([[:space:]]|$)" /etc/pam.d/su \
    || { echo "no pam_wheel group=$CB_SU_GROUP line"; exit 1; }
  echo "pam_wheel group=$CB_SU_GROUP"

group-empty

The su group exists and is empty.

script: |
  g=$(getent group "$CB_SU_GROUP") || { echo "group $CB_SU_GROUP missing"; exit 1; }
  m=${g##*:}
  echo "members: ${m:-none}"
  [ -z "$m" ]

Remediation

  1. Create the group: groupadd {{ su_group }}.
  2. Add to /etc/pam.d/su, before the @include common-auth line: auth required pam_wheel.so use_uid group={{ su_group }}
Restartnone
Disruptionlow

Rollback

Remove the line from /etc/pam.d/su.

configure

getent group "$CB_SU_GROUP" >/dev/null || groupadd "$CB_SU_GROUP"
f=/etc/pam.d/su
if ! grep -Eq "^[[:space:]]*auth[[:space:]]+required[[:space:]]+pam_wheel\.so.*group=$CB_SU_GROUP" "$f"; then
  cb_backup "$f"
  sed -i "0,/^@include common-auth/s//auth       required   pam_wheel.so use_uid group=$CB_SU_GROUP\n&/" "$f"
fi

Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.

References

Mappings

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

People and lifecycle

Authors
Verified2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. Generated check, fix and check again for profiles core and server-hardened; state kept after reboot.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. Generated check, fix and check again for profiles core and server-hardened; state kept after reboot.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr