We closed Module 4 with a list of debts, and this lesson settles the first and most uncomfortable one: svc-tramontana exists "by magic" and the tramontana group does not exist at all. You have been writing chown svc-tramontana:tramontana on files whose group nobody had ever created. That ends today. You are going to see what an identity really is in Linux — a number, not a name — which files are the source of truth and what each field contains, how person accounts and service accounts are created (they look nothing alike), and why the most expensive mistake in the whole module fits into a single missing letter: usermod -G instead of usermod -aG.

Contents

  1. The identity model: UID, GID and supplementary groups
  2. The four files of truth, field by field
  3. Account types and the UID range in /etc/login.defs
  4. Creating users: useradd, adduser and /etc/skel
  5. Passwords: setting, locking and the meaning of ! and *
  6. Modifying and deleting accounts, and orphaned files
  7. Account expiry with chage
  8. Groups: creating, modifying and why you have to log in again
  9. Service accounts: why the app never runs as root
  10. Auditing: who is who and who has logged in
  11. Tramontana case: settling the previous module's debt

  1. The identity model: UID, GID and supplementary groups

The kernel does not know what operator is. The kernel only handles numbers:

  • UID (User ID): identifies the user. The owner of a file is a number stored in the inode, not a string.
  • GID (Group ID): identifies a group. Every user has one primary group (the one a new file they create inherits) and zero or more supplementary groups.

Names exist only for our convenience, and the number→name translation is done by the system library reading /etc/passwd and /etc/group. That is why, if you delete a user, ls -l starts showing raw numbers: the file keeps UID 1003 even though nobody is called that any more.

$ id operator
uid=1000(operator) gid=1000(operator) groups=1000(operator),4(adm),27(sudo)

Read it like this: operator is 1000, its primary group is operator (1000) and it also belongs to adm and sudo as supplementary groups. That distinction matters:

The primary group is defined in field 4 of /etc/passwd and is the one assigned to the files the user creates; the supplementary ones are defined in field 4 of /etc/group and only widen access. A process's effective group can be changed on the fly with newgrp or sg.

On Ubuntu, by default each user gets their own group with the same name (the UPG scheme, User Private Group). It is not a whim: it lets you work with umask 002 without exposing your files to half the machine, because "the group" is only you.

  1. The four files of truth, field by field

/etc/passwd — who exists

Seven fields separated by :. It is readable by everybody (644) because any process needs to translate a UID into a name for a simple ls -l.

$ grep -E '^(root|operator|svc-tramontana):' /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:1000:1000:Tramontana Operator,,,:/home/operator:/bin/bash
svc-tramontana:x:997:1002:Tramontana Bookings Service:/opt/tramontana:/usr/sbin/nologin
# Field Example Notes
1 Login name operator Unique; lower case by convention
2 Password x The x means "it is in /etc/shadow"
3 UID 1000 What really matters
4 Primary GID 1000 Points to /etc/group
5 GECOS Tramontana Operator,,, Real name, office, phone numbers; edited with chfn
6 and 7 Home and shell /home/operator, /bin/bash The home may not exist on system accounts; /usr/sbin/nologin for anyone who must not log in

/etc/shadow — the secret

Nine fields. Permissions 640, ownership root:shadow: readable by root and by the shadow group, invisible to everyone else. That is where the hash lives, and a hash readable by anybody is a hash somebody will eventually break with a GPU.

$ sudo grep -E '^(operator|svc-tramontana):' /etc/shadow
operator:$y$j9T$Qk3rP0mZ7uY2xB1c$Fq8sT...ZR1:20310:0:90:7:14::
svc-tramontana:!:20310:0:99999:7:::
# Field Example value Meaning
1 User operator Links to passwd
2 Hash $y$j9T$... See below
3 Last change 20310 Days since 1970-01-01
4 and 5 Minimum and maximum 0 / 90 Days before it can be changed and days of validity
6 and 7 Warning and inactivity 7 / 14 Advance warning and grace period after expiry before disabling
8 and 9 Account expiry and reserved (empty) Absolute date in days since 1970; the 9th is unused

The hash has the format $id$parameters$salt$hash. On Ubuntu 24.04 the default algorithm is yescrypt ($y$), which replaced SHA-512 ($6$) because it resists GPU and ASIC attacks thanks to its memory cost:

Prefix Algorithm Status
$y$ yescrypt Default on Ubuntu 24.04
$6$ SHA-512 Still valid; legacy
! or !$y$... Locked account The hash may still be underneath
* No usable password Typical of system accounts
(empty) No password: anybody gets in A real danger, fix it now

