by @blakkheim
Last updated: 05/19/2026
This page lists the changes I make to a vanilla install of Arch Linux for security hardening, as well as some other changes I find useful. Most of the changes will work on any Linux distro that's reasonably up to date. It's not a one-size-fits-all setup, but hopefully certain pieces will be useful to anyone wanting a more secure Linux system. From a security perspective, Arch is worth considering for a few reasons: The install size: The base install is relatively minimal compared to a "prebuilt" distro like Fedora or Mint. This lets me focus on adding just what I want, rather than constantly trying to strip out things I don't need and disable background services I don't want running. The kernel: A common misconception about the Linux kernel is that it's inherently secure, or that users can go a long time without worrying about security updates. Neither of these is even remotely true. New versions of Linux are released almost every week, often containing important security fixes among the other changes. These releases don't mention which commits have security implications. As a result, many "stable" or "LTS" distributions don't know which commits should be backported to their old kernels, or even that something needs backporting at all. If the problem has a CVE assigned to it, maybe your distribution will pick it up. Maybe not. Even if a CVE exists, at least in the case of Ubuntu and Debian especially, users are often left with kernels full of known holes (maybe even over 1,000 of them) for months at a time or indefinitely. Red Hat and similar "enterprise" distros have the same problem and have been called out publicly about it on more than one occasion. Moreover, the Linux kernel security team doesn't request CVEs for any vulnerabilities, partly because there are just too many to track. Downstream's culture of trying to cherry-pick security fixes in the name of stability does not work. The reality of the situation is that every Linux user should be getting a new kernel and rebooting for security updates every 10 days or so. Arch doesn't play the backporting game, instead opting to provide the newest stable kernels shortly after their upstream release.
Security Hardening
To start, consider using full disk encryption along with a Logical Volume Manager setup. Disk encryption protects data at rest, while LVM allows for some flexibility that can be quite useful. A simple disk layout might look like this:
/dev/sda1 (a small, unencrypted
EFI System Partition,
FAT32)
mounted at /efi (assuming this is
a PC with UEFI, otherwise not needed)
/dev/sda2 (a small, unencrypted ext4 partition)
mounted at /boot.
/dev/sda3 (using the rest of the drive space)
as the encrypted
LUKS container
for LVM
/,
/var, and /home in the install. For a typical
desktop, you probably want to give /home most of the disk space.
The other two don't need much unless there's a specific use case in mind.
25GB and 8GB are used in this example. If you need to have a huge database
in /var or something, make adjustments accordingly.
There are a lot of user-writable directories in Linux, each one providing
an opportunity for attackers to execute their own binaries.
Once the fstab
file is created, add the noexec and nodev flags
to /var and /home. Doing so will disallow execution
of binaries on these mount points, as well as prevent interpreting character
or block special devices on them. Two temporary filesystems (/tmp
and /dev/shm) can also be locked down with the same flags by
adding the following:
# /etc/fstab [...] tmpfs /tmp tmpfs rw,noexec,nodev,size=1G,mode=1777 0 0 tmpfs /dev/shm tmpfs rw,noexec,nodev,size=1G 0 0Adjust the
1G size limit value as desired.
Once booted into the finished installation, it should look something like this:
# lvs LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert home lvm -wi-ao---- 189.12g root lvm -wi-ao---- 25.00g var lvm -wi-ao---- 8.00g # mount | egrep '(lvm|/tmp|shm)' | sort /dev/mapper/lvm-home on /home type ext4 (rw,nodev,noexec,relatime) /dev/mapper/lvm-root on / type ext4 (rw,relatime) /dev/mapper/lvm-var on /var type ext4 (rw,nodev,noexec,relatime) tmpfs on /dev/shm type tmpfs (rw,nodev,noexec,size=1048576k) tmpfs on /tmp type tmpfs (rw,nodev,noexec,relatime,size=1048576k)Another user-writable directory to consider is
/run, specifically
the /run/user/$UID directories that systemd spawns when someone
logs in, but their transient nature is
annoying
and complicated.
I have yet to find the perfect solution there that won't break other things.
FUSE is another way
for non-root users to create new mount points and execute binaries. If FUSE
functionality isn't needed, the kernel module can be
blacklisted.
In general it's a good idea to blacklist kernel modules for obscure protocols
that you don't actually use. An even better approach (if your workload allows
for it) would be to look at the kernel.modules_disabled sysctl
and set it to 1 after you're up and running with everything you need. Be sure
to understand the implications first though.
# /etc/pacman.d/mirrorlist Server = https://example.com/[...]/$repo/os/$archCheck the mirrorlist generator to see a list of TLS-capable servers near you.
Linux kernel development moves quickly, so out-of-tree patches will always require extra work to maintain. For reasons unknown to me, no attempt ever seems to be made to upstream these hundred or so local patches. It's just an endless loop of trying to backport them and falling way behind. The "hardened" kernel is simply not worth using in its current state.
This is particularly unfortunate because a package like linux-hardened is desperately needed in the Linux ecosystem. Pretty much every distro ships a horribly configured kernel in the name of maintaining the status quo. Sometimes they'll make progress in the right direction... but then cave in to demands about performance or breaking some obscure backward compatibility. Pair that with the fact that most distros also ship old kernels full of known security holes, and you'll see just how bleak the situation is. It's a shame, but there are a few ways to make your kernel a little safer without going rogue and recompiling.
Runtime configuration can be done in a number of ways. Desired flags may be passed on startup in the form of kernel parameters, of which there is an extensive list. Parameters are usually passed by the bootloader, so configuration details vary depending on whether the system uses GRUB, systemd-boot, or something else.
The following options, split up into categories, are worth considering for security improvements:
l1tf=full,force spec_store_bypass_disable=on spectre_v2=on l1d_flush=on gather_data_sampling=force spec_rstack_overflow=ibpbThese are additional mitigations for certain CPU security flaws. While the
mitigations=auto option is used by default in upstream
Linux, some of the mitigations it enables have been "toned down" for
performance reasons.
Examples of this include the
L1TF
and
Microarchitectural Data Sampling
vulnerabilities, which can't be fully mitigated unless HyperThreading
is disabled.
The
Speculative Store Bypass
vulnerability is only partially mitigated by default, with applications being
allowed to opt-in for protections via prctl or seccomp.
We also enable all mitigations (including those against userspace) for
Spectre V2,
enable the opt-in mechanism to
flush the L1D cache
on context switch,
and enable full mitigations for the
Gather Data Sampling
and
Speculative Return Stack Overflow
vulnerabilities on affected hardware.
apparmor=1 lsm=landlock,lockdown,yama,apparmor lockdown=XXXThese enable the Landlock, AppArmor, Yama, and Lockdown features, with the lockdown mode left for the reader to choose. Valid options are
integrity and confidentiality,
both described briefly
here.
Replace XXX with
whichever you see fit, or omit this option entirely if the feature isn't
wanted.
For what it's worth, running in confidentiality mode on my
desktop hasn't caused any problems. Your mileage and use case may vary.
Lockdown can break suspend-to-disk and any out-of-tree kernel modules
like ZFS, as well as
DKMS modules.
init_on_alloc=1 init_on_free=1 page_alloc.shuffle=1 slab_nomerge vsyscall=noneThis group will instruct the kernel to zero newly allocated pages and heap objects, zero freed pages and heap objects, tell the page allocator to randomize its free lists, disable merging of slabs with similar size, and disable vsyscalls due to their history of making exploits easier.
randomize_kstack_offset=1This provides kernel stack randomization, making some memory corruption attacks more difficult.
The full list of kernel parameters to be used must be specified on a single line, separated by spaces, in the bootloader's config file. An example for GRUB might look like this:
# /etc/default/grub [...] GRUB_CMDLINE_LINUX_DEFAULT="apparmor=1 init_on_alloc=1 init_on_free=1 l1tf=full,force l1d_flush=on gather_data_sampling=force spec_rstack_overflow=ibpb lockdown=confidentiality lsm=landlock,lockdown,yama,apparmor page_alloc.shuffle=1 slab_nomerge spec_store_bypass_disable=on spectre_v2=on vsyscall=none randomize_kstack_offset=1" [...]Depending on the bootloader in use, the file may need to be regenerated after any edits are made.
Changes to the kernel parameters won't take effect until after a reboot. To verify they were applied, run:
$ cat /proc/cmdline
More kernel runtime options can be configured through the
sysctl utility.
The values specified by any .conf files in the
/etc/sysctl.d directory will be loaded during the boot sequence.
Here are some sysctl settings to consider:
# /etc/sysctl.d/99-sysctl.conf # prevent the automatic loading of line disciplines # https://lore.kernel.org/patchwork/patch/1034150 dev.tty.ldisc_autoload=0 # additional protections for fifos, hardlinks, regular files, and symlinks # https://patchwork.kernel.org/patch/10244781 # slightly tightened up from the systemd default values of "1" for each fs.protected_fifos=2 fs.protected_hardlinks=1 fs.protected_regular=2 fs.protected_symlinks=1 # prevent unprivileged users from viewing the dmesg buffer kernel.dmesg_restrict=1 # prevents processes from creating new io_uring instances # https://security.googleblog.com/2023/06/learnings-from-kctf-vrps-42-linux.html kernel.io_uring_disabled=2 # disable the kexec system call (can be used to replace the running kernel) # https://lwn.net/Articles/580269 kernel.kexec_load_disabled=1 # impose restrictions on exposing kernel pointers # https://lwn.net/Articles/420403 kernel.kptr_restrict=2 # restrict use of the performance events system by unprivileged users # https://lwn.net/Articles/696216 kernel.perf_event_paranoid=3 # disable the "magic sysrq key" functionality # https://security.stackexchange.com/questions/138658 # https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1861238 # uncomment if the use of this feature is not needed #kernel.sysrq=0 # harden the BPF JIT compiler and restrict unprivileged use of BPF # https://www.zerodayinitiative.com/advisories/ZDI-20-350 # https://lwn.net/Articles/660331 net.core.bpf_jit_harden=2 kernel.unprivileged_bpf_disabled=1 # disable unprivileged user namespaces # https://lwn.net/Articles/673597 # (these two values are redundant, but not all kernels support the first one) kernel.unprivileged_userns_clone=0 user.max_user_namespaces=0 # enable yama ptrace restrictions # https://www.kernel.org/doc/Documentation/security/Yama.txt # set to "3" if the use of ptrace is not needed kernel.yama.ptrace_scope=2 # reverse path filtering to prevent some ip spoofing attacks # (default in some distributions) net.ipv4.conf.all.rp_filter=1 net.ipv4.conf.default.rp_filter=1 # disable icmp redirects and RFC1620 shared media redirects net.ipv4.conf.all.accept_redirects=0 net.ipv4.conf.all.secure_redirects=0 net.ipv4.conf.all.send_redirects=0 net.ipv4.conf.all.shared_media=0 net.ipv4.conf.default.accept_redirects=0 net.ipv4.conf.default.secure_redirects=0 net.ipv4.conf.default.send_redirects=0 net.ipv4.conf.default.shared_media=0 net.ipv6.conf.all.accept_redirects=0 net.ipv6.conf.default.accept_redirects=0 # disallow source-routed packets net.ipv4.conf.all.accept_source_route=0 net.ipv4.conf.default.accept_source_route=0 net.ipv6.conf.all.accept_source_route=0 net.ipv6.conf.default.accept_source_route=0 # ignore pings sent to a broadcast address (common for smurf attacks) net.ipv4.icmp_echo_ignore_broadcasts=1 # ignore bogus icmp error responses net.ipv4.icmp_ignore_bogus_error_responses=1 # protect against time-wait assassination hazards in tcp # https://tools.ietf.org/html/rfc1337 net.ipv4.tcp_rfc1337=1 # selective tcp acks have resulted in remotely exploitable crashes # https://lwn.net/Articles/791409 # uncomment to potentially guard against future attacks # (may introduce a performance hit in highly congested networks) #net.ipv4.tcp_sack=0 #net.ipv4.tcp_dsack=0 # disable tcp timestamps to avoid leaking some system information # https://www.whonix.org/wiki/Disable_TCP_and_ICMP_Timestamps net.ipv4.tcp_timestamps=0 # increase aslr effectiveness for mmap # https://lwn.net/Articles/667790 vm.mmap_rnd_bits=32 vm.mmap_rnd_compat_bits=16 # ignore icmp echo requests # uncomment if this system doesn't need to respond to pings #net.ipv4.icmp_echo_ignore_all=1 # disable creation of ipv6 addresses on network interfaces # uncomment (or set the ipv6.disable=1 kernel parameter) if ipv6 is not in use #net.ipv6.conf.all.disable_ipv6=1 #net.ipv6.conf.default.disable_ipv6=1 #net.ipv6.conf.lo.disable_ipv6=1The extensive kernel documentation has even more information about each of these options.
To apply the new configuration to a running system, run:
# sysctl -p /etc/sysctl.d/99-sysctl.confChanges will also be picked up automatically on the next reboot.
# pacman -S ufw # sed -i -e 's/^\([^#].*\)/# \1/g' /etc/ufw/sysctl.conf # ufw deny in # ufw allow out # systemctl enable ufw # ufw enableThis would create a basic firewall ruleset that blocks incoming connections and allows outgoing ones. If that's what you want, you're done. (One annoying part about UFW is the
/etc/ufw/sysctl.conf file
that comes with it. This file will override certain values in the main
sysctl configuration, so I comment out everything there.)
If you're never going to actually read the logs UFW creates, might as well turn that feature off.
# ufw logging off
Another option to consider is a stricter policy for outgoing traffic: one that only permits connections on ports you actually use, and only to hosts that you want to allow. Here's an example.
# ufw deny in # ufw allow out proto tcp to any port 22 # ufw allow out proto tcp to any port 443 # ufw allow out proto udp to 192.168.1.1 port 123 # ufw allow out proto udp to 192.168.1.1 port 53 # ufw reject outThis would allow outgoing SSH and HTTPS connections to any host, allow outgoing DNS and NTP to a local server, and block all other outgoing traffic. Logging might be more useful in that case to detect misbehaving programs.
Make sure the ordering of your ruleset is correct and exactly what you want. Despite being inspired by OpenBSD's PF syntax, UFW uses a "first match wins" system rather than PF's "last match wins" approach. In other words, if you block all connections in rule 1 and allow a specific connection in rule 2, it will still be blocked by the first one.
My recommended sudo setup allows a regular user to do some administrative tasks as root without typing a password. These tasks, in my case, include updating packages and rebooting. Once my machine is set up the way I like it, that's really all I ever need to do as root.
# /etc/sudoers Defaults env_reset Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" Defaults umask=0022 Defaults umask_override root ALL=(ALL:ALL) ALL Cmnd_Alias PACMAN = /usr/bin/pacman -Syu Cmnd_Alias REBOOT = /sbin/reboot "" Cmnd_Alias SHUTDOWN = /sbin/poweroff "" bh ALL=(root) NOPASSWD: PACMAN bh ALL=(root) NOPASSWD: REBOOT bh ALL=(root) NOPASSWD: SHUTDOWNSuch a setup would allow my user to run
pacman -Syu as root
(but not any other pacman commands) as well as allow me to reboot and
shut down the computer.
For any other administrative tasks, I would log in as root on a virtual console (ctrl + alt + f2) and do them there. This is slightly inconvenient, but the danger of X11 keylogging is real. Don't believe me? Here's a tiny keylogger that can capture the sudo password as it is being typed.
Firejail is the tool I like the most for this task. It's easy to set up, provides reasonable defaults, and can be further hardened with straightforward config files. To install it on Arch, issue the following:
# pacman -S firejail
# systemctl enable --now apparmor
# apparmor_parser -r /etc/apparmor.d/firejail-default
# firecfg
# echo XXX > /etc/firejail/firejail.users
Replace XXX with your regular,
non-root username.
Once that's in place, firejail will create symbolic links to
any installed applications for which it has a profile. These are placed in
the /usr/local/bin directory, meaning that your PATH
environment variable should point there first. This can be achieved by
exporting the variable in the user's shell rc file (~/.bashrc
or similar) or in /etc/profile for all users.
[...] export PATH=/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbinConsider running
firecfg after you've got all the programs
installed that you'll need. Otherwise, it should be run again after any new
applications are installed in case Firejail has a profile for them.
Additional capability restrictions can be put in place via custom profiles
in the user's ~/.config/firejail directory. If you wanted to
disallow the Clementine music player from overwriting tags in your music
collection, the following example might work.
# /home/user/.config/firejail/clementine.profile
whitelist ${HOME}/music
read-only ${HOME}/music
include /etc/firejail/clementine.profile
To test if it's working, use the --list flag while the sandboxed
application is running.
$ firejail --list 14609:bh::/usr/bin/firejail /sbin/clementine 17755:bh::/usr/bin/firejail /sbin/firefoxMore information and examples can be found in the building custom profiles section of the documentation. Also check out the X11 guide for some tips on further isolating GUI applications.
Bubblewrap is another sandboxing option to consider.
$ rfkill list ID TYPE DEVICE SOFT HARD 0 bluetooth hci0 blocked unblocked 1 wlan phy0 blocked unblockedBlocking either or both at startup can be done by enabling the relevant service:
# systemctl enable --now rfkill-block@all.serviceReplace
all with bluetooth or wlan as desired.
There are a variety of options available. The base Arch install includes (and any systemd-based distribution will include) systemd-timesyncd, but I'm not a big fan of it. If the thought of installing another package for a task that can technically be done with what you already have sends a shiver down your spine, avert your eyes now and use that one. I recommend OpenNTPD instead. It's lightweight and has an excellent security track record.
# pacman -S openntpd # systemctl disable --now systemd-timesyncd # systemctl enable openntpdIt will use the ntp.org pool by default for time synchronization. That's "fine," but the ntp.org pool includes a lot of low-quality servers. Some of them are run in virtual machines. Some of them don't work anymore and were never removed. The round-robin DNS might even give you a server that's 100ms or more away from your actual location. Taking that into consideration, I'd recommend using some known-good servers in the config file as well.
server time.apple.com server time.cloudflare.com server pool.ntp.org constraint from "https://example.com"If having company names in the config file scares you for some reason, there are plenty of other options. I only suggest the Apple and Cloudflare pools because they have high-quality nodes throughout the world and aren't likely to disappear any time soon.
The servers keyword instructs OpenNTPD to use multiple IPs from
the domain, while server means it will only use the first one.
It would use one from each pool in this case.
The Rust-based ntpd-rs package is another option to consider.
# /home/user/.config/pulse/daemon.conf avoid-resampling = true flat-volumes = noDoing so will prevent the audio quality from being needlessly degraded. It also prevents some frustrating issues with volume control.
Other options to consider can be found in the audiophile-linux repository.
umask 77 somewhere in the user's shell
rc file (~/.bashrc or similar) and run:
# chmod -R go-rwx /home/*Never change root's umask value. Doing so will cause all sorts of problems.
~/.cache directory in tmpfs to
reduce disk writes.
# /etc/fstab [...] tmpfs /home/bh/.cache tmpfs rw,size=250M,noexec,noatime,nodev,uid=bh,gid=bh,mode=700 0 0Everything that goes there is junk anyway.
sysctl settings have been
useful in my experience:
# /etc/sysctl.d/98-misc.conf net.ipv4.tcp_congestion_control=bbr vm.swappiness=10Linux supports multiple TCP congestion control algorithms. The BBR algorithm gives more consistent network throughput than the default in my experience, especially for transatlantic file transfers. It may help a lot, or it may not make a difference at all, depending on the use case.
The swappiness
value controls how aggressively the kernel will swap out memory pages to disk.
The default value of 60 is way too high for me, so I turn it down
to 10 to prevent so much swapping.
# /etc/systemd/journald.conf.d/99-limit.conf [Journal] Compress=yes SystemMaxUse=100MAdjust the size to whatever you think is reasonable.
If you've read this far and want to chat with some other Linux users, consider joining IRC: #baot on irc.rizon.net (SSL required)