Skip to content

Instantly share code, notes, and snippets.

@dev1lsconf
Forked from umbernhard/arch-secure-install.md
Created August 31, 2022 16:43
Show Gist options
  • Save dev1lsconf/da00f2c2689a35815768bc622f70ea7c to your computer and use it in GitHub Desktop.
Save dev1lsconf/da00f2c2689a35815768bc622f70ea7c to your computer and use it in GitHub Desktop.
Building a Secure Arch Linux Device

Building a Secure Arch Linux Device

Locking down a linux machine is getting easier by the day. Recent advancements in systemd-boot have enabled a host of features to help users ensure that their machines have not been tampered with. This guide provides a walkthrough of how to turn on many of these features during installation, as well as reasoning for why certain features help improve security.

The steps laid out below draw on a wide variety of existing resources, and in places I'll point to them rather than attempt to regurgitate full explanations of the various security components. The most significant one, which I highly encourage everyone to read, is Rod Smith's site about secure boot, which is the most comprehensive and cogent explanation of UEFI, boot managers and boot loaders, and secure boot. Another incredibly useful resources is Safeboot, which encapsulates many of the setup steps below in a Debian application. Finally, the Arch Wiki pages for Secure Boot, disk encryption, and systemd-boot. Many of the decisions to use certain technologies were inspired by Matthew Garret's Producing a trustworthy x86-based Linux appliance.

How best to use this guide

Using Linux is designed to be an empowering and enriching experience. Therefore, I'd like to issue a word of caution about blindly following this gude (no judgment though, I do it too). At many points in this guide there will be a series of steps with fairly complex commands, but I urge you to pause and think about each one. It may be slow and painful, and it will definitely require some googling, but if you don't proceed with intent it's possible to end up with a system that has and does a bunch of stuff that you don't understand. As this is a guide about security, that's, like, bad. Not that you should know how every intricate detail of Arch Linux, UEFI, TPMs, etc. work; I'm pretty sure no one does. But you should at least know how to find out if you come across something that is causing you trouble. It's a hard-earned skill, but you can get almost all of the way there by just paying close attention. I'll try to tip you off to when you should pay closer attention where I can, but neither of us is perfect, so make your best effort and I'll make mine.

I'm getting off the high horse now and getting on with the show.

Getting Started

The first step is to install Arch normally, following the installation guide, up to partitioning the disks. Much of this guide's format and content was stolen from @OdinsPlasmaRifle's really excellent guide. Where possible I've tried to expound a bit on why we're doing certain things, as well as modifying the guide to include additional steps that will make later implementing security features a little easier.

Setting up the partitions

From the archiso, run gdisk. For me, it looks like this:

gdisk /dev/nvme0n1

Wipe out the existing partition table, and add an EFI system partition (ESP) in the first 512 MB on the disk:

o
n
(Enter)
(Enter)
512M
ef00

This partition will contain the bootloader, and will eventually be mounted at boot. Now create the LVM system partition, which will take up the whole rest of your disk. This partition will eventually be encrypted via LUKS2 and can contain subpartitions with different read/write permissions.

n
(Enter)
(Enter)
(Enter)
8e00

After you're done, printing the GPT should show the following (though your partition sizes will likely be different; the table below was on a 120GB nvme drive):

Number  Start (sector)  End (sector)    Size      Code  Name
   1        2048           1048576    511.0 MiB   ef00  EFI system partition
   2      10506224        250069646   118.7 Gib   8e00  Linux LVM

Write the partition table to the disk:

w
Y

Now format the ESP (for compatability reasons, it must be FAT32 format):

mkfs.fat -F32 /dev/nvme0n1p1

Encrypting the disk

The next step is to set up disk encryption. Depending on your application, an encrypted disk may be unnecessary (we'll see a way later on that will still verify the contents of the disk even if they aren't secret), but for most applications it's a good idea.

First, make sure the dm-crypt module is loaded:

modprobe dm-crypt

Encrypt the disk using cryptsetup:

cryptsetup luksFormat /dev/nvme0n1p2

