Hardening NixOS
Click to Expand Table of Contents
- Minimal Installation with LUKS
- Manual Encrypted Install Following the Manual
- Guided Encrypted BTRFS Subvol install using disko
- Installing Software
- Impermanence
- Replace timesyncd with a chron job that enables Network Time Security (NTS)
- Secure Boot
- Choosing your Kernel
- Kernel Security Settings
- Further Hardening with sysctl
- Hardening Boot Parameters
- Hardened Memory Allocator
- Hardening Systemd
- Lynis and other tools
- Securing SSH
- Key generation
- Encrypted Secrets
- Auditd
- USB Port Protection
- Doas over sudo
- Firejail
- Flatpak
- SeLinux/AppArmor MAC (Mandatory Access Control)
- Resources
Securing your NixOS system begins with a philosophy of minimalism, explicit configuration, and proactive control.
⚠️ Warning: I am not a security expert. This guide presents various options for hardening NixOS, but it is your responsibility to evaluate whether each adjustment suits your specific needs and environment. Security hardening and process isolation can introduce stability challenges, compatibility issues, or unexpected behavior. Additionally, these protections often come with performance tradeoffs. Always conduct thorough research, there are no plug and play one size fits all security solutions.
That said, I typically write about what I'm implementing myself to deepen understanding and share what works for me.
--Sourcemeans the proceeding paragraph came from--Source, you can often click to check for yourself. If you use some common sense with a bit of caution you could end up with a more secure NixOS system that fits your needs.
Much of this guide draws inspiration or recommendations from the well-known Linux Hardening Guide by Madaidan's Insecurities. Madaidan’s work is widely regarded in technical and security circles as one of the most comprehensive and rigorously researched sources on practical Linux security, frequently cited for its depth and actionable advice. For example, much of the original basis for hardening for nix-mineral came from this guide as well. This can be a starting point but shouldn't be blindly followed either, always do your own research, things change frequently.
For an article with apposing perspectives, see debunking-madaidans-insecurities. We can learn from both and hopefully find something in between that is closer to the truth.
❗ Note on SELinux and AppArmor: While NixOS can provide a high degree of security through its immutable and declarative nature, it's important to understand the limitations regarding Mandatory Access Control (MAC) frameworks. Neither SELinux nor AppArmor are fully supported or widely used in the NixOS ecosystem. You can do a lot to secure NixOS but if anonymity and isolation are paramount, I recommend booting into a Tails USB stick. Or using Whonix.
☝️ The unique file structure of NixOS, particularly the immutable /nix/store,
makes it difficult to implement and manage the file-labeling mechanisms that
these frameworks rely on. There are ongoing community efforts to improve
support, but as of now, they are considered experimental and not a standard part
of a typical NixOS configuration. For an immutable distro that implements
SELinux by default at a system level as well as many other hardening techniques,
see Fedora secureblue.
Containers and VMs are beyond the scope of this chapter but can also enhance security and sandboxing if configured correctly. See Running NixOS in a VM for more details on running NixOS in a Secureblue VM for additional security.
It's crucial to document every change you make. By creating smaller, feature-complete commits, each with a descriptive message, you're building a clear history. This approach makes it far simpler to revert a breaking change and quickly identify what went wrong. Over time, this discipline allows you to create security-focused checklists and ensure all angles are covered, building a more robust and secure system.
Check out the Hardening NixOS Baseline Hardening README for baseline hardening recommendations and best practices.
There is something to be said about the window manager you use. GNOME, KDE Plasma, and Sway secure privileged Wayland protocols like screencopy. This means that on environments outside of GNOME, KDE, and Sway, applications can access screen content of the entire desktop. This implicitly includes the content of other applications. It's primarily for this reason that Silverblue, Kinoite, and Sericea images are recommended. COSMIC has plans to fix this. --secureblue Images
For example, to disable Xwayland for sway on home-manager you would add:
wayland.windowManager.sway = {
enable = true;
extraConfig = ''
xwayland disable
'';
}
- You may get an error saying you're only able to disable xwayland at boot, restart your system and you'll be all set.
You can explicitly disable xdg-desktop-portal-wlr with systemd in your
configuration.nix like this:
# configuration.nix
systemd.user.services."xdg-desktop-portal-wlr" = {
enable = false; # Masks/stops the wlr service
};
Minimal Installation with LUKS
Begin with NixOS’s minimal installation image. This gives you a base system with only essential tools and no extras that could introduce vulnerabilities.
Manual Encrypted Install Following the Manual
Encryption is the process of using an algorithm to scramble plaintext data into ciphertext, making it unreadable except to a person who has the key to decrypt it.
Data at rest is data in storage, such as a computer's or a servers hard disk.
Data at rest encryption (typically hard disk encryption), secures the documents, directories, and files behind an encryption key. Encrypting your data at rest prevents data leakage, physical theft, unauthorized access, and more as long as the key management scheme isn't compromised.
-
The NSA, CISA, and NIST warn that nation-state actors are likely stockpiling encrypted data now, preparing for a future when quantum computers could break today’s most widely used encryption algorithms. Sensitive data with long-term secrecy needs is especially at risk.
-
This is a wake-up call to use the strongest encryption available today and to plan early for post-quantum security.
-
NIST First 3 Post-Quantum Encryption Standards Organizations and individuals should prepare to migrate cryptographic systems to these new standards as soon as practical.
-
They chose Four Quantum-Resistant Cryptographic Algorithms warning that public-key cryptography is especially vulnerable and widely used to protect digital information.
Guided Encrypted BTRFS Subvol install using disko
Use LUKS encryption to protect your data at rest, the following guide is a minimal disko encrypted installation: Encrypted Install
Installing Software
Most users don't fully understand that running any software without sandboxing gives it unrestricted access to their user data and system resources. There is a widespread lack of awareness that Linux apps generally run with the full permissions of the user. It's easy to overlook the fact that "trusted source" doesn't mean "safe to run uncontained".
I suggest that you try using an editor from Flatpak so you can see and experience running an editor in a sandbox. It felt weird to me because I had gotten used to being able to see all my files with Yazi or do insecure customizations to my editor unfettered. With Flatseal, you can see which permissions the app ships with and adjust them accordingly.
nixpkgs-unstable Security Overview
-
nixpkgs-unstabletracks the master branch of the Nixpkgs repo and is constantly updated. -
This branch gets security updates faster, patching vulnerabilities faster.
-
Since it's a rolling-release, packages are less thoroughly tested. This increases the risk of new, undiscovered bugs or regressions. Some of which could have security implications.
-
The packages are generally the most recent upstream versions, which is important for security-sensitive software like browsers and kernels, as old versions may have publicly known, unpatched vulnerabilities.
-
As the name states,
nixpkgs-unstableis less stable and an update is more likely to cause your system to fail to build due to breaking changes in Nix expressions.
Stable (e.g., nixos-24.05) Security Overview
Stable Nixpkgs channels correspond to point release (e.g., released every 6 months) and are supported for a limited period (typically one month past the next release).
-
Stable channels generally only receive conservative bug and security fixes. Major version bumps for features are typically avoided to maintain "stability against deliberate changes", which means you won't get the latest upstream features or general bug fixes.
-
While critical security updates are backported quickly, updates for less critical packages may be slower or not happen at all if they require a significant refactoring or version bump.
-
Stable channels are generally more stable, meaning updates are less likely to introduce breaking changes to your configuration or system environment.
-
Many packages will be older versions. If a critical security vulnerability requires a major upstream version update (which is often avoided in a stable channel), the maintainers must backport the patch, a process which can introduce its own set of risks and delays.
What should you use?
The primary security trade-off is between patching speed for known vulnerabilities and stability/exposure to new bugs:
-
Choose
unstableif you prioritize getting the latest security fixes (especially for end-user apps like browsers) as soon as they are available upstream, accepting a higher risk of non-security-related system breakage or new, undiscovered bugs. -
Choose
stableif you prioritize system predictability and stability, relying on dedicated backports for critical vulnerabilities, while accepting that non-critical security and bug fixes will be delayed or absent until the next major release.
A common hybrid approach is to use the stable channel as the base for the OS
and selectively pin specific packages from unstable to ensure they receive
rapid security updates.
With flakes it's easy to add both stable and unstable as flake inputs and
access each with some simple logic.
For example:
Click to expand
Impermanence
Impermanence, especially when using a tmpfs as the root filesystem, provides
several significant security benefits. The core principle is that impermanence
defeats persistence, a fundamental goal for any attacker.
When you use a root-as-tmpfs setup on NixOS, the boot process loads the entire
operating system from the read-only Nix store into a tmpfs in RAM. The mutable
directories, such as /etc and /var, are then created on this RAM disk. When
the system is shut down, the tmpfs is wiped, leaving the on-disk storage
untouched and secure.
This means you get a fresh, secure boot every time, making it much harder for an
attacker to maintain a presence on your system.
-
-
Encrypted BTRFS Impermanence Guide
Only follow this guide if you also followed the encrypted disko install,
impermanence is designed to be destructive and needs to match your config
exactly.
Replace timesyncd with a chron job that enables Network Time Security (NTS)
This is implementing the GrapheneOS/secureblue NTS chrony settings to NixOS:
# configuration.nix (or any module you import)
{ config, pkgs, ... }:
let
# ----------------------------------------------------------------------
# How often the timer should run (change to "hourly", "*:0/30", etc.)
# ----------------------------------------------------------------------
chronyNtsTimer = "daily";
# ----------------------------------------------------------------------
# The hardened Chrony configuration (the secureblue one)
# ----------------------------------------------------------------------
chronyConf = pkgs.writeText "chrony.conf" ''
# Copyright © 2014-2025 GrapheneOS
# (full license text omitted for brevity – it will be preserved)
server time.cloudflare.com iburst nts
server ntppool1.time.nl iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server time.dfm.dk iburst nts
server time.cifelli.xyz iburst nts
minsources 3
authselectmode require
# EF
dscp 46
driftfile /var/lib/chrony/drift
dumpdir /var/lib/chrony
ntsdumpdir /var/lib/chrony
leapsectz /usr/share/zoneinfo/leap-seconds.list
makestep 1.0 3
rtconutc
rtcsync
cmdport 0
noclientlog
'';
# ----------------------------------------------------------------------
# Service that writes the config and reloads Chrony
# ----------------------------------------------------------------------
chronyNtsService = {
description = "Write hardened Chrony config with NTS and reload";
serviceConfig = {
Type = "oneshot";
ExecStartPre = pkgs.writeShellScript "write-chrony-nts.sh" ''
set -euo pipefail
# Ensure directories exist
mkdir -p /var/lib/chrony /etc
# Atomically write the config
${pkgs.coreutils}/bin/install -m 0644 ${chronyConf} /etc/chrony.conf
# Reload (or restart if reload fails)
${pkgs.systemd}/bin/systemctl try-reload-or-restart chronyd
'';
};
wantedBy = [ "multi-user.target" ];
};
# ----------------------------------------------------------------------
# Timer that triggers the service
# ----------------------------------------------------------------------
chronyNtsTimerConfig = {
description = "Timer to keep hardened Chrony+NTS config applied";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = chronyNtsTimer;
Persistent = true; # run missed jobs after reboot
RandomizedDelaySec = "5m"; # jitter
};
};
in
{
# ----------------------------------------------------------------------
# 1. Enable Chrony (disable systemd-timesyncd)
# ----------------------------------------------------------------------
services.chrony.enable = true;
services.timesyncd.enable = false; # we’re using Chrony
# ----------------------------------------------------------------------
# 2. Install timer + service
# ----------------------------------------------------------------------
systemd.timers."chrony-nts" = chronyNtsTimerConfig;
systemd.services."chrony-nts" = chronyNtsService;
# ----------------------------------------------------------------------
# 3. Run once at boot (before the timer)
# ----------------------------------------------------------------------
systemd.services."chrony-nts-at-boot" = chronyNtsService // {
description = "Apply hardened Chrony+NTS config at boot";
after = [ "chronyd.service" ];
wantedBy = [ "multi-user.target" ];
};
}
Secure Boot
Enable a UEFI password or Administrator password where it requires
authentication in order to access the UEFI/BIOS.
Secure Boot helps ensure only signed, trusted kernels and bootloaders are
executed at startup.
Useful Resources:
✔️ Click to Expand Secure Boot Resources
Practical Lanzaboote Secure Boot setup for NixOS:
Guide:Secure Boot on NixOS with Lanzaboote
The Kernel
Given the kernel's central role, it's a frequent target for malicious actors,
making robust hardening essential.
NixOS provides a hardened profile that applies a set of security-focused
kernel and system configurations.
For flakes, you could do something like the following in your
configuration.nix or equivalent to import hardened.nix and enable
profiles.hardened:
# configuration.nix
{ pkgs, inputs, ... }: let
modulesPath = "${inputs.nixpkgs}/nixos/modules";
in {
imports = [ "${modulesPath}/profiles/hardened.nix" ];
}
-
There is a proposal to remove it completely that has gained ground, the
following thread discusses why:
Discourse Thread
-
PR #383438 Proposed removal
PR.
-
Check
hardened.nix
to see exactly what adding it enables to avoid duplicates and conflicts moving
on. I included this for completeness, the choice is yours if you want to use
it or not.
Choosing your Kernel
See which kernel you're currently using with:
# show the kernel release
uname -r
# show kernel version, hostname, and architecture
uname -a
Show the configuration of your current kernel:
zcat /proc/config.gz
# ...snip...
#
# Compression
#
CONFIG_CRYPTO_DEFLATE=m
CONFIG_CRYPTO_LZO=y
CONFIG_CRYPTO_842=m
CONFIG_CRYPTO_LZ4=m
CONFIG_CRYPTO_LZ4HC=m
CONFIG_CRYPTO_ZSTD=y
# end of Compression
# ...snip...
The NixOS Manual
states that the default Linux kernel configuration should be fine for most
users.
The Linux kernel is typically released under two forms: stable and long-term
support (LTS). Choosing either has consequences, do your research.
Stable vs. LTS kernels
OR, you can choose the hardened kernel for a kernel that prioritizes
security over everything else.
The Hardened Kernel
NOTE: Expect breakage when using the hardened kernel. linux-hardened
completely disables
unprivileged user namespaces, which
are required for Flatpak, chromium-based browsers, and more.
The linuxPackages_latest_hardened attribute has been deprecated. If you want
to use a hardened kernel, it is now recommended to use linux_hardened, which
is aliased to linux_default.kernel.
You can find the latest available hardened kernel packages by searching
pkgs/top-level/linux-kernels.nix.
It is recommended to use linux_hardened without specifying a version, such as:
boot.kernelPackages = pkgs.linuxPackages_hardened;
linux_hardened is aliased to the linux_default.kernel.
Note that this not only replaces the kernel, but also packages that are specific
to the kernel version, such as NVIDIA video drivers. This also removes your
ability to use the .extend kernel attribute, they are only available to
kernel package sets (e.g., linuxPackages_hardened)
- If you decide to use this, read further before rebuilding.
You can inspect
nixpkgs/pkgs/os-specific/linux/kernel/hardened/patches.json
to see the metadata of the patches that are applied. You can then follow the
links in the .json file to see the patch diffs.
sysctl
A tool for checking the security hardening options of the Linux kernel:
environment.systemPackages = [ pkgs.kernel-hardening-checker ];
sysctl is a tool that allows you to view or modify kernel settings and
enable/disable different features.
Check all sysctl parameters against the kernel-hardening-checker
recommendations:
sudo sysctl -a > params.txt
kernel-hardening-checker -l /proc/cmdline -c /proc/config.gz -s ./params.txt
Check the value of a specific parameter:
sudo sysctl -a | grep "kernel.kptr_restrict"
# Output:
kernel.kptr_restrict = 2
Check Active Linux Security Modules:
cat /sys/kernel/security/lsm
# Output:
File: /sys/kernel/security/lsm
capability,landlock,yama,bpf,apparmor
Check Kernel Configuration Options:
zcat /proc/config.gz | grep CONFIG_SECURITY_SELINUX
zcat /proc/config.gz | grep CONFIG_HARDENED_USERCOPY
zcat /proc/config.gz | grep CONFIG_STACKPROTECTOR
Since it is difficult to see exactly what enabling the hardened_kernel does.
Before rebuilding, you could do something like this to see exactly what is
added:
sudo sysctl -a > before.txt
And after the rebuild:
sudo sysctl -a > after.txt
And finally run a diff on them:
diff before.txt after.txt
You can also diff against after.txt for future changes to avoid duplicates,
this seems easier to me than trying to parse through the patches.
Kernel Security Settings
security = {
protectKernelImage = true;
lockKernelModules = false; # this breaks iptables, wireguard, and virtd
# force-enable the Page Table Isolation (PTI) Linux kernel feature
forcePageTableIsolation = true;
# User namespaces are required for sandboxing.
# this means you cannot set `"user.max_user_namespaces" = 0;` in sysctl
allowUserNamespaces = true;
# Disable unprivileged user namespaces, unless containers are enabled
unprivilegedUsernsClone = config.virtualisation.containers.enable;
allowSimultaneousMultithreading = true;
}
Further Hardening with sysctl
sysctl hardening settings further reinforce kernel-level protections. The
hardened kernel includes security patches and stricter defaults, but it doesn't
cover all runtime tunables. Refer to the above commands to get a diff of the
changes.
boot.kernel.sysctl:
Runtime parameters of the Linux kernel, as set by sysctl(8). Note that the
sysctl parameters names must be enclosed in quotes. Values may be a string,
integer, boolean, or null.
Check what each setting does sysctl-explorer
Refer to
madadaidans-insecurities#sysctl-kernel
for the following settings and their explainations.
Also see the
Kernel Self Protection Projects sysctls
boot.kernel.sysctl = {
"fs.suid_dumpable" = 0;
# prevent pointer leaks
"kernel.kptr_restrict" = 2;
# restrict kernel log to CAP_SYSLOG capability
"kernel.dmesg_restrict" = 1;
# Note: certian container runtimes or browser sandboxes might rely on the following
# restrict eBPF to the CAP_BPF capability
"kernel.unprivileged_bpf_disabled" = 1;
# should be enabled along with bpf above
# "net.core.bpf_jit_harden" = 2;
# restrict loading TTY line disciplines to the CAP_SYS_MODULE
"dev.tty.ldisk_autoload" = 0;
# prevent exploit of use-after-free flaws
"vm.unprivileged_userfaultfd" = 0;
# kexec is used to boot another kernel during runtime and can be abused
"kernel.kexec_load_disabled" = 1;
# Kernel self-protection
# SysRq exposes a lot of potentially dangerous debugging functionality to unprivileged users
# 4 makes it so a user can only use the secure attention key. A value of 0 would disable completely
"kernel.sysrq" = 4;
# disable unprivileged user namespaces, Note: Docker, NH, and other apps may need this
# "kernel.unprivileged_userns_clone" = 0; # commented out because it makes NH and other programs fail
# restrict all usage of performance events to the CAP_PERFMON capability
"kernel.perf_event_paranoid" = 3;
# Network
# protect against SYN flood attacks (denial of service attack)
"net.ipv4.tcp_syncookies" = 1;
# protection against TIME-WAIT assassination
"net.ipv4.tcp_rfc1337" = 1;
# enable source validation of packets received (prevents IP spoofing)
"net.ipv4.conf.default.rp_filter" = 1;
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.all.accept_redirects" = 0;
"net.ipv4.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.secure_redirects" = 0;
"net.ipv4.conf.default.secure_redirects" = 0;
# Protect against IP spoofing
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv6.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.send_redirects" = 0;
"net.ipv4.conf.default.send_redirects" = 0;
# prevent man-in-the-middle attacks
"net.ipv4.icmp_echo_ignore_all" = 1;
# ignore ICMP request, helps avoid Smurf attacks
"net.ipv4.conf.all.forwarding" = 0;
"net.ipv4.conf.default.accept_source_route" = 0;
"net.ipv4.conf.all.accept_source_route" = 0;
"net.ipv6.conf.all.accept_source_route" = 0;
"net.ipv6.conf.default.accept_source_route" = 0;
# Reverse path filtering causes the kernel to do source validation of
"net.ipv6.conf.all.forwarding" = 0;
"net.ipv6.conf.all.accept_ra" = 0;
"net.ipv6.conf.default.accept_ra" = 0;
## TCP hardening
# Prevent bogus ICMP errors from filling up logs.
"net.ipv4.icmp_ignore_bogus_error_responses" = 1;
# Disable TCP SACK
"net.ipv4.tcp_sack" = 0;
"net.ipv4.tcp_dsack" = 0;
"net.ipv4.tcp_fack" = 0;
# Userspace
# restrict usage of ptrace
"kernel.yama.ptrace_scope" = 2;
# ASLR memory protection (64-bit systems)
"vm.mmap_rnd_bits" = 32;
"vm.mmap_rnd_compat_bits" = 16;
# only permit symlinks to be followed when outside of a world-writable sticky directory
"fs.protected_symlinks" = 1;
"fs.protected_hardlinks" = 1;
# Prevent creating files in potentially attacker-controlled environments
"fs.protected_fifos" = 2;
"fs.protected_regular" = 2;
# Randomize memory
"kernel.randomize_va_space" = 2;
# Exec Shield (Stack protection)
"kernel.exec-shield" = 1;
## TCP optimization
# TCP Fast Open is a TCP extension that reduces network latency by packing
# data in the sender’s initial TCP SYN. Setting 3 = enable TCP Fast Open for
# both incoming and outgoing connections:
"net.ipv4.tcp_fastopen" = 3;
# Bufferbloat mitigations + slight improvement in throughput & latency
"net.ipv4.tcp_congestion_control" = "bbr";
"net.core.default_qdisc" = "cake";
};
❗ Note: The above settings are fairly aggressive and can break common
programs, read the comment warnings.
Hardening Boot Parameters
boot.kernelParams can be used to set additional kernel command line arguments
at boot time. It can only be used for built-in modules.
You can find the following settings in the above guide in the
Boot parameters section
# boot.nix
boot.kernelParams = [
# make it harder to influence slab cache layout
"slab_nomerge"
# enables zeroing of memory during allocation and free time
# helps mitigate use-after-free vulnerabilaties
"init_on_alloc=1"
"init_on_free=1"
# randomizes page allocator freelist, improving security by
# making page allocations less predictable
"page_alloc.shuffel=1"
# enables Kernel Page Table Isolation, which mitigates Meltdown and
# prevents some KASLR bypasses
"pti=on"
# randomizes the kernel stack offset on each syscall
# making attacks that rely on a deterministic stack layout difficult
"randomize_kstack_offset=on"
# disables vsyscalls, they've been replaced with vDSO
"vsyscall=none"
# disables debugfs, which exposes sensitive info about the kernel
"debugfs=off"
# certain exploits cause an "oops", this makes the kernel panic if an "oops" occurs
"oops=panic"
# only alows kernel modules that have been signed with a valid key to be loaded
# making it harder to load malicious kernel modules
# can make VirtualBox or Nvidia drivers unusable
"module.sig_enforce=1"
# prevents user space code excalation
"lockdown=confidentiality"
# "rd.udev.log_level=3"
# "udev.log_priority=3"
];
This is a thoughtful start to hardening boot parameters, there are more
recommendations in the guide.
Kernel modules for hardware devices are generally loaded automatically by
udev. You can force a module to be loaded via boot.kernelModules.
Blacklisting Kernel Parameters
Blacklisting unused kernel modules reduces the attack surface.
boot.blacklistedKernelModules:
List of names of kernel modules that should not be loaded automatically by the
hardware probing code.
You can find the following settings in the
Blacklisting Kernel Modules Section
boot.blacklistedKernelModules = [
# Obscure networking protocols
"dccp" # Datagram Congestion Control Protocol
"sctp" # Stream Control Transmission Protocol
"rds" # Reliable Datagram Sockets
"tipc" # Transparent Inter-Process Communication
"n-hdlc" # High-level Data Link Control
"ax25" # Amateur X.25
"netrom" # NetRom
"x25" # X.25
"rose"
"decnet"
"econet"
"af_802154" # IEEE 802.15.4
"ipx" # Internetwork Packet Exchange
"appletalk"
"psnap" # SubnetworkAccess Protocol
"p8023" # Novell raw IEE 802.3
"p8022" # IEE 802.3
"can" # Controller Area Network
"atm"
# Various rare filesystems
"cramfs"
"freevxfs"
"jffs2"
"hfs"
"hfsplus"
"udf"
# "squashfs" # compressed read-only file system used for Live CDs
# "cifs" # cmb (Common Internet File System)
# "nfs" # Network File System
# "nfsv3"
# "nfsv4"
# "ksmbd" # SMB3 Kernel Server
# "gfs2" # Global File System 2
# vivid driver is only useful for testing purposes and has been the
# cause of privilege escalation vulnerabilities
# "vivid"
];
As with the kernelParameters above, there are more suggestions in the guide, I
have used the above parameters along with the commented out ones and had no
issues.
Also see
SecureBlue's blacklist.conf
for more ideas.
Hardened Memory Allocator
NOTE: There is a performance cost to enabling a hardened memory allocator, and
some apps will not work without a workaround such as Firefox, Thunderbird,
Torbrowser, LibreWolf, and ZenBrowser to name a few.
The grapheneOS hardened_malloc is available for NixOS in two variants, add
either to your configuration.nix or equivalent to apply them:
-
environment.memoryAllocator.provider = "graphene-hardened";: This is the
default configuration template that has all normal optional security features
enabled. It's aggressive, you can expect app breakage and a performance cost.
-
environment.memoryAllocator.provider = "graphene-hardened-light";: The
light template disables the slap quarantines, write after free check, slot
randomization and raises the guard slab interval from 1 to 8 but leaves
zero-on-free and slab canaries enabled. This version has solid performance
and is still far more secure than the standard allocator.
libhardened_malloc.so is typically installed to
/usr/local/lib/libhardened_malloc.so and referenced from /etc/ld.so.preload.
-
-
-
-
Hardening Systemd
systemd is the core "init system" and service manager that controls how
services, daemons, and basic system processes are started, stopped and
supervised on modern Linux distributions, including NixOS. It provides a suite
of basic building blocks for a Linux system as well as a system and service
manager that runs as PID 1 and starts the rest of the system.
Because it launches and supervises almost all system services, hardening systemd
means raising the baseline security of your entire system.
dbus-broker is generally considered more secure and robust but isn't the
default as of yet. To set dbus-broker as the default:
users.groups.netdev = {};
services = {
dbus.implementation = "broker";
logrotate.enable = true;
journald = {
storage = "volatile"; # Store logs in memory
upload.enable = false; # Disable remote log upload (the default)
extraConfig = ''
SystemMaxUse=500M
SystemMaxFileSize=50M
'';
};
};
-
dbus-broker is more resilient to resource exhaustion attacks and integrates
better with Linux security features.
-
-
Setting storage = "volatile" tells journald to keep log data only in memory.
There is a tradeoff though, If you need long-term auditing or troubleshooting
after a reboot, this will not preserve system logs.
-
upload.enable is for forwarding log messages to remote servers, setting this
to false prevents accidental leaks of potentially sensitive or internal system
information.
-
Enabling logrotate prevents your disk from filling with excessive
legacy/service log files. These are the classic plain-text logs.
-
Systemd uses journald which stores logs in a binary format
You can check the security status with:
systemd-analyze security
# or for a detailed view of individual services security posture
systemd-analyze security NetworkManager
It is also recommended to disable and mask unused or vulnerable services such as
cups, geoclue, etc.
sudo systemctl disable cups
sudo systemctl mask cups
# To unmask use unmask:
# sudo systemctl unmask cups
Further reading on systemd:
✔️ Click to Expand Systemd Resources
The following is a repo containing many of the Systemd hardening settings in
NixOS format:
For example, to harden bluetooth you could add the following to your
configuration.nix or equivalent:
systemd.services = {
bluetooth.serviceConfig = {
ProtectKernelTunables = lib.mkDefault true;
ProtectKernelModules = lib.mkDefault true;
ProtectKernelLogs = lib.mkDefault true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
SystemCallFilter = [
"~@obsolete"
"~@cpu-emulation"
"~@swap"
"~@reboot"
"~@mount"
];
SystemCallArchitectures = "native";
};
}
As you can see from above, you typically use the serviceConfig attribute to
harden settings for systemd services.
systemd-analyze security bluetooth
→ Overall exposure level for bluetooth.service: 3.3 OK 🙂
Lynis and other tools
Lynis is a security auditing tool for systems based on UNIX like Linux, macOS,
BSD, and others.--lynis repo
chkrootkit was removed as it is unmaintained and archived upstream.
Installation:
environment.systemPackages = [
pkgs.lynis
pkgs.clamav
pkgs.aide
];
✔️ Click to Expand AIDE Example
AIDE is an intrusion detection system (IDS) that will notify us whenever it
detects that a potential intrusion has occurred. When a system is compromised,
attackers typically will try to change file permissions and escalate to the root
user account and start to modify system files, AIDE can detect this.
To set up AIDE on your system follow these steps:
- Create the
aide.conf:
sudo mkdir -p /var/lib/aide && cd /var/lib/aide/
sudo hx aide.conf
Add the following content to /var/lib/aide/aide.conf:
# aide.conf
# Example configuration file for AIDE.
@@define DBDIR /var/lib/aide
# The location of the database to be read.
database_in=file:@@{DBDIR}/aide.db.gz
# The location of the database to be written.
#database_out=sql:host:port:database:login_name:passwd:table
#database_out=file:aide.db.new
database_out=file:@@{DBDIR}/aide.db.new.gz
# Whether to gzip the output to database
gzip_dbout=yes
log_level=info
report_url=file:/var/log/aide/aide.log
report_url=stdout
#report_url=stderr
#NOT IMPLEMENTED report_url=mailto:root@foo.com
#NOT IMPLEMENTED report_url=syslog:LOG_AUTH
# These are the default rules.
#
#p: permissions
#i: inode:
#n: number of links
#u: user
#g: group
#s: size
#b: block count
#m: mtime
#a: atime
#c: ctime
#S: check for growing size
#md5: md5 checksum
#sha1: sha1 checksum
#rmd160: rmd160 checksum
#tiger: tiger checksum
#haval: haval checksum
#gost: gost checksum
#crc32: crc32 checksum
#R: p+i+n+u+g+s+m+c+md5
#L: p+i+n+u+g
#E: Empty group
#>: Growing logfile p+u+g+i+n+S
# You can create custom rules like this.
NORMAL = R+b+sha512
DIR = p+i+n+u+g
# Next decide what directories/files you want in the database.
/boot NORMAL
/bin NORMAL
/sbin NORMAL
/lib NORMAL
/opt NORMAL
/usr NORMAL
/root NORMAL
# Check only permissions, inode, user and group for /etc, but
# cover some important files closely.
/etc p+i+u+g
!/etc/mtab
/etc/exports NORMAL
/etc/fstab NORMAL
/etc/passwd NORMAL
/etc/group NORMAL
/etc/gshadow NORMAL
/etc/shadow NORMAL
/var/log p+n+u+g
# With AIDE's default verbosity level of 5, these would give lots of
# warnings upon tree traversal. It might change with future version.
#
#=/lost\+found DIR
#=/home DIR
Create the logfile:
sudo mkdir -p /var/log/aide
sudo touch /var/log/aide/aide.log
- Generate the initial database, this will store the checksums of all files
that it's configured to monitor. Take note of the location of the new
database, mine was
/etc/aide.db.new
sudo aide --config /var/lib/aide/aide.conf --init
- Move the new database and remove the
.new:
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
ls /var/lib/aide/
aide.conf aide.db.gz
- Check with AIDE:
sudo aide --check --config /var/lib/aide/aide.conf
Start timestamp: 2025-09-05 09:50:07 -0400 (AIDE 0.19.2)
AIDE found NO differences between database and filesystem. Looks okay!!
- Whenever you make changes to system files, or especially after running a
system update or installing new tools, you have to rescan all files to update
their checksums in the AIDE database:
sudo aide --update --config /var/lib/aide/aide.conf
Unfortunately, AIDE doesn't automatically replace the old database so you have
to rename the new one again:
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
And finally check again:
sudo aide --check --config /var/lib/aide/aide.conf
✔️ Click to Expand clamav.nix Example
{pkgs, ...}: {
environment.systemPackages = with pkgs; [
clamav
];
services.clamav = {
# Enable clamd daemon
daemon.enable = true;
updater.enable = true;
updater.frequency = 12; # Number of database checks per day
scanner = {
enable = true;
# 4:00 AM
interval = "*-*-* 04:00:00";
scanDirectories = [
"/home"
"/var/lib"
"/tmp"
"/etc"
"/var/tmp"
];
};
};
}
Lynis Usage:
sudo lynis show commands
# Output:
Commands:
lynis audit
lynis configure
lynis generate
lynis show
lynis update
lynis upload-only
sudo lynis audit system
# Output:
Lynis security scan details:
Hardening index : 79 [############### ]
Tests performed : 234
Plugins enabled : 0
Components:
- Firewall [V]
- Malware scanner [V]
Scan mode:
Normal [V] Forensics [ ] Integration [ ] Pentest [ ]
Lynis modules:
- Compliance status [?]
- Security audit [V]
- Vulnerability scan [V]
-
The "Lynis hardening index" is an overall impression on how well a system is
hardened. However, this is just an indicator on measures taken - not a
percentage of how safe a system might be. A score over 75 typically indicates
a system with more than average safety measures implemented.
-
Lynis will give you more recommendations for securing your system as well.
If you use clamscan, create the following log file:
sudo touch /var/log/clamscan.log
Example cron job for clamav & aide:
{pkgs, ...}: {
services.cron = {
enable = true;
# messages.enable = true;
systemCronJobs = [
# Every day at 2:00 AM, run clamscan as root and append output to a log file
"0 2 * * * root ${pkgs.clamav}/bin/clamscan -r /home >> /var/log/clamscan.log"
"0 11 * * * ${pkgs.aide}/bin/aide --check --config /var/lib/aide/aide.conf"
];
};
}
ClamAV usage:
You can run clamav manually with:
# Recursive Scan:
sudo clamscan -r ~/home
❗ NOTE: You only need either the individual pkgs.clamav with the cron job
OR the clamd-daemon module. clamdscan is for software integration and
uses a different user that doesn't have permission to scan your files. You can
use clamdscan --fdpass /path/to/scan to pass the necessary file permissions.
NOTE: clamdscan runs in the background, you can watch it with top.
Securing SSH
Security information: Changing SSH configuration settings can
significantly impact the security of your system(s). It is crucial to have a
solid understanding of what you are doing before making any adjustments. Avoid
blindly copying and pasting examples, including those from this Wiki page,
without conducting a thorough analysis. Failure to do so may compromise the
security of your system(s) and lead to potential vulnerabilities. Take the
time to comprehend the implications of your actions and ensure that any
changes made are done thoughtfully and with care. --NixOS Wiki
❗ NOTE: Choose one, either ssh-agent or gpg-agent
- Use normal SSH keys generated with
ssh-keygen, this is recommended unless
you have a good reason for not using it.
OR
- Use a GPG key with
gpg-agent (which acts as your SSH agent). Complex, and
harder to understand in my opinion.
My setup caused conflicts when enabling programs.ssh.startAgent so I chose
gpg-agent personally.
There are situations where you are required to use one or the other like for
headless CI/CD environments, ssh-keygen is required.
Further reading:
✔️ Click to Expand Resourses on OpenSSH
Key generation
ssh-keygen
The ed25519 algorithm is significantly faster and more secure when compared to
RSA. You can also specify the key derivation function (KDF) rounds to
strengthen protection even more.
For example, to generate a strong key for MdBook:
ssh-keygen -t ed25519 -a 32 -f ~/.ssh/id_ed25519_github_$(date +%Y-%m-%d) -C "SSH Key for MdBook"
-
-t is for type
-
-a 32 sets the number of KDF rounds. The standard is usually good enough,
adding extra rounds can make it harder to brute-force.
-
-f is for filename
OpenSSH Server
First of all, if you don't use SSH don't enable it in the first place. If you do
use SSH, it's important to understand what that opens you up to.
The following are some recommendations from Mozilla on OpenSSH:
The following OpenSSH setup is based on the above guidelines with strong
algorithms, and best practices:
{config, ...}: {
config = {
services = {
fail2ban = {
enable = true;
maxretry = 5;
bantime = "1h";
# ignoreIP = [
# "172.16.0.0/12"
# "192.168.0.0/16"
# "2601:881:8100:8de0:31e6:ac52:b5be:462a"
# "matrix.org"
# "app.element.io" # don't ratelimit matrix users
# ];
bantime-increment = {
enable = true; # Enable increment of bantime after each violation
multipliers = "1 2 4 8 16 32 64 128 256";
maxtime = "168h"; # Do not ban for more than 1 week
overalljails = true; # Calculate the bantime based on all the violations
};
};
openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitEmptyPasswords = false;
PermitTunnel = false;
UseDns = false;
KbdInteractiveAuthentication = false;
X11Forwarding = config.services.xserver.enable;
MaxAuthTries = 3;
MaxSessions = 2;
ClientAliveInterval = 300;
ClientAliveCountMax = 0;
AllowUsers = ["your-user"];
TCPKeepAlive = false;
AllowTcpForwarding = false;
AllowAgentForwarding = false;
LogLevel = "VERBOSE";
PermitRootLogin = "no";
KexAlgorithms = [
# Key Exchange Algorithms in priority order
"curve25519-sha256@libssh.org"
"ecdh-sha2-nistp521"
"ecdh-sha2-nistp384"
"ecdh-sha2-nistp256"
"diffie-hellman-group-exchange-sha256"
];
Ciphers = [
# stream cipher alternative to aes256, proven to be resilient
# Very fast on basically anything
"chacha20-poly1305@openssh.com"
# industry standard, fast if you have AES-NI hardware
"aes256-gcm@openssh.com"
"aes128-gcm@openssh.com"
"aes256-ctr"
"aes192-ctr"
"aes128-ctr"
];
Macs = [
# Combines the SHA-512 hash func with a secret key to create a MAC
"hmac-sha2-512-etm@openssh.com"
"hmac-sha2-256-etm@openssh.com"
"umac-128-etm@openssh.com"
"hmac-sha2-512"
"hmac-sha2-256"
"umac-128@openssh.com"
];
};
# These keys will be generated for you
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
};
};
}
TCP port 22 (ssh) is opened automatically if the SSH daemon is enabled
(services.openssh.enable = true;)
Much of the SSH hardening settings came from
ryanseipp's secure-ssh Guide
with some additions of my own.
Fail2Ban is an intrusion prevention software framework. It's designed to prevent
brute-force attacks by scanning log files for suspicious activity, such as
repeated failed login attempts.
OpenSSH is the primary tool for secure remote access for NixOS. Enabling it
activates the OpenSSH server on the system, allowing incoming SSH connections.
The above configuration is a robust setup for securing an SSH server by:
-
Preventing brute-force attacks with Fail2Ban
-
Eliminating password authentication in favor of more secure SSH keys
-
Restricting user access and preventing root login
-
Disabling potentially risky forwarding features (tunnel, TCP, agent)
-
Enforce the use of strong, modern cryptographic algorithms for all SSH
communications.
-
Enhanced logging for better auditing.
Further Reading:
Encrypted Secrets
Never store secrets in plain text in repositories. Use something like
sops-nix, which lets you keep encrypted
secrets under version control declaratively.
Another option is agenix
Sops-nix Guide
Protect your secrets, the following guide is on setting up Sops on NixOS:
Sops Encrypted Secrets
Auditd
To enable the Linux Audit Daemon (auditd) and define a very basic rule set,
you can use the following NixOS configuration. This example demonstrates how to
log every program execution (execve) on a 64-bit architecture.
# modules/security/auditd-minimal.nix (or directly in configuration.nix)
{
# start as early in the boot process as possible
boot.kernelParams = ["audit=1"];
security.auditd.enable = true;
security.audit.enable = true;
security.audit.rules = [
# Log all program executions on 64-bit architecture
"-a exit,always -F arch=b64 -S execve"
];
}
-
audit=1 Enables auditing at the kernel level very early in the boot process.
Without this, some events could be missed.
-
security.auditd.enable = true; Ensures the auditd userspace daemon is
started.
-
While often enabled together, security.audit.enable specifically refers to
enabling the NixOS module for audit rules generation.
-
execve (program executions)
-
This is just a basic configuration, there is much more that can be tracked.
USB Port Protection
It's important to protect your USB ports to prevent BadUSB attacks, data
exfiltration, unauthorized device access, malware injection, etc.
To get a list of your connected USB devices you can use lsusb from the
usbutils package.
lsusb
To list the devices recognized by USBGuard, run:
sudo usbguard list-devices
Change your-user to your username:
# usbguard.nix
{
config,
pkgs,
lib,
...
}: let
inherit (lib) mkIf;
cfg = config.custom.security.usbguard;
in {
options.custom.security.usbguard = {
enable = lib.mkEnableOption "usbguard";
};
config = mkIf cfg.enable {
services.usbguard = {
enable = true;
IPCAllowedUsers = ["root" "your-user"];
# presentDevicePolicy refers to how to treat USB devices that are already connected when the daemon starts
presentDevicePolicy = "allow";
rules = ''
# allow `only` devices with mass storage interfaces (USB Mass Storage)
allow with-interface equals { 08:*:* }
# allow mice and keyboards
# allow with-interface equals { 03:*:* }
# Reject devices with suspicious combination of interfaces
reject with-interface all-of { 08:*:* 03:00:* }
reject with-interface all-of { 08:*:* 03:01:* }
reject with-interface all-of { 08:*:* e0:*:* }
reject with-interface all-of { 08:*:* 02:*:* }
'';
};
environment.systemPackages = [pkgs.usbguard];
};
}
The above settings can be found in
RedHat UsbGuard
The only allow rule is for devices with only mass storage interfaces
(08:*:*) i.e., USB Mass storage devices, devices like keyboards and mice
(which use interface class 03:*:*) implicitly not allowed.
The reject rules reject devices with a suspicious combination of interfaces. A
USB drive that implements a keyboard or a network interface is very suspicious,
these reject rules prevent that.
The presentDevicePolicy = "allow"; allows any device that is present at daemon
start up even if they're not explicitly allowed. However, newly plugged in
devices must match an allow rule or get denied implicitly.
The presentDevicePolicy should be one of: # one of "apply-policy"(default,
evaluate the rule set for every present device), "block", "reject", "keep"
(keep whatever state the device is currently in), or "allow", which is used in
the example.
There is also the
usbguard-notifier
And enable it with the following in your configuration.nix or equivalent:
# configuration.nix
imports = [
./usbguard.nix
];
custom.security.usbguard.enable = true;
❗ If you are ever unsure about a setting that you want to harden and think
that it could possibly break your system you can always use a specialisation
reversing the action and choose it's generation at boot up. For example, to
force-reverse the above settings you could:
# configuration.nix
specialisation.no-usbguard.configuration = {
services.usbguard.enable = lib.mkForce false;
};
- This is a situation where I recommend this, it's easy to lock yourself out
of your keyboard, mouse, etc. when trying to configure this.
Further Reading:
Doas over sudo
NOTE: I have moved to run0 for authentication which is included by default
with systemd. It's actually a symlink to the existing systemd-run tool. It
behaves like a secure sudo alternative: it spawns a transient service under
PID 1 for privilege escalation, without relying on SUID (set user ID)
binaries.
-
SUID = "Set User ID": When a binary has the SUID bit set, it runs with the
privileges of the file's owner (often root). There is a long history of
vulnerabilities with SUID binaries.
-
run0 configuration is explained in the
Hardening README
For a more minimalist version of sudo with a smaller codebase and attack
surface, consider doas. Replace userName with your username:
# doas.nix
{
lib,
config,
pkgs, # Add pkgs if you need to access user information
...
}: let
cfg = config.custom.security.doas;
in {
options.custom.security.doas = {
enable = lib.mkEnableOption "doas";
};
config = lib.mkIf cfg.enable {
# Disable sudo
security.sudo.enable = false;
# Enable and configure `doas`.
security.doas = {
enable = true;
extraRules = [
{
# Grant doas access specifically to your user
users = ["userName"]; # <--- Only give access to your user
# persist = true; # Convenient but less secure
# noPass = true; # Convenient but even less secure
keepEnv = true; # Often necessary
# Optional: You can also specify which commands they can run, e.g.:
# cmd = "ALL"; # Allows running all commands (default if not specified)
# cmd = "/run/current-system/sw/bin/nixos-rebuild"; # Only allow specific command
}
];
};
# Add an alias to the shell for backward-compat and convenience.
environment.shellAliases = {
sudo = "doas";
};
};
}
You would then import this into your configuration.nix and enable/disable it
with the following:
# configuration.nix
imports = [
./doas.nix
];
custom.security.doas.enable = true;
❗ NOTE: Many people opt for the less secure groups = ["wheel"]; in the
above configuration instead of users = ["userName"]; to give wider access,
the choice is yours.
Firejail
❗️ Critics such as madaidan say that Firejail worsens security by acting as a
privilege escalation hole. Firejail requires the executable to be setuid,
meaning it runs with root privileges.This is risky because any vulnerability
in Firejail can lead to privilege escalation. This combined with many
convenience features and complicated command line flags leads to a large
attack surface.
-
I haven't personally tried
nix-bwrapper myself yet, but it's
another sandboxing option that looks interesting. Bubblewrap is known for
having a more minimal design and smaller attack surface.
- Also see: Flatpak section for another option for sandboxing.
-
nix-bubblewrap is another option.
-
-
❗ WARNING: Running untrusted code is never safe, sandboxing cannot change
this. --Arch Wiki
# firejail.nix
{
pkgs,
lib,
...
}: {
programs.firejail = {
enable = true;
wrappedBinaries = {
# Sandbox a web browser
librewolf = {
executable = "${lib.getBin pkgs.librewolf}/bin/librewolf";
profile = "${pkgs.firejail}/etc/firejail/librewolf.profile";
};
# Sandbox a file manager
thunar = {
executable = "${lib.getBin pkgs.xfce.thunar}/bin/thunar";
profile = "${pkgs.firejail}/etc/firejail/thunar.profile";
};
# Sandbox a document viewer
zathura = {
executable = "${lib.getBin pkgs.zathura}/bin/zathura";
profile = "${pkgs.firejail}/etc/firejail/zathura.profile";
};
};
};
}
wrappedBinaries is a list of applications you want to run inside a sandbox.
Only the apps in the wrappedBinaries attribute set will be automatically
firejailed when launched the usual way.
Other apps may be started manually using firejail <app>, or added to
wrappedBinaries if you want automatic sandboxing, just make sure the profile
exists.
To inspect which profiles are available, after rebuilding go to /nix/store/, I
used Yazi to search for /firejail and followed it to firejail/etc, where the
profiles are.
There are many flags and options available with firejail, I suggest checking out
man firejail.
There are comments explaining what's going on in:
firejail/package.nix
Firejail is a SUID program that reduces the risk of security breaches by
restricting the running environment of untrusted applications using
Linux namespaces and
seccomp-bpf--Firejail Security Sandbox
It provides sandboxing and access restriction per application, much like what
AppArmor/SELinux does at a kernel level. However, it's not as secure or
comprehensive as kernel-enforced MAC systems (AppArmor/SELinux), since it's a
userspace tool and can potentially be bypassed by privilege escalation exploits.
Flatpak
❗️NOTE: You cannot effectively use Firejail with Flatpak apps because of how
their sandboxing technologies operate.
Apps that don't have a flatpak equivalent can be further hardened with
bubblewrap independently but bubblewrap is not needed on Flatpak apps.
Because of this limited native MAC (Mandatory Access Control) support on NixOS,
using Flatpak is often a good approach to get sandboxing and isolation for many
GUI apps.
-
Flatpak bundles runtimes and sandbox mechanisms that provide app isolation
independently of the host system's AppArmor or SELinux infrastructure. This
can improve security and containment for GUI applications running on NixOS
despite the system lacking full native MAC coverage.
-
Flatpak apps benefit from sandboxing through bubblewrap, which isolate apps
and restrict access to user/home and system resources.
Add Flatpak with the FlatHub repository for all users:
services.flatpak.enable = true;
systemd.services.flatpak-repo = {
wantedBy = [ "multi-user.target" ];
path = [ pkgs.flatpak ];
script = ''
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
# Only apps that are verified
# flatpak remote-add --if-not-exists --subset=verified flathub-verified https://flathub.org/repo/flathub.flatpakrepo
'';
};
Then you can either find apps through FlatHub or on
the cmdline with flatpak search <app>. Flatpak is best used for GUI apps, some
CLI apps can be installed through it but not all.
-
There is also nix-flatpak, which
enables you to manage your flatpaks declaratively.
-
Flatseal is GUI
utility that enables you to review and modify permissions from your Flatpak
apps. Many apps by default come with smart-card support, X11 & Wayland
support, and more, disabling unnecessary permissions is recommended.
-
Warehouse provides
a simple UI to control complex Flatpak options, no cmdline required.
I have heard that it is not recommended to use Flatpak browsers because in order
for flatpak to work it has to disable some of the built-in browser sandboxing
which can reduce security. I haven't found any examples of Flatpak browsers
being exposed but it's something to keep in mind.
SeLinux/AppArmor MAC (Mandatory Access Control)
AppArmor is available on NixOS, but is still in a somewhat experimental and
evolving state. There are only a few profiles that have been adapted to NixOS,
see here
Discourse on default-profiles
Which guides you here
apparmor/includes.nix
where you can see some of the abstractions and tunables to follow progress.
SELinux: Experimental, not fully integrated, recent progress for
advanced/curious users; expect rough edges and manual intervention if you want
to try it. Most find SELinux more complex to configure and maintain than
AppArmor.
This isn't meant to be a comprehensive guide, more to get people thinking about
security on NixOS.
See the following guide on hardening networking:
Resources
Advanced Hardening with nix-mineral (Community Project)
✔️ Click to Expand section on `nix-mineral`
For users seeking a more comprehensive and opinionated approach to system
hardening beyond the built-in hardened profile, the community project
nix-mineral offers a declarative
NixOS module.
nix-mineral aims to apply a wide array of security configurations, focusing on
tweaking kernel parameters, system settings, and file permissions to reduce the
attack surface.
- Community Project Status:
nix-mineral is a community-maintained project
and is not officially part of the Nixpkgs repository or NixOS documentation.
Its development status is explicitly stated as "Alpha software," meaning it
may introduce stability issues or unexpected behavior.
For detailed information on nix-mineral's capabilities and current status,
refer directly to its
GitHub repository.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
STIGs are configuration standards developed by the Defense Information Systems
Agency (DISA) to secure systems and software for the U.S. Department of
Defense (DoD). They are considered a highly authoritative source for system
hardening.There are recommendations for hardening all kinds of software in the
Stig Viewer
-
-
-
-
-
-
-
tmpfs as the root filesystem, provides
several significant security benefits. The core principle is that impermanence
defeats persistence, a fundamental goal for any attacker.tmpfs in RAM. The mutable
directories, such as /etc and /var, are then created on this RAM disk. When
the system is shut down, the tmpfs is wiped, leaving the on-disk storage
untouched and secure.Encrypted BTRFS Impermanence Guide Only follow this guide if you also followed the encrypted disko install, impermanence is designed to be destructive and needs to match your config exactly.
# configuration.nix (or any module you import)
{ config, pkgs, ... }:
let
# ----------------------------------------------------------------------
# How often the timer should run (change to "hourly", "*:0/30", etc.)
# ----------------------------------------------------------------------
chronyNtsTimer = "daily";
# ----------------------------------------------------------------------
# The hardened Chrony configuration (the secureblue one)
# ----------------------------------------------------------------------
chronyConf = pkgs.writeText "chrony.conf" ''
# Copyright © 2014-2025 GrapheneOS
# (full license text omitted for brevity – it will be preserved)
server time.cloudflare.com iburst nts
server ntppool1.time.nl iburst nts
server nts.netnod.se iburst nts
server ptbtime1.ptb.de iburst nts
server time.dfm.dk iburst nts
server time.cifelli.xyz iburst nts
minsources 3
authselectmode require
# EF
dscp 46
driftfile /var/lib/chrony/drift
dumpdir /var/lib/chrony
ntsdumpdir /var/lib/chrony
leapsectz /usr/share/zoneinfo/leap-seconds.list
makestep 1.0 3
rtconutc
rtcsync
cmdport 0
noclientlog
'';
# ----------------------------------------------------------------------
# Service that writes the config and reloads Chrony
# ----------------------------------------------------------------------
chronyNtsService = {
description = "Write hardened Chrony config with NTS and reload";
serviceConfig = {
Type = "oneshot";
ExecStartPre = pkgs.writeShellScript "write-chrony-nts.sh" ''
set -euo pipefail
# Ensure directories exist
mkdir -p /var/lib/chrony /etc
# Atomically write the config
${pkgs.coreutils}/bin/install -m 0644 ${chronyConf} /etc/chrony.conf
# Reload (or restart if reload fails)
${pkgs.systemd}/bin/systemctl try-reload-or-restart chronyd
'';
};
wantedBy = [ "multi-user.target" ];
};
# ----------------------------------------------------------------------
# Timer that triggers the service
# ----------------------------------------------------------------------
chronyNtsTimerConfig = {
description = "Timer to keep hardened Chrony+NTS config applied";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = chronyNtsTimer;
Persistent = true; # run missed jobs after reboot
RandomizedDelaySec = "5m"; # jitter
};
};
in
{
# ----------------------------------------------------------------------
# 1. Enable Chrony (disable systemd-timesyncd)
# ----------------------------------------------------------------------
services.chrony.enable = true;
services.timesyncd.enable = false; # we’re using Chrony
# ----------------------------------------------------------------------
# 2. Install timer + service
# ----------------------------------------------------------------------
systemd.timers."chrony-nts" = chronyNtsTimerConfig;
systemd.services."chrony-nts" = chronyNtsService;
# ----------------------------------------------------------------------
# 3. Run once at boot (before the timer)
# ----------------------------------------------------------------------
systemd.services."chrony-nts-at-boot" = chronyNtsService // {
description = "Apply hardened Chrony+NTS config at boot";
after = [ "chronyd.service" ];
wantedBy = [ "multi-user.target" ];
};
}
✔️ Click to Expand Secure Boot Resources
hardened profile that applies a set of security-focused
kernel and system configurations.configuration.nix or equivalent to import hardened.nix and enable
profiles.hardened:# configuration.nix
{ pkgs, inputs, ... }: let
modulesPath = "${inputs.nixpkgs}/nixos/modules";
in {
imports = [ "${modulesPath}/profiles/hardened.nix" ];
}
There is a proposal to remove it completely that has gained ground, the following thread discusses why: Discourse Thread
PR #383438 Proposed removal PR.
Check hardened.nix to see exactly what adding it enables to avoid duplicates and conflicts moving on. I included this for completeness, the choice is yours if you want to use it or not.
# show the kernel release
uname -r
# show kernel version, hostname, and architecture
uname -a
zcat /proc/config.gz
# ...snip...
#
# Compression
#
CONFIG_CRYPTO_DEFLATE=m
CONFIG_CRYPTO_LZO=y
CONFIG_CRYPTO_842=m
CONFIG_CRYPTO_LZ4=m
CONFIG_CRYPTO_LZ4HC=m
CONFIG_CRYPTO_ZSTD=y
# end of Compression
# ...snip...
NOTE: Expect breakage when using the hardened kernel. linux-hardened
completely disables
unprivileged user namespaces, which
are required for Flatpak, chromium-based browsers, and more.
linuxPackages_latest_hardened attribute has been deprecated. If you want
to use a hardened kernel, it is now recommended to use linux_hardened, which
is aliased to linux_default.kernel.linux_hardened without specifying a version, such as:boot.kernelPackages = pkgs.linuxPackages_hardened;
linux_hardened is aliased to the linux_default.kernel..extend kernel attribute, they are only available to
kernel package sets (e.g., linuxPackages_hardened).json file to see the patch diffs.environment.systemPackages = [ pkgs.kernel-hardening-checker ];
sysctl is a tool that allows you to view or modify kernel settings and
enable/disable different features.sysctl parameters against the kernel-hardening-checker
recommendations:sudo sysctl -a > params.txt
kernel-hardening-checker -l /proc/cmdline -c /proc/config.gz -s ./params.txt
sudo sysctl -a | grep "kernel.kptr_restrict"
# Output:
kernel.kptr_restrict = 2
cat /sys/kernel/security/lsm
# Output:
File: /sys/kernel/security/lsm
capability,landlock,yama,bpf,apparmor
zcat /proc/config.gz | grep CONFIG_SECURITY_SELINUX
zcat /proc/config.gz | grep CONFIG_HARDENED_USERCOPY
zcat /proc/config.gz | grep CONFIG_STACKPROTECTOR
sudo sysctl -a > before.txt
sudo sysctl -a > after.txt
diff on them:diff before.txt after.txt
after.txt for future changes to avoid duplicates,
this seems easier to me than trying to parse through the patches.security = {
protectKernelImage = true;
lockKernelModules = false; # this breaks iptables, wireguard, and virtd
# force-enable the Page Table Isolation (PTI) Linux kernel feature
forcePageTableIsolation = true;
# User namespaces are required for sandboxing.
# this means you cannot set `"user.max_user_namespaces" = 0;` in sysctl
allowUserNamespaces = true;
# Disable unprivileged user namespaces, unless containers are enabled
unprivilegedUsernsClone = config.virtualisation.containers.enable;
allowSimultaneousMultithreading = true;
}
sysctl hardening settings further reinforce kernel-level protections. The
hardened kernel includes security patches and stricter defaults, but it doesn't
cover all runtime tunables. Refer to the above commands to get a diff of the
changes. boot.kernel.sysctl = {
"fs.suid_dumpable" = 0;
# prevent pointer leaks
"kernel.kptr_restrict" = 2;
# restrict kernel log to CAP_SYSLOG capability
"kernel.dmesg_restrict" = 1;
# Note: certian container runtimes or browser sandboxes might rely on the following
# restrict eBPF to the CAP_BPF capability
"kernel.unprivileged_bpf_disabled" = 1;
# should be enabled along with bpf above
# "net.core.bpf_jit_harden" = 2;
# restrict loading TTY line disciplines to the CAP_SYS_MODULE
"dev.tty.ldisk_autoload" = 0;
# prevent exploit of use-after-free flaws
"vm.unprivileged_userfaultfd" = 0;
# kexec is used to boot another kernel during runtime and can be abused
"kernel.kexec_load_disabled" = 1;
# Kernel self-protection
# SysRq exposes a lot of potentially dangerous debugging functionality to unprivileged users
# 4 makes it so a user can only use the secure attention key. A value of 0 would disable completely
"kernel.sysrq" = 4;
# disable unprivileged user namespaces, Note: Docker, NH, and other apps may need this
# "kernel.unprivileged_userns_clone" = 0; # commented out because it makes NH and other programs fail
# restrict all usage of performance events to the CAP_PERFMON capability
"kernel.perf_event_paranoid" = 3;
# Network
# protect against SYN flood attacks (denial of service attack)
"net.ipv4.tcp_syncookies" = 1;
# protection against TIME-WAIT assassination
"net.ipv4.tcp_rfc1337" = 1;
# enable source validation of packets received (prevents IP spoofing)
"net.ipv4.conf.default.rp_filter" = 1;
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.all.accept_redirects" = 0;
"net.ipv4.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.secure_redirects" = 0;
"net.ipv4.conf.default.secure_redirects" = 0;
# Protect against IP spoofing
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv6.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.send_redirects" = 0;
"net.ipv4.conf.default.send_redirects" = 0;
# prevent man-in-the-middle attacks
"net.ipv4.icmp_echo_ignore_all" = 1;
# ignore ICMP request, helps avoid Smurf attacks
"net.ipv4.conf.all.forwarding" = 0;
"net.ipv4.conf.default.accept_source_route" = 0;
"net.ipv4.conf.all.accept_source_route" = 0;
"net.ipv6.conf.all.accept_source_route" = 0;
"net.ipv6.conf.default.accept_source_route" = 0;
# Reverse path filtering causes the kernel to do source validation of
"net.ipv6.conf.all.forwarding" = 0;
"net.ipv6.conf.all.accept_ra" = 0;
"net.ipv6.conf.default.accept_ra" = 0;
## TCP hardening
# Prevent bogus ICMP errors from filling up logs.
"net.ipv4.icmp_ignore_bogus_error_responses" = 1;
# Disable TCP SACK
"net.ipv4.tcp_sack" = 0;
"net.ipv4.tcp_dsack" = 0;
"net.ipv4.tcp_fack" = 0;
# Userspace
# restrict usage of ptrace
"kernel.yama.ptrace_scope" = 2;
# ASLR memory protection (64-bit systems)
"vm.mmap_rnd_bits" = 32;
"vm.mmap_rnd_compat_bits" = 16;
# only permit symlinks to be followed when outside of a world-writable sticky directory
"fs.protected_symlinks" = 1;
"fs.protected_hardlinks" = 1;
# Prevent creating files in potentially attacker-controlled environments
"fs.protected_fifos" = 2;
"fs.protected_regular" = 2;
# Randomize memory
"kernel.randomize_va_space" = 2;
# Exec Shield (Stack protection)
"kernel.exec-shield" = 1;
## TCP optimization
# TCP Fast Open is a TCP extension that reduces network latency by packing
# data in the sender’s initial TCP SYN. Setting 3 = enable TCP Fast Open for
# both incoming and outgoing connections:
"net.ipv4.tcp_fastopen" = 3;
# Bufferbloat mitigations + slight improvement in throughput & latency
"net.ipv4.tcp_congestion_control" = "bbr";
"net.core.default_qdisc" = "cake";
};
❗ Note: The above settings are fairly aggressive and can break common programs, read the comment warnings.
boot.kernelParams can be used to set additional kernel command line arguments
at boot time. It can only be used for built-in modules.# boot.nix
boot.kernelParams = [
# make it harder to influence slab cache layout
"slab_nomerge"
# enables zeroing of memory during allocation and free time
# helps mitigate use-after-free vulnerabilaties
"init_on_alloc=1"
"init_on_free=1"
# randomizes page allocator freelist, improving security by
# making page allocations less predictable
"page_alloc.shuffel=1"
# enables Kernel Page Table Isolation, which mitigates Meltdown and
# prevents some KASLR bypasses
"pti=on"
# randomizes the kernel stack offset on each syscall
# making attacks that rely on a deterministic stack layout difficult
"randomize_kstack_offset=on"
# disables vsyscalls, they've been replaced with vDSO
"vsyscall=none"
# disables debugfs, which exposes sensitive info about the kernel
"debugfs=off"
# certain exploits cause an "oops", this makes the kernel panic if an "oops" occurs
"oops=panic"
# only alows kernel modules that have been signed with a valid key to be loaded
# making it harder to load malicious kernel modules
# can make VirtualBox or Nvidia drivers unusable
"module.sig_enforce=1"
# prevents user space code excalation
"lockdown=confidentiality"
# "rd.udev.log_level=3"
# "udev.log_priority=3"
];
udev. You can force a module to be loaded via boot.kernelModules. boot.blacklistedKernelModules = [
# Obscure networking protocols
"dccp" # Datagram Congestion Control Protocol
"sctp" # Stream Control Transmission Protocol
"rds" # Reliable Datagram Sockets
"tipc" # Transparent Inter-Process Communication
"n-hdlc" # High-level Data Link Control
"ax25" # Amateur X.25
"netrom" # NetRom
"x25" # X.25
"rose"
"decnet"
"econet"
"af_802154" # IEEE 802.15.4
"ipx" # Internetwork Packet Exchange
"appletalk"
"psnap" # SubnetworkAccess Protocol
"p8023" # Novell raw IEE 802.3
"p8022" # IEE 802.3
"can" # Controller Area Network
"atm"
# Various rare filesystems
"cramfs"
"freevxfs"
"jffs2"
"hfs"
"hfsplus"
"udf"
# "squashfs" # compressed read-only file system used for Live CDs
# "cifs" # cmb (Common Internet File System)
# "nfs" # Network File System
# "nfsv3"
# "nfsv4"
# "ksmbd" # SMB3 Kernel Server
# "gfs2" # Global File System 2
# vivid driver is only useful for testing purposes and has been the
# cause of privilege escalation vulnerabilities
# "vivid"
];
kernelParameters above, there are more suggestions in the guide, I
have used the above parameters along with the commented out ones and had no
issues.NOTE: There is a performance cost to enabling a hardened memory allocator, and some apps will not work without a workaround such as Firefox, Thunderbird, Torbrowser, LibreWolf, and ZenBrowser to name a few.
hardened_malloc is available for NixOS in two variants, add
either to your configuration.nix or equivalent to apply them:environment.memoryAllocator.provider = "graphene-hardened";: This is the
default configuration template that has all normal optional security features
enabled. It's aggressive, you can expect app breakage and a performance cost.
environment.memoryAllocator.provider = "graphene-hardened-light";: The
light template disables the slap quarantines, write after free check, slot
randomization and raises the guard slab interval from 1 to 8 but leaves
zero-on-free and slab canaries enabled. This version has solid performance
and is still far more secure than the standard allocator.
libhardened_malloc.so is typically installed to
/usr/local/lib/libhardened_malloc.so and referenced from /etc/ld.so.preload.systemd is the core "init system" and service manager that controls how
services, daemons, and basic system processes are started, stopped and
supervised on modern Linux distributions, including NixOS. It provides a suite
of basic building blocks for a Linux system as well as a system and service
manager that runs as PID 1 and starts the rest of the system.dbus-broker is generally considered more secure and robust but isn't the
default as of yet. To set dbus-broker as the default: users.groups.netdev = {};
services = {
dbus.implementation = "broker";
logrotate.enable = true;
journald = {
storage = "volatile"; # Store logs in memory
upload.enable = false; # Disable remote log upload (the default)
extraConfig = ''
SystemMaxUse=500M
SystemMaxFileSize=50M
'';
};
};
dbus-broker is more resilient to resource exhaustion attacks and integrates
better with Linux security features.
Setting storage = "volatile" tells journald to keep log data only in memory.
There is a tradeoff though, If you need long-term auditing or troubleshooting
after a reboot, this will not preserve system logs.
upload.enable is for forwarding log messages to remote servers, setting this
to false prevents accidental leaks of potentially sensitive or internal system
information.
Enabling logrotate prevents your disk from filling with excessive
legacy/service log files. These are the classic plain-text logs.
Systemd uses journald which stores logs in a binary format
systemd-analyze security
# or for a detailed view of individual services security posture
systemd-analyze security NetworkManager
sudo systemctl disable cups
sudo systemctl mask cups
# To unmask use unmask:
# sudo systemctl unmask cups
✔️ Click to Expand Systemd Resources
configuration.nix or equivalent:systemd.services = {
bluetooth.serviceConfig = {
ProtectKernelTunables = lib.mkDefault true;
ProtectKernelModules = lib.mkDefault true;
ProtectKernelLogs = lib.mkDefault true;
ProtectHostname = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
SystemCallFilter = [
"~@obsolete"
"~@cpu-emulation"
"~@swap"
"~@reboot"
"~@mount"
];
SystemCallArchitectures = "native";
};
}
serviceConfig attribute to
harden settings for systemd services.systemd-analyze security bluetooth
→ Overall exposure level for bluetooth.service: 3.3 OK 🙂
chkrootkit was removed as it is unmaintained and archived upstream.environment.systemPackages = [
pkgs.lynis
pkgs.clamav
pkgs.aide
];
✔️ Click to Expand AIDE Example
AIDE is an intrusion detection system (IDS) that will notify us whenever it detects that a potential intrusion has occurred. When a system is compromised, attackers typically will try to change file permissions and escalate to the root user account and start to modify system files, AIDE can detect this.
To set up AIDE on your system follow these steps:
- Create the
aide.conf:
sudo mkdir -p /var/lib/aide && cd /var/lib/aide/
sudo hx aide.conf
Add the following content to /var/lib/aide/aide.conf:
# aide.conf
# Example configuration file for AIDE.
@@define DBDIR /var/lib/aide
# The location of the database to be read.
database_in=file:@@{DBDIR}/aide.db.gz
# The location of the database to be written.
#database_out=sql:host:port:database:login_name:passwd:table
#database_out=file:aide.db.new
database_out=file:@@{DBDIR}/aide.db.new.gz
# Whether to gzip the output to database
gzip_dbout=yes
log_level=info
report_url=file:/var/log/aide/aide.log
report_url=stdout
#report_url=stderr
#NOT IMPLEMENTED report_url=mailto:root@foo.com
#NOT IMPLEMENTED report_url=syslog:LOG_AUTH
# These are the default rules.
#
#p: permissions
#i: inode:
#n: number of links
#u: user
#g: group
#s: size
#b: block count
#m: mtime
#a: atime
#c: ctime
#S: check for growing size
#md5: md5 checksum
#sha1: sha1 checksum
#rmd160: rmd160 checksum
#tiger: tiger checksum
#haval: haval checksum
#gost: gost checksum
#crc32: crc32 checksum
#R: p+i+n+u+g+s+m+c+md5
#L: p+i+n+u+g
#E: Empty group
#>: Growing logfile p+u+g+i+n+S
# You can create custom rules like this.
NORMAL = R+b+sha512
DIR = p+i+n+u+g
# Next decide what directories/files you want in the database.
/boot NORMAL
/bin NORMAL
/sbin NORMAL
/lib NORMAL
/opt NORMAL
/usr NORMAL
/root NORMAL
# Check only permissions, inode, user and group for /etc, but
# cover some important files closely.
/etc p+i+u+g
!/etc/mtab
/etc/exports NORMAL
/etc/fstab NORMAL
/etc/passwd NORMAL
/etc/group NORMAL
/etc/gshadow NORMAL
/etc/shadow NORMAL
/var/log p+n+u+g
# With AIDE's default verbosity level of 5, these would give lots of
# warnings upon tree traversal. It might change with future version.
#
#=/lost\+found DIR
#=/home DIR
Create the logfile:
sudo mkdir -p /var/log/aide
sudo touch /var/log/aide/aide.log
- Generate the initial database, this will store the checksums of all files
that it's configured to monitor. Take note of the location of the new
database, mine was
/etc/aide.db.new
sudo aide --config /var/lib/aide/aide.conf --init
- Move the new database and remove the
.new:
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
ls /var/lib/aide/
aide.conf aide.db.gz
- Check with AIDE:
sudo aide --check --config /var/lib/aide/aide.conf
Start timestamp: 2025-09-05 09:50:07 -0400 (AIDE 0.19.2)
AIDE found NO differences between database and filesystem. Looks okay!!
- Whenever you make changes to system files, or especially after running a system update or installing new tools, you have to rescan all files to update their checksums in the AIDE database:
sudo aide --update --config /var/lib/aide/aide.conf
Unfortunately, AIDE doesn't automatically replace the old database so you have to rename the new one again:
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
And finally check again:
sudo aide --check --config /var/lib/aide/aide.conf
✔️ Click to Expand clamav.nix Example
{pkgs, ...}: {
environment.systemPackages = with pkgs; [
clamav
];
services.clamav = {
# Enable clamd daemon
daemon.enable = true;
updater.enable = true;
updater.frequency = 12; # Number of database checks per day
scanner = {
enable = true;
# 4:00 AM
interval = "*-*-* 04:00:00";
scanDirectories = [
"/home"
"/var/lib"
"/tmp"
"/etc"
"/var/tmp"
];
};
};
}
sudo lynis show commands
# Output:
Commands:
lynis audit
lynis configure
lynis generate
lynis show
lynis update
lynis upload-only
sudo lynis audit system
# Output:
Lynis security scan details:
Hardening index : 79 [############### ]
Tests performed : 234
Plugins enabled : 0
Components:
- Firewall [V]
- Malware scanner [V]
Scan mode:
Normal [V] Forensics [ ] Integration [ ] Pentest [ ]
Lynis modules:
- Compliance status [?]
- Security audit [V]
- Vulnerability scan [V]
The "Lynis hardening index" is an overall impression on how well a system is hardened. However, this is just an indicator on measures taken - not a percentage of how safe a system might be. A score over 75 typically indicates a system with more than average safety measures implemented.
Lynis will give you more recommendations for securing your system as well.
clamscan, create the following log file:sudo touch /var/log/clamscan.log
clamav & aide:{pkgs, ...}: {
services.cron = {
enable = true;
# messages.enable = true;
systemCronJobs = [
# Every day at 2:00 AM, run clamscan as root and append output to a log file
"0 2 * * * root ${pkgs.clamav}/bin/clamscan -r /home >> /var/log/clamscan.log"
"0 11 * * * ${pkgs.aide}/bin/aide --check --config /var/lib/aide/aide.conf"
];
};
}
clamav manually with:# Recursive Scan:
sudo clamscan -r ~/home
❗ NOTE: You only need either the individual pkgs.clamav with the cron job
OR the clamd-daemon module. clamdscan is for software integration and
uses a different user that doesn't have permission to scan your files. You can
use clamdscan --fdpass /path/to/scan to pass the necessary file permissions.
NOTE: clamdscan runs in the background, you can watch it with top.
Security information: Changing SSH configuration settings can significantly impact the security of your system(s). It is crucial to have a solid understanding of what you are doing before making any adjustments. Avoid blindly copying and pasting examples, including those from this Wiki page, without conducting a thorough analysis. Failure to do so may compromise the security of your system(s) and lead to potential vulnerabilities. Take the time to comprehend the implications of your actions and ensure that any changes made are done thoughtfully and with care. --NixOS Wiki
❗ NOTE: Choose one, either ssh-agent or gpg-agent
ssh-keygen, this is recommended unless
you have a good reason for not using it.gpg-agent (which acts as your SSH agent). Complex, and
harder to understand in my opinion.programs.ssh.startAgent so I chose
gpg-agent personally.ssh-keygen is required.✔️ Click to Expand Resourses on OpenSSH
ed25519 algorithm is significantly faster and more secure when compared to
RSA. You can also specify the key derivation function (KDF) rounds to
strengthen protection even more.ssh-keygen -t ed25519 -a 32 -f ~/.ssh/id_ed25519_github_$(date +%Y-%m-%d) -C "SSH Key for MdBook"
-t is for type
-a 32 sets the number of KDF rounds. The standard is usually good enough,
adding extra rounds can make it harder to brute-force.
-f is for filename
{config, ...}: {
config = {
services = {
fail2ban = {
enable = true;
maxretry = 5;
bantime = "1h";
# ignoreIP = [
# "172.16.0.0/12"
# "192.168.0.0/16"
# "2601:881:8100:8de0:31e6:ac52:b5be:462a"
# "matrix.org"
# "app.element.io" # don't ratelimit matrix users
# ];
bantime-increment = {
enable = true; # Enable increment of bantime after each violation
multipliers = "1 2 4 8 16 32 64 128 256";
maxtime = "168h"; # Do not ban for more than 1 week
overalljails = true; # Calculate the bantime based on all the violations
};
};
openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitEmptyPasswords = false;
PermitTunnel = false;
UseDns = false;
KbdInteractiveAuthentication = false;
X11Forwarding = config.services.xserver.enable;
MaxAuthTries = 3;
MaxSessions = 2;
ClientAliveInterval = 300;
ClientAliveCountMax = 0;
AllowUsers = ["your-user"];
TCPKeepAlive = false;
AllowTcpForwarding = false;
AllowAgentForwarding = false;
LogLevel = "VERBOSE";
PermitRootLogin = "no";
KexAlgorithms = [
# Key Exchange Algorithms in priority order
"curve25519-sha256@libssh.org"
"ecdh-sha2-nistp521"
"ecdh-sha2-nistp384"
"ecdh-sha2-nistp256"
"diffie-hellman-group-exchange-sha256"
];
Ciphers = [
# stream cipher alternative to aes256, proven to be resilient
# Very fast on basically anything
"chacha20-poly1305@openssh.com"
# industry standard, fast if you have AES-NI hardware
"aes256-gcm@openssh.com"
"aes128-gcm@openssh.com"
"aes256-ctr"
"aes192-ctr"
"aes128-ctr"
];
Macs = [
# Combines the SHA-512 hash func with a secret key to create a MAC
"hmac-sha2-512-etm@openssh.com"
"hmac-sha2-256-etm@openssh.com"
"umac-128-etm@openssh.com"
"hmac-sha2-512"
"hmac-sha2-256"
"umac-128@openssh.com"
];
};
# These keys will be generated for you
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
};
};
}
services.openssh.enable = true;)Preventing brute-force attacks with Fail2Ban
Eliminating password authentication in favor of more secure SSH keys
Restricting user access and preventing root login
Disabling potentially risky forwarding features (tunnel, TCP, agent)
Enforce the use of strong, modern cryptographic algorithms for all SSH communications.
Enhanced logging for better auditing.
auditd) and define a very basic rule set,
you can use the following NixOS configuration. This example demonstrates how to
log every program execution (execve) on a 64-bit architecture.# modules/security/auditd-minimal.nix (or directly in configuration.nix)
{
# start as early in the boot process as possible
boot.kernelParams = ["audit=1"];
security.auditd.enable = true;
security.audit.enable = true;
security.audit.rules = [
# Log all program executions on 64-bit architecture
"-a exit,always -F arch=b64 -S execve"
];
}
audit=1 Enables auditing at the kernel level very early in the boot process.
Without this, some events could be missed.
security.auditd.enable = true; Ensures the auditd userspace daemon is
started.
While often enabled together, security.audit.enable specifically refers to
enabling the NixOS module for audit rules generation.
execve (program executions)
This is just a basic configuration, there is much more that can be tracked.
lsusb from the
usbutils package.lsusb
sudo usbguard list-devices
your-user to your username:# usbguard.nix
{
config,
pkgs,
lib,
...
}: let
inherit (lib) mkIf;
cfg = config.custom.security.usbguard;
in {
options.custom.security.usbguard = {
enable = lib.mkEnableOption "usbguard";
};
config = mkIf cfg.enable {
services.usbguard = {
enable = true;
IPCAllowedUsers = ["root" "your-user"];
# presentDevicePolicy refers to how to treat USB devices that are already connected when the daemon starts
presentDevicePolicy = "allow";
rules = ''
# allow `only` devices with mass storage interfaces (USB Mass Storage)
allow with-interface equals { 08:*:* }
# allow mice and keyboards
# allow with-interface equals { 03:*:* }
# Reject devices with suspicious combination of interfaces
reject with-interface all-of { 08:*:* 03:00:* }
reject with-interface all-of { 08:*:* 03:01:* }
reject with-interface all-of { 08:*:* e0:*:* }
reject with-interface all-of { 08:*:* 02:*:* }
'';
};
environment.systemPackages = [pkgs.usbguard];
};
}
allow rule is for devices with only mass storage interfaces
(08:*:*) i.e., USB Mass storage devices, devices like keyboards and mice
(which use interface class 03:*:*) implicitly not allowed.reject rules reject devices with a suspicious combination of interfaces. A
USB drive that implements a keyboard or a network interface is very suspicious,
these reject rules prevent that.presentDevicePolicy = "allow"; allows any device that is present at daemon
start up even if they're not explicitly allowed. However, newly plugged in
devices must match an allow rule or get denied implicitly.presentDevicePolicy should be one of: # one of "apply-policy"(default,
evaluate the rule set for every present device), "block", "reject", "keep"
(keep whatever state the device is currently in), or "allow", which is used in
the example.configuration.nix or equivalent:# configuration.nix
imports = [
./usbguard.nix
];
custom.security.usbguard.enable = true;
❗ If you are ever unsure about a setting that you want to harden and think that it could possibly break your system you can always use a specialisation reversing the action and choose it's generation at boot up. For example, to force-reverse the above settings you could:
# configuration.nix
specialisation.no-usbguard.configuration = {
services.usbguard.enable = lib.mkForce false;
};
- This is a situation where I recommend this, it's easy to lock yourself out of your keyboard, mouse, etc. when trying to configure this.
NOTE: I have moved to run0 for authentication which is included by default
with systemd. It's actually a symlink to the existing systemd-run tool. It
behaves like a secure sudo alternative: it spawns a transient service under
PID 1 for privilege escalation, without relying on SUID (set user ID)
binaries.
SUID = "Set User ID": When a binary has the SUID bit set, it runs with the privileges of the file's owner (often root). There is a long history of vulnerabilities with SUID binaries.
run0 configuration is explained in the
Hardening README
sudo with a smaller codebase and attack
surface, consider doas. Replace userName with your username:# doas.nix
{
lib,
config,
pkgs, # Add pkgs if you need to access user information
...
}: let
cfg = config.custom.security.doas;
in {
options.custom.security.doas = {
enable = lib.mkEnableOption "doas";
};
config = lib.mkIf cfg.enable {
# Disable sudo
security.sudo.enable = false;
# Enable and configure `doas`.
security.doas = {
enable = true;
extraRules = [
{
# Grant doas access specifically to your user
users = ["userName"]; # <--- Only give access to your user
# persist = true; # Convenient but less secure
# noPass = true; # Convenient but even less secure
keepEnv = true; # Often necessary
# Optional: You can also specify which commands they can run, e.g.:
# cmd = "ALL"; # Allows running all commands (default if not specified)
# cmd = "/run/current-system/sw/bin/nixos-rebuild"; # Only allow specific command
}
];
};
# Add an alias to the shell for backward-compat and convenience.
environment.shellAliases = {
sudo = "doas";
};
};
}
configuration.nix and enable/disable it
with the following:# configuration.nix
imports = [
./doas.nix
];
custom.security.doas.enable = true;
❗ NOTE: Many people opt for the less secure groups = ["wheel"]; in the
above configuration instead of users = ["userName"]; to give wider access,
the choice is yours.
❗️ Critics such as madaidan say that Firejail worsens security by acting as a privilege escalation hole. Firejail requires the executable to be setuid, meaning it runs with root privileges.This is risky because any vulnerability in Firejail can lead to privilege escalation. This combined with many convenience features and complicated command line flags leads to a large attack surface.
I haven't personally tried nix-bwrapper myself yet, but it's another sandboxing option that looks interesting. Bubblewrap is known for having a more minimal design and smaller attack surface.
- Also see: Flatpak section for another option for sandboxing.
nix-bubblewrap is another option.
❗ WARNING: Running untrusted code is never safe, sandboxing cannot change this. --Arch Wiki
# firejail.nix
{
pkgs,
lib,
...
}: {
programs.firejail = {
enable = true;
wrappedBinaries = {
# Sandbox a web browser
librewolf = {
executable = "${lib.getBin pkgs.librewolf}/bin/librewolf";
profile = "${pkgs.firejail}/etc/firejail/librewolf.profile";
};
# Sandbox a file manager
thunar = {
executable = "${lib.getBin pkgs.xfce.thunar}/bin/thunar";
profile = "${pkgs.firejail}/etc/firejail/thunar.profile";
};
# Sandbox a document viewer
zathura = {
executable = "${lib.getBin pkgs.zathura}/bin/zathura";
profile = "${pkgs.firejail}/etc/firejail/zathura.profile";
};
};
};
}
wrappedBinaries is a list of applications you want to run inside a sandbox.
Only the apps in the wrappedBinaries attribute set will be automatically
firejailed when launched the usual way.firejail <app>, or added to
wrappedBinaries if you want automatic sandboxing, just make sure the profile
exists./nix/store/, I
used Yazi to search for /firejail and followed it to firejail/etc, where the
profiles are.man firejail.❗️NOTE: You cannot effectively use Firejail with Flatpak apps because of how their sandboxing technologies operate.
Flatpak bundles runtimes and sandbox mechanisms that provide app isolation independently of the host system's AppArmor or SELinux infrastructure. This can improve security and containment for GUI applications running on NixOS despite the system lacking full native MAC coverage.
Flatpak apps benefit from sandboxing through bubblewrap, which isolate apps and restrict access to user/home and system resources.
services.flatpak.enable = true;
systemd.services.flatpak-repo = {
wantedBy = [ "multi-user.target" ];
path = [ pkgs.flatpak ];
script = ''
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
# Only apps that are verified
# flatpak remote-add --if-not-exists --subset=verified flathub-verified https://flathub.org/repo/flathub.flatpakrepo
'';
};
flatpak search <app>. Flatpak is best used for GUI apps, some
CLI apps can be installed through it but not all.There is also nix-flatpak, which enables you to manage your flatpaks declaratively.
Flatseal is GUI utility that enables you to review and modify permissions from your Flatpak apps. Many apps by default come with smart-card support, X11 & Wayland support, and more, disabling unnecessary permissions is recommended.
Warehouse provides a simple UI to control complex Flatpak options, no cmdline required.
nix-mineral (Community Project)✔️ Click to Expand section on `nix-mineral`
For users seeking a more comprehensive and opinionated approach to system
hardening beyond the built-in hardened profile, the community project
nix-mineral offers a declarative
NixOS module.
nix-mineral aims to apply a wide array of security configurations, focusing on
tweaking kernel parameters, system settings, and file permissions to reduce the
attack surface.
- Community Project Status:
nix-mineralis a community-maintained project and is not officially part of the Nixpkgs repository or NixOS documentation. Its development status is explicitly stated as "Alpha software," meaning it may introduce stability issues or unexpected behavior.
For detailed information on nix-mineral's capabilities and current status,
refer directly to its
GitHub repository.
STIGs are configuration standards developed by the Defense Information Systems Agency (DISA) to secure systems and software for the U.S. Department of Defense (DoD). They are considered a highly authoritative source for system hardening.There are recommendations for hardening all kinds of software in the Stig Viewer