Hardened Linux Installation

11-19-2021

Linux is not a secure operating system. Significant hardening is required to obtain respectable security, and even then the model is fundamentally flawed. The kernel is written in primarily memory-unsafe languages, contains massive amounts of attack surface, and security is not a priority for the developers.

However, that doesn't mean we can't do our best to improve these issues. In this post I'm going to take you through a hardened Linux installation, which is far from trivial. I'm using Artix Linux for the following reasons:

  • I wanted a distro without systemd. systemd doesn't really belong in a security-focused install. It contains massive attack surface, has a history of ignoring major security issues, and is far from minimalistic.
  • I wanted access to the linux-hardened kernel. It provides a lot of hardening patches and removes tons of attack surface.
  • I wanted access to the AUR and the pacman package manager, because I'm very comfortable with them and the repositories are huge.
  • Artix Linux, even more so than Arch Linux, is very minimalistic and lightweight.
  • I wanted a rolling release model. Although the later kernel versions have a lot more attack surface, I'd rather get bugfixes right away rather than waiting weeks or months while a known vulnerability is out in the wild.

Before we proceed, I want to make sure to clarify that this tutorial only provides a starting point and by no means guarantees a secure system. Missing elements include but are not limited to:

  • Secure boot: I might look into this in a future tutorial! However, secure boot is notoriously difficult on Linux. This leaves the system vulnerable to attacks like Evil Maid.
  • musl libc: musl is more minimal, strict, and secure than glibc. However, using musl requires jumping through a lot of hoops at this time.
  • Control Flow Integrity (CFI): compiling the kernel with CFI is a lot of work. However, without it we're left vulnerable to ROP and JOP chains.
  • Strong sandboxing and Mandatory Access Control (MAC): We will install AppArmor, but in and of itself that doesn't help much without a lot of additional configuration. I'll likely write in the future about using bubblewrap and apparmor together to provide strong sandboxing and fine-grained permissions for individual applications.
  • grsecurity patches: These address several notable security flaws in the kernel; however they are only available commercially.
  • Hardened memory allocator: The hardened_malloc package readily provides this functionality; however, in my experience it breaks a lot of applications, for example wlroots and dhcpcd.

Without further ado, let's get started! Grab yourself an ISO and hop into a priveleged bash shell:

$ bash
$ sudo su

Next step is partitioning the disks. For this setup, I wanted three partitions:

  • 256M EFI partition: For the bootloader, kernel, and initrd.
  • 20G root partition: Will be LUKS with underlying LVM for fine-grained control over partitions with the fstab.
  • The rest of the disk: Will contain individual encrypted home directories we can mount at /home.

Use whatever partition manager you want to create the partitions. I used fdisk with a GPT label.

Next, I'm going to encrypt the second partition, which will be at /dev/sda2.

$ cryptsetup -y -v --use-random luksFormat /dev/sda2
$ cryptsetup luksOpen /dev/sda2 crypt

Now we can create our logical volumes:

$ pvcreate /dev/mapper/crypt
$ vgcreate vg /dev/mapper/crypt
$ lvcreate --size 4G vg --name swap
$ lvcreate --size 4G vg --name tmp
$ lvcreate --size 4G vg --name var
$ lvcreate -l 100%FREE vg --name root

Obviously change the sizes to whatever you want.

Let's make the filesystems:

$ mkfs.vfat -F32 /dev/sda1
$ mkfs.ext4 /dev/sda3
$ mkfs.ext4 /dev/mapper/vg-root
$ mkfs.ext4 /dev/mapper/vg-tmp
$ mkfs.ext4 /dev/mapper/vg-var
$ mkswap /dev/mapper/vg-swap

With that out of the way, we can mount everything to prepare for the chroot:

$ mount /dev/mapper/vg-root /mnt
$ mkdir /mnt/var
$ mkdir /mnt/tmp
$ mkdir /mnt/home
$ mkdir /mnt/boot
$ mount /dev/sda3 /mnt/home
$ mount /dev/sda1 /mnt/boot
$ mount /dev/mapper/vg-tmp /mnt/tmp
$ mount /dev/mapper/vg-var /mnt/var

Since I'm going to be using the linux-hardened kernel, we'll need Arch repository support:

$ pacman -Sy artix-archlinux-support

Add the following lines to /etc/pacman.conf:

[extra]
Include = /etc/pacman.d/mirrorlist-arch
[community]
Include = /etc/pacman.d/mirrorlist-arch