(Remember that the second partition on our disk is the LVM partition. I'm using an NVMe disk, but if you're running a different style of disk the preceding information may be different, e.g. your partition may be something like /dev/sda2)

Running this will prompt you to make sure you're ready to encrypt the disk. Type YES at the prompt to proceed. Next, you will be prompted to set a disk encryption passsword. This password is one of two security critical passwords on the device, so make it a good one (remember, length is better than character variation). Additionally, DO NOT FORGET THIS PASSWORD. Doing so will render your machine completely unsuable, and you will lose all data stored on the disk. Mitigate this risk by choosing a memorable (but also unique not easily guessable) password, and performing frequent backups of your data).

Once you have set your password, the disk is now encrypted and we are ready to begin setting up our LVM partitions. Start by decrypting the disk you just encrypted:

cryptsetup luksOpen /dev/nvme0n1p2 cryptlvm

cryptlvm is a label that you can subsequently use to reference the now-decrypted disk. LUKS mounts the disk in the dev/mapper folder, which is also where our LVM partitions will be mounted. Make sure the disk was decrypted and mounted properly:

ls /dev/mapper/cryptlvm

Configuring LVM

LVM stands for Logical Volume Manager. For historical reasons I don't really understand, physical volumes, i.e. real hard disks, and logical volumes, software-managed "disks" that may or may not correspond to physical disks are not usually easy to manage. Logical volumes may take up only part of one physical disk, or may span multiple physical disks (akin to RAID0), so the filesystem needs an intelligent way of keeping track of which partitions/filesystems live on which disks. LVM does all of this abstraction for you. At least, that's my understanding, I'm not super well informed on LVM. I just know that it works for our purposes here.

First, we tell LVM that there is a physical volume on our LVM partition (the disk itself knows that this partition exists; it doesn't know about the logical partitions we create next, that's what LVM manages).

pvcreate /dev/mapper/cryptlvm

Now we create a volume group, which is how LVM keeps track of which logical volumes are associated with a physical volume:

vgcreate volume /dev/mapper/cryptlvm

Now that we have a logical volume, tell LVM about our new logical partitions, starting with swap:

lvcreate -L4G volume -n swap

