Software Engineering WikiSE Wiki

Users, permissions and SELinux

Manage accounts, groups and sudo rules, read and fix file modes, ACLs and capabilities, and decode SELinux denials on Fedora and RHEL.

Reviewed MarkdownEdit

On this page

Cheatsheet#

TaskCommand
Create a user with a home and shelluseradd -m -s /bin/bash alice
Create a system account, no home, no loginuseradd -r -s /usr/sbin/nologin -d /var/lib/my-app my-app
Add to a supplementary group, keep the othersusermod -aG wheel alice
Set or change a password non-interactivelyprintf '%s' "$NEW_PW" | passwd --stdin alice
Force a password change at next loginchage -d 0 alice
Show password ageing and expirychage -l alice
Lock the password, then also block key loginsusermod -L alice; usermod -e 1 alice
Remove a user and their homeuserdel -r alice
Who am I, in which groups, with which SELinux contextid, id -Z
Resolve a name through every source (files, sssd, LDAP)getent passwd alice, getent group wheel
What may I run with sudosudo -l
Edit sudoers safelyvisudo -f /etc/sudoers.d/my-app
Check all sudoers files parsevisudo -c
Every permission on a path, level by levelnamei -l /srv/www/html/index.html
Mode, owner, group and contextstat -c '%A %U:%G %n' file, ls -Z file
ACLs on a filegetfacl file
Grant one user read on a file with an ACLsetfacl -m u:alice:r file
File capabilitiesgetcap /usr/bin/ping
SELinux modegetenforce, sestatus
Fix labels under a pathrestorecon -Rv /srv/www
Recent SELinux denialsausearch -m AVC,USER_AVC -ts recent -i
Why was it deniedausearch -m AVC -ts recent | audit2why
Unlock an account locked by failed loginsfaillock --user alice --reset

Commands assume shadow-utils 4.14+, sudo 1.9, and SELinux with the targeted policy as shipped by Fedora 42+ and RHEL 9/10. The semanage, audit2allow and sepolicy commands come from policycoreutils-python-utils; sealert from setroubleshoot-server; sesearch from setools-console. References: shadow-utils, the sudoers manual and the RHEL SELinux guide.

How access is decided#

A process carries a real and effective UID and GID, a list of supplementary groups, a capability set and an SELinux context. When it opens a file the kernel checks, in order: discretionary access (the mode bits and ACL), then the mandatory SELinux policy. Both must allow the operation. A denial from the mode bits and a denial from SELinux both return EACCES, so Permission denied alone does not tell you which layer refused; ausearch does.

Group membership is read at login. A user added to a group keeps the old membership in every running session until they log in again, which is why usermod -aG docker alice appears not to work until the shell is restarted. newgrp docker starts a new shell with the group active, and loginctl terminate-user alice ends the stale sessions.

Accounts#

/etc/passwd holds names, UIDs, primary GIDs, home directories and shells. /etc/shadow holds password hashes and ageing, readable only by root. /etc/group holds supplementary membership. Defaults for new accounts come from /etc/login.defs (UID_MIN, SYS_UID_MAX, UMASK, HOME_MODE) and /etc/default/useradd (useradd -D), and the home directory is copied from /etc/skel.

useradd -m -s /bin/bash -c 'Alice Example' -G wheel,developers alice   # -m creates the home from /etc/skel
useradd -r -s /usr/sbin/nologin -d /var/lib/my-app -M my-app            # -r: UID below SYS_UID_MAX; -M: no home directory
useradd -u 2001 -g developers -e 2026-12-31 contractor                   # fixed UID, primary group, expiry date

usermod -aG docker alice        # -a is essential: without it the user is removed from every other supplementary group
usermod -s /usr/sbin/nologin alice
usermod -d /home/alice2 -m alice # -m moves the existing home directory contents
usermod -L alice                 # lock: prefixes the hash in /etc/shadow with "!"
usermod -U alice                 # unlock
usermod -e 1 alice               # account expired on 1970-01-02: blocks every login method, including SSH keys
usermod -e '' alice              # remove the expiry

