Host hardening patterns

Updated

July 30, 2026

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.

Warning

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

  1. Confirm console access
  2. Confirm admin key SSH works
  3. nixos-rebuild test or boot
  4. Open new SSH session before closing old
  5. Only then switch if using switch and tests pass
sudo nixos-rebuild test --flake .#lab
# new ssh session:
ssh lab
sudo -v

Worked 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 carefully

Keep this in docs/hardening.md.


Exercises

Exercise 1 — Inventory surface

ss -tulpn | sort
systemctl list-unit-files --state=enabled | head -80

Journal: 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 -v

Then 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 -tulpn

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