/etc/group and /etc/gshadow

$ getent group tramontana adm sudo
tramontana:x:1002:operator,luis,svc-tramontana
adm:x:4:syslog,operator
sudo:x:27:operator

Four fields: name, password (x), GID and list of supplementary members. Careful: anyone who has that group as their primary does not appear in that list. /etc/gshadow (640, root:shadow) holds the group password — an oddity almost nobody uses — and the group administrators, who can add members with gpasswd without being root.

  1. Account types and the UID range in /etc/login.defs

Type UID Examples Traits
Superuser 0 root Skips every permission check
System / service 1–999 daemon, syslog, svc-tramontana (997) No login, no expiry, created with -r
Person ≥ 1000 operator (1000), luis (1001) Home, shell, password, expiry
nobody 65534 nobody Minimal identity for unprivileged processes

The one deciding those ranges is /etc/login.defs:

$ grep -E '^(UID_MIN|UID_MAX|SYS_UID_MIN|SYS_UID_MAX|ENCRYPT_METHOD|UMASK|PASS_MAX_DAYS)' /etc/login.defs
PASS_MAX_DAYS	99999
UID_MIN			 1000
UID_MAX			60000
SYS_UID_MIN		  100
SYS_UID_MAX		  999
ENCRYPT_METHOD YESCRYPT
UMASK			022

The important part: UID 0 is root not because of the name, but because of the number. If you create a second account with UID 0, you have two roots and no audit trail able to tell them apart in the inode. Never do it.

  1. Creating users: useradd, adduser and /etc/skel

useradd is the low-level tool of the shadow suite: it does exactly what you ask, not one thing more. Its key options:

Option Effect
-m Creates the home and copies /etc/skel (without it, there is no home)
-d PATH A home other than the default
-s SHELL Login shell
-u UID A specific UID
-g GROUP Primary group
-G g1,g2 Supplementary groups
-c "TEXT" GECOS field; -N avoids creating the user's own group
-r System account: UID from the low range, no expiry, no home unless -m
$ sudo useradd -m -s /bin/bash -c "Luis Ferrer,Development,,," luis
$ sudo passwd luis
New password:
Retype new password:
passwd: password updated successfully

adduser is a Debian Perl script that wraps useradd and does what a human expects: it picks a free UID, creates the home with the right permissions, creates the user's own group, copies /etc/skel, asks for the password and fills in the GECOS interactively. An honest comparison:

Aspect useradd adduser
Portability Universal on Linux Debian/Ubuntu only
Interactive No Yes (ideal by hand)
Home Only with -m Always
Password Separately, with passwd It asks for it
Scripts Yes, this is the right one Requires --disabled-password and care

A practical rule: by hand, adduser; in a script, useradd, because its behaviour does not depend on a distribution-supplied configuration file.

/etc/skel (which on Ubuntu contains .bashrc, .profile and .bash_logout) is the home template: everything inside it is copied when the account is created with -m. It is the place to impose a corporate .bashrc or the red production prompt from Module 3.

  1. Passwords: setting, locking and the meaning of ! and *

passwd                       # your own: asks for the current one before changing it
sudo passwd luis             # root changes anybody's without knowing the previous one
echo 'luis:ALongAndUniquePassword' | sudo chpasswd   # in bulk, non-interactive
sudo passwd -e luis          # force a change on the next login (= chage -d 0)

Locking and unlocking:

sudo passwd -l luis          # prepends '!' to the hash
sudo passwd -S luis          # -> luis L 2026-08-18 0 99999 7 -1
sudo passwd -u luis          # removes the '!' and restores the original hash

The L from passwd -S means "locked"; P would be a usable password and NP no password, which is an emergency.

Content of field 2 Meaning Can you go back?
$y$... Valid password —
!$y$... Locked: the hash is intact underneath Yes, passwd -u
! Locked and with no hash to restore No, you have to set a new one
* Never had a password; password login impossible Not applicable
(empty) Gets in without typing anything Fix it immediately

usermod -L does the same as passwd -l to the hash. The practical difference is not between them, but in what neither of them does: locking the password does not prevent logging in with an SSH key, nor does it kill open sessions. For a real staff departure you need the full combination:

sudo usermod -L -e 1 -s /usr/sbin/nologin luis   # locks, expires the account and removes the shell
sudo pkill -u luis                               # closes whatever they had open