userdel alice                    # keeps the home directory and mail spool
userdel -r alice                 # deletes them; files owned by the UID elsewhere on disk remain: find / -xdev -nouser

getent consults every source in /etc/nsswitch.conf, so it finds sssd, LDAP and IdM users as well as local ones. grep alice /etc/passwd does not.

A locked password does not stop SSH keys

passwd -l and usermod -L only invalidate the hash. sshd with PubkeyAuthentication never consults it, so a user with an authorized_keys file still logs in. To disable an account, set the expiry (usermod -e 1) or change the shell to nologin; the expiry is checked by PAM’s account phase for every login method.

Passwords and ageing#

passwd alice                     # interactive; as root no old password is asked for
passwd -S alice                  # status: L locked, NP no password, P usable password, plus ageing fields
passwd -e alice                  # expire now; user must change at next login
passwd -d alice                  # remove the password: empty password, allowed only where nullok is configured
chage -l alice                   # human-readable ageing
chage -M 90 -m 1 -W 14 alice     # max 90 days, min 1 day between changes, warn 14 days ahead
chage -E 2026-12-31 alice        # account (not password) expiry; -E -1 removes it
chage -d 0 alice                 # last change "never": forces a change at next login

Password quality rules live in /etc/security/pwquality.conf and apply to passwd runs by non-root users; root can set anything. Hash algorithm and rounds come from /etc/login.defs (ENCRYPT_METHOD YESCRYPT on Fedora and RHEL 9+).

Groups#

groupadd developers
groupadd -r -g 950 my-app        # system group, fixed GID
gpasswd -a alice developers      # add a member
gpasswd -d alice developers      # remove a member
groupmems -g developers -l       # list members
groupdel developers              # refused while it is any user's primary group
getent group developers          # name:x:gid:member,member
id alice                         # uid, primary gid and every group, from the databases, not a running session

Each user gets a private primary group of the same name by default (USERGROUPS_ENAB yes). Files created with a umask of 002 are then group-writable only by that one user, which is the reason a shared directory needs a setgid bit and a real shared group.

sudo#

sudo reads /etc/sudoers and then every file in /etc/sudoers.d/ whose name contains no . or ~, in lexical order. A later matching rule wins over an earlier one. Always edit through visudo: it locks the file, parses it before saving, and refuses to install a rule set that would lock everyone out.

visudo                              # /etc/sudoers
visudo -f /etc/sudoers.d/my-app     # a drop-in; created with mode 0440
visudo -c                           # parse every file; run after any change made by configuration management
visudo -cf /path/to/candidate       # check a file before installing it
sudo -l                             # rules that apply to me
sudo -l -U alice                    # rules that apply to alice (root only)
sudo -u my-app -i                   # login shell as another user
sudo -k                             # drop the cached credential
sudo -n true                        # non-interactive: fails instead of prompting; use in scripts
sudoedit /etc/my-app/config.ini     # edit as root through a copy, with your own editor, no shell escape as root

A drop-in that grants a service account exactly what it needs:

# /etc/sudoers.d/my-app  (0440 root:root)
Cmnd_Alias MY_APP = /usr/bin/systemctl restart my-app.service, \
                    /usr/bin/systemctl status my-app.service, \
                    /usr/bin/journalctl -u my-app.service *
deploy ALL=(root) NOPASSWD: MY_APP
%developers ALL=(root) /usr/bin/journalctl -u my-app.service *
Defaults:deploy !requiretty

Rules that look restrictive but are not:

