SOURCE:https://vez.mrsk.me/linux-hardening.html
Last updated: 01/05/2022
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 secure, or that users can go a long time without worrying about security updates. Neither of these are even remotely true. New versions of Linux are released almost every week, often containing important security fixes among the many other changes. These releases typically don't make explicit mention of 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 for months at a time. Red Hat and similar "enterprise" distros have the same problem. 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 cherrypick security fixes in the name of stability does not work. Arch doesn't play the backporting game, instead opting to provide the newest stable kernels shortly after their 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 with what options. This BSD-borrowed concept makes interacting with the package system simple and transparent.
Now on to how I set things up.
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
/efi
/dev/sda2
/boot
/dev/sda3
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.
/
/var
/home
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 file-systems (/tmp and /dev/shm) can also be locked down with the same flags by adding the following:
noexec
nodev
/tmp
/dev/shm
# /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.
1G
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 transience 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.
/run
/run/user/$UID
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.
Using an HTTPS mirror with Pacman is especially important because it doesn't validate the package database files and it runs everything as root. HTTPS doesn't mitigate either of these problems, but it is one line of defense against a MITM attack. I hope the developers will make fixing these two security issues a priority for the project soon. Other package managers have been doing it the right way for a long time.
The linux-hardened kernel package in Arch includes some compile-time security improvements that can't be set at runtime. If your distribution doesn't have a package for it, applying the patchset to upstream sources and building your own kernel is pretty easy. If you go that route, have a look at the kconfig-hardened-check script for more compile-time settings to consider.
The main problem with linux-hardened is that it's consistently out of date. Upstream Linux development moves quickly, so out-of-tree patches will always require extra work to maintain. Why the (relatively small) patches aren't upstreamed is unknown to me. Sadly, the linux-hardened patchset frequently lags weeks or months behind the latest upstream kernel, thus missing out on many important security fixes. The prospective user is then faced with the choice of using a more secure kernel with known vulnerabilities or a less secure kernel with fewer known vulnerabilities. Not a great situation.
It's a shame that pretty much every distro ships a horribly configured kernel in the name of maintaining the status quo, but there are some ways to make things better without recompiling.
Runtime configuration of the kernel 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 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
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. Finally, we enable all mitigations (including those against userspace) for Spectre V2 and enable the opt-in mechanism to flush the L1D cache on context switch.
mitigations=auto
apparmor=1 lsm=lockdown,yama,apparmor lockdown=XXX
These enable the 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.
integrity
confidentiality
XXX
For what it's worth, running in confidentiality mode on my desktop hasn't caused any problems. Your mileage and use case may vary. Lock-down 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 fill newly allocated pages and heap objects with zeroes, fill freed pages and heap objects with zeroes, 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.
slub_debug=F
This enables sanity checks in the SLUB allocator. Two other flags to consider for non-hardened kernels are Z (redzoning, to detect when a slab is overwritten past its real size) and P (to enable poisoning on slab cache allocations).
Z
P
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 lockdown=confidentiality lsm=lockdown,yama,apparmor page_alloc.shuffle=1 slab_nomerge slub_debug=F 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
Yet more runtime options of the kernel 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:
.conf
/etc/sysctl.d
# /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 # 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 kernel.unprivileged_userns_clone=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=1 # 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.
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.)
/etc/ufw/sysctl.conf
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 how PF does it the other way around. 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.
Many people use sudo, 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.
pacman -Syu
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 save the sudo password being typed for later exploitation.
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.
firejail
/usr/local/bin
PATH
~/.bashrc
/etc/profile
[...] 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.
firecfg
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.
~/.config/firejail
# /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.
--list
$ 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 buiding 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.
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.
all
bluetooth
wlan
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.
servers
server
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.
This section is for random tidbits that didn't really fit into the other parts.
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:
umask 77
# chmod -R go-rwx /home/*
Never change root's umask value. Doing so will cause all sorts of problems.
I also like putting my user's ~/.cache directory in tmpfs to reduce disk writes.
~/.cache
# /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.
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.
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 more 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.
The following non-security-related sysctl settings have been useful in my experience:
sysctl
# /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.
60
10
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.