-e 1 sets the expiry to 2 January 1970: the account is expired and PAM rejects access whichever way it comes.

  1. Modifying and deleting accounts, and orphaned files

usermod changes what already exists. And here is the module's classic mistake:

# WRONG: replaces ALL supplementary groups with 'tramontana'
sudo usermod -G tramontana operator

# RIGHT: adds without touching the ones it already had
sudo usermod -aG tramontana operator

Without the -a (append), operator drops out of sudo and out of adm at the same instant. And if the root session was closed as well, you have just lost administration of the machine and it is time to boot into recovery mode. Always check before and after with id.

Other common uses:

sudo usermod -c "Luis Ferrer,Development,,," luis   # GECOS
sudo usermod -l lferrer luis                        # rename the login (does not move the home)
sudo usermod -d /home/lferrer -m lferrer            # move the home with its contents

Deleting:

sudo userdel luis          # deletes the account, keeps the home
sudo userdel -r luis       # also deletes the home and the mail spool

userdel -r does not find what the user left outside their home: a file in /srv, a cron job, a temporary file. Those files are left with a nameless UID and, worse still, the next user who receives that UID will inherit their ownership. A mandatory audit before reassigning:

$ sudo find / -xdev \( -nouser -o -nogroup \) 2>/dev/null
$ sudo find /srv /opt /var -xdev -uid 1001 2>/dev/null
/srv/tramontana/backups/temp/luis-report.csv

Compliance warning. Staff onboarding and offboarding are not an isolated technical task: in a real environment they must follow an approved procedure (who authorises it, within what deadline access is revoked, what is done with the employee's files, which may contain guests' personal data). Document every onboarding and every offboarding with a date and the person who authorised it, and have the security or compliance officer (GDPR) validate the procedure before you apply it.

  1. Account expiry with chage

chage handles fields 3 to 8 of /etc/shadow without you having to count days since 1970.

$ sudo chage -l operator
Last password change					: Aug 18, 2026
Password expires					: Nov 16, 2026
Password inactive					: Nov 30, 2026
Account expires						: never
Maximum number of days between password change		: 90
Option Effect
-M 90 Maximum password validity
-m 1 Minimum between changes (stops people rotating it five times to get back to the same one)
-W 7 Warning days
-I 14 Grace days after expiry
-E 2026-12-31 Expiry date of the account (temporary contracts)
-d 0 Forces a change at the next login
sudo chage -M 90 -m 1 -W 7 -I 14 operator

The real password policy — minimum length, complexity, dictionary, history — does not live here: it is imposed by PAM with pam_pwquality, and it is the subject of 06-06. chage only sets the deadlines.

  1. Groups: creating, modifying and why you have to log in again

sudo groupadd -g 1002 tramontana     # explicit GID, reproducible across machines
sudo groupmod -n tramontana-app tramontana   # rename
sudo groupdel old-group              # fails if it is somebody's primary group

sudo gpasswd -a luis tramontana      # add a member
sudo gpasswd -d luis tramontana      # remove them
sudo gpasswd -A operator tramontana  # 'operator' can administer the group without being root

gpasswd -a and usermod -aG do the same thing; gpasswd has the advantage of not allowing the forgotten--a mistake, because its syntax is always additive.

And now the detail that costs people whole afternoons: adding somebody to a group does not affect their open sessions.

$ sudo usermod -aG tramontana operator
$ id operator   # the database: 1002(tramontana) already shows
$ id            # YOUR session: it does not show yet
uid=1000(operator) gid=1000(operator) groups=1000(operator),4(adm),27(sudo)

The reason is that supplementary groups are set at login time, get recorded in the process's credentials and are inherited by its children. No configuration file is going to change a process that is already running. The ways out:

newgrp tramontana    # opens a subshell with the new group (an emergency fix)
exec su - "$USER"    # a clean re-login on the spot

The right thing to do is log out and log back in. And the same applies to services: if you change the groups of svc-tramontana, you have to restart the service, reloading is not enough.

  1. Service accounts: why the app never runs as root

An application exposed on port 8080 is the most likely way into your server. If it runs as root, any remote execution flaw is game over: the whole machine. If it runs as svc-tramontana, the attacker inherits an identity that cannot read /etc/shadow, cannot write to /etc, cannot install anything and cannot read other users' files.

What distinguishes a service account:

  • A system UID (-r, below 1000) and no expiry: a service account that expires brings the service down at three in the morning.
  • No login shell: /usr/sbin/nologin (which prints a message and exits with an error) or /bin/false.
  • No usable password (! or * in shadow) and no home of its own: it normally points at the application directory.

  1. Auditing: who is who and who has logged in

