Linux Security Hardening and Other Tweaks

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.

  • The Arch Build System: Having enjoyed the ports system of FreeBSD and OpenBSD for a long time, the ABS has been a pleasure to use. It makes building/rebuilding packages easy. It makes updating packages easy. It shows how things are actually built and what options are used. This BSD-borrowed concept makes interacting with the package system simple and transparent. More importantly, if a package is outdated, it's very easy to jump ahead of the maintainer and update it locally for yourself.
  • Now on to how I set things up.


    Security Hardening

    Other Tweaks


    Disk Layout

    This section contains a few tips to consider during your initial disk layout creation. The concepts should apply to any distribution.

    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
  • Splitting up the logical volumes for different mount points provides some benefits, including the ability to set mount flags on specific directories. Consider creating separate logical volumes for /, /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 0
    
    Adjust 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.


    Pacman

    Package managers usually don't need much additional configuration. Pacman, the one Arch uses, is no different. My recommendation for any package manager is simply to make sure that only HTTPS mirrors are used.
    # /etc/pacman.d/mirrorlist
    
    Server = https://example.com/[...]/$repo/os/$arch
    
    Check the mirrorlist generator to see a list of TLS-capable servers near you.


    Kernel Options

    The linux-hardened kernel package in Arch includes some compile-time security improvements that can't be set at runtime. The main problem with linux-hardened is that it's consistently out of date. Both the patchset and the resulting binary package frequently lag weeks or months behind the latest upstream kernel, thus missing out on a lot of very important security fixes.

    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=ibpb
    
    These 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=XXX
    
    These 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=none
    
    This 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=1
    
    This 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=1
    
    The 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.conf
    
    Changes will also be picked up automatically on the next reboot.


    Firewall

    Most systems should have some kind of firewall in place. There are a number of choices for this task on Linux. The simplest frontend I've found is called UFW. It uses an OpenBSD PF-like syntax and only takes a minute to get going.
    # pacman -S ufw
    # sed -i -e 's/^\([^#].*\)/# \1/g' /etc/ufw/sysctl.conf
    # ufw deny in
    # ufw allow out
    # systemctl enable ufw
    # ufw enable
    
    This 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 out
    
    This 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.


    Sudo

    Many people use sudo (or sudo-rs) but few use it safely. Some distributions configure it in a way that allows regular users to become root by simply typing their own password. My concern with any usage of sudo that involves typing a password to elevate privileges stems from the fact that X11 allows any application to capture keystrokes.

    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: SHUTDOWN
    
    Such 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.


    Application Sandboxing

    If a bug in a program allows the process to be compromised, an attacker can essentially do anything that program can do: connect to the internet, read files, write files, and so on. Sandboxing is a way to limit the potential damage a compromised process can do.

    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/sbin
    
    Consider 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/firefox
    
    More 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 (Disable WiFi / Bluetooth)

    If your hardware has wireless or Bluetooth capabilities and you want to disable them in software, rfkill can do that. It's included with the util-linux package.
    $ rfkill list
    ID TYPE      DEVICE    SOFT      HARD
     0 bluetooth hci0   blocked unblocked
     1 wlan      phy0   blocked unblocked
    
    Blocking either or both at startup can be done by enabling the relevant service:
    # systemctl enable --now rfkill-block@all.service
    
    Replace all with bluetooth or wlan as desired.


    NTP (Network Time Protocol)

    As the clocks on most computers have a tendency to drift over time, it's a good idea to run some kind of NTP client.

    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 openntpd
    
    It 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.


    PulseAudio

    My only recommendation for PulseAudio (other than to avoid using it) is to enable two options in the config file:
    # /home/user/.config/pulse/daemon.conf
    
    avoid-resampling = true
    flat-volumes = no
    
    Doing 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.


    Miscellaneous

    This section is for random tidbits that didn't really fit into the other parts.

    Umask / Directory Permissions

    I run my user with umask 77 by default, meaning that any newly created files will be unreadable by other users. My home directory is also mode 700. If a process running as another non-root user is compromised, it would be unable to read my files. To accomplish this, put 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.

    More tmpfs

    I also like putting my user's ~/.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 0
    
    Everything that goes there is junk anyway.

    Hidden PIDs

    It's possible to hide non-root users' processes from each other. This is mainly useful on a multiuser system, but it might be worth doing on a regular desktop computer too. The less information an adversary can get about your setup, the better.

    DNSCrypt

    Outgoing DNS lookups can be encrypted with dnscrypt-proxy. There are other options like DNS over HTTPS and DNS over TLS, but I like the DNSCrypt protocol because it doesn't rely on the certificate authority model. dnscrypt-proxy (or unbound) can also be used to filter out some ads and malware through blacklists.

    More sysctl

    The following non-security-related sysctl settings have been useful in my experience:
    # /etc/sysctl.d/98-misc.conf
    
    net.ipv4.tcp_congestion_control=bbr
    vm.swappiness=10
    
    Linux 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.

    Journal Size

    The systemd log can get pretty huge if you don't place any limit on it. Compression can also be enabled so that more information will fit in the smaller file.
    # /etc/systemd/journald.conf.d/99-limit.conf
    
    [Journal]
    Compress=yes
    SystemMaxUse=100M
    
    Adjust 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)