Oracle Free Tier: Install Debian on the x86 micro instances with full disk encryption

Oracle Cloud offers AMD VPSes with 0,1 virtual CPU core and 1GB RAM for free through their Oracle Free Tier program. Unfortunately, Debian is not part of their standard range of OSes available to install. For the free ARM nodes Oracle offers, reinstalling is very simple through the use of netboot.xyz and selecting a netinstall image. Unfortunately, reinstalling their x86 instances is a bit more cumbersome.

This document shows how to do an in-place install of Debian 12 with full disk encryption and remote unlocking on an instance after first choosing the Ubuntu 22.04 minimal image in the Oracle Cloud console. As root file system, a choice is given between ext4 and btrfs.

Installing Alpine Linux

In order to reinstall the instance, we will need to minimise the amount of resources the system uses. By installing Alpine Linux and then using that to install Debian, we get a very minimal temporary OS using less than 100 MB of RAM and can be moved from the boot volume into RAM while continuing to run stable.

First, SSH into the Ubuntu instance you deployed on Oracle Cloud.

ssh ubuntu@REPLACE_WITH_YOUR_INSTANCES_PUBLIC_IP_ADDRESS

Change to the root user:

sudo -i

Then, execute the following commands to replace Ubuntu with Alpine Linux:

cd /
wget https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-virt-3.18.4-x86_64.iso
dd if=alpine-virt-3.18.4-x86_64.iso of=/dev/sda
sync
reboot

The system will now reboot into an unconfigured Alpine Linux install.

Configure Alpine Linux

After the system is rebooted, you’ll lose SSH access. Therefore, use the Oracle Cloud Console to access the Alpine Linux VM. Deploy the cloud console by going to Compute -> Instances -> Instance details (by clicking on the instance you want to install). Scroll down and click ‘Console connection’ -> ‘Launch Cloud Shell connection’.

A new connection will deploy. Hit enter to get a login prompt after the console prints Instance Console Connection reached state: ACTIVE

On the resulting command prompt (localhost login:), login using username root and without a password.

Next, configure networking:

vi /etc/network/interfaces

Press i to start editing. The Oracle Cloud shell supports copy pasting.

auto eth0
iface eth0 inet dhcp

Press escape and type :wq to save and exit.

Restart networking

/etc/init.d/networking restart

Setup SSH using the Alpine SSH wizard.

setup-sshd
Which ssh server? ('openssh', 'dropbear' or 'none') [openssh] 
Allow root ssh login? ('?' for help) [prohibit-password] 
Enter ssh key or URL for root (or 'none') [none] 
 * service sshd added to runlevel default
 * Caching service dependencies ...
 [ ok ]
ssh-keygen: generating new host keys: RSA ECDSA ED25519 
 * Starting sshd ...
 [ ok ]

Add your SSH public key

mkdir .ssh
vi .ssh/authorized_keys

Paste in your SSH public key and save.

Remove Alpine from the boot volume

SSH into the Alpine Linux install. Remove old IP and matching fingerprint lines from your .ssh/known_hosts if you get any errors and retry.

ssh root@REPLACE_WITH_YOUR_INSTANCES_PUBLIC_IP_ADDRESS

Move Alpine into RAM and unmount the boot volume by executing the following commands line by line.