Command Answers
id [user] UID, GID and groups according to the database
groups [user] Just the group names
getent passwd luis Queries all the sources (/etc/passwd, LDAP…), not just the file
getent group tramontana The same for groups
who / w Who is connected right now; w adds the load and what they are running
last -n 5 Login history (it reads /var/log/wtmp)
lastlog Last login of each account; it gives away dead accounts
$ w | tail -2
operator pts/0    10.0.2.1         09:02    0.00s  0.09s  0.01s w
luis     pts/1    10.0.2.1         08:47   12:03   0.04s  0.04s -bash

getent is always preferable to grep /etc/passwd: the day the company adds a corporate directory, grep will lie and getent will still be telling the truth.

  1. Tramontana case: settling the previous module's debt

The starting state on srv-tramontana: operator (1000) and luis (1001) exist; svc-tramontana and the tramontana group do not, even though half a module talked about them. We fix it in order, and with a prior backup of the identity files as the course's convention requires.

sudo cp -a /etc/passwd /etc/passwd.bak-$(date +%F)   # prior backup, as always
sudo cp -a /etc/group  /etc/group.bak-$(date +%F)
# 1) The shared group, with a fixed GID so it matches on every machine
sudo groupadd -g 1002 tramontana

# 2) The service account: system, no shell, no home of its own, no password
sudo useradd -r -u 997 -g tramontana \
    -d /opt/tramontana -M \
    -s /usr/sbin/nologin \
    -c "Tramontana Bookings Service" svc-tramontana
# 3) The people who work with the application join the group
sudo usermod -aG tramontana operator
sudo usermod -aG tramontana luis

-M stops the home being created: /opt/tramontana already exists and we do not want useradd dumping /etc/skel on top of it. We verify before touching anything else:

$ getent passwd svc-tramontana && getent group tramontana
svc-tramontana:x:997:1002:Tramontana Bookings Service:/opt/tramontana:/usr/sbin/nologin
tramontana:x:1002:operator,luis

Now the ownership of the directories, which until today was a promise:

sudo chown -R svc-tramontana:tramontana /opt/tramontana
sudo chown -R operator:tramontana /srv/tramontana/backups
sudo chmod -R u=rwX,g=rX,o= /opt/tramontana
sudo chmod -R u=rwX,g=rwX,o= /srv/tramontana/backups
sudo chmod 2770 /opt/tramontana/shared/uploads

ls -ld /opt/tramontana /opt/tramontana/app /srv/tramontana/backups
drwxr-x---  5 svc-tramontana tramontana 4096 Aug 18 09:20 /opt/tramontana
lrwxrwxrwx  1 svc-tramontana tramontana   22 Aug 12 18:02 /opt/tramontana/app -> releases/3.2.1
drwxrwx---  5 operator       tramontana 4096 Aug 18 04:20 /srv/tramontana/backups

Notice the o=: nobody outside the group sees anything, applying the convention of "a shared group instead of permissions for others". That 2 in 2770 on uploads is the SGID bit, which makes everything created inside belong to the tramontana group regardless of who creates it; we take it apart in the next lesson.

And the documentation, which is part of the job:

sudo tee -a /opt/tramontana/HISTORY >/dev/null <<'END'
2026-08-18  Identities formalised (operator)
  - groupadd -g 1002 tramontana ; useradd -r -u 997 ... svc-tramontana
  - operator and luis added to 'tramontana' (requires logging in again)
  - chown svc-tramontana:tramontana /opt/tramontana ; operator:tramontana on backups
END

Common Mistakes and Tips

  • usermod -G without -a. The most expensive mistake in the lesson: it replaces the whole list and can throw you out of sudo. Before touching groups, save the output of id user; afterwards, compare it.
  • Expecting a new group to work in the current session. It will not. id user queries the database, plain id queries your session. Log out and back in, and restart the affected services.
  • Editing /etc/passwd or /etc/shadow with nano. Use vipw and vipw -s (and vigr for the groups): they lock the file and validate the syntax on exit. A corrupt shadow is a server nobody can get into.
  • Creating the service account with a shell. If svc-tramontana has /bin/bash, any route that manages to execute as that user gets an interactive shell. nologin is not cosmetic.
  • Confusing locking with disabling. passwd -l does not prevent access with an SSH key. For a departure: usermod -L -e 1 -s /usr/sbin/nologin and pkill -u. And do not reuse the UID of a deleted account without auditing first with find -uid.
  • Tip: set the service UIDs and GIDs (997, 1002) explicitly and write them down. The day you restore a backup on another machine, the numbers have to match or the permissions will slip out of place across the board.