RuleWhy it is rootSafer form
alice ALL=(root) /usr/bin/vim /etc/my-app/*vim has :!sh, and * matches ../shadowalice ALL=(root) sudoedit /etc/my-app/*
alice ALL=(root) /usr/bin/less /var/log/*less runs !sh; also find, awk, tar, tee, systemctl (pager)Defaults:alice !env_reset is not it; grant journalctl --no-pager or use NOEXEC:
alice ALL=(root) ALL, !/usr/bin/suNegation is bypassed by copying su or using sudo bashEnumerate the commands allowed instead
alice ALL=(root) /usr/bin/systemctlAny argument, including systemctl editGive the full argument list, or systemctl restart my-app.service and nothing after it
alice ALL=(root) /usr/bin/pip install *pip runs arbitrary setup.pyPackage the software instead

Useful Defaults, set once in /etc/sudoers.d/00-defaults:

Defaults use_pty                    # commands run in a pseudo-terminal; blocks a background process from stealing the tty
Defaults log_output                 # record sessions under /var/log/sudo-io; replay with sudoreplay -l
Defaults!/usr/bin/sudoreplay !log_output
Defaults timestamp_timeout=5        # minutes the cached credential lasts; 0 asks every time
Defaults passwd_tries=3
Defaults env_reset, secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
Defaults env_keep += "HTTPS_PROXY NO_PROXY"
Defaults logfile=/var/log/sudo.log  # in addition to the journal

NOEXEC: in front of a command stops it executing other programs, which closes the pager and editor escapes for dynamically linked binaries. sha256:<digest> /usr/local/bin/tool in place of a bare path only matches when the binary hashes correctly, which protects a rule against a replaced executable.

File modes and ownership#

stat -c '%A %a %U:%G %n' /srv/www/html      # drwxr-sr-x 2755 root:www /srv/www/html
namei -l /srv/www/html/index.html            # each component with its owner and mode; the fastest answer to "why can't I open this"
chown alice:developers file
chown -R --reference=/srv/www/html /srv/www/staging   # copy owner and group from another path
chmod 640 file                               # u=rw g=r o=
chmod -R u=rwX,g=rX,o= /srv/app              # X: execute only on directories and files already executable
chmod g+s /srv/shared                        # setgid directory: new files inherit the directory's group
chmod +t /srv/shared                         # sticky: only a file's owner (or root) can delete it
chmod u+s /usr/local/bin/my-tool             # setuid: runs as the file's owner; audit every one of these
umask                                        # 0022 (or 0077 for root on Fedora/RHEL); subtracted from 666/777 for new files
find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -ls   # every setuid/setgid binary
find /srv -xdev -perm -o+w -not -type l -ls  # world-writable without the sticky bit protection
find / -xdev \( -nouser -o -nogroup \) -ls   # owned by a deleted UID/GID

Directory execute is traverse: without x on every directory in the path the file is unreachable whatever its own mode, and ls on the directory shows names but no metadata. Read on a directory without execute lists names only.

Numeric modes carry the special bits in a fourth leading digit: 4755 setuid, 2775 setgid, 1777 sticky (the mode of /tmp). chmod 755 on a setgid directory clears the setgid bit on some systems and keeps it on others; use g+s explicitly after a recursive chmod. The setuid bit on a directory does nothing on Linux, and setuid on a shell script is ignored by the kernel.

chattr +i file makes a file immutable even for root (lsattr shows it, chattr -i removes it), and chattr +a allows append only, which is useful for log files. A file that root cannot modify, with a mode that says it can, is usually one of these two.

Shared directories#

The setgid bit, a group, and a default ACL together give a directory where a team can write each other’s files:

groupadd developers
mkdir -p /srv/shared
chgrp developers /srv/shared
chmod 2770 /srv/shared                        # setgid: new entries get group developers
setfacl -m d:g:developers:rwx /srv/shared     # default ACL: new entries are group-writable regardless of umask

ACLs#

Access control lists add per-user and per-group entries beyond the single owner/group/other triple. The group mode bits become the ACL mask, an upper bound applied to every named user and group entry, so chmod g-w silently strips write from everyone named in the ACL. ls -l shows a + after the mode when an ACL is present.

getfacl file                                  # owner, group, every entry, mask
getfacl -R /srv/shared > /root/shared.acl     # backup in a format setfacl can restore
setfacl -m u:alice:rw file                    # user entry
setfacl -m g:developers:rx,o::- file          # several entries; o::- removes other's bits
setfacl -m m::r file                          # tighten the mask: no named entry exceeds read
setfacl -x u:alice file                       # remove one entry
setfacl -b file                               # remove every extended entry; the mode bits stay
setfacl -R -m u:alice:rX /srv/shared          # recursive; X as in chmod
setfacl -m d:u:alice:rwx /srv/shared          # default entry: inherited by new children of this directory
setfacl -k /srv/shared                        # remove the default entries
setfacl --restore=/root/shared.acl            # restore the backup, including owners and modes

cp drops ACLs unless run as cp -a or cp --preserve=all; rsync needs -A; tar needs --acls. NFSv4 has its own ACL model (nfs4_getfacl), and SMB shares map to it through Samba’s vfs_acl_xattr, so an ACL that works locally may not survive export.

Capabilities#

Capabilities split root’s power into about 40 flags so a binary or service can bind a low port or open raw sockets without being root for everything else. They are stored on the file as the security.capability extended attribute.

getcap /usr/bin/ping                          # /usr/bin/ping cap_net_raw=ep
getcap -r / 2>/dev/null                       # every file capability on the system
setcap cap_net_bind_service=+ep /usr/local/bin/my-app   # bind ports below 1024 as a normal user
setcap -r /usr/local/bin/my-app               # remove
getpcaps "$(pidof my-app)"                    # effective capabilities of a running process
grep Cap /proc/"$(pidof my-app)"/status       # CapInh, CapPrm, CapEff, CapBnd, CapAmb as hex
capsh --decode=0000000000000400               # cap_net_bind_service
capsh --print                                 # capabilities of the current shell

e (effective), p (permitted) and i (inheritable) in setcap map to the process sets the binary starts with; =ep is what a normal binary needs. File capabilities are ignored on filesystems mounted nosuid and stripped by cp without --preserve=xattr, rsync without -X and by package upgrades that replace the file. For a service, prefer the unit file over the binary: AmbientCapabilities=CAP_NET_BIND_SERVICE with CapabilityBoundingSet=CAP_NET_BIND_SERVICE in systemd survives upgrades and shows up in systemctl show. For the specific case of low ports, sysctl net.ipv4.ip_unprivileged_port_start=80 removes the need entirely.

CAP_SYS_ADMIN, CAP_DAC_OVERRIDE, CAP_SETUID, CAP_SYS_PTRACE, CAP_SYS_MODULE and CAP_DAC_READ_SEARCH are each root-equivalent or close to it. Granting them to a binary is granting root to anyone who can run it.

PAM#

Every login path (sshd, login, sudo, su, GDM, passwd) runs the stack in /etc/pam.d/<service>, which on Fedora and RHEL includes system-auth or password-auth. A stack has four phases: auth (who are you), account (are you allowed right now: expiry, pam_access, pam_nologin, faillock), password (changing credentials) and session (limits, home directory creation, pam_systemd). Control flags decide how a module’s result combines: required (must pass, but the stack continues so the failure is not revealed), requisite (must pass, stops immediately), sufficient (pass ends the phase successfully unless an earlier required failed), optional, and include/substack.

system-auth and password-auth are generated by authselect on Fedora and RHEL 8+, and a manual edit is overwritten at the next authselect apply-changes. Change features instead:

authselect current                            # profile and enabled features
authselect list                               # local, sssd, winbind, ...
authselect list-features sssd
authselect select sssd with-faillock with-mkhomedir --force   # rewrites /etc/pam.d/{system,password}-auth and nsswitch.conf
authselect enable-feature with-pamaccess      # then edit /etc/security/access.conf
authselect check                              # reports files changed outside authselect
authselect create-profile my-site -b sssd     # custom profile under /etc/authselect/custom when a feature is not enough

Files the modules read:

ModuleFilePurpose
pam_faillock/etc/security/faillock.confdeny = 5, unlock_time = 900, even_deny_root; state in /var/run/faillock/
pam_pwquality/etc/security/pwquality.confminlen, dcredit, dictcheck
pam_access/etc/security/access.conf- : ALL EXCEPT wheel developers : ALL restricts login to listed groups
pam_limits/etc/security/limits.conf, limits.d/nofile, nproc, per user or group; not used by systemd services
pam_nologin/etc/nologin, /run/nologinNon-root logins refused while the file exists; systemd creates /run/nologin during boot
pam_sss/etc/sssd/sssd.confIdM, AD and LDAP users
pam_wheel (in /etc/pam.d/su)Uncomment to restrict su to group wheel
faillock --user alice                         # failed attempts recorded
faillock --user alice --reset                 # clear them
journalctl -t sshd -t login -t sudo --since -1h   # PAM messages are logged under the service's tag

SELinux#

SELinux labels every process and object with a context user:role:type:level, for example system_u:object_r:httpd_sys_content_t:s0, and the targeted policy allows an operation only when a rule permits the process type (its domain) to perform that access on the object type. The mode bits are still checked first; SELinux can only refuse further. Nearly all administration is about the type field.

getenforce                                    # Enforcing, Permissive or Disabled
sestatus                                      # mode, policy name, and whether the config file and runtime differ
setenforce 0                                  # permissive until reboot: denials are logged, not enforced
setenforce 1
grep ^SELINUX= /etc/selinux/config            # boot-time mode; disabled here needs a reboot and a relabel to re-enable
ls -Z /srv/www/html                           # file contexts
ps -eZ | grep -w httpd                        # process domains: httpd_t
id -Z                                         # unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
ss -ltnZ                                      # listening sockets with their domain

Permissive is a diagnostic tool, not a fix. Passing the kernel argument enforcing=0 does the same for one boot. SELINUX=disabled in the config file has been ignored by the kernel since Fedora 34 and RHEL 9; disabling now needs selinux=0 on the kernel command line, after which every file created is unlabelled and a relabel of the whole filesystem is needed to turn it back on.

File contexts#

A new file inherits the type of its directory unless a policy transition rule says otherwise. mv keeps the source label because it is a rename; cp creates a new file and labels it by the destination, unless cp -a or --preserve=context was used. A file moved from a home directory into /var/www therefore carries user_home_t and Apache gets 403, with an AVC in the audit log.

matchpathcon /srv/www/html/index.html         # what the policy says the label should be
semanage fcontext -l | grep -E '^/var/www'    # rules shipped by the policy
semanage fcontext -l -C                       # local additions only
semanage fcontext -a -t httpd_sys_content_t '/srv/www(/.*)?'   # add a rule; regex, anchored at the start
semanage fcontext -a -e /var/www /srv/www     # equivalence: label /srv/www exactly as /var/www would be
semanage fcontext -d '/srv/www(/.*)?'         # remove the rule
restorecon -Rv /srv/www                       # apply rules; -v prints each change
restorecon -RvF /srv/www                      # -F also resets the user and role fields, not only the type
restorecon -Rvn /                             # dry run: what a full relabel would change
chcon -t httpd_sys_content_t /srv/www/index.html   # temporary: undone by the next restorecon or relabel
fixfiles -F onboot                            # relabel everything at next boot (writes /.autorelabel); slow on large disks

semanage writes the rule; nothing changes on disk until restorecon runs. chcon is the opposite: it changes the file without a rule, and is lost.

Ports#

Confined services may only bind ports labelled for them. Moving SSH to 2222 or a web server to 8081 needs the port labelled first.

semanage port -l | grep -E '^(ssh|http)_port_t'
semanage port -a -t ssh_port_t -p tcp 2222    # add; fails with "already defined" if another type owns that port
semanage port -m -t http_port_t -p tcp 8081   # modify an existing assignment instead
semanage port -d -t ssh_port_t -p tcp 2222
sepolicy network -p 2222                      # which types may use a port

Booleans#

Booleans switch optional rule sets without writing policy. Check for one before writing a custom module; most “service cannot reach X” problems are covered.

getsebool -a | grep httpd                     # every boolean for a domain
semanage boolean -l -C                        # booleans changed from the default
setsebool httpd_can_network_connect on        # runtime only
setsebool -P httpd_can_network_connect on     # -P persists; it rebuilds the policy and takes a few seconds
sesearch -A -s httpd_t -t httpd_sys_content_t -c file -p read   # is there an allow rule (setools-console)
sesearch -A -s httpd_t -c tcp_socket -p name_connect -b httpd_can_network_connect  # rules a boolean enables

httpd_can_network_connect_db, httpd_use_nfs, httpd_enable_homedirs, nis_enabled, container_manage_cgroup and virt_use_nfs are the ones reached for most often. semanage boolean -l prints a description for each.

Reading a denial#

Denials are written to /var/log/audit/audit.log by auditd, and setroubleshootd, if installed, posts a readable summary to the journal. Rules marked dontaudit in the policy are not logged at all; semodule -DB disables those rules temporarily so everything shows, and semodule -B restores them.

ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recent -i      # last 10 minutes, interpreted
ausearch -m AVC -ts today -c httpd                      # by command name
ausearch -m AVC -ts today | audit2why                   # explains each: missing rule, boolean, or mislabelled file
journalctl -t setroubleshoot --since -1h                # "SELinux is preventing ... For complete message run sealert -l UUID"
sealert -l 8c4a...                                      # full analysis with the suggested fix, ranked by confidence
sealert -a /var/log/audit/audit.log                     # analyse every denial in the file

A raw record:

type=AVC msg=audit(1758700000.123:4567): avc:  denied  { read } for  pid=1234 comm="httpd" name="index.html" dev="dm-0" ino=98765 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Read it as: the process in domain scontext (httpd_t) tried { read } on an object of class tclass (file) labelled tcontext (user_home_t). permissive=0 means it was blocked. Decide from the target type: a type that does not belong under that path is a labelling problem (restorecon); a correct type the domain is not allowed to touch is a boolean or a missing rule; a port class with a numbered port is semanage port.

Writing a local module#

When no boolean or label fixes it, generate a module from the denials, read it, and install it. Never install what audit2allow prints without reading it; it happily writes rules that allow the domain everything the denial mentions, and a run under semodule -DB produces rules for things the policy deliberately hides.

ausearch -m AVC -ts recent -c my-app | audit2allow -M my-app   # writes my-app.te (source) and my-app.pp (compiled)
cat my-app.te                                                  # review every allow line
semodule -i my-app.pp                                          # install; persists across reboots
semodule -l | grep my-app
semodule -r my-app                                             # remove
semanage permissive -a my_app_t                                # one domain permissive, the rest enforcing
semanage permissive -d my_app_t
semanage export > selinux-local.conf                           # every local customisation: fcontext, port, boolean, permissive

Podman and Docker run containers as container_t and allow access only to files labelled container_file_t. A bind mount needs :Z (private label) or :z (shared label) on the volume, which relabels the host directory in place; never use them on /, /home or /usr. See Docker.

Oneliners#

# Accounts that can log in: a real shell and a usable password or an SSH key
awk -F: '$7 !~ /(nologin|false)$/ {print $1}' /etc/passwd

# Accounts with UID 0 other than root
awk -F: '$3 == 0 && $1 != "root"' /etc/passwd

# Accounts with an empty password field
awk -F: '$2 == ""' /etc/shadow

# Password expiry for every human user
for u in $(awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd); do printf '%-16s %s\n' "$u" "$(chage -l "$u" | awk -F: '/Password expires/ {print $2}')"; done

# Members of wheel, from every source
getent group wheel | cut -d: -f4 | tr , '\n'

# Every sudo rule in effect for a user, including from sudoers.d
sudo -l -U alice

# Who used sudo today
journalctl _COMM=sudo --since today -o cat | grep -E 'COMMAND='

# Failed logins in the last hour by user
journalctl -t sshd --since -1h -o cat | grep -oE 'Failed password for (invalid user )?\S+' | sort | uniq -c | sort -rn

# Currently logged-in sessions
loginctl list-sessions

# Setuid and setgid files outside the package database (unowned by any rpm)
find / -xdev -type f -perm /6000 -exec sh -c 'rpm -qf "$1" >/dev/null 2>&1 || echo "$1"' _ {} \;

# World-writable directories without the sticky bit
find / -xdev -type d -perm -0002 -not -perm -1000 -ls

# Files with ACLs under a tree
getfacl -Rs /srv 2>/dev/null | grep '^# file:'

# Copy ACLs, owner and mode from one tree to another with the same layout
getfacl -R /srv/prod | sed 's#^# file: prod#\# file: staging#' | (cd /srv && setfacl --restore=-)

# Every file capability on the system
getcap -r / 2>/dev/null

# Effective capabilities of every process that has any
for p in /proc/[0-9]*; do c=$(awk '/CapEff/ {print $2}' "$p/status"); [ "$c" != 0000000000000000 ] && printf '%s %s %s\n' "${p#/proc/}" "$(cat "$p/comm")" "$c"; done

# Denials since boot, one line each, deduplicated by domain, target type and class
ausearch -m AVC -ts boot 2>/dev/null | grep -oE 'scontext=\S+ tcontext=\S+ tclass=\S+' | sort | uniq -c | sort -rn

# Files under a path whose label differs from the policy
restorecon -Rvn /srv

# Processes running in unconfined_service_t (a service without a policy)
ps -eo pid,comm,label | awk '$3 ~ /unconfined_service_t/'

# Which service unit a denied PID belongs to
systemctl status 1234 --no-pager | head -1

# Port labels for a service
semanage port -l | grep -w http_port_t

# Everything SELinux-related changed locally on this host, for reproducing on another
semanage export

Scripts#

Reports local accounts that can still log in, with their password age and last login, for a periodic access review.

#!/usr/bin/env bash
set -euo pipefail
# Human accounts (UID_MIN..60000) with a login shell, their password ageing and last login.
printf '%-16s %-8s %-12s %-12s %s\n' USER STATUS CHANGED EXPIRES LAST_LOGIN
while IFS=: read -r user _ uid _ _ _ shell; do
  (( uid >= 1000 && uid < 60000 )) || continue
  case $shell in */nologin|*/false) continue ;; esac
  status=$(passwd -S "$user" | awk '{print $2}')          # P, L or NP
  changed=$(chage -l "$user" | awk -F': ' '/Last password change/ {print $2}')
  expires=$(chage -l "$user" | awk -F': ' '/^Account expires/ {print $2}')
  last=$(lastlog -u "$user" | awk 'NR==2 {print ($3 ~ /Never/) ? "never" : $(NF-5)" "$(NF-4)" "$(NF-3)" "$NF}')
  printf '%-16s %-8s %-12s %-12s %s\n' "$user" "$status" "${changed:0:12}" "${expires:0:12}" "$last"
