Auditing permissions, SUID/SGID and umask
Find risky files and set sane defaults.
Permissions drift. Packages add SUID programs, someone makes a directory world-writable to get past an error, an account is deleted and leaves its files behind. Each of these can hand a local user more than you meant to give. In this lesson you audit a server for the permissions that give privilege away: SUID and SGID programs, file capabilities, world-writable and ownerless files, and the modes of the account files and home directories. You remove a SUID bit so that it stays removed, set a umask policy for logins and services, and keep a baseline to compare against. What SUID, SGID, the sticky bit and the umask are is covered in Linux essentials; how attackers abuse them is covered in linux-det.
Every SUID and SGID program, accounted for
A SUID root program runs as root whoever starts it, so a bug in it is a way to root for every local user. Start with the list, on every local filesystem: find -xdev stays on the filesystem where it starts, so name each mount point.
-perm /6000 matches SUID or SGID, and /boot is scanned separately because it is its own filesystem (the vfat /boot/efi cannot hold these bits at all). Linux essentials walked through this list; in short it holds account tools that let users change their own entries (passwd, chfn, SGID shadow helpers), privilege tools (su and sudo-rs), mount helpers and a few service helpers such as dbus's launch helper and ssh-keysign. An audit asks one question of each entry: does this server need it?
Three entries are installed but not in use. /usr/bin/sudo.ws is classic sudo, kept as an alternative to sudo-rs; /usr/lib/cargo/bin/su is sudo-rs's su, while the active su is util-linux's /usr/bin/su. Both are still SUID root, so both are code any user can run as root, and both belong to packages the system needs. ntfs-3g lets ordinary users mount NTFS disks, which a server rarely needs, and nothing requires it:
ubuntu-standard and udisks2 only recommend ntfs-3g, and apt keeps a package installed when one of its recommendations is removed, so the simulated purge removes ntfs-3g and nothing else. Removing an unused SUID program is the strongest fix: there is no code left to exploit. On RHEL 10 the set differs: pkexec, pam_timestamp_check and grub2-set-bootflag are SUID, and unix_chkpwd is SUID root because /etc/shadow is mode 0 there.
Removing a SUID bit so that it stays removed
Some SUID programs must stay installed although nobody on this server should run them, such as sudo.ws and sudo-rs's su above. For those the fix is to remove the bit. ntfs-3g serves as the harmless example here (on a real server you would purge it). The obvious fix is chmod u-s. Watch what the next package update does to it; a reinstall stands in for the update:
dpkg sets every file to the mode in the package, silently. The Debian way to make a local decision stick is dpkg-statoverride, which records owner, group and mode for a path and applies them whenever dpkg installs that file. --update also applies them now.
The bit stays off through the reinstall. Ubuntu itself uses the same mechanism: crontab's SGID mode and dbus's launch helper are statoverrides set by their packages. The impact is that ordinary users can no longer run the program with root's powers; root still can. Removing unused SUID programs, or their bit when the package must stay, is SecOpsLog advice. CIS benchmarks ask you to review SUID and SGID programs and do not list which to remove, because that depends on what the server does. To roll back, remove the override and restore the mode:
RPM has no statoverride. An update restores the packaged mode, and rpm -V reports the difference with an M in the mode column, which makes it a quick drift check for packaged files on RHEL:
Capabilities instead of SUID
A file capability grants a program part of root's power instead of all of it (Linux essentials showed the default set: ping and mtr-packet with cap_net_raw, snap-confine with several). find -perm does not see capabilities, so the audit lists them separately:
What matters for hardening is how capabilities appear and disappear. The packaging shows the choice a package makes:
The install script tries a capability first and falls back to SUID only when the filesystem cannot store one. Capabilities live in an extended attribute, security.capability, and a plain cp drops it: the copy above has none. The same attribute can be added to any file by root with setcap, which is why a baseline must list capabilities as well as mode bits. Judge each entry by what the capability allows: cap_net_raw is narrow, while cap_setuid, cap_dac_override and cap_sys_admin each lead to full root. For your own services, grant a capability with AmbientCapabilities= in the unit instead of on the file (the sandboxing lesson).
World-writable and ownerless files
A world-writable directory without the sticky bit lets any user delete or replace any file in it, including a script that root runs from there. A world-writable file can be changed by anyone. A file whose owner no longer exists is inherited by the next account created with that UID. The CIS RHEL 10 Level 1 server profile checks all three, and they apply unchanged to Ubuntu. A fresh install has none of them:
No output is the result you want. The lab machine has been given one of each: a shared drop directory with a script in it, and a file left by a deleted account (UID 4242).
Removing write access for "other" fixes the directory and the script; the sticky bit (chmod +t) is the alternative for a directory that must stay shared, as /tmp is. The orphan now belongs to root until someone decides whether to keep it. The impact of these fixes is on whatever depended on the loose mode, so find the owner before you change a production path. To roll back, set the old mode again.
Account files, home directories and the umask
The two platforms protect the same files differently, and both are consistent. Ubuntu makes /etc/shadow readable by group shadow so the SGID unix_chkpwd can check passwords; RHEL makes it mode 0 and unix_chkpwd SUID root. Do not "fix" one to look like the other; check that neither has drifted. Home directories are 0750 on Ubuntu and 0700 on RHEL (HOME_MODE); the CIS RHEL 10 Level 1 profile requires 0750 or stricter. /home/lima.linux belongs to the lab's VM tool.
The umask decides the mode of every new file. On Ubuntu, pam_umask sets it at login. /etc/login.defs has no UMASK line, so the session keeps the 022 it inherits from sshd, and pam_umask(8) applies only its USERGROUPS_ENAB adjustment: because USERGROUPS_ENAB is yes and the user's primary group has their own name, it copies the owner bits to the group bits. The test needs an account that logs in over SSH with a key; the lab creates one, with the key in deploy's ~/.ssh:
The first ssh to localhost asks you to accept the host key. A real SSH login then shows the result:
The login gets 0002: new files are 664 and directories 775, so the user's private group (only the user) may write and everyone else may read. Commands run through sudo get 0022.
The CIS RHEL 10 Level 1 server profile asks for a user umask of 027, so that "other" gets nothing. On Ubuntu that means one UMASK line in /etc/login.defs, read by pam_umask at the next login; login.defs(5) treats a line as a comment when # is its first non-blank character.
# SecOpsLog: new files get no permissions for "other". With# USERGROUPS_ENAB yes, pam_umask turns this into 007 for users# whose primary group has their own name.UMASK 027
The new login gets 0007, not 0027: pam_umask applied USERGROUPS_ENAB and copied the owner bits to the group, as its man page describes. Others get nothing, which is the goal, and group members of a shared setgid directory can still write, which is what user private groups are for. The surprise is sudo: /etc/pam.d/sudo includes common-session-noninteractive, which runs pam_umask too, so everything created through sudo is now 0640 or 0750. A config file written with sudo tee into a new directory can become unreadable for a service that runs as its own user, so check such paths after the change. Services started by systemd are not affected at all: cron.service still has systemd's default UMask=0022. To roll back, delete the lines from /etc/login.defs; new logins pick the change up.
RHEL 10 has UMASK 022 in login.defs but no pam_umask in its PAM stack, so an SSH login keeps the 0022 it inherits from sshd, whatever login.defs says. The CIS RHEL 10 profile therefore sets 027 in /etc/login.defs, /etc/profile and /etc/bashrc.
A service that writes secrets or reports needs its own setting, UMask= in the unit. This small unit writes a report as root; create it with sudo systemctl edit --force --full hard-perms-writer.service:
[Unit]Description=Writes a report file (SecOpsLog lab)[Service]Type=oneshotStateDirectory=hard-perms-writerExecStart=/bin/sh -c 'echo report > /var/lib/hard-perms-writer/report.txt'
A baseline to compare against
An audit is a snapshot; drift is found by comparing snapshots. This script lists SUID and SGID files and file capabilities on every local filesystem, sorted so that diff shows only what changed:
#!/bin/sh# SecOpsLog: SUID/SGID files and file capabilities on the local# filesystems only, sorted, so that two snapshots can be compared# with diff. Network and pseudo filesystems are never walked.{for fs in $(findmnt -n -l -o TARGET -t ext4,xfs,btrfs); dofind "$fs" -xdev -type f -perm /6000 -printf '%M %u:%g %p\n'find "$fs" -xdev -type f -exec getcap {} +done} | sort
The capability check runs inside the same per-filesystem loop on purpose. getcap -r /, fine for a one-off look, walks every mount, including NFS and CIFS shares, so a daily job would read shared storage and report files that belong to other hosts. Even so the script reads the metadata of every file on the local filesystems: schedule it outside busy hours. Save it with sudoedit, make it executable, and take the first snapshot:
The lab then planted two files: an empty SUID root file in /var/tmp and a copy of true with cap_setuid. The diff names both and exits 1, which is what a scheduled job alerts on. Keep the baseline where an intruder cannot rewrite it (root-only, or better off the host), run the comparison from a timer (the scheduled jobs lesson), and refresh the baseline after each reviewed package update. AIDE, covered later, watches file contents as well; linux-det shows the same checks from the attacker's side.
Try this
On your Ubuntu lab machine, run sudo chmod u-s /usr/bin/ntfs-3g, then sudo apt-get install --reinstall ntfs-3g, and confirm with ls -l that the SUID bit is back. Repeat with sudo dpkg-statoverride --update --add root root 0755 /usr/bin/ntfs-3g and confirm it stays off after the reinstall; then remove the override and run sudo chmod 4755 /usr/bin/ntfs-3g. (On a server you manage, the better end state is sudo apt purge ntfs-3g.) Create a directory with mode 0777 under /var/tmp, find it with the world-writable search, and fix it. Finally save a snapshot with perms-snapshot, give a copy of /usr/bin/true in /var/tmp the cap_setuid=ep capability with setcap, and confirm that the diff names it. Delete the copy when you are done.
Takeaway
Know every SUID, SGID and capability on the host and why it is there, make removals stick with dpkg-statoverride, and compare against a baseline instead of trusting a one-off audit. Set the umask for logins and for services separately, because each is configured in a different place.