Skip to content

Instantly share code, notes, and snippets.

@ok-ryoko
Last active July 28, 2024 23:48
Show Gist options
  • Save ok-ryoko/1ff42a805d496cb1ca22e5cdf6ddefb0 to your computer and use it in GitHub Desktop.
Save ok-ryoko/1ff42a805d496cb1ca22e5cdf6ddefb0 to your computer and use it in GitHub Desktop.
SUID-root Binaries in Fedora Server 38

SUID-root Binaries in Fedora Server 38

by OK Ryoko, revision 2024-07-28.1

Assumed audience: Linux system administrators, Linux utility authors and Fedora Linux package maintainers. Familiarity with process credentials, capabilities, syscalls, strace, Linux PAM and SELinux is assumed.

I dive into all the SUID-root binaries that come with a minimal installation of Fedora Server 38. I also discuss the use of file capabilities to limit the level of privilege attainable by those programs.

Skip ahead to the section titled “The findings at a glance” for a high-level summary of outcomes.

Appendix A expands the abbreviations that appear in this report.

Table of contents

Introduction

Set-user-ID-root (SUID-root) binaries enable a Linux system’s unprivileged users to perform actions ordinarily reserved for the super user (root). Although important for the day-to-day use of some systems, SUID-root binaries can enable exploits of local privilege escalation vulnerabilities in themselves (e.g., PwnKit) as well as their execution environment (e.g., Looney Tunables).

Most Linux distributions and base container images ship with a handful of SUID-root binaries. A crucial step in securing these systems prior to deployment is to handle these binaries appropriately. Unfortunately, this process is hampered by an absence of specific public data and guidance. To help fill this gap, I examine the SUID-root binaries that come with a minimal installation of Fedora Server 38, my current daily driver. For each binary, I answer the following questions with respect to one or two reference use cases:

  • Why does it need to be SUID-root?
  • Can the SUID bit be substituted by zero or more file capabilities?
  • If not, can the SUID bit be supplemented with zero or more file capabilities?
  • Does the program drop privileges? If so, when?
  • How does the default SELinux policy constrain the level of privilege of the program?

The outcomes of this work aren’t comprehensive. Instead, they establish a baseline of understanding that members of the assumed audience can adapt to their needs.

Characterization of the environment

My findings are for a minimal installation (a “Fedora Custom Operating System” in the Anaconda installer with no additional software selected) of Fedora Server 38 1.6, virtualized using the libvirt 9.0.0 API with the QEMU/KVM driver from the image at the following URL:

https://download.fedoraproject.org/pub/fedora/linux/releases/38/Server/x86_64/iso/Fedora-Server-dvd-x86_64-38-1.6.iso

This image had SHA256 checksum 66b52d7cb39386644cd740930b0bef0a5a2f2be569328fef6b1f9b3679fdc54d.

A minimal installation of Fedora Server isn’t the same thing as an installation of Fedora Minimal, which is outside the scope of this report.

When creating the VM, I added a virtiofs shared file system so that I could extract traces, logs and original source code easily.

Here’s some basic system information:

[user@fedora ~]$ uname -a # a: all
Linux fedora 6.2.9-300.fc38.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Mar 30 22:32:58 UTC 2023 x86_64 GNU/Linux

SELinux is enabled and enforcing:

[user@fedora ~]$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

The selinux-policy-targeted-38.8-2.fc38.noarch package provides the targeted policy.

During installation, I created a single unprivileged and unconfined user, adding them to the wheel group:

[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

By default, Anaconda locks the root account:

[user@fedora ~]$ sudo passwd -S root # S: status
root LK 1969-12-30 0 99999 7 -1 (Password locked.)

My shell had a full bounding capability set but no other capabilities:

[user@fedora ~]$ cat /proc/$$/status | grep '^Cap'
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

No securebits base or locked flags had been set.

I didn’t upgrade any package on the system explicitly after the installation procedure.

I installed the following packages from the fedora repository to gain access to utilities and development headers for my analyses:

I also installed the following packages from the fedora-debuginfo repository to help me interpret syscall stack traces:

My first time using dnf(8) to install packages, I was asked whether to import the following GPG key:

Importing GPG key 0xEB10B464:
 Userid     : "Fedora (38) <[email protected]>"
 Fingerprint: 6A51 BBAB BA3D 5467 B617 1221 809A 8D7C EB10 B464
 From       : /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-38-x86_64
Is this ok [y/N]:

I confirmed after comparing the fingerprint to the value listed on https://fedoraproject.org/security/ for equality.

Conventions for presenting results

I obfuscated timestamps, IP addresses and UUIDs because they were irrelevant to the analyses.

I omitted all sudo(8) password prompts for brevity.

I normalized all hard tabs to spaces in command output.

I always had strace(1) write output to a file using the --output option, which I omitted from the reported commands because it doesn’t change the content of the trace.

Likewise, I always redirected the output of tail(1)–grep(1) pipelines and journalctl(1) to a file.

The SUID-root binaries in detail

There are 14 such binaries on the system:

[user@fedora ~]$ sudo find / -ignore_readdir_race -perm /u=s -user root
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/umount
/usr/bin/chage
/usr/bin/newgrp
/usr/bin/su
/usr/bin/pkexec
/usr/bin/passwd
/usr/bin/sudo
/usr/sbin/grub2-set-bootflag
/usr/sbin/pam_timestamp_check
/usr/sbin/unix_chkpwd
/usr/libexec/openssh/ssh-keysign

Unless stated otherwise, all these binaries have owner root, group root and mode -rwsr-xr-x with no extended attributes other than security.selinux.

/usr/bin/passwd

Overview

  • Program to update authentication tokens using PAM
  • Provided by passwd-0.80-14.fc38.x86_64
  • Has SHA256 checksum 3baf3bad788ee383b3844096f3cc2e86d31904a70919fb3d4c0d659de7cfcd7d
  • Has security context system_u:object_r:passwd_exec_t:s0 set by the usermanage 1.19.0 module

Why does this binary need to be SUID-root?

passwd(1) must be able to update a user’s password via PAM, which involves running the stack defined by the service configuration at /etc/pam.d/passwd:

#%PAM-1.0
# This tool only uses the password stack.
password    substack      system-auth
-password   optional      pam_gnome_keyring.so use_authtok
password    substack      postlogin

Source: cat /etc/pam.d/passwd after inserting whitespace for readability

The following modules are included from the system-auth stack:

password    requisite     pam_pwquality.so local_users_only
password    sufficient    pam_unix.so yescrypt shadow nullok use_authtok
password    sufficient    pam_sss.so use_authtok
password    required      pam_deny.so

Source: cat /etc/pam.d/system-auth | grep -E '^-?password', after truncating whitespace for readability, where -E is short for --extended-regexp

Of these, the pam_unix module must be able to read and update /etc/shadow, requiring CAP_CHOWN, CAP_DAC_OVERRIDE and, if not root, CAP_FOWNER to do so.

No modules are included from the postlogin stack:

[user@fedora ~]$ cat /etc/pam.d/postlogin | grep -E '^-?password'
[user@fedora ~]$

In addition, passwd(1) must be able to write to the kernel audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

No. Updating /etc/shadow as a nonroot user requires CAP_FOWNER to bypass UID-related permission checks. However, the SELinux policy that applies to /usr/bin/passwd doesn’t allow this capability (see discussion below).

Can we supplement the SUID bit?

I was able to change my password successfully after making the following changes to the binary:

[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_audit_write=ep /usr/bin/passwd
[user@fedora ~]$ passwd
Changing password for user user.
Current password:
New password:
Retype new password:
passwd: all authentication tokens updated successfully.

Here’s the corresponding entry in the audit log:

type=USER_CHAUTHTOK msg=audit(0.0:402): pid=830 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 msg='op=PAM:chauthtok grantors=pam_pwquality,pam_unix acct="user" exe="/usr/bin/passwd" hostname=fedora addr=? terminal=tty1 res=success'UID="user" AUID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'passwd' | grep 'success'

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, passwd(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).

What role does SELinux play in constraining the privilege of the program?

The binary /usr/bin/passwd has type passwd_exec_t, which has an entrypoint to the passwd_t application domain:

[user@fedora ~]$ sudo sesearch --allow -s passwd_t -t passwd_exec_t -c file -p entrypoint # s: source, t: target, c: class, p: perm
allow passwd_t passwd_exec_t:file { entrypoint execute getattr ioctl lock map open read };

I wanted to determine whether running this process would result in a domain transition from the unconfined_t domain to the passwd_t domain. If so, then passwd(1) would be subject to the constraints on the passwd_t domain. First, I noticed that the targeted SELinux policy does declare such a transition:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t passwd_exec_t # T: type_trans
type_transition unconfined_t passwd_exec_t:process passwd_t;

Is this transition allowed? Yes:

[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t passwd_t -c process -p transition
allow unconfined_t domain:process transition;

What’s more, processes in the unconfined_t domain can read and execute files with the passwd_exec_t type:

[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t passwd_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };

Finally, there is an association between the unconfined_r role and the passwd_t type:

[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o passwd_t # x: expand, r: role; o: only-matching
passwd_t

On the basis of this information, I concluded that I should be able to run /usr/bin/passwd as an unconfined user, and that the process should transition from the unconfined_t domain to the passwd_t domain. I observed this transition using strace(1):

849<strace> [unconfined_t] execve("/usr/bin/passwd" [passwd_exec_t], ["passwd"], 0x7ffd4ba96398 /* 15 vars */) = 0
849<passwd> [passwd_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)

Source: sudo strace -u user -fyY --secontext passwd, where -u is short for --user and -fyY for --follow-forks --decode-fds --decode-pids

Thus, passwd(1) should be subject to the constraints on the passwd_t domain. In particular, the passwd_t domain has the following permissions on the capability and capability2 classes:

[user@fedora ~]$ sudo sesearch --allow -t passwd_t -c capability,capability2
allow passwd_t passwd_t:capability net_bind_service; [ nis_enabled ]:True
allow passwd_t passwd_t:capability net_bind_service; [ nis_enabled ]:True
allow passwd_t passwd_t:capability { audit_write chown dac_override dac_read_search fsetid ipc_lock setgid setuid sys_admin sys_chroot sys_nice sys_resource };

I could now demonstrate that replacing the SUID bit on /usr/bin/passwd with file capabilities wouldn’t work, and also develop a rationale for why setting CAP_FOWNER would otherwise allow this. I started by making the following changes to the binary:

[user@fedora ~]$ sudo chmod u-s /usr/bin/passwd
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_fowner,cap_audit_write=ep /usr/bin/passwd

I then tried changing my password again:

[user@fedora ~]$ passwd
Changing password for user user.
Current password:
New password:
Retype new password:
passwd: Authentication token manipulation error

No dice. strace(1) revealed the failing syscall:

863<passwd> fchmod(5</etc/nshadow>, 0100000) = -1 EPERM (Operation not permitted)
 > /usr/lib64/libc.so.6(fchmod+0xb) [0x100d7b]
 > /usr/lib64/security/pam_unix.so(pam_sm_chauthtok+0x1120) [0xa260]
 > /usr/lib64/libpam.so.0.85.1(_pam_dispatch+0x1d2) [0x9782]
 > /usr/lib64/libpam.so.0.85.1(pam_chauthtok+0x71) [0xa171]
 > /usr/bin/passwd(main+0x9ef) [0x32df]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
 > /usr/bin/passwd(_start+0x24) [0x4744]

Source: sudo strace -u user -fyY -k passwd, where -k is short for --stack-traces, after installing pam-debuginfo-1.5.2-16.fc38.x86_64 and pam-libs-debuginfo-1.5.2-16.fc38.x86_64

passwd(1) made this syscall to set the mode of /etc/nshadow to the mode of /etc/shadow.1 /etc/nshadow was a temporary file with which passwd(1) would later replace /etc/shadow.2 According to the man page for fchmod(2), EPERM is the error set when either the process doesn’t have CAP_FOWNER or the file has the append-only (a) or immutable (i) extendable attribute. passwd(1) doesn’t create /etc/nshadow with either attribute, so the EPERM must have been caused by an SELinux denial, as passwd(1) was running confined:

type=AVC msg=audit(0.0:151): avc:  denied  { fowner } for  pid=863 comm="passwd" capability=3  scontext=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 tcontext=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 tclass=capability permissive=0

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'AVC' | grep 'denied' | grep 'passwd'

It is therefore necessary to retain the SUID bit so that the EUID (really, the FSUID) of passwd(1) matches the UID of /etc/nshadow. Ordinarily, CAP_FOWNER allows processes to bypass this permission check.

Comments

This program shouldn’t be confused with the utility of the same name provided by shadow-utils.

Source code references

The WITH_SELINUX macro is assumed to be defined.

1 In linux-pam/modules/pam_unix/[email protected], pam_sm_chauthtok calls _do_setpass (lines 860 and 861) to update the password database. Because the password is shadowed, _do_setpass goes on to call unix_update_shadow (line 499). There is the possibility that _do_setpass does not call unix_update_shadow but delegates to unix_update(8) (line 491); this happens when the unix_selinux_confined function determines that the process is running confined by SELinux (line 490). In linux-pam/modules/pam_unix/[email protected], unix_selinux_confined tests for confinement by first establishing whether SELinux is enabled (line 523) and then by attempting to open /etc/shadow via open(2) (line 529). If passwd(1) is running SUID-root or with CAP_DAC_OVERRIDE or CAP_DAC_READ_SEARCH, then the call to open(2) will succeed and unix_selinux_confined will conclude that the process is running unconfined by SELinux. Still in linux-pam/modules/pam_unix/[email protected], unix_update_shadow calls fopen(3) on SH_TMPFILE, a macro that expands to "/etc/nshadow" (line 336), and assigns the result to pwfile (line 954). unix_update_shadow later calls fchmod(2) on the file descriptor corresponding to pwfile to change the mode of /etc/nshadow to the mode stored in st (line 981). st contains the result of an fstat(2) call on the file descriptor corresponding to opwfile (line 968). opwfile is the result of an fopen(3) call on /etc/shadow (line 961).

2 In linux-pam/modules/pam_unix/[email protected], unix_update_shadow calls rename(2) to rename /etc/nshadow to /etc/shadow (line 1042)

/usr/bin/chage

Overview

  • Program to display and change user password expiry information
  • Provided by shadow-utils-2:4.13-6.fc38.x86_64
  • Has SHA256 checksum 555f3514ec66a0e2bc771ce2e05a6c79a02b6bdf856ceefc4a3ef4bc8365ccd2
  • Has security context system_u:object_r:passwd_exec_t:s0 set by the usermanage 1.19.0 module

Why does this binary need to be SUID-root?

chage(1) must allow any unprivileged user to view their password expiry information, which requires opening and reading /etc/shadow (CAP_DAC_READ_SEARCH). If an unprivileged user tries to run chage(1) for any other purpose, then the program must log a security violation by writing to the audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

I was able to get my password expiry information after making the following changes to the binary:

[user@fedora ~]$ sudo chmod u-s /usr/bin/chage
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/bin/chage
[user@fedora ~]$ chage -l user # l: list
Last password change                                    : Jan 01, 1970
Password expires                                        : never
Password inactive                                       : never
Account expires                                         : never
Minimum number of days between password change          : 0
Maximum number of days between password change          : 99999
Number of days of warning before password expires       : 7

When I tried to update my password expiry information, chage(1) refused to run:

[user@fedora ~]$ chage user
chage: Permission denied.

Here’s the corresponding entry in the audit log:

type=USER_MGMT msg=audit(0.0:113): pid=827 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023 msg='op=change-age acct="" exe="/usr/bin/chage" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'chage' | grep 'failed', where -n is short for --lines

Does this program drop privileges? If so, when?

chage(1) drops privileges immediately after opening /etc/shadow:1

838<chage> read(5</etc/shadow>, "root:!::0:99999:7:::\nbin:*:19378"..., 4096) = 694
838<chage> read(5</etc/shadow>, "", 4096) = 0
838<chage> setregid(1000, 1000) = 0
838<chage> setreuid(1000, 1000) = 0

Source: sudo strace -u user -fyY chage -l user after truncating whitespace for readability

What role does SELinux play in constraining the privilege of the program?

Please refer to the corresponding discussion for /usr/bin/passwd, which has the same security context as /usr/bin/chage. Observe that the file capabilities I set on the binary form a strict subset of the capabilities allowed by the targeted SELinux policy.

Comments

If unprivileged users receive password expiry information out of band or there isn’t a password expiry policy in place, then consider unsetting the SUID bit on /usr/bin/chage.

Source code references

All line numbers refer to points in source code after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c.

1 In shadow/src/[email protected], main calls open_files (line 796), which calls spw_open (line 571). In shadow/lib/[email protected], spw_open calls commonio_open, passing the address of shadow_db (line 151). In shadow/lib/[email protected], commonio_open boils down to an open(2) call on db->filename (lines 619–621), where db is the pointer to shadow_db. shadow_db is a global static struct of type commonio_db whose filename field is initialized to SHADOW_FILE (line 81 of shadow/lib/[email protected]). In shadow/lib/[email protected], SHADOW_FILE is a macro that expands to "/etc/shadow" (line 261). Back in shadow/src/[email protected], main calls setregid(2) to restore the EGID to the RGID (line 798) and setreuid(2) to restore the EUID to the RUID (line 799) after open_files returns.

/usr/bin/gpasswd

Overview

  • Program to administer user groups
  • Provided by shadow-utils-2:4.13-6.fc38.x86_64
  • Has SHA256 checksum e6681a06c02396f80a7f24c7ea3d274b4433fff87f92bdb1b38920a8b281060b
  • Has security context system_u:object_r:groupadd_exec_t:s0 set by the usermanage 1.19.0 module

Why does this binary need to be SUID-root?

gpasswd(1) must allow any nonroot user who is an administrative member of a group to administer that group. To this end, gpasswd(1) must be able to:

  • update /etc/group and /etc/gshadow (CAP_CHOWN and CAP_DAC_OVERRIDE);
  • set the UIDs to 0 (CAP_SETUID), and
  • write to the kernel audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

I decided to answer this question for the reference use case of adding a user to a group. First, I created a group and a user to add to that group:

[user@fedora ~]$ sudo groupadd -g 2000 gunbuster && sudo useradd noriko # g: gid

I then made the following changes to the binary:

[user@fedora ~]$ sudo chmod u-s /usr/bin/gpasswd
[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_setuid,cap_audit_write=ep /usr/bin/gpasswd

I ensured that I couldn’t yet add the new user to the new group:

[user@fedora ~]$ gpasswd -a noriko gunbuster # a: add
gpasswd: Permission denied.

gpasswd(1) logged my transgression properly:

type=USER_MGMT msg=audit(0.0:134): pid=846 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=add-user-to-group grp="gunbuster" acct="noriko" exe="/usr/bin/gpasswd" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'gpasswd' | grep 'failed'

Indeed, I had to first make myself an administrative member of the new group:

[user@fedora ~]$ sudo gpasswd -A user gunbuster # A: administrators

The new file capabilities took me the rest of the way:

[user@fedora ~]$ gpasswd -a noriko gunbuster
Adding user noriko to group gunbuster
[user@fedora ~]$ id noriko
uid=1001(noriko) gid=1001(noriko) groups=1001(noriko),2000(gunbuster)

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, gpasswd(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to the groupadd_t domain (the application domain for the groupadd_exec_t file type):

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t groupadd_exec_t
[user@fedora ~]$

Thus, gpasswd(1) runs in the unconfined_t domain, its capabilities unconstrained by SELinux.

Comments

If unprivileged users must be able to use the --root flag to administer groups in a different root file system, then it is necessary to also set CAP_SYS_CHROOT on /usr/bin/gpasswd.

If there are no use cases that involve group administration by one or more unprivileged users, then consider unsetting the SUID bit on /usr/bin/gpasswd without setting file capabilities.

gpasswd(1) becomes root unconditionally near the end of its lifetime:1

874<gpasswd> setuid(0) = 0

Source: sudo strace -u user -fyY gpasswd -a noriko gunbuster after truncating whitespace for readability

… in order to update /etc/group and /etc/gshadow.

gpasswd(1) later forks itself so that it can execute sss_cache(8) to invalidate all user and group records in the SSSD cache before exiting:

874<gpasswd> execve("/usr/sbin/sss_cache", ["sss_cache", "-UG"], 0x7fffef712d38 /* 0 vars */) = 0

Source: sudo strace -u user -fyY gpasswd -a noriko gunbuster

sss_cache(8) requires CAP_SETUID and CAP_SETGID to set its EUID and EGID to 0, respectively:

874<sss_cache> setresuid(-1, 0, -1) = 0
874<sss_cache> setresgid(-1, 0, -1) = 0

Source: sudo strace -u user -fyY gpasswd -a noriko gunbuster after truncating whitespace for readability

sss_cache(8) can perform these operations because it is executed by a root process whose bounding capability set is full.

Source code references

1 In shadow/src/[email protected] after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c, main calls setuid(2) to set all the UIDs to 0 and otherwise returns a nonzero value (lines 1104–1109)

/usr/bin/newgrp

Overview

  • Program to change the GIDs during a login session
  • Provided by shadow-utils-2:4.13-6.fc38.x86_64
  • Has SHA256 checksum a582658e9c6c2e930c200c650693f5f10eba8f081896f11546c4cd55b236f4bc
  • Has security context system_u:object_r:bin_t:s0

Why does this binary need to be SUID-root?

newgrp(1) must be able to:

  • read /etc/shadow and /etc/gshadow (CAP_DAC_READ_SEARCH);
  • set the GIDs (CAP_SETGID) and
  • write to the kernel audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

I was able to change the GIDs of my session to those of the wheel group (of which I’m a member) after making the following changes to the binary:

[user@fedora ~]$ sudo chmod u-s /usr/bin/newgrp
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_setgid,cap_audit_write=ep /usr/bin/newgrp
[user@fedora ~]$ newgrp - wheel
[user@fedora ~]$ id -gnr # g: group, n: name, r: real
wheel

I checked the audit log and found a matching entry:

type=CHGRP_ID msg=audit(0.0:115): pid=829 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=changing new_gid=10 id=1000 exe="/usr/bin/newgrp" hostname=fedora addr=? terminal=tty1 res=success'UID="user" AUID="user" NEW_GID="wheel" ID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'newgrp' | grep 'success'

I also confirmed that I could return to the original environment correctly:

[user@fedora ~]$ logout
[user@fedora ~]$ id -gnr
user

Does this program drop privileges? If so, when?

newgrp(1) drops privileges immediately after setting the GIDs:1

865<newgrp> setgid(10)   = 0
865<newgrp> getuid()     = 1000
865<newgrp> setuid(1000) = 0

Source: sudo strace -u user -fyY newgrp - wheel after truncating whitespace for readability

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t restrict the permissions of the bin_t domain on the capability and capability2 classes, so newgrp(1) runs unconfined.

Comments

If no unprivileged user needs to be able to change the GIDs of a login session, then consider unsetting the SUID bit on /usr/bin/newgrp without setting file capabilities.

Source code references

1 In shadow/src/[email protected] after applying the patches in src.fedoraproject.org/rpms/shadow-utils@fd05b1c, main calls setgid(2) to set all the GIDs to the ID of the new group (line 730), then calls setuid(2) to set all the UIDs to the RUID (line 742)

/usr/bin/mount

Overview

  • Program to mount file systems
  • Provided by util-linux-core-2.38.1-4.fc38.x86_64
  • Has SHA256 checksum ac8efce2afaab784c382bc7ec3843c023e3aa60bcd57467bbce3691a21eda24c
  • Has security context system_u:object_r:mount_exec_t:s0 set by the mount 1.16.1 module

Why does this binary need to be SUID-root?

Any unprivileged user must be able to mount any unmounted file system listed in /etc/fstab whose entry contains the user, users, owner or group option (CAP_SYS_ADMIN).

Can we replace the SUID bit with zero or more file capabilities?

mount(8) has an internal security model that depends on the SUID bit. When asked to perform certain operations within a “restricted context” (i.e., when running with RUID nonzero), mount(8) calls the suid_drop function.1 In turn, suid_drop calls drop_permissions, which makes the setgid(2) and setuid(2) syscalls to restore the GIDs and UIDs, respectively.2 However, as a result of C’s short-circuit evaluation, suid_drop calls drop_permissions only when the process has RUID nonzero and EUID zero.3 The only way to satisfy these conditions is to run mount(8) SUID-root as a nonroot user.

In effect, when the SUID bit is unset on /usr/bin/mount, mount(8) will never drop permissions. If /usr/bin/mount were to have a nonempty permitted capability set and its effective capability bit were set, then mount(8) would never have its effective capability set cleared during execution, potentially allowing unprivileged users to perform actions they shouldn’t be able to perform. This is because the mount(2) syscall is made unconditionally (with respect to privileges) via libmount.4

To test my hypothesis, I started by inspecting the following entry in /etc/fstab for the boot partition:

UUID=00000000-0000-0000-0000-000000000000 /boot xfs defaults 0 0

Source: cat /etc/fstab | grep 'boot' after truncating whitespace for readability

This file system is mounted by default. I first unmounted it:

[user@fedora ~]$ sudo umount /boot && findmnt /boot
[user@fedora ~]$

I confirmed that the binary in question was SUID-root and had no file capabilities:

[user@fedora ~]$ ls -lZ /usr/bin/mount # Z: context
-rwsr-xr-x. 1 root root system_u:object_r:mount_exec_t:s0 49248 Jan 20  2023 /usr/bin/mount
[user@fedora ~]$ getcap /usr/bin/mount
[user@fedora ~]$

Next, I tried to mount /boot:

[user@fedora ~]$ mount /boot
mount: /boot: must be superuser to use mount.
       dmesg(1) may have more information after failed mount system call.

I couldn’t. Here’s the failing mount(2) call:

844<mount> mount("/dev/vda2", "/boot", "xfs", 0, NULL) = -1 EPERM (Operation not permitted)
 > /usr/lib64/libc.so.6(mount+0xe) [0x113c9e]
 > /usr/lib64/libmount.so.1.1.0(do_mount+0xa91) [0x29b91]
 > /usr/lib64/libmount.so.1.1.0(mnt_context_do_mount+0x1a1) [0x2a741]
 > /usr/lib64/libmount.so.1.1.0(mnt_context_mount+0x1d8) [0x2d778]
 > /usr/bin/mount(main+0x171a) [0x686a]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
 > /usr/bin/mount(_start+0x24) [0x6fb4]

Source: sudo strace -u user -fyY -k mount /boot after installing libmount-debuginfo-2.38.1-4.fc38.x86_64

The syscall above failed due to a lack of privileges. Here’s the setuid(2) call made as part of an earlier invocation of suid_drop:

844<mount> setuid(1000) = 0
 > /usr/lib64/libc.so.6(setuid+0x2b) [0xdca5b]
 > /usr/bin/mount(suid_drop+0x106) [0x73e6]
 > /usr/bin/mount(main+0x1712) [0x6862]
 > /usr/lib64/libc.so.6(__libc_start_call_main+0x79) [0x27b49]
 > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x8a) [0x27c0a]
 > /usr/bin/mount(_start+0x24) [0x6fb4]

Source: sudo strace -u user -fyY -k mount /boot after installing libmount-debuginfo-2.38.1-4.fc38.x86_64 and truncating whitespace for readability

So far, mount(8) was working as intended.

I then unset the SUID bit on the binary and conferred the CAP_SYS_ADMIN capability:

[user@fedora ~]$ sudo chmod u-s /usr/bin/mount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/mount

Having satisfied the conditions to avoid calling drop_permissions, I was able to mount /boot as an unprivileged user:

[user@fedora ~]$ mount /boot && findmnt /boot
TARGET SOURCE    FSTYPE OPTIONS
/boot  /dev/vda2 xfs    rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota

This simple demo illustrates how the substitution of a SUID bit with file capabilities can extend the attack surface of a system. It is paramount to appreciate how each individual SUID-root binary has been engineered.

Can we supplement the SUID bit?

I was able to mount a file system on a virtual disk after making the following changes to the binary:

[user@fedora ~]$ sudo chmod u+s /usr/bin/mount
[user@fedora ~]$ sudo setcap cap_sys_admin=ep /usr/bin/mount

I first added a new VirtIO disk at /dev/vdb. I used fdisk(8) to format the disk and create a Linux partition at /dev/vdb1:

[user@fedora ~]$ (echo o; echo n; echo p; echo 1; echo; echo; echo w) | sudo fdisk /dev/vdb
[user@fedora ~]$ lsblk /dev/vdb
NAME   MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vdb    252:16   0   4G  0 disk
└─vdb1 252:17   0   4G  0 part

I then created an XFS file system on the partition:

[user@fedora ~]$ sudo mkfs.xfs -f /dev/vdb1 # f: force

Next, I created the mount point, appended the corresponding entry to /etc/fstab and reloaded the systemd manager configuration:

[user@fedora ~]$ sudo mkdir -p /mnt/upart # p: parents
[user@fedora ~]$ echo '/dev/vdb1 /mnt/upart xfs defaults,noauto,owner 0 0' | sudo tee -a /etc/fstab > /dev/null
[user@fedora ~]$ sudo systemctl daemon-reload

… where I set the owner option to allow the device’s owner to mount the file system. To enable this use case, I made myself the owner of the partition:

[user@fedora ~]$ sudo chown user /dev/vdb1

I could then mount the file system successfully:

[user@fedora ~]$ mount /mnt/upart && findmnt /mnt/upart
TARGET     SOURCE    FSTYPE OPTIONS
/mnt/upart /dev/vdb1 xfs    rw,nosuid,nodev,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota

Additional capabilities may be needed to mount particular types of file system (with particular options).

As a confidence check, I tried to mount /boot again, this time without success:

[user@fedora ~]$ sudo umount /boot
[user@fedora ~]$ findmnt /boot
[user@fedora ~]$ mount /boot
mount: drop permissions failed.
[user@fedora ~]$ findmnt /boot
[user@fedora ~]$

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to the mount_t domain through the mount_exec_t file type:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t mount_exec_t
[user@fedora ~]$

Thus, mount(8) runs unconfined when run from the command line.

However, the targeted SELinux policy defines many transitions to the mount_t domain through the mount_exec_t type:

[user@fedora ~]$ sudo sesearch -T -t mount_exec_t | wc -l # l: lines
42

Suppose an application were to execve(2) mount(8) and transition to the mount_t domain. To what extent would the policy limit the capabilities with which mount(8) would run?

[user@fedora ~]$ sudo sesearch --allow -t mount_t -c capability,capability2
allow mount_t mount_t:capability sys_module; [ secure_mode_insmod ]:False
allow mount_t mount_t:capability { audit_control audit_write chown dac_override dac_read_search fowner fsetid ipc_lock ipc_owner kill lease linux_immutable mknod net_admin net_bind_service net_broadcast net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_pacct sys_ptrace sys_rawio sys_resource sys_time sys_tty_config };
allow mount_t mount_t:capability2 { audit_read block_suspend bpf checkpoint_restore epolwakeup perfmon syslog wake_alarm };

It turns out that the policy would hardly limit mount(8)’s capabilities. This is because the mount module makes use of the unconfined_domain interface, which lifts many restrictions on the corresponding domain.5

Comments

If the user, users, owner or group options don’t need to be set for any entry in /etc/fstab, then consider unsetting the SUID bit without setting file capabilities.

Further reading

  • “Non-superuser mounts” in the mount(8) man page

Source code references

1 Lines 726, 942, 975, 1003, 1011, 1022 and 1038 of util-linux/sys-utils/[email protected]

2 Lines 367–382 of util-linux/include/[email protected]

3 Line 58 of util-linux/sys-utils/[email protected]

4 Line 1032 of util-linux/sys-utils/[email protected]

5 In selinux-policy/policy/modules/system/[email protected], the mount_t domain is passed to the unconfined_domain interface (line 360). In selinux-policy/policy/modules/system/[email protected], unconfined_domain wraps the unconfined_domain_noaudit interface (line 136), which grants all permissions to the given domain on the capability class excepting sys_module (line 22) and the capability2 class excepting mac_admin and mac_override (line 23).

/usr/bin/umount

Overview

  • Program to unmount file systems
  • Provided by util-linux-core-2.38.1-4.fc38.x86_64
  • Has SHA256 checksum 5a15dcb9c509fe1ac3e89ead094ff1cdb993b6a17dbb2863cb2b396ca0ce1e37
  • Has security context system_u:object_r:mount_exec_t:s0 set by the mount 1.16.1 module

Can we generalize the findings for /usr/bin/mount to this binary?

Yes, because both mount(8) and umount(8) depend on the same libmount functions and use an identically implemented suid_drop function.

/usr/sbin/unix_chkpwd

Overview

  • Helper for the pam_unix PAM module that verifies the current user’s password
  • Provided by pam-1.5.2-16.fc38.x86_64
  • Has SHA256 checksum 95aa56aba4eaeccd8ba6972b6453725fd0a4a6d25105cd6c61df0eb87c4c2c22
  • Has security context system_u:object_r:chkpwd_exec_t:s0 set by the authlogin 2.5.1 module

Why does this binary need to be SUID-root?

unix_chkpwd(8) must be able to read /etc/shadow (CAP_DAC_READ_SEARCH) and write to the kernel audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

As of Linux PAM v1.5.2, it’s no longer necessary to have this binary installed SUID-root per pull request #373 if one sets CAP_DAC_OVERRIDE on the binary.

Based on my understanding of the source code, unix_chkpwd(8) doesn’t need to write or execute /etc/shadow, so CAP_DAC_READ_SEARCH should suffice:

[user@fedora ~]$ sudo chmod u-s /usr/sbin/unix_chkpwd
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/sbin/unix_chkpwd

… where I also set CAP_AUDIT_WRITE to permit audit logging, as PAM is built with auditing support for Fedora Linux.

unix_chkpwd(8) is intended to be run indirectly, has an undocumented API and logs a security violation when invoked from an interactive terminal. These factors governed my decision to test my changes by writing a small wrapper in C (as unix_chkpwd_wrapper.c) instead of running the pam_unix module indirectly as part of a PAM application:

/*
 * SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define CHKPWD_HELPER "/usr/sbin/unix_chkpwd"

int main() {
    static char *args[] = { NULL, NULL, NULL, NULL };
    static char *envp[] = { NULL };
    args[0] = CHKPWD_HELPER;
    args[1] = "user";
    args[2] = "chkexpiry";
    execve(CHKPWD_HELPER, (char *const *) args, envp);
    perror("execve");
    exit(EXIT_FAILURE);
}

I compiled this program and updated its permissions like so:

[user@fedora ~]$ gcc -Wall unix_chkpwd_wrapper.c -o unix_chkpwd_wrapper
[user@fedora ~]$ chmod 0770 unix_chkpwd_wrapper

strace(1) output revealed that unix_chkpwd(8) was able to read /etc/shadow without the SUID bit:

841<unix_chkpwd> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = 3</etc/shadow>
841<unix_chkpwd> newfstatat(3</etc/shadow>, "", {st_mode=S_IFREG|000, st_size=728, ...}, AT_EMPTY_PATH) = 0
841<unix_chkpwd> lseek(3</etc/shadow>, 0, SEEK_SET) = 0
841<unix_chkpwd> read(3</etc/shadow>, "root:!::0:99999:7:::\nbin:*:19378"..., 4096) = 728
841<unix_chkpwd> close(3</etc/shadow>) = 0

Source: sudo strace -u user -fyY ./unix_chkpwd_wrapper after truncating whitespace for readability

I also tried to run unix_chkpwd(8) inappropriately from a tty:

[user@fedora ~]$ unix_chkpwd

The program slept for 10 seconds before exiting with code 4, which represents a PAM system error. Having granted CAP_AUDIT_WRITE, I expected to find a matching entry in the audit log, and I did:

type=ANOM_EXEC msg=audit(0.0:123): pid=843 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:chkpwd_t:s0-s0:c0.c1023 msg='op=PAM:unix_chkpwd acct="user" exe="/usr/sbin/unix_chkpwd" hostname=? addr=? terminal=? res=failed'UID="user" AUID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'unix_chkpwd' | grep 'failed'

Does this program drop privileges? If so, when?

Errors notwithstanding, this program drops privileges only if no password file entry exists for the user corresponding to the process’s RUID or said user doesn’t match the user specified by the invoking application.1

What role does SELinux play in constraining the privilege of the program?

The binary /usr/sbin/unix_chkpwd has type chkpwd_exec_t, which has an entrypoint to the chkpwd_t application domain:

[user@fedora ~]$ sudo sesearch --allow -s chkpwd_t -t chkpwd_exec_t -c file -p entrypoint
allow chkpwd_t chkpwd_exec_t:file { entrypoint execute getattr ioctl lock map open read };

I wanted to determine whether running this process would result in a domain transition from the unconfined_t domain to the chkpwd_t domain. If so, then unix_chkpwd(8) would be subject to the constraints on the chkpwd_t domain. First, I noticed that the targeted SELinux policy does declare such a transition:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t chkpwd_exec_t
type_transition unconfined_t chkpwd_exec_t:process chkpwd_t;

Is this transition allowed? Yes:

[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t chkpwd_t -c process -p transition
allow unconfined_t domain:process transition;

What’s more, processes in the unconfined_t domain can read and execute files with the chkpwd_exec_t type:

[user@fedora ~]$ sudo sesearch --allow -s unconfined_t -t chkpwd_exec_t -c file -p execute,read
allow files_unconfined_type file_type:file { append audit_access create execute execute_no_trans getattr ioctl link lock map mounton open quotaon read relabelfrom relabelto rename setattr swapon unlink watch watch_mount watch_reads watch_sb watch_with_perm write };

Finally, there is an association between the unconfined_r role and the chkpwd_t type:

[user@fedora ~]$ seinfo -x -r unconfined_r | grep -o 'chkpwd_t'
chkpwd_t

On the basis of this information, I concluded that I should be able to run /usr/sbin/unix_chkpwd as an unconfined user, and that the process should transition from the unconfined_t domain to the chkpwd_t domain. I observed this transition using strace(1):

1117<unix_chkpwd_wra> [unconfined_t] execve("/usr/sbin/unix_chkpwd" [chkpwd_exec_t], ["/usr/sbin/unix_chkpwd", "user", "chkexpiry"], 0x404060 /* 0 vars */) = 0
1117<unix_chkpwd> [chkpwd_t] access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)

Source: sudo strace -u user -fyY --secontext ./unix_chkpwd_wrapper

Thus, the process should be subject to the constraints on the chkpwd_t domain. In particular, the chkpwd_t domain has the following permissions on the capability and capability2 classes:

[user@fedora ~]$ sudo sesearch --allow -t chkpwd_t -c capability,capability2
allow chkpwd_t chkpwd_t:capability net_bind_service; [ nis_enabled ]:True
allow chkpwd_t chkpwd_t:capability net_bind_service; [ nis_enabled ]:True
allow chkpwd_t chkpwd_t:capability { audit_write dac_read_search setuid };

Observe that the file capabilities I set on the binary form a strict subset of the capabilities allowed by the policy.

Comments

If a program runs the pam_unix module but can’t read /etc/shadow (due to the absence of CAP_DAC_READ_SEARCH or CAP_DAC_OVERRIDE since /etc/shadow has mode 0000), then pam_unix will fall back to unix_chkpwd(8).2 Just before executing this helper, pam_unix will set the forked process’s UIDs to 0 if the EUID is 0.3 This is relevant because programs such as sudo(8) and pkexec(1) expect to run with EUID 0 unconditionally. Therefore, when the forking process’s bounding capability set is full, unix_chkpwd(8) will run with all capabilities.

Source code references

The HELPER_COMPILE macro influences the contents of linux-pam/modules/pam_unix/[email protected], which is used to compile both pam_unix and unix_chkpwd(8) per lines 42 and 49, respectively, of linux-pam/modules/pam_unix/[email protected]. HELPER_COMPILE is assumed to be undefined when compiling pam_unix and defined when compiling unix_chkpwd(8).

1 In linux-pam/modules/pam_unix/[email protected], main checks whether the RUID is 0 (line 133). If the RUID is not 0 (line 136), then main calls getuidname to retrieve the name of the user as which the process is running (line 137). (In linux-pam/modules/pam_unix/[email protected], getuidname boils down to a call to getpwuid(3) on line 1163.) If getuidname returns NULL or the name of the running user doesn’t match the name passed to unix_chkpwd(8), then main sets the UIDs to the RUID via setuid(2) (line 143).

2 In linux-pam/modules/pam_unix/[email protected], pam_sm_authenticate calls _unix_blankpasswd (line 146) and _unix_verify_password (line 173). In linux-pam/modules/pam_unix/[email protected], _unix_blankpasswd calls get_pwd_hash (line 637) twice. If get_pwd_hash returns PAM_UNIX_RUN_HELPER (line 639), then _unix_blankpasswd calls _unix_run_helper_binary (line 640) which, in turn, execve(2)s CHKPWD_HELPER (line 535). CHKPWD_HELPER is a macro that, on Fedora Linux 38, expands to "/usr/sbin/unix_chkpwd" per line 21 of linux-pam/modules/pam_unix/[email protected]. Similarly, _unix_verify_password calls get_pwd_hash (line 684). If get_pwd_hash returns PAM_UNIX_RUN_HELPER (line 700), then _unix_verify_password calls _unix_run_helper_binary (line 702). When does get_pwd_hash return PAM_UNIX_RUN_HELPER? In linux-pam/modules/pam_unix/[email protected], the return value of get_pwd_hash is either PAM_SUCCESS (line 279) or the return value of get_account_info (line 267). get_account_info returns PAM_UNIX_RUN_HELPER if either the password is a NIS+ password (lines 200 and 237) or the password is shadowed and pam_modutil_getspnam returns NULL (lines 239, 244, 245 and 248). In linux-pam/libpam/[email protected], pam_modutil_getspnam boils down to a call to getspnam_r(3) (lines 56–58), which retrieves the shadow password structure and returns NULL if an error occurred, e.g., EACCES due to a failure to read /etc/shadow.

3 In linux-pam/modules/pam_unix/[email protected], _unix_run_helper_binary calls setuid(2) to change all the UIDs to 0 when geteuid(2) returns 0 (lines 516–523)

/usr/sbin/pam_timestamp_check

Overview

  • Program to validate or remove the default timestamp (for cached authentication results)
  • Provided by pam-1.5.2-16.fc38.x86_64
  • Has SHA256 checksum 5509f331a360183da55b1041540349668b9a52ce8a512340cbb76738cd66e9d5
  • Has security context system_u:object_r:pam_timestamp_exec_t:s0 set by the authlogin 2.5.1 module

Why does this binary need to be SUID-root?

pam_timestamp_check(8) expects to run with EUID 0 unconditionally.1 It requires privileges to create and manipulate the directory tree rooted at /var/run/pam_timestamp.

Can we supplement the SUID bit?

pam_timestamp_check(8) does not require any capabilities as long as it runs with EUID 0. To demonstrate this, I started by creating a service configuration file at /etc/pam.d/timestamp with the following contents, taking after the example in the corresponding man page:

auth       sufficient    pam_timestamp.so verbose debug
auth       required      pam_unix.so
session    required      pam_unix.so
session    optional      pam_timestamp.so verbose debug

I then wrote a dummy application in timestamper.c to use this configuration. This application authenticates user, then opens and closes a session without doing any real work:

/*
 * SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <stdio.h>
#include <stdlib.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>

static struct pam_conv conv = {
    misc_conv,
    NULL
};

int main(int argc, const char **argv) {
    const char *progname = argv[0];
    int retval;
    pam_handle_t *pamh = NULL;

    retval = pam_start("timestamp", "user", &conv, &pamh);
    if (retval != PAM_SUCCESS) {
        fprintf(
            stderr,
            "%s: initializing PAM transaction: %s\n",
            progname,
            pam_strerror(pamh, retval)
        );
        return 1;
    }

    retval = pam_authenticate(pamh, 0);
    if (retval != PAM_SUCCESS) {
        fprintf(
            stderr,
            "%s: authenticating with PAM: %s\n",
            progname,
            pam_strerror(pamh, retval)
        );
        pam_end(pamh, retval);
        return 1;
    }

    retval = pam_open_session(pamh, 0);
    if (retval != PAM_SUCCESS) {
        fprintf(
            stderr,
            "%s: opening PAM session: %s\n",
            progname,
            pam_strerror(pamh, retval)
        );
        pam_end(pamh, retval);
        return 1;
    }

    retval = pam_close_session(pamh, 0);
    if (retval != PAM_SUCCESS) {
        fprintf(
            stderr,
            "%s: closing PAM session: %s\n",
            progname,
            pam_strerror(pamh, retval)
        );
        pam_end(pamh, retval);
        return 1;
    }

    retval = pam_end(pamh, PAM_SUCCESS);

    return 0;
}

After installing pam-devel-1.5.2-16.fc38.x86_64, I was able to compile this source code like so:

[user@fedora ~]$ gcc -Wall $(pkgconf --cflags --libs pam pam_misc) timestamper.c -o timestamper

… where I linked against libpam.so.0.85.1 and libpam_misc.so.0.82.1 provided by pam-libs-1.5.2-16.fc38.x86_64.

I gave myself execute permissions for the binary:

[user@fedora ~]$ chmod 0770 timestamper

To enable the application to run the pam_timestamp module successfully, I found it necessary to confer some capabilities to the binary:

[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override=ep timestamper

CAP_DAC_OVERRIDE lets pam_timestamp create /var/run/pam_timestamp and subdirectories thereof via mkdir(2) as well as the /var/pam_timestamp/_pam_timestamp_key file. In addition, CAP_CHOWN lets pam_timestamp change the owner of these directories and files to root via lchown(2) and fchown(2).

Next, I confirmed that the binary of interest was SUID-root and had no file capabilities of its own:

[user@fedora ~]$ ls -lZ /usr/sbin/pam_timestamp_check
-rwsr-xr-x. 1 root root system_u:object_r:pam_timestamp_exec_t:s0 16168 Jan 18  2023 /usr/sbin/pam_timestamp_check
[user@fedora ~]$ getcap /usr/sbin/pam_timestamp_check
[user@fedora ~]$

I could now test my application. I first checked whether I had a valid timestamp:

[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7

The exit code communicated that my timestamp wasn’t valid. I ran my application to change that:

[user@fedora ~]$ ./timestamper
Password:
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
0

Success! Here’s what the systemd journal had to say about what had just happened:

Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): becoming more verbose
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): becoming user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): currently user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): tty is `/dev/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): using timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:auth): cannot open timestamp `/var/run/pam_timestamp/user/tty1': No such file or directory
Jan 01 00:00:00 fedora timestamper[842]: pam_unix(timestamp:session): session opened for user user(uid=1000) by user(uid=1000)
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): becoming user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): currently user `user'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): tty is `/dev/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): using timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_timestamp(timestamp:session): updated timestamp file `/var/run/pam_timestamp/user/tty1'
Jan 01 00:00:00 fedora timestamper[842]: pam_unix(timestamp:session): session closed for user user

Source: sudo journalctl -eb, where -eb is short for --pager-end --boot

I had allowed the use of the pam_timestamp module for authentication in my service configuration file. To validate this functionality, I ran my application again:

[user@fedora ~]$ ./timestamper
Access has been granted (last access was 30 seconds ago).

Thus, I had confirmed that my application was working correctly. To recover my initial state, I cleared the timestamp:

[user@fedora ~]$ pam_timestamp_check -k
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7

I could now experiment with /usr/sbin/pam_timestamp_check. I ensured that the binary was SUID-root and assigned it an empty capability set:

[user@fedora ~]$ sudo chmod u+s /usr/sbin/pam_timestamp_check
[user@fedora ~]$ sudo setcap '' /usr/sbin/pam_timestamp_check
[user@fedora ~]$ getcap /usr/sbin/pam_timestamp_check
/usr/sbin/pam_timestamp_check =

I performed a confidence check to confirm that I didn’t have a valid timestamp:

[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7

Now, to put the file capabilities to the test. Could I acquire a valid timestamp through my application?

[user@fedora ~]$ ./timestamper
Password:
[user@fedora ~]$ ./timestamper
Access has been granted (last access was 3 seconds ago).

Yes. And could I remove the timestamp via pam_timestamp_check(8)?

[user@fedora ~]$ pam_timestamp_check -k
[user@fedora ~]$ pam_timestamp_check
[user@fedora ~]$ echo $?
7

Yes. Here’s the relevant strace(1) output, where we see access to and removal of the timestamp via newfstatat(2) and unlink(2), respectively:

865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/", {st_mode=S_IFDIR|0555, st_size=235, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/", {st_mode=S_IFDIR|0755, st_size=740, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/pam_timestamp", {st_mode=S_IFDIR|0700, st_size=100, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> newfstatat(AT_FDCWD</home/user>, "/var/run/pam_timestamp/user/tty1", {st_mode=S_IFREG|0600, st_size=106, ...}, AT_SYMLINK_NOFOLLOW) = 0
865<pam_timestamp_c> unlink("/var/run/pam_timestamp/user/tty1") = 0

Source: sudo strace -u user -fyY pam_timestamp_check -k

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, pam_timestamp_check(8) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to the pam_timestamp_t domain (the application domain for the pam_timestamp_exec_t file type):

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t pam_timestamp_exec_t
[user@fedora ~]$

Thus, pam_timestamp_check(8) runs in the unconfined_t domain, its capabilities unconstrained by SELinux.

Comments

If no application on the system uses the pam_timestamp module and there is no need to enable unprivileged users to check or clear their authentication timestamp, then consider unsetting the SUID bit on /usr/sbin/pam_timestamp_check.

Source code references

1 In linux-pam/modules/pam_timestamp/[email protected], main prints an error message and sets a nonzero return value when geteuid(2) returns a nonzero value (lines 763–767). Although main continues setting the program up, its efforts don’t effect any changes to the file system since the core functionality is guarded by conditional statements that check for a zero return value (lines 814, 820 and 826).

/usr/bin/su

Overview

  • Program to run a new shell or a command within a new shell as another user or group
  • Provided by util-linux-2.38.1-4.fc38.x86_64
  • Has SHA256 checksum 1acc69d71e2b22e6eed04fdfaa0f85f644fc03d3826635cd35586b93664a2de4
  • Has security context system_u:object_r:su_exec_t:s0 set by the su 1.12.0 module

Why does this binary need to be SUID-root?

su(1) must enable unprivileged users to start a new and possibly privileged shell as another user or group, possibly in a pseudoterminal. To this end, su(1) must be able to:

  • execute all types of module in the PAM stack defined by /etc/pam.d/su or /etc/pam.d/su-l (CAP_SETGID and CAP_SETUID);
  • set the UIDs, GIDs and supplementary groups for the new shell (CAP_SETGID and CAP_SETUID);
  • optionally, change the mode, owner and group of the pseudoterminal for the new shell (CAP_CHOWN and CAP_FOWNER), and
  • write to the kernel audit log (CAP_AUDIT_WRITE).

Can we replace the SUID bit with zero or more file capabilities?

No. Unless su(1) runs SUID-root, multiple PAM session modules will fail to run.

Each PAM stack uses the pam_keyinit session module, which changes the RUID and RGID to 0 temporarily to enable the calling process to interact with the kernel’s key management facility.1 To demonstrate this, I unlocked the root account and set a password:

[user@fedora ~]$ sudo passwd -fu root # f: force, u: unlock
Unlocking password for user root.
passwd: Success
[user@fedora ~]$ sudo passwd root
New password:
Retype new password:
passwd: all authentication tokens updated successfully.

I then traced the process of running su(1) to get a root shell and extracted the following fragment:

832<su> getuid()           = 1000
832<su> getgid()           = 1000
832<su> setregid(0, -1)    = 0
832<su> setreuid(0, -1)    = 0
832<su> keyctl(KEYCTL_JOIN_SESSION_KEYRING, NULL) = 857786151
832<su> keyctl(KEYCTL_LINK, KEY_SPEC_USER_KEYRING, KEY_SPEC_SESSION_KEYRING) = 0
832<su> setreuid(1000, -1) = 0
832<su> setregid(1000, -1) = 0

Source: sudo strace -u user -fyY su -l, where -l is short for --login, after truncating whitespace for readability

The setreuid(2) and setregid(2) calls that follow the keyctl(2) calls should result in a loss of all capabilities by design when the SUID bit is unset. Quoting the capabilities(7) man page:

“If one or more of the real, effective, or saved set user IDs was previously 0, and as a result of the UID changes all of these IDs have a nonzero value, then all capabilities are cleared from the permitted, effective, and ambient capability sets.”

When the SUID bit is unset, the EUID and saved SUID stay at 1000 while the RUID changes from 1000 to 0 and then back to 1000 (assuming CAP_SETUID). Therefore, after interacting with the kernel key management facility, su(1) effectively loses all its privileges. To demonstrate this, I started by making the following changes to the binary of interest:

[user@fedora ~]$ sudo chmod u-s /usr/bin/su
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_audit_write=ep /usr/bin/su

To get far enough into the execution of su(1) to see the anticipated error messages, I had to ensure that the pam_unix authentication module would run to completion. I had conferred neither CAP_DAC_OVERRIDE nor CAP_DAC_READ_SEARCH to /usr/bin/su, so su(1) would have to delegate to unix_chkpwd(8) for authentication.

I found that su(1) could not authenticate when /usr/sbin/unix_chkpwd had the SUID bit set and no security.capability extended attribute:

[user@fedora ~]$ ls -l /usr/sbin/unix_chkpwd
-rwsr-xr-x. 1 root root 32792 Jan 18  2023 /usr/sbin/unix_chkpwd
[user@fedora ~]$ getcap /usr/sbin/unix_chkpwd
[user@fedora ~]$ su -l
Password:
su: Authentication failure
874<su> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)
...
874<su> clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f73e6465a50) = 875<su>
...
875<su> geteuid() = 1000
875<su> execve("/usr/sbin/unix_chkpwd", ["/usr/sbin/unix_chkpwd", "root", "nullok"], 0x7f73e6313040 /* 0 vars */) = 0
...
875<unix_chkpwd> getuid()     = 1000
875<unix_chkpwd> setuid(1000) = 0
...
875<unix_chkpwd> openat(AT_FDCWD</home/user>, "/etc/shadow", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)

Source: sudo strace -u user -fyY su -l after truncating whitespace for readability and omitting select syscalls

Why couldn’t unix_chkpwd(8) open /etc/shadow? Because su(1) ran unix_chkpwd(8) as the user user but asked to verify password information for the user root—a condition under which the program drops privileges via setuid(2). By unsetting the SUID bit on /usr/sbin/unix_chkpwd and setting the capabilities identified earlier, I could make progress with su(1):

[user@fedora ~]$ sudo chmod u-s /usr/sbin/unix_chkpwd
[user@fedora ~]$ sudo setcap cap_dac_read_search,cap_audit_write=ep /usr/sbin/unix_chkpwd
[user@fedora ~]$ su -l
Password:
su: cannot set group id: Operation not permitted

Despite /usr/bin/su having CAP_SETGID, su(1) wasn’t able to set the GIDs:

901<su> setgid(0) = -1 EPERM (Operation not permitted)

Source: sudo strace -u user -fyY su -l after truncating whitespace for readability

The following excerpt from the systemd journal suggests that this failure was in the pam_keyinit session module. In addition, the pam_systemd and pam_lastlog session modules were also foiled:

Jan 01 00:00:00 fedora audit[887]: USER_AUTH pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[887]: USER_ACCT pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:accounting grantors=pam_unix,pam_localuser acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[887]: (to root) user on tty1
Jan 01 00:00:00 fedora audit[887]: CRED_ACQ pid=887 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora dbus-broker[656]: A security policy denied :1.24 to send method call /org/freedesktop/login1:org.freedesktop.login1.Manager.CreateSession to org.freedesktop.login1.
Jan 01 00:00:00 fedora su[887]: pam_systemd(su-l:session): Failed to create session: Access denied
Jan 01 00:00:00 fedora su[887]: pam_unix(su-l:session): session opened for user root(uid=0) by user(uid=1000)
Jan 01 00:00:00 fedora su[887]: pam_lastlog(su-l:session): unable to open /var/log/lastlog: Permission denied
Jan 01 00:00:00 fedora su[887]: pam_keyinit(su-l:session): Unable to change GID to 0 temporarily
Jan 01 00:00:00 fedora su[887]: pam_unix(su-l:session): session closed for user root

Source: sudo journalctl -eb

Can we supplement the SUID bit?

I was able to use su(1) to get a root shell after making the following changes to the binary:

[user@fedora ~]$ sudo chmod u+s /usr/bin/su
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_audit_write=ep /usr/bin/su

Here’s the result:

[user@fedora ~]$ su -l
Password:
[root@fedora ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

This shell was unrestricted because it had been executed by a root process whose bounding capability set was full:

[root@fedora ~]# getpcaps $$
912: =ep

So, even though I had constrained the level of privilege attained by su(1), I could still use the resulting shell to perform arbitrary administrative operations.

I also confirmed that I could exit the shell correctly…

[root@fedora ~]# logout
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

… and that there were no failing PAM modules this time around:

Jan 01 00:00:00 fedora audit[911]: USER_AUTH pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[911]: USER_ACCT pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:accounting grantors=pam_unix,pam_localuser acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: (to root) user on tty1
Jan 01 00:00:00 fedora audit[911]: CRED_ACQ pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: pam_unix(su-l:session): session opened for user root(uid=0) by user(uid=1000)
Jan 01 00:00:00 fedora audit[911]: USER_START pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:session_open grantors=pam_keyinit,pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_umask,pam_xauth acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora su[911]: pam_unix(su-l:session): session closed for user root
Jan 01 00:00:00 fedora audit[911]: USER_END pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:session_close grantors=pam_keyinit,pam_keyinit,pam_limits,pam_systemd,pam_unix,pam_umask,pam_xauth acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'
Jan 01 00:00:00 fedora audit[911]: CRED_DISP pid=911 uid=1000 auid=1000 ses=1 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:setcred grantors=pam_unix acct="root" exe="/usr/bin/su" hostname=fedora addr=? terminal=/dev/tty1 res=success'

Source: sudo journalctl -eb after omitting messages relating to systemd-hostnamed and BPF

I successfully reproduced the procedure above with the --pty flag, which tells su(1) to create a pseudoterminal for the session. I didn’t need to set CAP_CHOWN or CAP_FOWNER on /usr/bin/su to enable this use case since these capabilities are unnecessary when the binary is SUID-root.

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, su(1) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2) unless used to impersonate a nonroot user. This behavior is consistent with the purpose of the program.

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to any application domain for the su_exec_t file type:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t su_exec_t
[user@fedora ~]$

Thus, su(1) runs in the unconfined_t domain, its capabilities unconstrained by SELinux.

The policy allows more capabilities than I set on /usr/bin/su for the various application domain types, e.g.,

[user@fedora ~]$ sudo sesearch --allow -t sysadm_su_t -c capability,capability2
allow sysadm_su_t sysadm_su_t:capability { audit_control audit_write chown dac_read_search fowner net_bind_service setgid setuid sys_nice sys_resource };

I haven’t made an effort to rationalize these capabilities. If I were having trouble running su(1) with the smaller set above for a particular use case, then I would try adding one or more of the other capabilities allowed by the SELinux policy.

Comments

By default, it isn’t possible to use su(1) to get a root shell because root is locked. If this is the case, then consider unsetting the SUID bit on /usr/bin/su without setting file capabilities.

Source code references

1 In linux-pam/modules/pam_keyinit/[email protected], pam_sm_open_session calls do_keyinit (line 280). do_keyinit calls setregid(2) to change the RGID to the RGID of the PAM user (line 218) and setreuid(2) to change the RUID to the RUID of the PAM user (line 223) if the RGID and RUID aren’t already set to the desired values. do_keyinit then calls init_keyrings (line 230), which makes the keyctl(2) calls (lines 105–107 and 115–118). After init_keyrings returns, do_keyinit performs the inverse UID and GID transitions (lines 233 and 238, respectively).

/usr/bin/sudo

Overview

  • Program to execute a command as another user or group in a configurable manner
  • Provided by sudo-1.9.13-1.p2.fc38.x86_64
  • Installed with mode ---s--x--x
  • Has SHA256 checksum 5221d69e5eff19b57e1d285aa0beebd8cc89762811763c87be4a05ca4389643f
  • Has security context system_u:object_r:sudo_exec_t:s0 set by the sudo 1.10.0 module

Why does this binary need to be SUID-root?

sudo(8) expects to run with EUID 0 unconditionally.1 As for specific privileges, sudo(8) must be able to:

  • execute all types of module in the PAM stack defined by /etc/pam.d/sudo or /etc/pam.d/sudo-i (CAP_SETGID and CAP_SETUID);
  • set the UIDs, GIDs and supplementary groups for the command (CAP_SETGID and CAP_SETUID);
  • zero the hard limit on core dump file size by default (CAP_SYS_RESOURCE);
  • write to the kernel audit log (CAP_AUDIT_WRITE), and
  • read and write arbitrary files via /usr/bin/sudoedit (CAP_DAC_OVERRIDE and CAP_FOWNER, also CAP_CHOWN if using a pseudoterminal).

With the exception of accommodating sudoedit(8), this is similar to what I observed for su(1).

Can we supplement the SUID bit?

I was able to use sudo(8) to acquire an interactive root shell after making the following changes to the binary:

[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid,cap_sys_resource,cap_audit_write=ep /usr/bin/sudo
[user@fedora ~]$ sudo -i
[root@fedora ~]# id
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

As with su(1), the resulting root shell had all capabilities:

[root@fedora ~]# getpcaps $$
828: =ep

I also confirmed that I could exit the shell correctly:

[root@fedora ~]# logout
[user@fedora ~]$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

After extending the permitted capability set, I could also update root-owned files with sudoedit(8):

[user@fedora ~]$ sudo setcap cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid,cap_sys_resource,cap_audit_write=ep /usr/bin/sudo
[user@fedora ~]$ sudoedit /etc/fstab
"/var/tmp/fstab.ZRR7sWI8" 14L, 554B written

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, sudo(8) doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2) unless used to impersonate a nonroot user. This behavior is consistent with the purpose of the program.

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to any application domain for the sudo_exec_t file type:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t sudo_exec_t
[user@fedora ~]$

Thus, sudo(8) runs in the unconfined_t domain, its capabilities unconstrained by SELinux.

The policy allows more capabilities than I set on /usr/bin/sudo for the various application domain types, e.g.,

[user@fedora ~]$ sudo sesearch --allow -t sysadm_sudo_t -c capability,capability2
allow sysadm_sudo_t sysadm_sudo_t:capability { audit_control audit_write chown dac_override dac_read_search fowner setgid setuid sys_nice sys_resource };

As with su(1), I haven’t made an effort to rationalize these capabilities. If I were having trouble running sudo(8) with the smaller set above for a particular use case, then I would try adding one or more of the other capabilities allowed by the SELinux policy.

Comments

If the system doesn’t need to support any use cases in which unprivileged users perform privileged actions via sudo(8) or another utility such as pkexec(1) or doas(1) is preferred for this purpose, then consider unsetting the SUID bit on /usr/bin/sudo without setting file capabilities.

A typical recommendation for hardening sudo(8) is to ensure that the program uses a pseudoterminal by default, e.g.,

[user@fedora ~]$ sudo visudo /etc/sudoers.d/pty
Defaults use_pty

After creating this file, I was able to reproduce the operations above without any problems. (This is already the default behavior for Sudo 1.9.14; see issue #258.)

Source code references

The __linux__ and PR_GET_NO_NEW_PRIVS macros are assumed to be defined. The __TANDEM macro is assumed to be undefined.

1 In sudo/src/sudo.c@SUDO_1_9_13p2, main calls sudo_check_suid (line 186). If geteuid(2) doesn’t return a value equal to ROOT_UID (line 924), then sudo_check_suid either exits the program with an error if the no_new_privs attribute is set on the calling thread (lines 927 and 932) or calls sudo_fatalx (lines 963, 967 and 972). (ROOT_UID is a macro that expands to 0 per line 32 of sudo/include/sudo_util.h@SUDO_1_9_13p2.) In sudo/include/sudo_fatal.h@SUDO_1_9_13p2, sudo_fatalx is defined as a macro that either expands to or wraps sudo_fatalx_nodebug_v1 (lines 42 and 61–65). In sudo/lib/util/fatal.c@SUDO_1_9_13p2, sudo_fatalx_nodebug_v1 runs support code and then exits the program with an error (line 96).

/usr/bin/pkexec

Overview

  • Program to execute a command as another user following authorization by Polkit
  • Provided by polkit-122-3.fc38.x86_64
  • Has SHA256 checksum 464f0d701f86a274378458ccc6e6724b846667f814d2bace025e9b12b6ff6112
  • Has security context system_u:object_r:bin_t:s0

Why does this binary need to be SUID-root?

pkexec(1) expects to run with EUID 0 unconditionally.1

Following authorization by a separate agent, pkexec(1) must be able to open a PAM session per the service configuration file at /etc/pam.d/polkit-1. This stack includes the session management stack in /etc/authselect/system-auth:

[user@fedora ~]$ cat /etc/pam.d/polkit-1 | grep -E '^-?session'
session    include      system-auth

In order to run the modules in this stack, pkexec(1) must run with EUID 0 and its effective capability set must contain CAP_SETGID and CAP_SETUID (see the dicussion of /usr/bin/su).

Can we supplement the SUID bit?

To answer this question, I first had to work around issue #17 in a way that would let me trace pkexec(1) without also tracing the authentication agent and helper. I started by writing the following Bash script (as pkwrap.sh):

#!/bin/bash
#
# SPDX-FileCopyrightText: Copyright 2023 OK Ryoko
# SPDX-License-Identifier: GPL-3.0-or-later

set -eu

echo "Bash PID: ${BASHPID}"

read -n 1 -p 'Attempt pkexec? [y]: '
if ! [[ "${REPLY}" == 'y' || -z "${REPLY}" ]]; then
    echo 'Bailing...'
    exit 0
fi

pkexec true

exit 0

I ensured the script was executable:

[user@fedora ~]$ chmod 0770 pkwrap.sh

I then executed the script in tty1 without entering any input:

[user@fedora ~]$ ./pkwrap.sh
Bash PID: 830
Attempt pkexec? [y]:

I logged into tty2 as the same user and spun up the authentication agent using the provided PID:

[user@fedora ~]$ pkttyagent -p 830 # p: process

pkttyagent(1) is a textual Polkit authentication helper whose SUID bit is unset and that has no file capabilities:

[user@fedora ~]$ ls -lZ /usr/bin/pkttyagent
-rwxr-xr-x. 1 root root system_u:object_r:bin_t:s0 24472 Jan 19  2023 /usr/bin/pkttyagent
[user@fedora ~]$ getcap /usr/bin/pkttyagent
[user@fedora ~]$

Back in tty1, I gave the shell script the go-ahead. In tty2, I was greeted with and completed an authentication prompt:

==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/true' as the super user
Authenticating as: Fedora User (user)
Password:
==== AUTHENTICATION COMPLETE ====

I pressed Ctrl+C to kill pkttyagent(1) and returned to tty1 to find that the script had succeeded.

Having confirmed the end-to-end functionality of pkwrap.sh, I reproduced this procedure successfully after making the following changes to the binary:

[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid=ep /usr/bin/pkexec

Does this program drop privileges? If so, when?

pkexec(1) drops privileges explicitly only when the --user option is supplied on the command line; it otherwise runs the given program as root.

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t restrict the permissions of the bin_t domain on the capability and capability2 classes, so pkexec(1) runs unconfined.

Comments

If the system doesn’t need to support any use cases in which unprivileged users perform privileged actions via pkexec(1) or another utility such as sudo(8) or doas(1) is preferred for this purpose, then consider unsetting the SUID bit on /usr/bin/pkexec without setting file capabilities.

pkexec(1) becomes root before opening a PAM session and changing to the user as which the given program should run:2

919<pkexec> setreuid(0, 0, <unfinished ...>
920<pool-spawner> futex(0x562b40899a50, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...>
919<pkexec> <... setreuid resumed>) = 0
919<pkexec> geteuid() = 0
919<pkexec> getuid()  = 0
919<pkexec> getegid() = 0
919<pkexec> getgid()  = 0

Source: sudo strace -u user -fyY ./pkwrap.sh after truncating whitespace for readability

Therefore, unless pkexec(1) drops privileges before running the given program, the new process will have all capabilities:

[user@fedora ~]$ pkexec getpcaps "\${BASHPID}"
${BASHPID}: =ep

Source code references

All line numbers refer to polkit/src/programs/pkexec.c@122.

1 After parsing the command line, main errors out if geteuid(2) doesn’t return 0 (lines 577–582)

2 If not executing the given program as root, main calls setreuid(2) to change the RUID and EUID to 0, erroring out in the event of failure (lines 950–959)

/usr/lib/polkit-1/polkit-agent-helper-1

Overview

  • Polkit agent helper to re-authenticate a user
  • Provided by polkit-122-3.fc38.x86_64
  • Has SHA256 checksum d0969c74c61ee3d1266c15e7d25b7e4f40f66f36de81253eb668c880f2db4651
  • Has security context system_u:object_r:policykit_auth_exec_t:s0 set by the policykit 1.3.0 module

Why does this binary need to be SUID-root?

polkit-agent-helper-1 expects to run with EUID 0 unconditionally.1 As for specific privileges, polkit-agent-helper-1 must be able to write to the audit log (CAP_AUDIT_WRITE).

Can we supplement the SUID bit?

polkit-agent-helper-1 is a helper program intended to be run by other applications. It expects those applications to provide an authentication cookie over standard input. To answer this question, I needed to run this program indirectly so as to facilitate tracing. First, I logged into tty2 and collected the shell’s PID:

[user@fedora ~]$ echo $$
830

I then ran pkttyagent(1) in tty1:

[user@fedora ~]$ pkttyagent -p 830

Back in tty2, I tried to use pkexec(1) to run a trivial program as root:

[user@fedora ~]$ pkexec true

In tty1, I was asked by Polkit to authenticate:

==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/true' as the super user
Authenticating as: Fedora User (user)
Password:
==== AUTHENTICATION COMPLETE ====

I pressed Ctrl+C to kill pkttyagent(1) and returned to tty2 to find that the command had succeeded.

Having validated the end-to-end functionality of pkttyagent(1), I reproduced this procedure successfully after making the following changes to the binary:

[user@fedora ~]$ sudo setcap cap_audit_write=ep /usr/lib/polkit-1/polkit-agent-helper-1

I then repeated the procedure a third time, this time entering an incorrect password to ensure that a corresponding entry would be recorded in the audit log:

type=USER_AUTH msg=audit(0.0:126): pid=914 uid=1000 auid=1000 ses=3 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=? acct="user" exe="/usr/lib/polkit-1/polkit-agent-helper-1" hostname=fedora addr=? terminal=tty1 res=failed'UID="user" AUID="user"

Source: sudo tail -n 64 /var/log/audit/audit.log | grep 'polkit-agent-helper-1' | grep 'failed'

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, polkit-agent-helper-1 doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy defines many transitions to the policykit_auth_t domain through the policykit_auth_exec_t file type:

[user@fedora ~]$ sudo sesearch -T -t policykit_auth_exec_t | wc -l
31

Familiar examples of source domains include NetworkManager_t, system_dbusd_t and user_t. However, unconfined_t is not one of them:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t policykit_auth_exec_t
[user@fedora ~]$

Thus, when pkttyagent(1) is used as the authentication agent, polkit-agent-helper-1 runs unconfined.

What capabilities can processes running in the policykit_auth_t domain have?

[user@fedora ~]$ sudo sesearch --allow -t policykit_auth_t -c capability,capability2
allow policykit_auth_t policykit_auth_t:capability { audit_write ipc_lock setgid setuid sys_nice };

There should therefore not be any issues with setting CAP_AUDIT_WRITE on /usr/lib/polkit-1/polkit-agent-helper-1.

Comments

polkit-agent-helper-1 must be able to authenticate a user per the PAM service configuration file at /etc/pam.d/polkit-1, which includes the authentication stack in /etc/authselect/system-auth:

[user@fedora ~]$ cat /etc/pam.d/polkit-1 | grep -E '^-?auth'
auth       include      system-auth

polkit-agent-helper-1 doesn’t require any privileges to perform this action (see my comments on /usr/bin/su).

Further reading

  • Issue #168: polkit-agent-helper-1 is setuid root and runnable by ordinary users, does it need to be?

Source code references

1 In polkit/src/polkitagent/polkitagenthelper-pam.c@122, main errors out if geteuid(2) doesn’t return 0 (lines 93–105)

/usr/libexec/openssh/ssh-keysign

Overview

  • OpenSSH helper for host-based authentication
  • Provided by openssh-9.0p1-14.fc38.1.x86_64
  • Installed with mode -r-sr-xr-x
  • Has SHA256 checksum 07fba19bd34da8334a211295d3f0ccb19b3a8d40df3d47ef117609fd96ec4d14
  • Has security context system_u:object_r:ssh_keysign_exec_t:s0 set by the ssh 2.4.2 module

Why does this binary need to be SUID-root?

ssh-keysign(8) must be able to read the SSH host keys in /etc/ssh (CAP_DAC_READ_SEARCH).

Can we replace the SUID bit with zero or more file capabilities?

I was able to perform host-based authentication to another Fedora Server 38 VM after making the following changes to the binary on the client machine:

[user@fedora ~]$ sudo chmod u-s /usr/libexec/openssh/ssh-keysign
[user@fedora ~]$ sudo setcap cap_dac_read_search=ep /usr/libexec/openssh/ssh-keysign

Does this program drop privileges? If so, when?

Yes, after opening the host keys and fetching the password entry for the user:1

829<ssh-keysign> openat(AT_FDCWD</home/user>, "/etc/ssh/ssh_host_rsa_key", O_RDONLY) = 6</etc/ssh/ssh_host_rsa_key>
...
829<ssh-keysign> setresgid(1000, 1000, 1000) = 0
829<ssh-keysign> setresuid(1000, 1000, 1000) = 0

Source: sudo strace -u user -fyY ssh [email protected] after omitting syscalls related to pwgetuid(3)

What role does SELinux play in constraining the privilege of the program?

Recall that ssh-keysign(8) is intended to be run as a helper by ssh(1).

The targeted SELinux policy declares a process transition from the ssh_t domain to the ssh_keysign_t domain (the application domain for the ssh_keysign_exec_t file type)…

[user@fedora ~]$ sudo sesearch -T -s ssh_t -t ssh_keysign_exec_t
type_transition ssh_t ssh_keysign_exec_t:process ssh_keysign_t; [ ssh_keysign ]:True

… but no transition from unconfined_t to ssh_t:

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t ssh_t
[user@fedora ~]$

Thus, ssh(1) and ssh-keysign(8) run in the unconfined_t domain, their capabilities unconstrained by SELinux.

Comments

Host-based authentication is deemed insecure by the ssh(1) man page and disabled by default. Consider unsetting the SUID bit on /usr/libexec/openssh/ssh-keysign without setting file capabilities.

Source code references

The NO_UID_RESTORATION_TEST and __APPLE__ macros are assumed to be undefined.

1 In openssh-portable/ssh-keysign.c@V_9_0_P1, main tries to open up to five host key files (lines 202–206). main then calls getpwuid(3) to retrieve the password entry for the invoking user, copying the data to the pw local variable (lines 208–210). Finally, main passes pw to permanently_set_uid (line 212). In openssh-portable/uidswap.c@V_9_0_P1, permanently_set_uid calls setresgid(2) (line 195) and setresuid(2) (line 208) to assume the user and primary group stored in pw.

/usr/sbin/grub2-set-bootflag

Overview

  • Program to set a bootflag in the GRUB environment block
  • Provided by grub2-tools-minimal-1:2.06-89.fc38.x86_64
  • Has SHA256 checksum c2b68f378343f65fd471aa6a08926214520f7196b737c478f96d74eafc8ead11
  • Has security context system_u:object_r:bootloader_exec_t:s0 set by the bootloader 1.14.0 module

Why does this binary need to be SUID-root?

grub2-set-bootflag(1) sets its UIDs and GIDs to 0 unconditionally (CAP_SETUID and CAP_SETGID) so that it can read and write the GRUB environment block at /boot/grub2/grubenv while ensuring that the owner and group of the file remain root. The program also does this to protect itself from kill signals.1

Can we replace the SUID bit with zero or more file capabilities?

I was able to run the program to set the boot_success flag after making the following changes to the binary:

[user@fedora ~]$ sudo chmod u-s /usr/sbin/grub2-set-bootflag
[user@fedora ~]$ sudo setcap cap_setgid,cap_setuid=ep /usr/sbin/grub2-set-bootflag

To test these changes, I started by rebooting:

[user@fedora ~]$ shutdown -r now # r: reboot

After logging in, I checked whether the boot_success flag had been set:

[user@fedora ~]$ sudo cat /boot/grub2/grubenv | grep 'boot_success'
boot_success=0

It hadn’t. This is because the grub-boot-success service doesn’t run until two minutes into the user session:

[user@fedora ~]$ systemctl --no-pager --user show grub-boot-success.timer | grep '^TimersMonotonic'
TimersMonotonic={ OnActiveUSec=2min ; next_elapse=2min 8.612087s }

… giving me a small window in which I could test the file capabilities:

[user@fedora ~]$ grub2-set-bootflag boot_success
[user@fedora ~]$ sudo cat /boot/grub2/grubenv | grep 'boot_success'
boot_success=1

Does this program drop privileges? If so, when?

As far as I can tell from the source code and strace(1) output, grub2-set-bootflag(1) becomes root immediately after processing the command line and doesn’t drop privileges explicitly by calling any of setuid(2), seteuid(2), setreuid(2) or setresuid(2).

What role does SELinux play in constraining the privilege of the program?

The targeted SELinux policy doesn’t define a process transition from the unconfined_t domain to the bootloader_t domain (the application domain for the bootloader_exec_t file type):

[user@fedora ~]$ sudo sesearch -T -s unconfined_t -t bootloader_exec_t
[user@fedora ~]$

Thus, grub2-set-bootflag(1) runs in the unconfined_t domain, its capabilities unconstrained by SELinux.

Source code references

1 In grub/util/grub-set-bootflag.c after applying the patches in src.fedoraproject.org/rpms/grub2@48cf39d, main calls setuid(2) (line 99) and setgid(2) (line 106) to become root, erroring out if either syscall fails

The findings at a glance

I was able to substitute the SUID bit using file capabilities on the following binaries:

  • /usr/bin/chage
  • /usr/bin/gpasswd
  • /usr/bin/newgrp
  • /usr/sbin/unix_chkpwd
  • /usr/libexec/openssh/ssh-keysign
  • /usr/sbin/grub2-set-bootflag

The SUID bit on the remaining binaries had to be set. However, I could still set file capabilities on those binaries to limit the level of privilege attainable by the respective programs:

  • /usr/bin/passwd
  • /usr/bin/mount
  • /usr/bin/umount
  • /usr/sbin/pam_timestamp_check
  • /usr/bin/su
  • /usr/bin/sudo
  • /usr/bin/pkexec
  • /usr/lib/polkit-1/polkit-agent-helper-1

I determined that none of these programs are capability-aware, i.e., they don’t use the libcap(3) API.

For unconfined processes, I determined that the default targeted SELinux policy constrains the level of privilege attainable by the programs corresponding to the following binaries:

  • /usr/bin/passwd
  • /usr/sbin/unix_chkpwd

The remaining programs generally run unconfined.

To get a high-level idea of the privileges needed by the SUID-root binaries that come with Fedora Server 38, I tallied the file capabilities that I ended up setting to enable the reference use cases:

          CAP_CHOWN ┤██████ 3
   CAP_DAC_OVERRIDE ┤██████ 3
CAP_DAC_READ_SEARCH ┤████████ 4
         CAP_FOWNER ┤██ 1
         CAP_SETGID ┤████████████ 6
         CAP_SETUID ┤████████████ 6
      CAP_SYS_ADMIN ┤████ 2
   CAP_SYS_RESOURCE ┤██ 1
    CAP_AUDIT_WRITE ┤██████████████████ 9

Open questions

My work has led me to pose the following questions:

Supporting information

Appendix A: List of abbreviations

API Application programming interface

BPF Berkeley Packet Filter

CC Creative Commons

EGID Effective GID

EUID Effective UID

FSUID File system UID

GID Group ID

GNU GNU’s Not Unix

GRUB Grand Unified Bootloader

ID Identifier

IP Internet Protocol

NIS Network Information Service

PAM Pluggable Authentication Modules

PID Process ID

RGID Real GID

RPM RPM Package Manager

RUID Real UID

SELinux Security-Enhanced Linux

SHA Secure Hash Algorithm

SSH Secure Shell

SSSD System Security Services Daemon

SUID Set-UID

UID User ID

UUID Universally unique ID

VM Virtual machine

Feedback and support

Please connect with me if you have constructive comments about this article, especially if you have a use case I didn’t cover or are unable to reproduce a procedure in an identical environment.

Fedora Linux 38 reached end of life on May 21, 2024. I am therefore no longer maintaining this article. However, I may incorporate relevant constructive feedback into reports pertaining to newer releases of Fedora Linux.

Funding sources

The author acknowledges private funding by one anonymous sponsor.

Licensing

The C code for the programs unix_chkpwd_wrapper and timestamper, and the Bash script pkwrap.sh, are provided under the GNU General Public License v3.0 or later.

All other original copyrightable content in this document is marked with CC0 1.0 Universal.

Revision history

2024-07-28.1

  • Remove section titled “Disadvantages of setting file capabilities”
  • State discontinuation of maintenance of this article

2024-02-12.1

2024-02-09.4

Fix broken hyperlink in revision history

2024-02-09.3

  • Mention significance of CAP_SYS_RESOURCE for /usr/bin/sudo
  • Clarify use case for sudoedit(8)
  • Update capability histogram

2024-02-09.2

  • Shorten article name
  • Add fdisk(8) command line to procedure for /usr/bin/mount
  • Format fdisk as fdisk(8) in body text
  • Annotate short options to commands
  • Add SSSD, OpenSSH and GRUB hyperlinks
  • Trim stray whitespace

2024-02-09.1

  • Provide more exposition for source code references
  • Fix source code references for /usr/bin/mount
  • Reduce scope of source code reference for /usr/bin/gpasswd and add supporting line from strace(1) output
  • Be more careful when making statements about unconditional code execution
  • Expand NIS abbreviation in Appendix A

2024-02-07.1

  • Add Fedora Linux 38 GPG key prompt
  • Link to new article: SUID-root Binaries in Fedora Workstation 38
  • Delegate open question about Fedora Linux 39 to new article
  • Add section detailing feedback and support
  • Expand BPF, GNU, ID, IP, SELinux, SHA and UUID abbreviations in Appendix A
  • Update wording to accommodate stylistic preferences

2024-02-04.2

/usr/bin/su:

  • Expound su(1)–unix_chkpwd(8) interaction
  • Defer --pty use case

2024-02-04.1

/usr/bin/mount:

  • Add confidence check
  • Discuss permissions of mount_t domain on capability and capability2 classes

2024-01-29.1

  • Check for allowed permissions of sysadm_su_t, sysadm_sudo_t and policykit_auth_t on capability2
  • Compile unix_chkpwd_wrapper.c and timestamper.c with -Wall
  • Mention presence of security.selinux extended attribute on binaries
  • Add libvirt hyperlinks
  • Add SPDX-FileCopyrightText tag to source code headers
  • Ensure consistent ordering of strace(1) options
  • Avoid potentially ableist language: s/sanity check/confidence check/g
  • Correct typos

2024-01-13.1

  • Use common names for Fedora flavors, e.g., “Fedora Server 38” instead of “Fedora Linux 38 Server”
  • Be specific about the particular flavor of Fedora in question (Server)
  • Distinguish minimal installation of Fedora Server from installation of Fedora Minimal
  • Replace text with abbreviation
  • Push mention of libcap(3) to “The findings at a glance”
  • Call pkgconf(1) when compiling timestamper
  • Drop Arch Linux and Gentoo Linux as candidates for investigation because they emphasize choice and control, leading to relatively high variation across installations

2024-01-04.1

  • Add statements to clarify how output was captured
  • Format SELinux class names as code
  • Look up permissions on the capability2 class for the passwd_t and chkpwd_t domains
  • Obfuscate IPv4 address per RFC 5737
  • Replace list of capability counts with textual histogram

2023-12-18.2

Remove references to file capabilities for processes run as root (all UIDs 0) because they get ignored

2023-12-18.1

Initial revision

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment