Chapter 3 — Operating systems and identities
Lesson map
Lesson 03 published. Map, example, EX009.
flowchart TD
N0["Learning outcomes and connection"]
N1["3.1 What hardening means at technician level"]
N2["3.2 Reading sshd configuration correctly"]
N3["3.3 Accounts and identities"]
N4["3.5 Practical activity"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Worked example"]
EX --> Q["Exercise EX009"]
Logical drawing
flowchart LR
N0["Learning outcomes and connection"]
N1["3.1 What hardening means at technician level"]
N2["3.2 Reading sshd configuration correctly"]
N3["3.3 Accounts and identities"]
N4["3.5 Practical activity"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX009"]
[Learning outcomes and connection]
|
v
[3.1 What hardening means at technician level]
|
v
[3.2 Reading sshd configuration correctly]
|
v
[3.3 Accounts and identities]
|
v
[3.5 Practical activity]
|
v
[Chapter synthesis]
|
v
[Worked example] --> [Exercise EX009]
Learning outcomes and connection
After this chapter you should be able to:
- audit a Linux host that supports a control service: listening services, SSH daemon settings, local accounts and privileged groups;
- explain why
sshdtakes the first value it reads for most keywords, and how that defeats careless edits; - identify root-equivalent accounts, service accounts with interactive shells and accounts that no current person owns;
- turn findings into a prioritized hardening change that will not lock operators out.
Chapters 1–2 described the service and its traffic. The servers in the control centre — historian, SCADA server, jump host — run ordinary operating systems; this chapter makes their configuration something you can check, not assume.
3.1 What hardening means at technician level
Hardening is removing what is not needed and constraining what is. For a server in a control environment that means: services listening only on the interfaces they need; administrative access only through the management path; no shared or orphaned accounts; service accounts that cannot log in interactively; and configuration that is recorded so that drift can be detected (Chapter 14). NIST's general server guide gives the broader checklist (SP 800-123); the CISA Cross-Sector Cybersecurity Performance Goals summarize the account and credential practices most often missing (CISA CPGs).
3.2 Reading sshd configuration correctly
OpenSSH's daemon reads sshd_config from top to bottom and, for most keywords, the first value obtained is used; later lines for the same keyword are ignored (sshd_config(5)). Comments begin with #. So a file that has PasswordAuthentication yes near the top and someone's later fix PasswordAuthentication no at the bottom still allows passwords. The reliable check is the effective configuration: on a real host, sshd -T prints it; in Lab 3 the parser keeps the first value, as the daemon does.
Settings that matter on a control-centre host: ListenAddress restricted to the management interface; PermitRootLogin no; PasswordAuthentication no once keys or certificates work; X11Forwarding no; a low MaxAuthTries; and AllowGroups naming a group whose membership you also review.
3.3 Accounts and identities
On Linux, privilege is a number: UID 0 is root, whatever the account's name. An extra UID-0 account ("backup2", "support") is a second root password that nobody rotates. Service accounts (UIDs below 1000 by convention on many distributions) run daemons; they should have a non-interactive shell such as /usr/sbin/nologin, so that a stolen service credential cannot be used to log in. Person accounts should map to someone on the current authorization roster; a vendor account that sits in sudo permanently is standing privilege that should be time-bound (Chapter 10). Group membership is part of the identity: being in sudo or ops-admins is often more important than the account itself.
3.4 Worked example: auditing HIST-02 (synthetic)
hist-02 stores Kestrel Water's time series. KVROC policy: SSH only on the management address 10.90.0.21; no root login; keys only; at most three attempts; only J. Tan and R. Kaur authorized.
Lab 3 (save as lab03.py, run with python3 lab03.py):
# Lab 3 - audit a host configuration and its local accounts (offline, standard library only)
# Synthetic files from historian host HIST-02 at the Kestrel Vale operations centre.
sshd_config = """
Port 22
ListenAddress 0.0.0.0
PermitRootLogin yes
PasswordAuthentication yes
# PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding yes
MaxAuthTries 6
AllowGroups ops-admins
"""
passwd = """root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
histsvc:x:998:998:historian service:/opt/hist:/bin/bash
jtan:x:1001:1001:J. Tan:/home/jtan:/bin/bash
rkaur:x:1002:1002:R. Kaur:/home/rkaur:/bin/bash
backup2:x:0:0:temp backup:/root:/bin/bash
vendor:x:1003:1003:vendor support:/home/vendor:/bin/bash"""
group = """ops-admins:x:1100:jtan,vendor
sudo:x:27:jtan,rkaur,vendor"""
policy = {"PermitRootLogin": "no", "PasswordAuthentication": "no", "X11Forwarding": "no",
"ListenAddress": "10.90.0.21", "MaxAuthTries": "3"}
roster = {"jtan", "rkaur"} # people currently authorized on HIST-02
eff = {}
for line in sshd_config.splitlines():
line = line.split("#", 1)[0].strip()
if not line: continue
key, val = line.split(None, 1)
eff.setdefault(key, val) # sshd uses the FIRST value it reads for a keyword
findings = []
for k, want in policy.items():
if eff.get(k) != want: findings.append(f"sshd {k} is {eff.get(k)!r}, policy {want!r}")
for line in passwd.splitlines():
name, _, uid, gid, gecos, home, shell = line.split(":")
if uid == "0" and name != "root": findings.append(f"account {name} has UID 0 (root equivalent)")
if 0 < int(uid) < 1000 and shell.endswith("sh") and not shell.endswith("nologin"):
findings.append(f"service account {name} has interactive shell {shell}")
if int(uid) >= 1000 and name not in roster: findings.append(f"person account {name} not on roster")
for line in group.splitlines():
g, _, _, members = line.split(":")
for m in members.split(","):
if m and m not in roster: findings.append(f"group {g} contains {m} (not on roster)")
for f in findings: print(f)
print("findings:", len(findings))
Expected output (Python 3.13.5):
sshd PermitRootLogin is 'yes', policy 'no'
sshd PasswordAuthentication is 'yes', policy 'no'
sshd X11Forwarding is 'yes', policy 'no'
sshd ListenAddress is '0.0.0.0', policy '10.90.0.21'
sshd MaxAuthTries is '6', policy '3'
service account histsvc has interactive shell /bin/bash
account backup2 has UID 0 (root equivalent)
person account vendor not on roster
group ops-admins contains vendor (not on roster)
group sudo contains vendor (not on roster)
findings: 10
Ten findings. Note the PasswordAuthentication line: the file contains a commented no, which some reviewers read as "already fixed". It is a comment, and the effective value is yes. The most dangerous findings are backup2 with UID 0 (an unmanaged second root) and vendor in sudo (standing privileged supplier access). The historian service account with /bin/bash matters because the service's credentials are stored on disk for the historian to use.
A safe hardening order: (1) confirm a working key-based login for J. Tan and R. Kaur from jump-01; (2) restrict ListenAddress, PermitRootLogin, PasswordAuthentication in one change, keeping an existing session open while you test a new one; (3) remove backup2 after preserving its login history; (4) remove vendor from sudo and move vendor access to a time-bound procedure; (5) set the service account's shell to nologin after checking that no maintenance script logs in as it.
3.5 Practical activity
On a virtual machine you own, run sshd -T | sort and compare the output with the file. Add a duplicate keyword lower in the file, reload, and confirm that the first value wins. Record the exact OpenSSH version, because behaviour of Include and Match blocks varies by version.
Record the OpenSSH version and the path of every file that sshd -T shows as included. After you reload, open a second session before you close the first, so a bad ListenAddress or AllowGroups change cannot lock you out. Compare the effective values with Lab 3's policy table and note any keyword that appears twice in the file.
3.6 Common errors and limits
- Reading the file instead of the effective configuration.
- Fixing SSH settings from the only open session and locking everyone out; always test a second session first.
- Checking account names instead of UIDs.
- Removing a service account's shell without checking scripts that depend on it.
-
Lab 3 handles neither
Includefiles norMatchblocks; real audits must. -
Editing PasswordAuthentication at the bottom of the file while an earlier yes still wins.
- Removing vendor from sudo without a time-bound procedure for the next support visit.
- Checking account names instead of UIDs, so a second root named backup2 is missed.
Exercises — chapter 3
EX009 — First value wins. Objective: reason about effective configuration. Inputs: Lab 3's sshd_config. Steps: (1) predict the effective PasswordAuthentication if the commented line is uncommented and left where it is; (2) say where the fix must go. Expected evidence: prediction and placement. Rubric: 3 = still yes because the earlier line wins; the fix replaces the first occurrence (or the earlier line is deleted), verified with sshd -T; 2 = right answer, no verification; 1 = predicts no; 0 = none.
EX010 — Prioritize the HIST-02 findings. Objective: order changes by risk and safety. Inputs: Lab 3 output. Steps: group the ten findings into today / this week / next maintenance window, with one-line reasons and a preservation step where needed. Expected evidence: a three-group list. Rubric: 3 = UID-0 backup2 and vendor sudo today with login history preserved first; SSH binding and root/password settings as one tested change; service shell after script check; 2 = sensible but no preservation; 1 = unordered; 0 = none.
EX011 — Extend the audit. Objective: add a check. Inputs: Lab 3. Steps: add a check that flags any account other than root whose shell is interactive and whose home directory is /root or /. Report the new output. Expected evidence: code and output. Rubric: 3 = flags backup2 (home /root) and only it; explains why shared home directories hide accountability; 2 = works without explanation; 1 = attempted; 0 = none.
EX012 — Service account design. Objective: design a non-interactive identity. Inputs: the historian service needs to read files in /opt/hist and write to its database. Steps: specify UID range, shell, home, group, file permissions and how its credential is stored and rotated. Expected evidence: a short specification. Rubric: 3 = system UID, nologin shell, dedicated group, least-privilege file modes, credential readable only by the service, rotation owner and interval; 2 = one gap; 1 = generic; 0 = none.
Chapter synthesis
Operating-system hardening is checkable: effective settings, UIDs, shells and group memberships can be read and compared with a written policy. Know how the daemon actually reads its file, look at numbers rather than names, and order changes so that you never lock out the people who run the service.
Continue studying — chapter 03 of 18
← Chapter 02 · Course chapter index · Chapter 04 →
Continue to the next authored lesson.