Exercises

  1. A complete intern onboarding. Create the intern account with useradd: its own home, a Bash shell, GECOS "Operations Intern", supplementary member of tramontana, a password that must be changed at first login and an account expiry of 31 December 2026. Verify everything without logging in as them.
  2. Diagnosing a disaster. Somebody has run sudo usermod -G tramontana operator. Describe the exact symptom operator will see at their next login, how you would confirm it and how you repair it if you no longer have sudo.
  3. Account audit. Write a command that lists, from /etc/passwd, only the person accounts (UID ≥ 1000, excluding nobody) with their shell, and another that detects accounts with an empty password in /etc/shadow.

Solutions

1.

sudo useradd -m -s /bin/bash -G tramontana -c "Operations Intern,,," intern
echo 'intern:Provisional.2026!' | sudo chpasswd
sudo chage -d 0 -E 2026-12-31 intern     # -d 0 forces a change; -E expires the account
$ id intern
uid=1004(intern) gid=1004(intern) groups=1004(intern),1002(tramontana)
$ sudo chage -l intern | grep -i account
Account expires						: Dec 31, 2026

Here -G without -a is correct, because the account is being created and there are no previous groups to lose. The -a only matters in usermod.

2. The symptom: operator logs in normally, but any sudo answers operator is not in the sudoers file. This incident will be reported., and on top of that they lose the read access to the logs that adm gave them. Confirmation:

$ id operator
uid=1000(operator) gid=1000(operator) groups=1000(operator),1002(tramontana)

4(adm) and 27(sudo) are missing. Repair with privileges: sudo usermod -aG adm,sudo operator and log in again. Without sudo and without another administrator account there is no shortcut from inside: you have to reboot the VM, edit the GRUB entry to boot with init=/bin/bash, remount the root filesystem with mount -o remount,rw / and run usermod -aG adm,sudo operator. That recovery procedure is covered in depth in 07-01; the fact that it exists is precisely why physical access, or access to the VM's console, is in itself administrator access.

3.

$ awk -F: '$3 >= 1000 && $3 != 65534 { printf "%-10s uid=%-6s %s\n", $1, $3, $7 }' /etc/passwd
operator   uid=1000   /bin/bash
luis       uid=1001   /bin/bash
intern     uid=1004   /bin/bash
sudo awk -F: '$2 == "" { print "NO PASSWORD:", $1 }' /etc/shadow   # empty passwords

Silence in the output means there are none, which is exactly what you want to see: the "silence if all is well" convention applied to an audit.

Conclusion

There is no magic left in the identities on srv-tramontana. You know that a user is a number; you read the nine fields of /etc/shadow, you recognise a $y$ yescrypt hash and you tell !, * and the empty field apart; you separate system accounts from person accounts by the UID range in /etc/login.defs; you create accounts with useradd in scripts and with adduser by hand, knowing what /etc/skel contributes; you set deadlines with chage and leave the password policy for 06-06; and you have taken on board that usermod -G without -a is a quick way to lose the machine and that a new group does not exist until the session is reopened.

And, above all, you have settled the debt: the tramontana group (gid 1002) exists, svc-tramontana (uid 997) is a real system account with no shell and no password, operator and luis are in the group, /opt/tramontana and /srv/tramontana/backups have an owner and the change is written down in the HISTORY.

The other half of the equation is missing. You have identities, but sudo is still a magic word you type without knowing who decides what it lets you do, and the odd bits you glimpsed in 02-07 — that s instead of the x — are still unexplained, even though you have just used one by setting 2770 on uploads. In the next lesson, sudo and Special Permissions, both of those end: you will write a sudoers rule that lets operator restart and deploy the application without being root, you will really understand SUID, SGID and the sticky bit, you will replace a SUID with capabilities so that the app can listen on port 80, and you will give Luis access to the logs with an ACL without putting him in the adm group.

Linux Course: From Beginner to System Administrator

Module 1: Introduction to Linux

Module 2: Basic Linux Commands

Module 3: Advanced Command-Line Skills

Module 4: Shell Scripting

Module 5: System Administration

Module 6: Networking and Security

Module 7: Advanced Topics

Module 8: Practical Projects

© Copyright 2026. All rights reserved