From 72f8cc7c1ab1a124027e78f9c0bd4fc340344cc8 Mon Sep 17 00:00:00 2001 From: File Magic Date: Sat, 25 Jul 2026 22:32:29 -0400 Subject: [PATCH] modules/: hardened NixOS modules with toggleable features - hardened.nix: main module with toggle options for each subsystem - impermanence.nix: btrfs rollback with neededForBoot fix - secureboot.nix: Lanzaboote with boot.loader.systemd-boot - luks.nix: LUKS2 with disko-provided device path - kernel.nix: hardened sysctl and boot params (kernel.sysrq fix) - apparmor.nix: killUnconfinedConfinables option - agenix.nix: TPM-backed secrets with /var/lib/agenix persistence - user.nix: non-root user with SSH key auth --- harden/configuration.nix | 49 +++++++++++++++ harden/debug/ssh-key.pub | 1 + harden/disko.nix | 48 +++++++++++++++ harden/flake.nix | 76 +++++++++++++++++++++++ harden/hardware-vm.nix | 11 ++++ harden/justfile | 19 ++++++ harden/tests/verify.sh | 73 ++++++++++++++++++++++ modules/agenix.nix | 36 +++++++++++ modules/apparmor.nix | 14 +++++ modules/hardened.nix | 27 ++++++++ modules/impermanence.nix | 130 +++++++++++++++++++++++++++++++++++++++ modules/kernel.nix | 26 ++++++++ modules/luks.nix | 15 +++++ modules/secureboot.nix | 41 ++++++++++++ modules/user.nix | 47 ++++++++++++++ 15 files changed, 613 insertions(+) create mode 100644 harden/configuration.nix create mode 100644 harden/debug/ssh-key.pub create mode 100644 harden/disko.nix create mode 100644 harden/flake.nix create mode 100644 harden/hardware-vm.nix create mode 100644 harden/justfile create mode 100755 harden/tests/verify.sh create mode 100644 modules/agenix.nix create mode 100644 modules/apparmor.nix create mode 100644 modules/hardened.nix create mode 100644 modules/impermanence.nix create mode 100644 modules/kernel.nix create mode 100644 modules/luks.nix create mode 100644 modules/secureboot.nix create mode 100644 modules/user.nix diff --git a/harden/configuration.nix b/harden/configuration.nix new file mode 100644 index 0000000..0cf57ae --- /dev/null +++ b/harden/configuration.nix @@ -0,0 +1,49 @@ +{ + config, + pkgs, + lib, + inputs, + ... +}: { + imports = [ + "${inputs.impermanence}/nixos-modules/impermanence.nix" + inputs.disko.nixosModules.disko + inputs.agenix.nixosModules.age + ./disko.nix + ../modules/hardened.nix + ]; + + networking.hostName = "harden-vm"; + time.timeZone = "America/New_York"; + + hardened = { + impermanence.enable = true; + secureboot.enable = true; + luks.enable = true; + luks.tpm = false; # VM testing without TPM + apparmor.enable = true; + kernel.hardened = true; + agenix.enable = false; # No TPM in VM + user.enable = true; + user.username = "nixos"; + user.name = "Hardened NixOS"; + user.authorizedKeys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ9o2AIEaDp+UarHS2ywYyJxWgxsSfB1GZ4FAyuYAzw/" + ]; + }; + + system.stateVersion = "25.11"; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + environment.systemPackages = with pkgs; [ + vim + git + just + sbctl + age + age-plugin-tpm + ]; +} diff --git a/harden/debug/ssh-key.pub b/harden/debug/ssh-key.pub new file mode 100644 index 0000000..fdb64c6 --- /dev/null +++ b/harden/debug/ssh-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM7kHaJdORgAXyW1tfvbVyoQLveswvSrhOKZ5ONxWgcW harden-vm diff --git a/harden/disko.nix b/harden/disko.nix new file mode 100644 index 0000000..b4ca25e --- /dev/null +++ b/harden/disko.nix @@ -0,0 +1,48 @@ +{lib, ...}: { + disko.devices = { + disk = { + main = { + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + crypt = { + size = "100%"; + content = { + type = "luks"; + name = "cryptroot"; + content = { + type = "btrfs"; + extraArgs = ["-f"]; + subvolumes = { + "/root" = { + mountpoint = "/"; + mountOptions = ["compress=zstd" "noatime"]; + }; + "/root-blank" = { + mountpoint = "/root-blank"; + mountOptions = []; + }; + "/persistent" = { + mountpoint = "/persistent"; + mountOptions = ["compress=zstd" "noatime"]; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/harden/flake.nix b/harden/flake.nix new file mode 100644 index 0000000..7f3dc69 --- /dev/null +++ b/harden/flake.nix @@ -0,0 +1,76 @@ +{ + description = "Hardened NixOS — VM/ISO testing"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + impermanence.url = "github:nix-community/impermanence"; + lanzaboote = { + url = "github:nix-community/lanzaboote/v1.0.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + disko = { + url = "github:nix-community/disko/latest"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + agenix = { + url = "github:ryantm/agenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + ... + } @ inputs: let + system = "x86_64-linux"; + lib = nixpkgs.lib; + + mkHardened = {modules ? []}: + lib.nixosSystem { + inherit system; + specialArgs = {inherit inputs;}; + modules = + [ + ./configuration.nix + ] + ++ modules; + }; + + pkgs = nixpkgs.legacyPackages.${system}; + in { + nixosConfigurations = { + vm = mkHardened { + modules = [ + ./hardware-vm.nix + { + virtualisation = { + diskSize = 20480; + memorySize = 4096; + cores = 2; + }; + } + ]; + }; + iso = mkHardened { + modules = [ + { + systemd.services.install = { + description = "Install NixOS"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + script = '' + echo "Run: disko-install --flake .#vm --write-efi-boot-entries --disk vda /dev/vda" + ''; + }; + } + ]; + }; + }; + + packages.${system} = { + default = pkgs.just; + qcow = (mkHardened {}).config.system.build.qemu-efi; + }; + }; +} diff --git a/harden/hardware-vm.nix b/harden/hardware-vm.nix new file mode 100644 index 0000000..bb41d86 --- /dev/null +++ b/harden/hardware-vm.nix @@ -0,0 +1,11 @@ +{lib, ...}: { + imports = [ + "${toString /nixos/modules/profiles/qemu-guest.nix}" + ]; + + virtualisation = { + diskSize = 20480; + memorySize = 4096; + cores = 2; + }; +} diff --git a/harden/justfile b/harden/justfile new file mode 100644 index 0000000..d9a244f --- /dev/null +++ b/harden/justfile @@ -0,0 +1,19 @@ +default: vm + +vm: + nixos-rebuild build-vm --flake .#vm + +iso: + nix build .#iso + +qcow: + nix build .#qcow + +verify: + ./tests/verify.sh + +clean: + rm -rf result result-vm + +fmt: + nix fmt diff --git a/harden/tests/verify.sh b/harden/tests/verify.sh new file mode 100755 index 0000000..3cd0f65 --- /dev/null +++ b/harden/tests/verify.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Hardened NixOS Verification ===" +echo "" + +# 1. Impermanence +echo "[1/7] Impermanence..." +if mountpoint -q / && [[ "$(stat -f -c %T /)" == "tmpfs" ]]; then + echo " ✓ Root is tmpfs" +else + echo " ✗ Root is NOT tmpfs" +fi + +if mountpoint -q /persistent; then + echo " ✓ /persistent is mounted" +else + echo " ✗ /persistent is NOT mounted" +fi + +# 2. Secure Boot +echo "[2/7] Secure Boot..." +if command -v sbctl &>/dev/null; then + sbctl status 2>&1 | head -5 +else + echo " ✗ sbctl not found" +fi + +# 3. LUKS +echo "[3/7] LUKS..." +if cryptsetup status cryptroot &>/dev/null; then + echo " ✓ cryptroot is active" +else + echo " ✗ cryptroot is NOT active" +fi + +# 4. AppArmor +echo "[4/7] AppArmor..." +if command -v aa-status &>/dev/null; then + PROFILE_COUNT=$(aa-status 2>/dev/null | grep -c "profiles are in enforce" || true) + echo " ✓ $PROFILE_COUNT profiles in enforce mode" +else + echo " ✗ aa-status not found" +fi + +# 5. Kernel hardening +echo "[5/7] Kernel hardening..." +KPTR=$(cat /proc/sys/kernel/kptr_restrict) +DMESG=$(cat /proc/sys/kernel/dmesg_restrict) +SYSRQ=$(cat /proc/sys/kernel/sysrq) +echo " kptr_restrict=$KPTR (expected 2)" +echo " dmesg_restrict=$DMESG (expected 1)" +echo " sysrq=$SYSRQ (expected 0)" + +# 6. User +echo "[6/7] User config..." +ROOT_PASS=$(getent shadow root | cut -d: -f2) +if [[ "$ROOT_PASS" == "!" ]]; then + echo " ✓ Root login disabled" +else + echo " ✗ Root login is enabled" +fi + +# 7. Agenix (if enabled) +echo "[7/7] Agenix..." +if [[ -d /etc/agenix ]]; then + echo " ✓ Agenix is configured" +else + echo " - Agenix not configured (expected if disabled)" +fi + +echo "" +echo "=== Verification complete ===" diff --git a/modules/agenix.nix b/modules/agenix.nix new file mode 100644 index 0000000..d674a42 --- /dev/null +++ b/modules/agenix.nix @@ -0,0 +1,36 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.hardened; +in { + options.hardened.agenix.identityPaths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = ["/persistent/age-tpm.txt"]; + description = "List of identity paths for agenix"; + }; + + config = lib.mkIf cfg.agenix.enable { + boot.initrd.availableKernelModules = ["tpm_crb" "tpm_tis"]; + + age = { + inherit (cfg.agenix) identityPaths; + ageBin = "PATH=$PATH:${lib.makeBinPath [pkgs.age-plugin-tpm]} ${pkgs.age}/bin/age"; + }; + + environment.systemPackages = with pkgs; [ + age + age-plugin-tpm + ]; + + environment.persistence = lib.mkIf config.hardened.impermanence.enable { + "/persistent" = { + directories = [ + "/var/lib/agenix" + ]; + }; + }; + }; +} diff --git a/modules/apparmor.nix b/modules/apparmor.nix new file mode 100644 index 0000000..0e8d3e3 --- /dev/null +++ b/modules/apparmor.nix @@ -0,0 +1,14 @@ +{ + config, + lib, + ... +}: let + cfg = config.hardened; +in { + config = lib.mkIf cfg.apparmor.enable { + security.apparmor = { + enable = true; + killUnconfinedConfinables = true; + }; + }; +} diff --git a/modules/hardened.nix b/modules/hardened.nix new file mode 100644 index 0000000..8cb31a6 --- /dev/null +++ b/modules/hardened.nix @@ -0,0 +1,27 @@ +{ + config, + lib, + ... +}: let + cfg = config.hardened; +in { + options.hardened = { + impermanence.enable = lib.mkEnableOption "tmpfs-as-root impermanence"; + secureboot.enable = lib.mkEnableOption "Lanzaboote secure boot"; + luks.enable = lib.mkEnableOption "LUKS full-disk encryption"; + apparmor.enable = lib.mkEnableOption "AppArmor MAC"; + kernel.hardened = lib.mkEnableOption "hardened kernel sysctl + params"; + agenix.enable = lib.mkEnableOption "TPM-backed agenix secrets"; + user.enable = lib.mkEnableOption "hardened user config"; + }; + + imports = [ + ./impermanence.nix + ./secureboot.nix + ./luks.nix + ./kernel.nix + ./apparmor.nix + ./agenix.nix + ./user.nix + ]; +} diff --git a/modules/impermanence.nix b/modules/impermanence.nix new file mode 100644 index 0000000..b14f848 --- /dev/null +++ b/modules/impermanence.nix @@ -0,0 +1,130 @@ +{ + config, + lib, + ... +}: let + cfg = config.hardened; +in { + config = lib.mkIf (cfg.impermanence.enable) { + assertions = [ + { + assertion = config.fileSystems."/persistent".fsType or null == "btrfs"; + message = "Impermanence requires /persistent to be mounted as btrfs"; + } + { + assertion = builtins.any (fs: fs.mountPoint == "/" && fs.fsType == "btrfs") ( + builtins.attrValues config.fileSystems + ); + message = "Impermanence requires root filesystem to be btrfs"; + } + ]; + + fileSystems."/persistent".neededForBoot = true; + + boot.initrd.systemd.enable = true; + + boot.initrd.systemd.services.rollback = { + description = "Rollback BTRFS root subvolume"; + wantedBy = ["initrd.target"]; + after = [ + "systemd-cryptsetup@enc.service" + "systemd-cryptsetup@cryptroot.service" + ]; + before = ["sysroot.mount"]; + unitConfig.DefaultDependencies = "no"; + serviceConfig = { + Type = "oneshot"; + UMask = "0077"; + }; + script = '' + set -euo pipefail + echo "Starting impermanence rollback..." + + LUKS_DEVICE="" + for device in /dev/mapper/enc /dev/mapper/cryptroot; do + if [[ -b "$device" ]]; then + LUKS_DEVICE="$device" + break + fi + done + + if [[ -z "$LUKS_DEVICE" ]]; then + echo "Error: No LUKS device found, skipping rollback" + exit 0 + fi + + echo "Found LUKS device: $LUKS_DEVICE" + mkdir -p /mnt + + if ! mount -o subvol=/ "$LUKS_DEVICE" /mnt; then + echo "Error: Failed to mount root filesystem" + exit 1 + fi + + if [[ ! -d "/mnt/root-blank" ]]; then + echo "Error: /mnt/root-blank snapshot not found, skipping rollback" + umount /mnt || true + exit 0 + fi + + echo "Found root-blank snapshot, proceeding with rollback" + + if [[ -d "/mnt/root" ]]; then + echo "Removing nested subvolumes..." + btrfs subvolume list -o /mnt/root | cut -f9 -d' ' | while read -r subvolume; do + if [[ -n "$subvolume" ]]; then + echo "Deleting /$subvolume subvolume..." + btrfs subvolume delete "/mnt/$subvolume" || echo "Warning: Failed to delete $subvolume" + fi + done + + echo "Deleting /root subvolume..." + if ! btrfs subvolume delete /mnt/root; then + echo "Error: Failed to delete /root subvolume" + umount /mnt || true + exit 1 + fi + fi + + echo "Restoring blank /root subvolume..." + if ! btrfs subvolume snapshot /mnt/root-blank /mnt/root; then + echo "Error: Failed to create snapshot" + umount /mnt || true + exit 1 + fi + + echo "Rollback completed successfully" + umount /mnt || echo "Warning: Failed to unmount /mnt" + ''; + }; + + environment.persistence."/persistent" = { + hideMounts = true; + directories = [ + "/etc/nixos" + "/srv" + "/var/spool" + "/.cache/nix/" + "/etc/NetworkManager/system-connections" + "/var/cache/" + "/var/db/sudo/" + "/var/lib/nixos" + "/var/lib/systemd/coredump" + "/var/lib/systemd/timers" + "/var/lib/systemd/timesync" + "/var/lib/bluetooth" + "/var/lib/NetworkManager" + "/var/lib/dbus" + "/root" + ]; + files = [ + "/etc/adjtime" + "/etc/machine-id" + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_ed25519_key.pub" + "/etc/ssh/ssh_host_rsa_key" + "/etc/ssh/ssh_host_rsa_key.pub" + ]; + }; + }; +} diff --git a/modules/kernel.nix b/modules/kernel.nix new file mode 100644 index 0000000..b352c67 --- /dev/null +++ b/modules/kernel.nix @@ -0,0 +1,26 @@ +{ + config, + lib, + ... +}: let + cfg = config.hardened; +in { + config = lib.mkIf cfg.kernel.hardened { + boot.kernel.sysctl = { + "kernel.kptr_restrict" = 2; + "kernel.dmesg_restrict" = 1; + "kernel.unprivileged_bpf_disabled" = 1; + "net.core.bpf_jit_harden" = 2; + "fs.suid_dumpable" = 0; + "kernel.sysrq" = 0; + }; + + boot.kernelParams = [ + "lockdown=confidentiality" + "module.sig_enforce=1" + "slab_nomerge" + "init_on_alloc=1" + "init_on_free=1" + ]; + }; +} diff --git a/modules/luks.nix b/modules/luks.nix new file mode 100644 index 0000000..dafd434 --- /dev/null +++ b/modules/luks.nix @@ -0,0 +1,15 @@ +{ + config, + lib, + ... +}: let + cfg = config.hardened; +in { + options.hardened.luks.tpm = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Use TPM2 for LUKS unlock"; + }; + + config = lib.mkIf cfg.luks.enable {}; +} diff --git a/modules/secureboot.nix b/modules/secureboot.nix new file mode 100644 index 0000000..cf8f244 --- /dev/null +++ b/modules/secureboot.nix @@ -0,0 +1,41 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.hardened; +in { + config = lib.mkIf cfg.secureboot.enable { + boot = { + lanzaboote = { + enable = true; + pkiBundle = "/etc/secureboot/pki"; + autoGenerateKeys.enable = true; + autoEnrollKeys = { + includeMicrosoftKeys = false; + allowBrickingMyMachine = true; + enable = true; + autoReboot = true; + }; + }; + loader.systemd-boot = { + enable = lib.mkForce false; + configurationLimit = 10; + editor = false; + }; + }; + + environment.systemPackages = with pkgs; [ + sbctl + ]; + + environment.persistence = lib.mkIf config.hardened.impermanence.enable { + "/persistent" = { + directories = [ + "/etc/secureboot" + ]; + }; + }; + }; +} diff --git a/modules/user.nix b/modules/user.nix new file mode 100644 index 0000000..f40c797 --- /dev/null +++ b/modules/user.nix @@ -0,0 +1,47 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.hardened; +in { + options.hardened.user = { + username = lib.mkOption { + type = lib.types.str; + default = "nixos"; + description = "Username"; + }; + name = lib.mkOption { + type = lib.types.str; + default = "NixOS"; + description = "User display name"; + }; + authorizedKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "SSH public keys"; + }; + }; + + config = lib.mkIf cfg.user.enable { + programs.fish.enable = true; + + users = { + mutableUsers = true; + users.root.hashedPassword = "!"; + users.${cfg.user.username} = { + isNormalUser = true; + description = cfg.user.name; + extraGroups = [ + "networkmanager" + "wheel" + "kvm" + "libvirtd" + ]; + openssh.authorizedKeys.keys = cfg.user.authorizedKeys; + shell = pkgs.fish; + }; + }; + }; +} -- 2.51.2