(Note that the size of this partition is up to you. General guidelines is something like half of RAM, but disks are so large these days in comparison to RAM that I've seen swap partitions as large as 4-5x the RAM. In case you're wondering, swap is used by the OS when RAM starts to get full. The OS can flush parts of RAM that aren't being used right this second to disk, effectively increasing the RAM size a little bit with a performance hit (which depends on the technology of disk you have; SSDs are pretty quick!)

Next we create a root partition. This corresponds to the / directory in Unix-flavored systems, and this presents another opportunity to implement a security feature. Taking a note from Safeboot's spin on System Integrity Protection (SIP), we can choose a smaller root partition that will later on enable us to hash it using the dm-verity module. Doing so will allow us to extend the tamper-evidence that Secure Boot provides up into the operating system, as if the hashing on the root partition doesn't match an expected value, we will know that it may have been tampered with. Additionally, setting this partition to read-only prevents some attacks that rely on modifying the kernel.

The downside to choosing a smaller root partition and mouting it as read-only is that it can make updating the system tricky. It also means we'll need more partitions over all, as we'll have to split out many folders that are traditionally kept on the root partition, like /var, as these partitions contain logging data that by definition needs to be writeable.

Mounting your root partition at read-only is probably overkill for most applications. It's probably only a good idea you are setting up a machine that is not likely to receive updates frequently and needs a higher degree of protection. Doing this in Arch Linux is therefore not a great move, as the rolling update cycle will cause paine with a read-only root partition. Other distros, like Debian and Ubuntu, are less likely to have issues with this as they release more slowly and all at once. This is why Safeboot works with Ubuntu (in addition to it being a popular distro).

If you are not planning to later add SIP, create a root partition that is larger than the one prescribed below (20G is probably enough, but 40G guarantees you'll never run out of space).

lvcreate -L12G volume -n root

If you are creating a read-only root partition, make sure to include a partition for /var (this may be a good idea for other reasons too):

lvcreate -L60G volume -n var

Finally, create a home partition, where user data will be stored, with the rest of the space on the disk:

lvcreate -l 100%FREE volume -n home

Once you're done, you can run lvdisplay to check your work. [TODO: add what mine looks like?]

Making the filesystems

Now that we've created our logical volumes, we need to initialize file systems on each of them. The choice of file system is up to you. It's hard to go wrong with ext4, but more recent systems like ZFS or BTRFS may offer better data reliability options.

mkfs.ext4 /dev/volume/root
mkfs.ext4 /dev/volume/var
mkfs.ext4 /dev/volume/home

Also don't forget to setup the swap partition:

mkswap /dev/volume/swap

Now we can mount our file systems and proceed with installation!

mount /dev/volume/root /mnt
mkdir /mnt/home
mkdir /mnt/var
mkdir /mnt/boot
mount /dev/volume/home /mnt/home
mount /dev/volume/var /mnt/var
mount /dev/nvme0n1p1 /mnt/boot
swapon /dev/volume/swap

Installation

Bootstrap Arch onto our newly created filesystem, and install utilities:

pacstrap /mnt base base-devel linux linux-firmware lvm2 vim

(This may take a while depending on your network connection. I have the pleasure of doing this on a Lenovo Flex 14 with really shoddy wifi drivers, which means its taking even longer than usual.)

Once that's done, generate fstab, the file used by the operating system to setup the disks at boot time:

genfstab -U /mnt >> /mnt/etc/fstab

Now it's time to chroot into system. This basically lets you walk around inside the environment we've spent the last however many minutes setting up, and spruce up the place before you boot off the disk for real.

arch-chroot /mnt

Set time locale (choose a relevant locale):

ln -sf /usr/share/zoneinfo/Africa/Johannesburg /etc/localtime

Set clock:

hwclock --systohc

Uncomment en_US.UTF-8 UTF-8 en_US ISO-8859-1 and other needed localizations in /etc/locale.gen. This is really important, as it is needed by most programs to figure out what language localizations to run with. This includes not just the English (or your preferred language), but also the programming languages, like the version of C supported by your system.

locale-gen

Create locale config file:

locale > /etc/locale.conf

Set the lang variable in the above file:

LANG=en_US.UTF-8

Add an hostname (any hostname of your choice as one line in the file. eg. myhostname). This is the name of your device, that will be displayed at login and shouted at you through your bluetooth headphones when you connect, so pick something good!

vim /etc/hostname

Update /etc/hosts. This overrides DNS settings when your machine does name resolution, so adding the following lines will ensure that whenever a program tries to connect to localhost it gets the right IP address (in this case, the loopback address). I'm only including an IPv4 address. If you need IPv6, or more complicated settings, godspeed.

127.0.0.1   localhost

Because our filesystem is on LVM we will need to enable the correct mkinitcpio hooks. These allow the initcpio, Arch's initial RAM file system that gets loaded into ram on boot, to include programs that can decrypt our hard drive.

Edit the /etc/mkinitcpio.conf. Look for the HOOKS variable and move keyboard to before the filesystems. If you don't do that, you may not be able to type in your password! Additionally, add sd-encrypt and lvm2 after keyboard. sd-encrypt is systemd-boot's hook, which will enable us to use systemd-cryptsetup later on). You can just use encrypt if you don't plan on using systemd-cryptsetup and are content to just enter a password on boot up every time (See the discussion below for why that may be the more secure thing to do depending on your needs).

HOOKS="base udev autodetect modconf block keyboard sd-encrypt lvm2 filesystems fsck"

If you're running an Intel-based system, you may also need to add i915 to the MODULES field. Google it.

Regenerate the initramfs (the -p flag here specifies the name of the image you're regenerating. To regenerate all of them, use -P)

mkinitcpio -p linux

Install the systemd-boot bootloader. Note that if you did something other than mount your boot partition at /mnt/boot above, you may need to pass the --path flag to tell bootctl where your ESP is.

bootctl install

Create the bootloader. Edit /boot/loader/loader.conf. Replace the file's contents with:

default arch
timeout 0
editor 0

The editor 0 ensures the configuration can't be changed on boot.

Next create a bootloader entry in /boot/loader/entries/arch.conf

title Arch Linux
linux /vmlinuz-linux
initrd /initramfs-linux.img
options cryptdevice=UUID={UUID}:cryptlvm root=/dev/mapper/volume-root quiet rw

Replace {UUID} with the UUID of your LUKS drive. You can get it by running blkid /dev/nvme0n1p2, and I prefer piping this output into the bootloader entry file and editing it from there:

blkid /dev/nvme0n1p2 >> /boot/loader/entries/arch.conf

There are a few more things to explain here. The bootloader entry tells systemd-boot what files to load for this entry (the linux and initrd, i.e. "INITial Ram Disk"), and it passese options to the kernel command line through the options entry. These paramters, visible from the /proc/cmdline are basically parameters like any other program that tell the kernel to do certain things. Obviously the cryptdevice tells the kernel (and by extension the disk encryption kernel module) that the disk with that UUID is encrypted. The root parameter tells the kernel where the root file system is once the cryptdevice is decrypted. quiet tells the kernel not to show a bunch of the logging that happens on boot (you saw what this looks like when you booted from the thumb drive). splash tells the kernel to boot with a splash screen if there is one, and rw tells the kernel to mount the root partition in read/write mode.

We could go ahead and finish the rest of our security setup below now, in which case this last parameter could be set to read-only. However, you're reading this guide, which means you may not know how to do this stuff. Putting off marking root as read-only gives us wiggle room to experiment later.

Reboot

With that, we're done with installation! Now we'll exit the chroot and reboot.

exit
reboot

In theory you should be good to go now. However, there's a good chance that things didn't go as planned and now you're either in a weird state (I was after I finished a trial install to write this, so you're in good company). This probably means you just typo'd something above. Don't freak out. Read whatever error message you're seeing and try googling it. Then, reboot back into the archiso. You can unlock and remount your file system and chroot into it just as we did when we set it up (though you don't have to do all the LVM configuration again). You can fix whatever may be wrong and then reboot again to see if you figured it out. It usually takes me a few iterations of that process to get things right, but don't give up. This is one place where Arch is significantly less fun that other distros with nice installers. But hey, you wanted bleeding edge, you get the bleeding.

