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
- Impermanence
- Secure Boot
- Choosing your Kernel
- Kernel Security Settings
- Further Hardening with sysctl
- Hardening Boot Parameters
- Hardening Systemd
- Lynis and other tools
- Securing SSH
- Key generation
- Encrypted Secrets
- Auditd
- USB Port Protection
- Doas over sudo
- Firejail
- 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.
--Source
means 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.
❗ 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.
☝️ 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.
Containers and VMs are beyond the scope of this chapter but can also enhance security if configured correctly.
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.
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
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.
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
The linuxPackages_latest_hardened
attribute has been deprecated. If you want
to use a hardened kernel, you must specify a versioned package that is currently
supported.
You can find the latest available hardened kernel packages by searching pkgs/top-level/linux-kernels.nix
For example, to use the latest available 6.15
, you would configure it like
this:
boot.kernelPackages = pkgs.linux_6_15_hardened;
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.
❗ NOTE: Always check the
linux-kernels.nix
file for the latest available versions, as older kernels are regularly removed from Nixpkgs.
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.
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
.
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.
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
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
Installation:
environment.systemPackages = [
pkgs.lynis
pkgs.chkrootkit
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 chkrootkit
& clamav
:
{pkgs, ...}: {
services.cron = {
enable = true;
# messages.enable = true;
systemCronJobs = [
# Every Sunday at 2:10 AM, run chkrootkit as root, log output for review
"10 2 * * 0 root ${pkgs.chkrootkit}/bin/chkrootkit | logger -t chkrootkit"
# 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"
];
};
}
The above cron job will use chkrootkit
to automatically scan for known rootkit
signatures. It can detect hidden processes and network connections. To run
manually:
sudo chkrootkit
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 theclamd-daemon
module.clamdscan
is for software integration and uses a different user that doesn't have permission to scan your files. You can useclamdscan --fdpass /path/to/scan
to pass the necessary file permissions. NOTE:clamdscan
runs in the background, you can watch it withtop
.
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
orgpg-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 theauditd
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
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 ofusers = ["userName"];
to give wider access, the choice is yours.
Firejail
❗ 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.
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