done < /etc/passwd

Creates users from a CSV of name,fullname,groups, each with a locked random password that must be changed at first login; rerunnable because existing users are skipped.

#!/usr/bin/env bash
set -euo pipefail
csv=${1:?usage: mkusers users.csv}
while IFS=, read -r name fullname groups; do
  [[ -z $name || $name == \#* ]] && continue
  if getent passwd "$name" >/dev/null; then printf 'skip %s: exists\n' "$name"; continue; fi
  useradd -m -s /bin/bash -c "$fullname" ${groups:+-G "$groups"} "$name"
  pw=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 20)
  printf '%s:%s\n' "$name" "$pw" | chpasswd
  chage -d 0 -M 90 -W 14 "$name"                            # change at first login, then every 90 days
  printf 'created %s (initial password delivered out of band: %s)\n' "$name" "$pw" >&2   # stderr only; never log this
done < "$csv"

Summarises SELinux denials since boot per domain and target and suggests the class of fix, for a first pass on a new host before anyone reaches for setenforce 0.

#!/usr/bin/env bash
set -euo pipefail
# Group AVC denials since boot and print audit2why's verdict for each distinct one.
ausearch -m AVC,USER_AVC -ts boot -i 2>/dev/null > "${TMPDIR:-/var/tmp}/avc.$$" || { echo "no denials since boot"; exit 0; }
trap 'rm -f "${TMPDIR:-/var/tmp}/avc.$$"' EXIT
grep -oE 'comm=\S+ .*scontext=\S+ tcontext=\S+ tclass=\S+' "${TMPDIR:-/var/tmp}/avc.$$" \
  | sed -E 's/ (pid|name|dev|ino|path)=\S+//g' | sort | uniq -c | sort -rn | head -20
echo
echo '--- audit2why ---'
ausearch -m AVC,USER_AVC -ts boot 2>/dev/null | audit2why | grep -E '^\s+(Was caused by|You can use|Missing|Unknown)' | sort | uniq -c | sort -rn

Troubleshooting#

SymptomCauseFix
Permission denied on a file whose mode looks rightA directory in the path lacks x, or the ACL mask, or SELinuxnamei -l /path; getfacl; ausearch -m AVC -ts recent
Permission denied running a script or binaryFilesystem mounted noexec, or nosuid for a setuid/capability binaryfindmnt -T /path -o TARGET,OPTIONS
Root cannot modify or delete a fileImmutable or append-only attributelsattr file; chattr -i file
usermod -aG had no effectGroup membership is read at loginLog out and in, newgrp, or loginctl terminate-user; verify with id user versus id in the session
All other groups disappeared after usermod -GMissing -aRe-add them: usermod -aG g1,g2 user from a previous id output or backups of /etc/group
Account locked due to 5 failed loginspam_faillockfaillock --user alice; faillock --user alice --reset; tune /etc/security/faillock.conf
Your account has expiredchage -E date passed or usermod -e 1chage -E -1 alice
This account is currently not availableShell is nologinusermod -s /bin/bash alice; a shell must be listed in /etc/shells
SSH key ignored after restoring a home directory.ssh label is not ssh_home_t, or modes too openrestorecon -Rv ~/.ssh; chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys; see SSH
Login refused for everyone but root/etc/nologin or /run/nologin exists, or pam_access rulerm /etc/nologin; journalctl -t sshd names the module
sudo: alice is not in the sudoers fileNo rule, or the user is in the group only in a stale sessionsudo -l -U alice; check id alice versus id
sudo: /etc/sudoers.d/x is world writable or syntax errorA drop-in written without visudovisudo -cf the file; fix mode to 0440; if sudo itself is broken, use su - or pkexec visudo
sudo: unable to resolve hostHostname not in /etc/hosts or DNSAdd it to /etc/hosts, or Defaults !fqdn
Service gets 403 or EACCES only under SELinux enforcingWrong label after mv, port unlabelled, or a boolean offausearch -m AVC -ts recent | audit2why; restorecon -Rv, semanage port -a, setsebool -P
setsebool or semanage are slow or report Could not ...Policy store rebuild, or a stale lock from an interrupted runWait; check semodule -l; semodule -B rebuilds
Denial happens but nothing in the audit logA dontaudit rule, or auditd not runningsemodule -DB then reproduce, then semodule -B; systemctl status auditd
setcap: Operation not permittedFilesystem without xattr support, NFS, or nosuidMove the binary, or use AmbientCapabilities= in the unit
Container cannot read a bind mountHost directory labelled for the host, not container_file_tMount with :Z; check with ls -Z

Further reading#