mkdir /media/setup
cp -a /media/sda/* /media/setup
mkdir /lib/setup
cp -a /.modloop/* /lib/setup
/etc/init.d/modloop stop
umount /dev/sda
mv /media/setup/* /media/sda/
mv /lib/setup/* /.modloop/

Setup APK by entering the following command and accepting the default settings.

setup-apkrepos

Install Alpine packages

apk update
apk add dosfstools e2fsprogs debootstrap cryptsetup nano gptfdisk partx btrfs-progs
modprobe btrfs

Partition the disk

gdisk /dev/sda

Press 3 to create a new GPT partition table. Press n to create the boot partition. Make it 1024MB by answering the wizard as detailed below.

Command (? for help): n
Partition number (1-128, default 1): 
First sector (34-104857566, default = 2048) or {+-}size{KMGTP}: 
Last sector (2048-104857566, default = 104855551) or {+-}size{KMGTP}: +1024M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): 
Changed type of partition to 'Linux filesystem'

Press n to create the EFI partition. Make it 512MB and set it to EFI.

Command (? for help): n
Partition number (2-128, default 2): 
First sector (34-104857566, default = 2099200) or {+-}size{KMGTP}: 
Last sector (2099200-104857566, default = 104855551) or {+-}size{KMGTP}: +512M
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): EF00
Changed type of partition to 'EFI system partition'

Press n to create the root file system partition. Make it use all available remaining disk space.

Command (? for help): n
Partition number (3-128, default 3): 
First sector (34-104857566, default = 3147776) or {+-}size{KMGTP}: 
Last sector (3147776-104857566, default = 104855551) or {+-}size{KMGTP}: 
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): 
Changed type of partition to 'Linux filesystem'

Press p to display the partition table. It should give the following result:

ommand (? for help): p
Disk /dev/sda: 104857600 sectors, 50.0 GiB
Model: BlockVolume     
Sector size (logical/physical): 512/4096 bytes
Disk identifier (GUID): 57C983BB-7A42-4B74-BD69-F9A0AEFD5199
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 104857566
Partitions will be aligned on 2048-sector boundaries
Total free space is 4029 sectors (2.0 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048         2099199   1024.0 MiB  8300  Linux filesystem
   2         2099200         3147775   512.0 MiB   EF00  EFI system partition
   3         3147776       104855551   48.5 GiB    8300  Linux filesystem

If this is not the case or if you’d like to make changes to this setup, start over by pressing o to recreate the partition table. Otherwise, press w and y to exit and save your changes.

Formatting the new partitions

Remove the old partition table

partx -v -d /dev/sda
partition: none, disk: /dev/sda, lower: 0, upper: 0
/dev/sda: partition #1 removed
/dev/sda: partition #2 removed
/dev/sda: partition #3 removed

Add the new partition table

partx -v -a /dev/sda
partition: none, disk: /dev/sda, lower: 0, upper: 0
/dev/sda: partition table type 'gpt' detected
range recount: max partno=3, lower=0, upper=0
/dev/sda: partition #1 added
/dev/sda: partition #2 added
/dev/sda: partition #3 added

Format the boot and EFI partitions

mkfs.ext4 /dev/sda1
mkfs.vfat /dev/sda2

Encrypt the root partition using a strong password.

cryptsetup luksFormat -c aes-xts-plain64 -s 512 -h sha256 --pbkdf argon2i /dev/sda3

Unlock the newly created encrypted partition

cryptsetup luksOpen --allow-discards /dev/sda3 luks1

Format root filesystem

Depending on your preference, you can use ext4 or btrfs, or your own choice as the main file system. The first two are options I’ve personally tried. Both work well. Choose one.

Option A: Format and mount the root partition (ext4)

mkfs.ext4 /dev/mapper/luks1

mount -t ext4 /dev/mapper/luks1 /mnt

Option B: Format and mount the root partition (btrfs)

Format the root partition (btrfs)

mkfs.btrfs /dev/mapper/luks1

mount /dev/mapper/luks1 /mnt

btrfs subvolume create /mnt/@rootfs
btrfs subvolume create /mnt/@swap

Unmount the BTRFS filesystem and mount the @rootfs subvolume.

umount /mnt

mount -t btrfs -o defaults,discard,subvol=@rootfs,compress=zstd:2 /dev/mapper/luks1 /mnt

Add the /boot partition as a mount

mkdir /mnt/boot
mount -t ext4 /dev/sda1 /mnt/boot

And finally, the EFI partition

mkdir /mnt/boot/efi
mount -t vfat /dev/sda2 /mnt/boot/efi

We are now ready to start debootstrapping Debian.

Debootstrapping Debian

mkdir -p /mnt/var/tmp
chmod 1777 /mnt/var/tmp
debootstrap bookworm /mnt

This step will take a while and it may sometimes even seem like it is hanging, especially when resolving dependencies and unpacking the base system. For me, the debootstrap command took around 15 minutes.

Configuring hostname

echo YOUR_PREFERRED_HOSTNAME > /mnt/etc/hostname

cat <<EOF >> /mnt/etc/hosts
127.0.1.1 YOUR_PREFERRED_HOSTNAME.YOUR_DOMAIN YOUR_PREFERRED_HOSTNAME
EOF

Configuring networking

Choose one of these two:

Option A: ifupdown

cat <<EOF >> /mnt/etc/network/interfaces

auto lo
iface lo inet loopback

auto ens3
iface ens3 inet dhcp
EOF

Option B: systemd-networkd

rm -rf /mnt/etc/network

cat <<EOF >> /mnt/etc/systemd/network/ens3.network

[Match]
Name=ens3

[Network]
DHCP=yes
EOF

Configuring apt

cat  <<EOF > /mnt/etc/apt/sources.list 
deb http://ftp.nl.debian.org/debian/ bookworm main contrib non-free non-free-firmware
#deb-src http://ftp.nl.debian.org/debian/ bookworm main contrib non-free non-free-firmware
deb http://security.debian.org/ bookworm-security main contrib non-free non-free-firmware
#deb-src http://security.debian.org/ bookworm-security main contrib non-free non-free-firmware

# bookworm-updates, previously known as 'volatile'
deb http://ftp.nl.debian.org/debian/ bookworm-updates main contrib non-free non-free-firmware
#deb-src http://ftp.nl.debian.org/debian/ bookworm-updates main contrib non-free non-free-firmware

# bookworm-backports, previously on backports.debian.org
deb http://ftp.nl.debian.org/debian/ bookworm-backports main contrib non-free non-free-firmware
#deb-src http://ftp.nl.debian.org/debian/ bookworm-backports main contrib non-free non-free-firmware
EOF

Enter chroot

Create system bind mounts

mount --rbind /dev  /mnt/dev
mount --rbind /proc /mnt/proc
mount --rbind /sys  /mnt/sys

enter Debian

LANG=C chroot /mnt /bin/bash
apt update
apt dist-upgrade -y
apt install locales -y
dpkg-reconfigure locales

Select en_US.UTF-8 and possible other languages and set a default language.

dpkg-reconfigure tzdata

Choose your timezone

Enabling systemd-networkd if desired

If you chose to use systemd-networkd above, enable it here and uninstall ifupdown, otherwise don’t

apt purge ifupdown

systemctl enable systemd-networkd

Setting up mounts

Let’s first include the description you would see when installing Debian the conventional way.

cat <<EOF > /etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
EOF

and add the /, /boot and /boot/efi mount points

cat <<EOF >> /etc/fstab

# / partition
EOF

Option A: Ext4 root partition


echo UUID=$(blkid -s UUID -o value \
      /dev/mapper/luks1) \
      / ext4 defaults 0 2 >> /etc/fstab

Option B: BTRFS root partition

echo UUID=$(blkid -s UUID -o value \
      /dev/mapper/luks1) \
      / btrfs defaults,discard,subvol=@rootfs,compress=zstd:2 0 2 >> /etc/fstab

/boot and /boot/efi partition

cat <<EOF >> /etc/fstab

# /boot partition
EOF

echo UUID=$(blkid -s UUID -o value \
      /dev/sda1) \
      /boot ext4 defaults 0 2 >> /etc/fstab

cat <<EOF >> /etc/fstab

# UEFI /boot/efi partition (fat32)
EOF
echo PARTUUID=$(blkid -s PARTUUID -o value \
      /dev/sda3) \
      /boot/efi vfat nofail,x-systemd.device-timeout=1 0 1 >> /etc/fstab

Crypttab

echo luks1 UUID=$(blkid -s UUID -o value \
      /dev/sda3) none \
      luks,discard,initramfs >> /etc/crypttab

Create a swap file

The Oracle free tier micro instances only have 1GB of RAM. To prevent out-of-memory errors, I created a 2GB swap file.

Option A: Create a swapfile on ext4

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap -f /swapfile

cat <<EOF >> /etc/fstab

# Swapfile
/swapfile none swap sw 0 0
EOF

swapon -av      

Option B: Create a swapfile on btrfs

apt install btrfs-progs

First, we mount the earlier created @swap subvolume.

mkdir /swap

cat <<EOF >> /etc/fstab

# /swap subvolume
EOF

echo UUID=$(blkid -s UUID -o value \
      /dev/mapper/luks1) \
      /swap btrfs defaults,subvol=@swap 0 2 >> /etc/fstab

mount -o defaults,subvol=@swap /dev/mapper/luks1 /swap
btrfs filesystem mkswapfile --size 2G /swap/swapfile
cat <<EOF >> /etc/fstab

# Swapfile
/swap/swapfile none swap sw 0 0
EOF

swapon -av

Install GRUB bootloader

apt install -y grub-efi
grub-install --target=x86_64-efi --efi-directory=/boot/efi --recheck --no-floppy

Enable SSD Trim support

sed -i "s/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet\"/GRUB_CMDLINE_LINUX_DEFAULT=\"rd.luks.options=discard\"/g" /etc/default/grub

Enable the serial console, in order to be able to continue using the Oracle cloud shell

cat <<EOF >> /etc/default/grub
GRUB_TERMINAL_INPUT="console serial"
GRUB_TERMINAL_OUTPUT="gfxterm serial"
GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200"
GRUB_CMDLINE_LINUX_DEFAULT="\${GRUB_CMDLINE_LINUX_DEFAULT} console=tty0 console=ttyS0,115200"
EOF
update-grub2

Install Linux kernel and basic system packages

apt install -y linux-image-amd64
apt install -y cryptsetup ssh nano sudo man fail2ban tmux systemd-timesyncd pollinate haveged wget curl net-tools git gpg build-essential dosfstools btrfs-progs qemu-guest-agent unattended-upgrades

Fix Fail2ban SSH error

sed -i "s/backend = auto/backend = systemd/g" /etc/fail2ban/jail.conf

Harden SSH and prevent language setting errors

sed -i "s/#PasswordAuthentication yes/PasswordAuthentication no/g" /etc/ssh/sshd_config
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin no/g" /etc/ssh/sshd_config
sed -i "s/AcceptEnv LANG LC_*/#AcceptEnv LANG LC_*/g" /etc/ssh/sshd_config

