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:
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.linux-hardened
kernel. It provides a lot of hardening patches and removes tons of attack surface.
pacman
package manager, because I'm very comfortable with them and the repositories are huge.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:
bubblewrap
and apparmor
together to provide strong sandboxing and fine-grained permissions for individual applications.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:
/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.