Host hardening patterns
Host hardening patterns
Goal: Apply a small, documented hardening module for a lab server: SSH posture, sudo policy, disable unused services, and careful sysctl/kernel defaults—without cargo-culting every CIS line item.
Hardening can lock you out of a remote VM. Keep a console/VNC path or test with nixos-rebuild test / boot generation rollback ready.
Why this chapter exists
Secrets tooling protects credentials. Hardening reduces attack surface and enforces least privilege. Production-shaped labs should not run with:
- Password SSH
- Root SSH login
- Unnecessary network services
- Empty sudo policies that surprise you later
This chapter builds a module you can import on server roles—not a security theater checklist of 200 toggles.
Theory 1 — Threat model for this lab
Assume:
| Asset | Threat |
|---|---|
| SSH on a public IP | Password spray, key theft |
| Local users | Accidental privilege escalation |
| Services you enable next | Extra open ports |
| Laptop multi-boot lab | Lower remote risk, still practice |
Non-goals: full SELinux policy authoring, corporate MDM, compliance certifications.
Role split
| Role | Hardening intensity |
|---|---|
server |
Strict SSH, fewer conveniences |
workstation |
Keys still; keep desktop UX |
Theory 2 — SSH baseline (NixOS)
# modules/common/hardening/ssh.nix
{
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "no";
X11Forwarding = false;
# MaxAuthTries = 4;
# AllowUsers = [ "alice" ];
};
};
}| Setting | Intent |
|---|---|
| No passwords | Keys only |
| No root login | Use sudo from admin user |
| No X11 forward | Reduce attack surface on servers |
Ensure your user has openssh.authorizedKeys.keys before applying password-off if you ever had passwords.
Optional allowlists
services.openssh.settings.AllowUsers = [ "alice" ];
# services.openssh.listenAddresses = [ { addr = "0.0.0.0"; port = 22; } ];Theory 3 — sudo and wheel
# modules/common/hardening/sudo.nix
{
security.sudo = {
enable = true;
wheelNeedsPassword = true; # or false for lab convenience — document choice
execWheelOnly = true;
};
# users.users.alice.extraGroups = [ "wheel" ];
}| Policy | Trade-off |
|---|---|
wheelNeedsPassword = true |
Safer; needs password/TTY |
= false |
Convenient lab; weaker if key stolen + local |
Prefer true on anything network-exposed.
Theory 4 — Disable what you do not use
Examples (only if present/enabled):
{
services.avahi.enable = false;
services.printing.enable = false;
hardware.bluetooth.enable = false;
}Do not randomly disable things you depend on (NetworkManager on a laptop role). Use roles:
# modules/roles/server.nix
{
imports = [ ../common/hardening ];
}# modules/roles/workstation.nix
{
# lighter hardening; keep user conveniences
}Theory 5 — Kernel / sysctl care
NixOS exposes many boot.kernel.sysctl knobs. Apply few, understand each:
{
boot.kernel.sysctl = {
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.tcp_syncookies" = 1;
# Do not paste a 50-line blog dump without reading
};
}Kernel package pinning and hardened profiles exist in the ecosystem; treat full “hardened kernel” as optional advanced work—not required for this part.
Theory 6 — Automatic updates? (judgment)
Unattended upgrades on NixOS are a product decision:
- Flake pins mean “latest” is not automatic unless you automate lock bumps + rebuilds
- Auto-reboot policies can surprise
For this book: manual, deliberate updates (lock discipline) unless you later build CI-driven update PRs.
Theory 7 — Module shape
modules/common/hardening/
default.nix # imports ssh.nix sudo.nix misc.nix
ssh.nix
sudo.nix
misc.nix
# default.nix
{
imports = [ ./ssh.nix ./sudo.nix ./misc.nix ];
}Import from server role or common—document if workstations share it.
Theory 8 — Assertions as guardrails
{ config, lib, ... }:
{
assertions = [
{
assertion = config.services.openssh.settings.PasswordAuthentication == false;
message = "PasswordAuthentication must be false on this host role";
}
];
}Assertions fail evaluation early—better than silent drift.
Theory 9 — Safe apply order
- Confirm console access
- Confirm admin key SSH works
nixos-rebuild testorboot- Open new SSH session before closing old
- Only then
switchif using switch and tests pass
sudo nixos-rebuild test --flake .#lab
# new ssh session:
ssh lab
sudo -vWorked example — Recovery card
# Recovery if SSH rejects
1. Hypervisor / cloud serial console
2. Login locally
3. sudo nixos-rebuild switch --rollback
# or pick previous generation at bootloader
4. Fix flake; re-apply carefullyKeep this in docs/hardening.md.
Exercises
Exercise 1 — Inventory surface
ss -tulpn | sort
systemctl list-unit-files --state=enabled | head -80Journal: what is listening, what you do not recognize.
Exercise 2 — Write hardening module
Create modules/common/hardening/ with SSH + sudo + 2–3 sensible disables for your role.
Exercise 3 — Apply carefully
sudo nixos-rebuild test --flake .#lab
# new ssh session
ssh lab
sudo -vThen switch if satisfied.
Exercise 4 — Lockout recovery drill (document)
Write exact steps to recover if SSH rejects you. Do not skip writing this.
Exercise 5 — Assertions (optional polish)
Add an assertion for PasswordAuthentication false (or your critical policy).
Exercise 6 — Diff and explain
ss -tulpnExplain each remaining listener in one line (sshd, etc.).
Exercise 7 — Key-before-password-off proof
Confirm authorizedKeys present for your admin user before disabling passwords.
Exercise 8 — wheel policy document
Write one sentence in docs/hardening.md for wheelNeedsPassword choice.
Exercise 9 — Role wiring
Import hardening from modules/roles/server.nix (or equivalent). Rebuild.
Exercise 10 — PermitRootLogin check
sudo sshd -T | grep -i -E 'passwordauthentication|permitrootlogin'Exercise 11 — Disable list rationale
For each disabled service, one-line reason in docs.
Exercise 12 — Sysctl minimalism
If you added sysctls, write what each does. Remove any you cannot explain.
Exercise 13 — Workstation exception note
If you also use this flake on a laptop role, document what differs.
Exercise 14 — Generation rollback rehearsal (console available)
From console, practice listing generations; do not necessarily roll back if healthy.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Locked out via SSH | Console + previous generation |
| PasswordAuth off before keys | Fix via console; add keys |
| Hardening module on laptop breaks UX | Split server/workstation roles |
| Blind sysctl paste | Remove unknowns |
PermitRootLogin yes “for debug” left on |
Remove before network exposure |
| No recovery notes | Write recovery card before remote travel |
| test vs switch confusion | Prefer test when changing SSH |
| AllowUsers typo lockout | Console fix |
| Firewall not yet disciplined | Next chapters—but do not open random ports here |
Checkpoint
- Hardening module exists and is imported on the lab role
- Password SSH off; root SSH off
- sudo policy explicit
- Unnecessary services disabled with reasons
- Recovery path documented
- Rebuild + new SSH session verified
- Listeners explained
- Assertions or checklist for critical SSH settings
Journal (optional)
Write three personal gotchas before continuing.