Add an admin user

adduser management

Give sudo privileges

adduser management sudo

Become the management user.

su - management

Add your public SSH keys

mkdir ~/.ssh
nano ~/.ssh/authorized_keys

exit

Setup remote unlocking

apt install dropbear-initramfs initramfs-tools -y
nano /etc/dropbear/initramfs/dropbear.conf

Adjust the following settings

DROPBEAR_OPTIONS="-I 180 -j -k -p 2222 -s -c cryptroot-unlock"
IFDOWN=*

Also make sure to open port 2222 in the VCN security list in the Oracle cloud console.

nano /etc/dropbear/initramfs/authorized_keys

Add your SSH public key in here. You will now be able to unlock the encrypted root disk at boot by going to root@REPLACE_WITH_YOUR_INSTANCES_PUBLIC_IP_ADDRESS

Update the initramfs to add these changes and update grub just to be sure:

update-initramfs -u -k all
update-grub2

Cleanup

apt --purge autoremove

Rebooting into Debian

exit the chroot environment

exit

reboot

reboot

Conclusion

If you enjoyed reading this post and/or have suggestions for improvement, please let me know by leaving a comment down below or contacting me through the contact form.

As a final word of caution, be careful with how much you rely on freely offered services and take proper precautions wrt backups and recovery in case these offerings change.

Next steps could be to disable the sudo password prompt or to install zsh.

If you used BTRFS as your file system, now, with a clean installation, might be a good time to create your first snapshot. That way, you can repurpose this VM as often as you like without having to start from scratch.

sudo mkdir /.snapshots
sudo chmod 750 /.snapshots
sudo btrfs subvolume snapshot / /.snapshots/2023-11-10-base-install

References