Ubuntu · Logging & auditing

Audit every use of setuid and setgid programs

When it fails: Use of privileged programs is not recorded. Programs that run with more rights than their caller are the main path to privilege escalation and are used without a record.

CB-UBU-AUDIT-026mediumactiveautomatedautomated fixprofile server-hardenedprofile desktop-hardened

As released in v0.1.2 (beta). Current state.

Description

One auditd rule per setuid or setgid program found on the local file systems that allow them (not mounted nosuid or noexec), key privileged, in /etc/audit/rules.d/60-cirsec-privileged.rules. Each rule records every run of the program by a user who logged in. The list is rebuilt from the file system each time the check or the fix runs.

The scan covers local file systems listed by findmnt --real. Programs on network file systems are not covered. Snap images (squashfs mounts under /snap/) are skipped: they are read-only, replaced at each snap refresh (which would make the check fail every time), and their setuid copies are not used by the host. /usr/lib/snapd/snap-confine itself is on the root file system and is covered.

Rationale

passwd, su, sudo, mount, chsh and similar programs run as root on behalf of a user. Recording each use shows who used them and is the first place to look after a privilege escalation.

Impact

Installing or removing a package that ships a setuid program makes the check fail until the fix runs again. Once the audit configuration is locked (CB-UBU-AUDIT-029), the new rule only loads at the next reboot.

Setting

Location/etc/audit/rules.d/60-cirsec-privileged.rules
DefaultNo audit rules
RecommendedRules loaded (auditctl -l) and present in /etc/audit/rules.d/60-cirsec-privileged.rules

Audit

  1. Run auditctl -l as root. It must list the rules shown below (keys privileged).
  2. Run grep -h . /etc/audit/rules.d/*.rules. The same rules must be present, so they load at boot.

Expected rules, as auditctl -l prints them:

-a always,exit -S all -F path=<program> -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged

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

rules

Rules loaded in the kernel and present in /etc/audit/rules.d.

script: |
  command -v auditctl >/dev/null 2>&1 || { echo "auditctl not found: auditd is not installed"; exit 1; }
  want() {
  # every setuid or setgid file on local file systems mounted without nosuid or noexec, snaps excluded
  for m in $(findmnt -rn --real -t nosquashfs -O nonosuid,nonoexec -o TARGET); do
    find "$m" -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null
  done | sort -u | while IFS= read -r p; do
    echo "-a always,exit -S all -F path=$p -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged"
  done
  }
  loaded=$(auditctl -l 2>&1) || { echo "$loaded"; exit 3; }
  disk=$(cat /etc/audit/rules.d/*.rules 2>/dev/null)
  n=0 rc=0
  while IFS= read -r r; do
    [ -n "$r" ] || continue
    n=$((n + 1))
    printf '%s\n' "$loaded" | grep -Fxq -- "$r" || { echo "not loaded: $r"; rc=1; }
    printf '%s\n' "$disk" | grep -Fxq -- "$r" || { echo "not in /etc/audit/rules.d: $r"; rc=1; }
  done <<EOF
  $(want)
  EOF
  [ "$rc" = 1 ] || echo "$n rules loaded and present in /etc/audit/rules.d"
  exit "$rc"

Remediation

  1. Write the expected rules to /etc/audit/rules.d/60-cirsec-privileged.rules (mode 0640).
  2. Run augenrules --load. If auditctl -s shows enabled 2, the configuration is locked: reboot to load the new rules.
Restartnone
Disruptionnone

Rollback

Delete /etc/audit/rules.d/60-cirsec-privileged.rules and run augenrules --load (or reboot if the configuration is locked).

rules-file

command -v augenrules >/dev/null 2>&1 || { echo "auditd is not installed" >&2; exit 1; }
want() {
# every setuid or setgid file on local file systems mounted without nosuid or noexec, snaps excluded
for m in $(findmnt -rn --real -t nosquashfs -O nonosuid,nonoexec -o TARGET); do
  find "$m" -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null
done | sort -u | while IFS= read -r p; do
  echo "-a always,exit -S all -F path=$p -F perm=x -F auid>=1000 -F auid!=-1 -F key=privileged"
done
}
f=/etc/audit/rules.d/60-cirsec-privileged.rules
t=$(mktemp)
{ echo '# Written by CIRSEC Baselines CB-UBU-AUDIT-026'; want; } >"$t"
if ! cmp -s "$t" "$f"; then
  cb_backup "$f"
  cat "$t" >"$f"
fi
rm -f "$t"
chmod 640 "$f"
if auditctl -s | grep -q '^enabled 2'; then
  echo "audit rules are locked (-e 2): reboot to load $f"
else
  augenrules --load >/dev/null
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-19 by Sn0wAlice on 20.04, Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic. Fails after auditd install; rules written and loaded (augenrules), all present in auditctl -l with auditd 2.8.5; passes after reboot with enabled 2. Snap squashfs mounts skipped.
Verified2026-10-19 by Sn0wAlice on 22.04, Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic. Fails after auditd install; rules written and loaded (augenrules), all present in auditctl -l with auditd 3.0.7; passes after reboot with enabled 2. Snap squashfs mounts skipped.
Verified2026-10-19 by Sn0wAlice on 24.04, Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic. Fails after auditd install; rules written and loaded (augenrules), all present in auditctl -l with auditd 3.1.2; passes after reboot with enabled 2. Snap squashfs mounts skipped.
Verified2026-10-19 by Sn0wAlice on 26.04, Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic. Fails after auditd install; rules written and loaded (augenrules), all present in auditctl -l with auditd 4.1.2; passes after reboot with enabled 2. Snap squashfs mounts skipped.
Introduced inv0.1.0
Created, updated2026-10-19, 2026-10-19
Translationsfr