After the first boot

Congrats! You've now setup and Arch Linux system! What's that? You don't like just having a black terminal for an operating system?

This section isn't related to security setup, but it will make your quality of life better. I'm assuming you're a human being, and you have probably used computers before, which means it might be nice to have a GUI. There's a lot of ways to accomplish this on Arch, but my preferred way is the GNOME desktop environment. People say it's bloated, and that's sort of true, but it does all the things I need it to do and as of yet I haven't found another desktop environment (more commonly abbreviated DE) that does it better. Checkout Reddit or the Wiki if you want to mess around with DEs and window managers (WMs) yourself.

Depending on your choice of DE and your device, you may have to fight with getting graphics, mouse, fingerprint reader, and other drivers loaded. Trying to put all of that stuff in this guide would make it a book, so I'm going to skip it and tell you to rely on the Wiki, Reddit, StackOverflow, and other excellent Internet resources.

There are only a few things I'm going to recommend. The first is and Arch User Repository (AUR) manager. I like yay because it's fun to welcome packages onto my system with enthusiasm. Google around if you're more dour.

I'm also going to recommend Plymouth, which is a splash screen manager for the boot process. The only reason I recommend it is because one of the security features we're going to add later, TPM2-TOTP, has an integration for it that displays nicely with it (it also provides a more aesthetically pleasing way to enter your disk encryption password, but oh my god, who cares). If you're not doing that step, I'd skip Plymouth altogether and keep your boot process faster. Make sure you follow the instructions for the systemd-boot hooks specifically (e.g. add sd-plymouth to your mkinitcpio.conf HOOKS variable instead of just plymouth).

One other note about Plymouth: if you aren't seeing it display properly, it's possible that it's because your machine is too fast for it to work right. Confusingly, setting the ShowDelay variable to 0 in /etc/plymouth/plymouthd.conf ought to fix that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment