A composable, production-ready NixOS hardening framework with orthogonal profile+user axes.
Not a distribution — this is a library of hardening modules plus reference templates. It is designed to be honest about what it enforces, what is merely staged, and what remains the operator's responsibility.
| I want... | Start here | Template |
|---|---|---|
| A secure, private Linux workstation with minimal effort | templates/workstation/README.md |
Single-user daily profile |
| Maximum privacy with paranoid+daily split | templates/default/README.md |
Two-user paranoid+daily |
| Deep customization / my own template | docs/CUSTOMIZATION.md + docs/maps/PROFILE-POLICY.md |
Build your own |
| To debug issues or fix failures | docs/pipeline/RECOVERY.md + docs/guides/TROUBLESHOOTING.md |
Any |
New users: Start with the workstation template for fastest path to a hardened system. Privacy-focused users: Use the default template for paranoid profile isolation.
Read docs/governance/PROJECT-STATE.md before adapting any part to another machine.
paranoid: default hardened workstation baselinedaily: boot specialization for gaming, social, and recovery-friendly use- orthogonal profile+user axes: profiles define system posture, users define identity/persistence
- one encrypted Btrfs/tmpfs-root system with cross-profile mount isolation
- current stable target = complete
docs/pipeline/INSTALL-GUIDE.md+docs/pipeline/TEST-PLAN.mdon the real target machine docs/pipeline/POST-STABILITY.mdis non-blocking follow-up work after the stable baseline is already usable on the machine
base: shared hardening substrate only; not a standalone bootable profileparanoid: strongest workstation-safe hardening/privacy posture (staged asnixosModules.profile-paranoid)daily: softens only the controls needed for socialization, gaming, and recovery-friendly daily use (staged asnixosModules.profile-daily)- Users are declared via
myOS.users.<name>withactiveOnProfilesdetermining profile bindings - The default template demonstrates one daily-style user (persistent home, wheel) + one paranoid-style user (tmpfs home, no wheel)
Navigation hub: docs/README.md — central index of all documentation.
templates/workstation/README.mdortemplates/default/README.mddocs/pipeline/INSTALL-GUIDE.mddocs/CUSTOMIZATION.mddocs/pipeline/RECOVERY.md+docs/guides/TROUBLESHOOTING.md
Policy model (read in order):
docs/maps/PROFILE-POLICY.md— Profile governance (base/paranoid/daily)docs/maps/HARDENING-TRACKER.md— Every hardening knob and its statedocs/maps/SOURCE-COVERAGE.md— External source influence mapping
Verification:
docs/maps/AUDIT-STATUS.md— What's proven vs pendingdocs/maps/FEATURES.md— Complete feature inventorydocs/pipeline/TEST-PLAN.md— Validation checklistdocs/pipeline/POST-STABILITY.md— Deferred work after baseline
For agent/AI entry, read AGENTS.md first, then:
docs/governance/PROJECT-STATE.mdREFERENCES.mddocs/maps/AUDIT-STATUS.mddocs/pipeline/INSTALL-GUIDE.mddocs/pipeline/TEST-PLAN.mddocs/pipeline/POST-STABILITY.mddocs/pipeline/RECOVERY.md
| Path | Purpose |
|---|---|
modules/, profiles/ |
Framework library (reusable NixOS modules) |
templates/default/ |
Reference implementation (paranoid+daily split) |
templates/workstation/ |
Minimal single-user template |
docs/ |
Documentation hub |
tests/ |
Test suite (tests/run.sh) |
scripts/ |
Helper scripts only |
flake.nix |
Framework exports (40 nixosModules.* outputs) |
This repo now exposes itself as a reusable NixOS framework. You can consume it without forking:
nix flake init -t github:oestradiol/NixOS#workstation
# edit flake.nix for your hostName, GPU, user identity
sudo nixos-rebuild switch --flake .#workstationSee templates/workstation/README.md for the bootstrap checklist.
Import only the hardening surface you need:
{
inputs.hardening.url = "github:oestradiol/NixOS";
outputs = { nixpkgs, hardening, ... }: {
nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
modules = [
hardening.nixosModules.core
hardening.nixosModules.security-kernel-hardening
hardening.nixosModules.desktop-plasma
# ... your own modules
];
};
};
}All 40 nixosModules.* outputs are documented in flake.nix.
Fork if you need to change framework internals (governance invariants, PAM binding experiments, browser wrappers). Keep the framework boundary clear: modules/ and profiles/ are the reusable substrate; templates/default/hosts/ and templates/default/accounts/ are the deployment-specific layer.
Deployment-specific values such as git identity, audio aliases, and local paths belong in account definitions or local overrides:
templates/default/accounts/player.nix— daily reference accounttemplates/default/accounts/ghost.nix— paranoid reference account
The reference template intentionally leaves operator-owned identity fields unset. Forks may use any user naming scheme and identity values.
The repo ships a three-layer test suite runnable offline:
./tests/run.sh # full sweep
./tests/run.sh --layer static # eval + governance; no booted machine required
./tests/run.sh --layer runtime # probes the booted system
./tests/run.sh --layer bugs # regressions for known historical bugsSee tests/README.md for per-file coverage.
When the repo passes the current pre-install, install, and test-plan pipeline on the target machine, treat that as the first stable version.
After that point:
- keep using the machine from the stable baseline
- move all further tightening, experiments, and optional rollouts into
docs/pipeline/POST-STABILITY.md - do not mix deferred work back into the baseline path unless it is revalidated and moved into
docs/pipeline/TEST-PLAN.md
These are deliberate design choices, not debt. Do not "simplify" them.
lib.mkForceinprofiles/daily.nixis intentional — daily explicitly overrides the hardened basenetworking.firewall.interfaces.<iface>.allowedUDPPorts = [ 9 ]is WoL-over-UDP compatibility (see modules/security/networking.nix:16-29)services.avahi.enable = lib.mkForce falsein modules/desktop/vr.nix is required because upstream wivrn.nix sets it without mkDefaultservices.geoclue2.enable = lib.mkForce falsein modules/desktop/base.nix is required because Plasma 6 enables it via mkDefaulttemplates/default/hosts/nixos/default.niximportslocal.nixonly via lib.optional (builtins.pathExists ./local.nix) — this is the sanctioned extension point for per-install hardware quirks--show-traceon every flake-* rebuild alias is debug-phase posture; drop once first fully-clean rebuild lands (HARDENING-TRACKER.md operator decision C1)
- wrapper isolation is not VM-equivalent; same-kernel containment only
- the desktop stack is not high assurance
- passing static review is not runtime proof
- staged features (see
HARDENING-TRACKER.md) are not part of the baseline until explicitly graduated