Let's update pacman with the new repositories:

$ pacman -Sy

Almost ready for the chroot! Let's install some packages first. Replace intel-ucode with amd-ucode if needed. We'll use all of these packages later on, so I'm just installing them now.

$ basestrap /mnt base base-devel runit elogind-runit linux-hardened linux-firmware linux-hardened-headers efibootmgr lvm2 mkinitcpio dhcpcd-runit intel-ucode git efitools artix-archlinux-support grub pam_mount apparmor

Finally, let's create the fstab so we can chroot!

$ fstabgen -U /mnt >> /mnt/etc/fstab
$ artix-chroot /mnt

We'll modify /etc/fstab to have fine-grained permissions. We also can't use UUIDs for the logical volumes, so those need to use labels intead.

/dev/mapper/vg-root	/	ext4	defaults				1 1
/dev/mapper/vg-tmp	/tmp	ext4	defaults,nosuid,noexec,nodev		1 2
/dev/mapper/vg-var	/var	ext4	defaults,nosuid				1 2
[UUID of /dev/sda3]	/home	ext4	defaults,nosuid,noexec,nodev		1 2
[UUID of /dev/sda1]	/boot	vfat	defaults,nosuid,noexec,nodev		1 2
/dev/mapper/vg-swap	none	swap	defaults				0 0
proc			/proc	proc	nosuid,nodev,noexec,hidepid=2,gid=proc	0 0

Again, we need to modify /etc/pacman.conf:

[extra]
Include = /etc/pacman.d/mirrorlist-arch
[community]
Include = /etc/pacman.d/mirrorlist-arch

Now we set the system time:

$ ln -sf /usr/share/zoneinfo/UTC /etc/localtime
$ hwclock --systohc

Next is system locale. Edit /etc/locale.gen and uncomment any desires locales, then run:

$ locale-gen

Optionally, we can set system-wide locales in /etc/locale.conf:

export LANG="en_US.UTF-8"
export LC_COLLATE="C"

Next let's create our users. Make sure not to add the -m flag, we don't want to create the home directory yet!

$ useradd user
$ useradd admin
$ passwd user
$ passwd admin

This setup locks the root account, so admin will be our sole user with root permissions. Let's set that up with visudo:

$ visudo -f /etc/sudoers.d/admin-account
admin ALL=(ALL) ALL

We'll leave /etc/securetty blank to reduce attack surface:

$ echo "" > /etc/securetty

By default su logs into root, let's turn that off by adding a line to /etc/pam.d/su:

auth	required	pam_wheel.so	use_uid

Now we can lock the root account for good. From now on we'll just use sudo from the admin account.

$ passwd -l root

Let's modify /etc/pam.d/system-auth so we can use pam_mount:

auth		required			pam_faillock.so	preauth
auth		[success=1 default=ignore]	pam_unix.so	try_first_pass nullok
auth		[default=die]			pam_faillock.so	authfail
auth		optional			pam_mount.so
auth		optional			pam_permit.so
auth		required			pam_env.so
auth		required			pam_faillock.so	authsucc

account		required			pam_unix.so
account		optional			pam_permit.so
account		required			pam_time.so

password	optional			pam_mount.so
password	required			pam_unix.so	try_first_pass nullok shadow sha512
password	optional			pam_permit.so

session		optional			pam_mount.so
session		required			pam_limits.so
session		required			pam_unix.so
session		optional			pam_permit.so

We also need to add a tag to /etc/security/pam_mount.conf.xml right before the closing </pam_mount>:

<volume fstype="crypt" path="/home/.%(USER).img" mountpoint="/home/%(USER)" user="*" options="loop,noatime" />

Now let's create the encrypted home directories for each user. You can of course change the size of the directories to whatever you need.

$ dd if=/dev/urandom of=/home/.user.img bs=1G count=32 status=progress
$ losetup /dev/loop0 /home/.user.img
$ cryptsetup luksFormat /dev/loop0
$ cryptsetup luksOpen /dev/loop0 user
$ mkfs.ext4 /dev/mapper/user
$ cryptsetup luksClose user

Make sure the password is the same as the user's, because pam_mount requires they be the same. Let's do the same for the admin account:

$ dd if=/dev/urandom of=/home/.admin.img bs=1G count=32 status=progress
$ losetup /dev/loop1 /home/.admin.img
$ cryptsetup luksFormat /dev/loop1
$ cryptsetup luksOpen /dev/loop1 admin
$ mkfs.ext4 /dev/mapper/admin
$ cryptsetup luksClose admin

