Day 36 — Hardening baseline

Updated

July 30, 2026

Day 36 — Hardening baseline

Stage IV · ~4h
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 (Day 13).

Why this day 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

Today 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 in Stage IV Extra open ports
Laptop multi-boot lab Lower remote risk, still practice

Non-goals today: full SELinux policy authoring, corporate MDM, compliance certifications.


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;
    };
  };
}
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.


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;
  # bluetooth on servers:
  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 ];
  # server-specific disables
}
# 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 gate IV.


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 (Day 24) 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.


Worked example — Safe application order

  1. Confirm console access
  2. Confirm admin key SSH works
  3. nixos-rebuild boot (activate next reboot) or test
  4. Open new SSH session before closing old
  5. Only then switch if using switch
# keep existing session; test new config in another tty/ssh
sudo nixos-rebuild test --flake .#lab

Lab 1 — Inventory surface

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

Journal: what is listening, what you do not recognize.


Lab 2 — Write hardening module

Create modules/common/hardening/ with SSH + sudo + 2–3 sensible disables for your role.


Lab 3 — Apply carefully

sudo nixos-rebuild test --flake .#lab
# new ssh session
ssh lab
sudo -v

Then switch if satisfied.


Lab 4 — Lockout recovery drill (document)

Write exact steps to recover if SSH rejects you:

  • Hypervisor console
  • Boot previous generation
  • nixos-rebuild switch --rollback from console

Do not skip writing this.


Lab 5 — Assertions (optional polish)

{ config, lib, ... }:
{
  assertions = [
    {
      assertion = config.services.openssh.settings.PasswordAuthentication == false;
      message = "PasswordAuthentication must be false on this host role";
    }
  ];
}

Lab 6 — Diff and explain

# compare pre/post listening ports
ss -tulpn

Explain each remaining listener in one line (sshd, etc.).


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 Lab 4 before remote travel

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

Commit

git add .
git commit -m "day36: hardening baseline module"

Write three personal gotchas before continuing.


Tomorrow

Day 37 — Firewall discipline: deny-by-default posture, per-service ports, and an audit of what is actually open.