Users, groups and account lifecycle
UID 0, service accounts and off-boarding.
Accounts are how every other control knows who is acting. In this lesson you audit which accounts are root-equivalent, which can log in and which can become root, create service accounts that cannot log in, off-board a person so that none of their credentials work any more, and set password ageing to match current NIST guidance instead of habit. The account files and useradd are covered in Linux essentials; password quality and lockout rules are the next lesson.
Who is root, and who can log in
Start on an untouched install. The kernel grants root's powers to user ID 0, not to the name "root", so any second account with UID 0 is a full root account that survives a change of root's password. An empty password field in /etc/shadow is the other classic finding: wherever the PAM stack allows empty passwords, that account logs in with none.
Only root has UID 0 and no account has an empty password field. The CIS RHEL 10 Level 1 server profile (in scap-security-guide 0.1.82) checks both, and both checks apply unchanged to Ubuntu. passwd -S -a shows each account's password state: L is locked (no usable password), NP would be an empty password, P a usable one. Every account here is L, including root, because Ubuntu locks root's password at installation and the two people-shaped accounts log in with SSH keys; on your own server the account the installer created has a password and shows P. The columns after the date are the ageing values; -1 means unset.
Only four accounts have a real shell. sync is historical: its "shell" flushes the disks, and its password is locked. lima belongs to the lab's VM tool and deploy is the administrator. Every other system account uses nologin, which is what a service account should look like.
Who can become root
Ubuntu grants full sudo to two groups, sudo and admin. admin is empty, but anyone added to it becomes an administrator, so review both. Group membership is not the whole picture: files in /etc/sudoers.d can grant rights to a named user. Here cloud-init gave the image's default user (lima on this VM, ubuntu on a stock Ubuntu cloud image) NOPASSWD: ALL, and the lab gave deploy the same so labs run unattended. On a real server both lines are findings: whoever holds that account's SSH key is root without typing a password. Narrowing sudo is the subject of the sudo lesson; the review belongs here, and it should run after every off-boarding.
ubuntu) has a locked password and root is locked too, so deleting its NOPASSWD line leaves nobody able to use sudo. First give the user a password (sudo passwd ubuntu) and keep a second session open with sudo -i. Edit with sudo visudo -f /etc/sudoers.d/90-cloud-init-users, then in a new session run sudo -k; sudo true: it must ask for the password and succeed before you close the root shell.On RHEL 10 the administrators' group is wheel, and new accounts get bash and a 0700 home directory where Ubuntu gives /bin/sh and 0750.
Service accounts
A daemon should run as its own unprivileged account, not as root and not as a person. --system picks a UID below 1000 and sets no password ageing; nologin as the shell refuses interactive logins.
The account has a locked password and no ageing (-1). su - starts the account's login shell, and nologin prints its refusal. sudo -u still works, because it runs the command directly without the account's shell. That is intended: nologin stops people from getting a shell, and systemd's User= and sudo -u still run programs as the account. For a daemon you write yourself, systemd can also create a temporary account per run with DynamicUser=, covered in the sandboxing lesson.
Off-boarding someone who uses SSH keys
The threat is a former employee or contractor whose access still works. The lab's leaver, hard-acct-leaver, is an administrator in the sudo group who logs in with an SSH key. To build the same starting point, create the account and give it a key whose private half deploy keeps to test with; the directory and file must belong to the account, with modes 700 and 600:
The leaver also left a session running: a second connection, ssh -f -i ~/.ssh/leaver_test hard-acct-leaver@localhost 'sleep 600'. Linux essentials showed that sudo passwd -l does not stop a key login; here it is once more as the starting point:
The lock only puts a ! in front of the hash, which a key login never reads; passwd(1) says locking "does not disable the account" and points to usermod --expiredate 1. The order that closes every path:
Expire the account first, then try the key again:
Account expiry is checked for every login method: sshd refuses the key login in PAM's account stage and says why, and the journal records it under sshd-session, the per-connection process OpenSSH has used since version 9.8. Expiry does not touch sessions that are already open.
The leaver's sleep was still running over the old connection until loginctl terminate-user ended every session and process of the account. Now remove the credentials, so that re-enabling the account by mistake does not restore access, and the rights that came with group membership.
sshd -T prints the effective server configuration: keys are read from both authorized_keys and authorized_keys2, so check both. Moving the file to root's home keeps it as evidence. crontab -l -u finds scheduled jobs; also look for systemd timers and user services, sudoers.d files naming the account, and files it owns (find / -xdev -user NAME). userdel -r deletes the home directory and mail spool, so archive the home first if policy says so; the warning only means there was no mail. Keys the person created for automation elsewhere, and shared secrets they knew, must be rotated separately.
Every one of those checks looks for the leaver's name. Someone who held root and wanted to keep access would not leave anything under it: they would add a key to root's or a service account's authorized_keys, a NOPASSWD line for another account, a second UID 0 or sudo member, a root-owned cron job or unit, or a CA in sshd's TrustedUserCAKeys. For a leaver who had root, compare the host with a known-good source (your configuration management, and the file-integrity baseline from the AIDE lesson): every authorized_keys file (sshd -T shows where keys are read from, including an AuthorizedKeysCommand), /etc/sudoers.d, UID 0 and the sudo and admin groups, /etc/cron.d and the units under /etc/systemd/system, and sshd's trust settings. Rotate the host secrets they could read. On a fleet, rebuilding a host is often cheaper than proving it clean.
On a fleet, a local account must be expired on every host, and the host you forget still accepts the key, so fleets use central identity (SSSD with IdM or LDAP) or push the expiry through configuration management, and pair it with the revocation list from the SSH keys lesson. userdel also frees the UID: useradd(8) gives a new account the smallest UID above every existing one, so if the leaver had the highest UID, the next person created inherits it, and with it their files on shared storage and in backups. Keep a record of used UIDs and never hand one out twice.
The operational catch is shared accounts: you cannot expire a login that five people use, which is the best argument against them. The rollback for a mistaken expiry is sudo usermod --expiredate -1 NAME (and passwd -u for the password). On RHEL 10 the commands and their output are the same, since passwd there also comes from shadow (shadow-utils):
Password ageing: current guidance
Both distributions ship PASS_MAX_DAYS 99999, so passwords never expire (the Rocky value is shown above). NIST SP 800-63B-4 (July 2025) supports that default: verifiers "SHALL NOT require subscribers to change passwords periodically", but "SHALL force a change if there is evidence that the authenticator has been compromised". It also sets a minimum of 15 characters for a password used on its own (8 when it is one factor of several), no composition rules, and a check against a list of known-bad passwords, which is the next lesson's job. The CIS RHEL 10 Level 1 profile in scap-security-guide 0.1.82 still sets a 365-day maximum, and its DISA STIG profile 60 days. SecOpsLog advice: keep the distribution default and use the controls that match real risk, unless an audit framework you are bound by demands the ageing value, in which case record it as an exception.
Two ageing tools remain useful. A temporary account gets an end date when it is created, and a suspected compromise forces a change at next login.
The account stops working on 31 December without anyone remembering to remove it; chage -E sets the same date on an existing account and chage -E -1 removes it. passwd -e marks the password as expired, so the next login that passes through PAM must set a new one, and that includes key logins: sshd runs PAM's account stage for them too, and pam_unix(8) checks password ageing there. Give hard-acct-temp a key the same way as the leaver (the lab calls it ~/.ssh/temp_test) and try it:
The key was accepted and a session opened, but sshd then demanded a new password, and with no terminal to type it into, the login ended with exit status 1; an interactive user would be made to change it first. The journal also shows uid=1002 for hard-acct-temp, the UID the deleted leaver had, which is the reuse described above. A key-only automation account whose password reaches a maximum age (CIS's 365 days, for example) breaks the same way a year later, which is one more reason to leave PASS_MAX_DAYS alone for such accounts.
Do not rely on "Password inactive" to catch dormant accounts: shadow(5) counts it from the day the password expires, which with 99999 is never. Find dormant accounts from login records instead. On Ubuntu 26.04 that means the journal, because last and lastlog2 are not installed by default; this prints an account's most recent SSH login, and no output means none in the journal's history:
/etc/login.defs, put any comment on a line of its own. login.defs(5) treats a line as a comment only when # is its first non-blank character, so PASS_MAX_DAYS 365 # policy is not a clean value of 365.Try this
On your Ubuntu lab machine, create a user with a key as the leaver above and log in with it over ssh localhost. Run sudo passwd -l on the account and confirm that the key login still works. Then run sudo usermod --expiredate 1 and confirm that the next attempt prints "Your account has expired" and that journalctl -u ssh shows the pam_unix(sshd:account) line. Leave a sleep 600 running over a second connection before you expire the account, and prove with ps -u that it survives until sudo loginctl terminate-user.
Takeaway
Disable a person by expiring the account and ending their sessions, then remove keys, groups and scheduled work; a locked password alone leaves every key login open. Keep passwords non-expiring and force a change only when there is evidence of compromise.