# 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.

Canonical: https://www.wiki.jodisand.me/users/
Reviewed: 2026-09-24
Related: [SSH](https://www.wiki.jodisand.me/ssh/index.md), [systemd](https://www.wiki.jodisand.me/systemd/index.md), [FreeIPA / Red Hat IdM](https://www.wiki.jodisand.me/idm/index.md), [Bash](https://www.wiki.jodisand.me/bash/index.md), [Docker](https://www.wiki.jodisand.me/docker/index.md)


## Cheatsheet

| Task | Command |
| --- | --- |
| Create a user with a home and shell | `useradd -m -s /bin/bash alice` |
| Create a system account, no home, no login | `useradd -r -s /usr/sbin/nologin -d /var/lib/my-app my-app` |
| Add to a supplementary group, keep the others | `usermod -aG wheel alice` |
| Set or change a password non-interactively | `printf '%s' "$NEW_PW" \| passwd --stdin alice` |
| Force a password change at next login | `chage -d 0 alice` |
| Show password ageing and expiry | `chage -l alice` |
| Lock the password, then also block key logins | `usermod -L alice; usermod -e 1 alice` |
| Remove a user and their home | `userdel -r alice` |
| Who am I, in which groups, with which SELinux context | `id`, `id -Z` |
| Resolve a name through every source (files, sssd, LDAP) | `getent passwd alice`, `getent group wheel` |
| What may I run with sudo | `sudo -l` |
| Edit sudoers safely | `visudo -f /etc/sudoers.d/my-app` |
| Check all sudoers files parse | `visudo -c` |
| Every permission on a path, level by level | `namei -l /srv/www/html/index.html` |
| Mode, owner, group and context | `stat -c '%A %U:%G %n' file`, `ls -Z file` |
| ACLs on a file | `getfacl file` |
| Grant one user read on a file with an ACL | `setfacl -m u:alice:r file` |
| File capabilities | `getcap /usr/bin/ping` |
| SELinux mode | `getenforce`, `sestatus` |
| Fix labels under a path | `restorecon -Rv /srv/www` |
| Recent SELinux denials | `ausearch -m AVC,USER_AVC -ts recent -i` |
| Why was it denied | `ausearch -m AVC -ts recent \| audit2why` |
| Unlock an account locked by failed logins | `faillock --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](https://github.com/shadow-maint/shadow), the [sudoers manual](https://www.sudo.ws/docs/man/sudoers.man/) and the [RHEL SELinux guide](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_selinux/index).

## 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`.

```sh
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](https://www.wiki.jodisand.me/idm/#users-and-groups) users as well as local ones. `grep alice /etc/passwd` does not.

> [!WARNING] 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

```sh
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

```sh
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.

```sh
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:

```ini
# /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:

| Rule | Why it is root | Safer form |
| --- | --- | --- |
| `alice ALL=(root) /usr/bin/vim /etc/my-app/*` | `vim` has `:!sh`, and `*` matches `../shadow` | `alice 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/su` | Negation is bypassed by copying `su` or using `sudo bash` | Enumerate the commands allowed instead |
| `alice ALL=(root) /usr/bin/systemctl` | Any argument, including `systemctl edit` | Give 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.py` | Package the software instead |

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

```ini
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

```sh
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:

```sh
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.

```sh
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.

```sh
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](https://www.wiki.jodisand.me/systemd/#writing-a-unit) 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:

```sh
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:

| Module | File | Purpose |
| --- | --- | --- |
| `pam_faillock` | `/etc/security/faillock.conf` | `deny = 5`, `unlock_time = 900`, `even_deny_root`; state in `/var/run/faillock/` |
| `pam_pwquality` | `/etc/security/pwquality.conf` | `minlen`, `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/nologin` | Non-root logins refused while the file exists; systemd creates `/run/nologin` during boot |
| `pam_sss` | `/etc/sssd/sssd.conf` | IdM, AD and LDAP users |
| `pam_wheel` (in `/etc/pam.d/su`) | | Uncomment to restrict `su` to group wheel |

```sh
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.

```sh
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.

```sh
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.

```sh
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.

```sh
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.

```sh
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:

```text
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.

```sh
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](https://www.wiki.jodisand.me/docker/#volumes-and-bind-mounts).

## Oneliners

```sh
# 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.

```sh
#!/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.

```sh
#!/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`.

```sh
#!/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

| Symptom | Cause | Fix |
| --- | --- | --- |
| `Permission denied` on a file whose mode looks right | A directory in the path lacks `x`, or the ACL mask, or SELinux | `namei -l /path`; `getfacl`; `ausearch -m AVC -ts recent` |
| `Permission denied` running a script or binary | Filesystem mounted `noexec`, or `nosuid` for a setuid/capability binary | `findmnt -T /path -o TARGET,OPTIONS` |
| Root cannot modify or delete a file | Immutable or append-only attribute | `lsattr file`; `chattr -i file` |
| `usermod -aG` had no effect | Group membership is read at login | Log out and in, `newgrp`, or `loginctl terminate-user`; verify with `id user` versus `id` in the session |
| All other groups disappeared after `usermod -G` | Missing `-a` | Re-add them: `usermod -aG g1,g2 user` from a previous `id` output or backups of `/etc/group` |
| `Account locked due to 5 failed logins` | `pam_faillock` | `faillock --user alice`; `faillock --user alice --reset`; tune `/etc/security/faillock.conf` |
| `Your account has expired` | `chage -E` date passed or `usermod -e 1` | `chage -E -1 alice` |
| `This account is currently not available` | Shell is `nologin` | `usermod -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 open | `restorecon -Rv ~/.ssh`; `chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys`; see [SSH](https://www.wiki.jodisand.me/ssh/#troubleshooting) |
| Login refused for everyone but root | `/etc/nologin` or `/run/nologin` exists, or `pam_access` rule | `rm /etc/nologin`; `journalctl -t sshd` names the module |
| `sudo: alice is not in the sudoers file` | No rule, or the user is in the group only in a stale session | `sudo -l -U alice`; check `id alice` versus `id` |
| `sudo: /etc/sudoers.d/x is world writable` or `syntax error` | A drop-in written without `visudo` | `visudo -cf` the file; fix mode to `0440`; if `sudo` itself is broken, use `su -` or `pkexec visudo` |
| `sudo: unable to resolve host` | Hostname not in `/etc/hosts` or DNS | Add it to `/etc/hosts`, or `Defaults !fqdn` |
| Service gets `403` or `EACCES` only under SELinux enforcing | Wrong label after `mv`, port unlabelled, or a boolean off | `ausearch -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 run | Wait; check `semodule -l`; `semodule -B` rebuilds |
| Denial happens but nothing in the audit log | A `dontaudit` rule, or auditd not running | `semodule -DB` then reproduce, then `semodule -B`; `systemctl status auditd` |
| `setcap: Operation not permitted` | Filesystem without xattr support, NFS, or `nosuid` | Move the binary, or use `AmbientCapabilities=` in the unit |
| Container cannot read a bind mount | Host directory labelled for the host, not `container_file_t` | Mount with `:Z`; check with `ls -Z` |

## Further reading

- [sudoers manual](https://www.sudo.ws/docs/man/sudoers.man/)
- [RHEL: Using SELinux](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/using_selinux/index)
- [SELinux project notebook](https://github.com/SELinuxProject/selinux-notebook)
- [Linux-PAM System Administrators' Guide](https://github.com/linux-pam/linux-pam/blob/master/doc/sag/Linux-PAM_SAG.txt)
- [authselect documentation](https://github.com/authselect/authselect/tree/master/doc)
- [capabilities(7)](https://man7.org/linux/man-pages/man7/capabilities.7.html) and [acl(5)](https://man7.org/linux/man-pages/man5/acl.5.html)