Let's setup the hostname:

$ echo "artix" > /etc/hostname

Then add the following lines to /etc/hosts:

127.0.0.1   localhost
::1         localhost
127.0.1.1   artix.localdomain   artix

Now we need to generate our initrd by editing the hooks in /etc/mkinitcpio.conf:

HOOKS=(base udev autodetect modconf block encrypt keyboard keymap lvm2 resume filesystems fsck)

With that done, we can go ahead and generate it:

$ mkinitcpio -p linux-hardened

Now we can install our bootloader:

$ grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub_uefi

Let's modify our kernel parameters to detect the LUKS volume in /etc/default/grub:

GRUB_CMDLINE_LINUX_DEFAULT="cryptdevice=/dev/sda2:crypt slab_nomerge slub_debug=FZ init_on_alloc=1 init_on_free=1 page_alloc.shuffle=1 pti=on debugfs=off oops=panic module.sig_enforce=1 lockdown=confidentiality mce=0 random.trust_cpu=off intel_iommu=on quiet loglevel=0"

We also add all the hardened boot parameters here. If using an AMD CPU, substitute intel_iommu with amd_iommu. Now we can generate the grub config:

$ grub-mkconfig -o /boot/grub/grub.cfg

Let's minimize attack surface by soft-blocking with rfkill. Afterwards you can turn back on selectively just the items you need.

$ rfkill block all

We're all set to boot into the new system!

$ exit
$ exit
$ umount -R /mnt
$ reboot

You should now be able to log in to one of the accounts you've created, but not root. When you log on, the home directory should be decrypted and mounted on /home.

First things first: Let's get internet up, now that we have the /run directory:

$ sudo ln -s /etc/runit/sv/dhcpcd /run/runit/service

Next we'll set up an AUR package manager--feel free to use a different one!

$ git clone https://aur.archlinux.org/paru-bin.git
$ cd paru-bin
$ makepkg -si

Now we'll setup a DKMS module from the AUR called LKRG, which tries to post-detect kernel exploits. We need to create our own runit service, since the package is built for systemd :(

$ paru -S lkrg-dkms
$ sudo mkdir /etc/runit/sv/lkrg

Now let's create /etc/runit/sv/lkrg/run with the following content:

#!/bin/sh
modprobe p_lkrg

Now let's make the service executable and start it up:

$ sudo chmod a+x /etc/runit/sv/lkrg/run
$ sudo ln -s /etc/runit/sv/lkrg /run/runit/service

Now might be a good time to create a .bashrc and .bash_profile in your home directories. Feel free to use mine as an example. Also make sure the ownership of the home directories is correct (it shouldn't be root) and that the permissions are 700 (only the owner can read, write, or execute).

Now let's change kernel parameters by creating /etc/sysctl.conf to harden the system:

kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.printk=3 3 3 3
kernel.unprivileged_bpf_disabled=1
net.core.bpf_jit_harden=2
dev.tty.ldisc_autoload=0
vm.unprivileged_userfaultfd=0
kernel.kexec_load_disabled=1
kernel.sysrq=4
kernel.unprivileged_userns_clone=0
kernel.perf_event_paranoid=3
kernel.core_pattern=|/bin/false

net.ipv4.tcp_syncookies=1
net.ipv4.tcp_rfc1337=1
net.ipv4.tcp_timestamps=0
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.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
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
net.ipv4.icmp_echo_ignore_all=1
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
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.accept_ra=0
net.ipv4.tcp_sack=0
net.ipv4.tcp_dsack=0
net.ipv4.tcp_fack=0

kernel.yama.ptrace_scope=2
vm.mmap_rnd_bits=32
vm.mmap_rnd_compat_bits=16
vm.swappiness=1
fs.protected_symlinks=1
fs.protected_hardlinks=1
fs.protected_fifos=2
fs.protected_regular=2
fs.suid_dumpable=0

For an in-depth explanation of these settings, see here.

Let's fix permissions for some important directories:

$ sudo chmod 700 /boot /usr/src /lib/modules /usr/lib/modules

Finally, let's update our umask in /etc/profile:

umask 0077

That's it! We now have a base hardened system. This could be a good starting point for a server, or perhaps a desktop system with a Wayland-based window manager.