Full disk encryption on a VPS works without reinstalling, and on Debian 13 there are two routes: encrypt a data disk or a file container with LUKS2 (any plan, no boot ceremony), or encrypt the root disk in place and unlock it after every reboot by typing the passphrase over SSH into a tiny server called dropbear inside the initramfs. We built both on a Debian 13 virtual machine with 2 GB of RAM on 29 October 2026. The data-disk route cost us 15 to 20 percent of random 4 KB IOPS and nothing else. The root route worked too, but only after three problems that quick tutorials skip: Debian's GRUB cannot open a LUKS2 volume whose keyslot uses the default Argon2id, so /boot must stay on its own unencrypted partition; /etc/crypttab is ignored until the separate systemd-cryptsetup package is installed; and Argon2id wants about 850 MB of RAM, which makes a 2 GB server unlock slowly.
One honest sentence first. Encryption at rest protects the bytes on the disk image, in a copy of that image and on a drive that leaves the data centre; it does not protect a running server from whoever controls the hypervisor, because the key sits in the virtual machine's memory while the disk is unlocked.
Key facts, checked on 29 October 2026:
- Debian 13 ships cryptsetup 2:2.7.5-2 and dropbear-initramfs 2025.89-1~deb13u1 (packages.debian.org: cryptsetup, dropbear-initramfs).
cryptsetup --helpon our server printed the LUKS2 defaults: aes-xts-plain64, 512-bit key, Argon2id with 2,000 ms, 1,048,576 kB of memory and 4 threads. - In-place encryption needs free space at the front: "The recommended minimum size is twice the default LUKS2 header size (--reduce-device-size 32M) for encryption mode" (cryptsetup-reencrypt(8)).
- "Overwriting the LUKS header in part or in full is the most common reason why access to LUKS containers is lost permanently" (cryptsetup FAQ, section 6).
- The "
/bootpartition which must remain unencrypted"; guided encrypted LVM "will also create a separate/bootpartition" (Debian 13 installation guide, 6.3.4). - Remote unlock is Debian's own feature: section 8 of cryptsetup's README.Debian and the dropbear-initramfs README in
/usr/share/doc/; the static address syntax is the kernel's "ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>" (kernel documentation, nfsroot).
Six words, explained once
LUKS is the standard format for an encrypted Linux disk: a 16 MiB header holds keyslots, each a copy of the real disk key locked with a passphrase or keyfile; LUKS2 is the current version and cryptsetup its command. The cipher scrambles each sector; the default aes-xts-plain64 is AES in XTS mode, which is why its "512-bit key" is two 256-bit keys. Argon2id turns a passphrase into a key slowly and with much memory on purpose, to make offline guessing expensive. A keyfile is random bytes used instead of a passphrase, so a disk unlocks without a human, at the price that whoever holds the file holds the disk. The initramfs is the small temporary Linux loaded with the kernel; it runs before the root filesystem is mounted, so it is the only place an encrypted root can be unlocked. dropbear is a very small SSH server, and dropbear-initramfs puts it in there.
What vps encryption at rest protects, and what it does not
With LUKS the disk image is ciphertext. That covers a copy of the image read elsewhere, a replaced or decommissioned drive, and anyone who reaches the raw volume but not your running kernel; for a server in Prishtina it stacks with where the data physically is (see our CLOUD Act hosting checklist). What it does not cover: while the volume is open, the key lives in the guest kernel's keyring, which is memory on the host; the FAQ says "the keys of a mapped (open) container are now stored in the kernel key-store". Whoever fully controls the hypervisor can in principle dump that memory, and the console you type a boot passphrase into is the host's console. LUKS answers "what if someone gets the disk", not "what if the provider is hostile while my server runs". Pick the provider for the second question; use LUKS for the first.
Which route, which plan
| Your situation | Route | Plan and city |
|---|---|---|
| Encrypted storage for uploads, databases or backups, no reboot ceremony | Path A: LUKS2 file container or second disk, keyfile unlock at boot | Any plan from VPS Nano (1 vCPU, 1 GB, 20 GB NVMe); in Prishtina the free weekly backups then hold ciphertext |
| Same, but the key must never be on the server | Path A, passphrase typed over SSH after boot | VPS Micro (2 vCPU, 2 GB) or larger; on 1 GB cap the Argon2 memory as shown below |
| Everything encrypted, including OS, logs and swap | Path B: root encryption with dropbear remote unlock; needs a separate /boot and a person after every reboot | VPS Mini (4 vCPU, 4 GB, 80 GB) or any VDS; Argon2 defaults fit and the unlock is quick |
| Fresh server and you can boot an installer | Debian installer, guided encrypted LVM, then add dropbear (installer not tested by us) | VPS Mini, or VDS Small (4 vCPU, 8 GB, 240 GB) when it also carries real load |
| Secrets and passwords only | Path A under a password manager | VPS Nano or Micro; see Vaultwarden on a VPS |
All plans are KVM with NVMe and an own IPv4 plus IPv6 in Amsterdam, Dublin and Prishtina; VPS plans share vCPUs, VDS plans have dedicated ones, which helps AES throughput under load. If data residency is part of why you encrypt, the VPS in Kosovo page explains how our Prishtina data centre and the local law fit together.
Path A: encrypt vps disk space without touching the OS
Test machine: one Debian 13.7 VM (kernel 6.12.113, 2 vCPU, 2 GB, UEFI) with a 10 GB root disk and a 5 GB second disk on our Amsterdam test host; every output below is copied from it. If your provider gives you a second volume, use it. Our plans have one NVMe disk, so the practical version is a file container, a big file that LUKS treats as a disk; same performance, same backups. We tested both, only the device path differs.
# as root
apt install cryptsetup systemd-cryptsetup
mkdir -p /var/lib/luks /etc/luks && chmod 700 /var/lib/luks /etc/luks
fallocate -l 20G /var/lib/luks/vault.img # skip for a real second disk
The second package is the one everybody forgets. On the Debian 13 cloud image, apt install cryptsetup did not pull it in, and our crypttab entry did nothing at boot: systemctl status systemd-cryptsetup@data.service said "Unit could not be found". Installing it created the unit on the spot. And never write /dev/sdb into a config file: our data disk was sda on one boot and sdb on the next. Use /dev/disk/by-id/ or the LUKS UUID.
# as root; DEV is /var/lib/luks/vault.img or /dev/disk/by-id/<your disk>
cryptsetup luksFormat --type luks2 --label data-luks "$DEV"
dd if=/dev/urandom of=/etc/luks/data.key bs=512 count=8 && chmod 400 /etc/luks/data.key
cryptsetup luksAddKey "$DEV" /etc/luks/data.key
cryptsetup luksDump "$DEV" | grep -E 'cipher|PBKDF|Memory|Key:'
Warning: keyslot operation could fail as it requires more than available memory.
cipher: aes-xts-plain64
Key: 512 bits
PBKDF: argon2id
Memory: 887628
The passphrase is your recovery path, the keyfile is what the server uses alone. The warning matters: cryptsetup chose about 850 MB per keyslot for our 2 GB machine and warned it might not get it. It worked every time, but on a 1 GB server add --pbkdf-memory 262144 to both commands so Argon2 uses 256 MB and more passes instead.
# as root
cryptsetup open "$DEV" data --key-file /etc/luks/data.key
mkfs.ext4 -L data /dev/mapper/data && mkdir -p /srv/data
echo "data UUID=$(cryptsetup luksUUID "$DEV") /etc/luks/data.key luks,discard" >> /etc/crypttab
echo "/dev/mapper/data /srv/data ext4 defaults,nofail,x-systemd.requires=systemd-cryptsetup@data.service 0 2" >> /etc/fstab
systemctl daemon-reload && mount /srv/data && df -h /srv/data
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/data 4.9G 1.3M 4.6G 1% /srv/data
For the file container the crypttab line names the file: vault /var/lib/luks/vault.img /var/lib/luks/vault.key luks,discard; systemd attaches a loop device itself. nofail turns a missing key into a missing mount instead of a boot stuck in emergency mode; discard passes TRIM through. If the key must not live on the server, leave the keyfile out, add noauto, and after each reboot run ssh -t root@your-server cryptsetup open /dev/disk/by-label/data-luks data, which prompts Enter passphrase for /dev/disk/by-label/data-luks:. After the reboot, one number surprised us:
# as root
systemd-analyze blame | grep cryptsetup
18.840s systemd-cryptsetup@data.service
5.668s systemd-cryptsetup@vault.service
The same keyfile unlock took 4 seconds on the running system and 19 to 24 seconds during boot, when the 850 MB Argon2 pass competes with everything else starting; the vault container, formatted with the 256 MB cap, took 5.7 seconds. On 2 GB, cap the memory; from 4 GB up the defaults are fine.
Back up the header, then never benchmark the raw device
# as root
cryptsetup luksHeaderBackup "$DEV" --header-backup-file /root/data-luks-header.img
ls -l /root/data-luks-header.img
-r-------- 1 root root 16777216 Oct 29 16:10 /root/data-luks-header.img
Sixteen megabytes holding the only copies of your disk key: keep it off the server, as protected as the passphrase. The other half of the lesson: our first throughput test ran fio random writes against /dev/sda, the raw disk, with the LUKS volume open on it, and shredded the header at offset 0. The open mapping kept working because the key was already in the kernel; we made a filesystem, wrote a file, rebooted, and got back 5 GB of random bytes. Make the backup first, and benchmark the mapper device, never the device under it.
The cost of encryption, measured
fio with direct I/O against the plain second disk and against the LUKS mapping on top of it, alternating three rounds. The disk image sat in our host's page cache, so the sequential figures are far above any real NVMe; they measure how fast two vCPUs run AES-XTS, and that ceiling is the useful number.
| Test (fio, direct=1) | Plain disk | LUKS2 aes-xts-plain64 | Change |
|---|---|---|---|
| Sequential write, 1 MiB blocks, 3 rounds | 3,421 / 3,850 / 3,830 MiB/s | 1,734 / 1,705 / 1,880 MiB/s | CPU cap near 1.7 to 1.9 GB/s |
| Sequential read, 1 MiB blocks, 3 rounds | 3,959 / 3,719 / 4,208 MiB/s | 2,065 / 2,327 / 2,116 MiB/s | CPU cap near 2.1 to 2.3 GB/s |
| Random read, 4 KiB, queue depth 32, 2 rounds | 33.5k / 32.4k IOPS | 27.7k / 27.5k IOPS | 15 to 17 percent fewer |
| Random write, 4 KiB, queue depth 32, 2 rounds | 27.5k / 28.2k IOPS | 22.7k / 22.7k IOPS | 17 to 20 percent fewer |
cryptsetup benchmark rated aes-xts 512-bit at 1,896 MiB/s encrypting and 1,878 MiB/s decrypting, matching the ceiling. A single NVMe VPS rarely reaches 1.8 GB/s anyway, so the random-IOPS loss is what you feel, and few notice a sixth; a VDS with dedicated cores loses less.
Path B: full disk encryption vps root with luks remote unlock ssh
This is the route the search query means. We did it on an installed Debian 13 without reinstalling, and it worked, after three checks you must do first.
Three checks before you start
1. Is /boot its own partition? Run findmnt /boot. If it prints nothing, in-place encryption will encrypt the kernel and GRUB's configuration too. We did that on purpose: the next boot showed error: no such device: 510c8fe4-... and a bare grub> prompt. There, insmod argon2 answered file '/boot/grub/x86_64-efi/argon2.mod' not found and cryptomount said error: Invalid passphrase. with the right passphrase: Debian's GRUB 2.12 opens LUKS2 only with PBKDF2 keyslots, and even then would ask on the console, which defeats remote unlock. We repaired it from a rescue system by shrinking the root and adding a 1.4 GB third partition for /boot; make that partition before you encrypt, or reinstall with the installer's encrypted LVM option, which creates it.
2. Console and backup. Everything below runs over the provider's console, and the man page says in capitals: "ALWAYS BE SURE YOU HAVE RELIABLE BACKUP BEFORE USING THIS ACTION." Our restic off-site backup guide is the kind that counts.
3. Memory. Argon2id wants 650 to 900 MB on a 2 GB machine even in the initramfs; lowering it to 256 MB afterwards cut our unlock to under two seconds and removed a confusing timeout, shown below.
Encrypt the root in place from the initramfs
The filesystem must be unmounted while it is encrypted, and you cannot unmount your own root, so you stop the next boot inside the initramfs. By default it has no resize2fs, so add a hook first:
# as root
cat > /etc/initramfs-tools/hooks/e2fstools <<'EOF'
#!/bin/sh
case "$1" in prereqs) echo ""; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /sbin/e2fsck /sbin
copy_exec /sbin/resize2fs /sbin
EOF
chmod +x /etc/initramfs-tools/hooks/e2fstools
apt install cryptsetup-initramfs dropbear-initramfs
update-initramfs -u
lsinitramfs /boot/initrd.img-$(uname -r) | grep -E 'sbin/(cryptsetup|e2fsck|resize2fs)$'
usr/sbin/cryptsetup
usr/sbin/e2fsck
usr/sbin/resize2fs
Add break=premount to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub, run update-grub, reboot with the console open, and you land in a BusyBox shell at the (initramfs) prompt. Find your root partition with blkid by its UUID, because the sdX letter can differ from the running system (ours did). The mkdir is not optional: our first attempt died with Failed to acquire read lock on device because the initramfs has no /run/cryptsetup directory for lock files.
# in the (initramfs) shell; replace sdb2 with your root partition
mkdir -p /run/cryptsetup
e2fsck -f /dev/sdb2
resize2fs -M /dev/sdb2
cryptsetup reencrypt --encrypt --reduce-device-size 32M /dev/sdb2
Resizing the filesystem on /dev/sdb2 to 477216 (4k) blocks.
WARNING!
========
This will overwrite data on LUKS2-temp-b0f272bb-....new irrevocably.
Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for LUKS2-temp-b0f272bb-....new:
Verify passphrase:
resize2fs -M shrinks the filesystem to its minimum so the 32 MiB LUKS needs at the front is certainly free; the data is shifted forward and the header written in the gap. Because our first run on the 10 GB partition died after the header was written, the second asked "Do you wish to resume previously initialised operation?" and finished the job. Now open it, grow the filesystem back, and let the boot continue:
# in the (initramfs) shell
cryptsetup open /dev/sdb2 root_crypt
resize2fs /dev/mapper/root_crypt
exit
Enter passphrase for /dev/sdb2:
The filesystem on /dev/mapper/root_crypt is now 2591483 (4k) blocks long.
rootfs: clean, 30201/1278720 files, 338683/2591483 blocks
The boot found the root by its filesystem UUID inside the mapping and came up normally. Make it permanent before any reboot, because right now nothing tells the initramfs to unlock anything; and this is where a missing separate /boot bites, since update-grub writes onto the encrypted root.
dropbear-initramfs debian 13: configure the remote unlock
# as root, on the booted system
echo "root_crypt UUID=$(cryptsetup luksUUID /dev/sdb2) none luks,discard,initramfs" >> /etc/crypttab
cat ~/.ssh/id_ed25519.pub > /etc/dropbear/initramfs/authorized_keys
sed -i 's|^ssh-|no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-|' /etc/dropbear/initramfs/authorized_keys
chmod 600 /etc/dropbear/initramfs/authorized_keys
sed -i 's|^#DROPBEAR_OPTIONS=""|DROPBEAR_OPTIONS="-p 2222 -s -j -k -I 180"|' /etc/dropbear/initramfs/dropbear.conf
sed -i 's/ break=premount//' /etc/default/grub
sed -i 's|^GRUB_CMDLINE_LINUX=""|GRUB_CMDLINE_LINUX="ip=203.0.113.10::203.0.113.1:255.255.255.0:myserver:enp5s0:off"|' /etc/default/grub
update-initramfs -u && update-grub
lsinitramfs /boot/initrd.img-$(uname -r) | grep -E 'dropbear|authorized_keys|crypttab'
cryptroot/crypttab
etc/dropbear/dropbear.conf
etc/dropbear/dropbear_ed25519_host_key
root-Q5aZvpzuGW/.ssh/authorized_keys
The crypttab option initramfs forces the entry into early boot. The authorized_keys file is dropbear's own, passwords are off by design, and the prefix forbids tunnelling through the boot-time SSH. In DROPBEAR_OPTIONS, -p 2222 uses another port so your client does not panic about a changed host key (the initramfs has its own, deliberately not the main system's, since /boot is unencrypted), -j and -k disable port forwarding and -I 180 drops idle sessions. The ip= parameter gives address, gateway, netmask, hostname and interface, with off meaning no DHCP; where the provider runs DHCP, ip=dhcp works too.
Reboot and unlock from your laptop
IP-Config: enp5s0 complete:
address: 10.141.188.244 broadcast: 10.141.188.255 netmask: 255.255.255.0
Please unlock disk root_crypt:
# on your laptop
ssh -p 2222 -o UserKnownHostsFile=~/.ssh/known_hosts_initramfs root@203.0.113.10 cryptroot-unlock
Please unlock disk root_crypt:
cryptsetup: root_crypt set up successfully
Connection to 10.141.188.244 closed.
The first block is the console waiting; the others are your laptop. Less than a minute after Enter, systemd-analyze on the server reported "Startup finished in 22.526s (kernel) + 21.210s (userspace)", with / on /dev/mapper/root_crypt, /boot on its own partition and the Path A disk unlocked by its keyfile. If nobody connects, the console prompt takes the passphrase too. With the default Argon2id settings our first unlocks printed Error: Timeout reached while waiting for PID 328. and closed, though the server booted: cryptroot-unlock waits ten seconds for cryptsetup and the 850 MB derivation took longer at boot on 2 GB. Lowering the keyslot's memory made it go away:
# as root, on the booted system
cryptsetup luksConvertKey /dev/sdb2 --key-slot 0 --pbkdf argon2id --pbkdf-memory 262144 --iter-time 2000
cryptsetup luksDump /dev/sdb2 | grep -E 'Time cost|Memory'
Time cost: 14
Memory: 262144
Fourteen passes over 256 MB instead of a handful over 850 MB keeps the two-second target and fits a small server; cryptsetup open --test-passphrase then took 1.9 seconds on the running system. If you can boot the Debian netinst image through your console instead, the installer's guided encrypted LVM option creates the separate /boot, encrypts the rest and writes crypttab; you then add only the dropbear part. We did not run the installer in this lab, so that is Debian's documentation, not our test.
Warrant canaries and Kosovo law, briefly
The query behind this article asked for a "warrant canary policy" next to encryption. A warrant canary is a regularly published statement that a provider has not received secret legal process; if it stops appearing, readers infer something arrived. The EFF wrote in 2014 that canaries were untested in court (EFF warrant canary FAQ) and in 2016 closed its Canary Watch project, noting how hard they were to read: wording changed, updates ran late, pages vanished and returned (EFF, May 2016). Treat a canary as a weak, past-tense signal and weigh more what you can verify: where the server is, which law applies, whether the disk is ciphertext when it leaves your control. This is not legal advice.
For a server in Prishtina the statute is Kosovo's Law No. 06/L-082 on Protection of Personal Data, published in Official Gazette No. 6 on 25 February 2019 (Official Gazette of the Republic of Kosovo), modelled on the GDPR and supervised by the Information and Privacy Agency. Keeping the data in the country removes the cross-border transfer question; why buy a VPS in Kosovo covers the practical side.
Frequently asked questions
Can I enable full disk encryption on an existing VPS without reinstalling?
Yes, on Debian 13 with cryptsetup reencrypt --encrypt --reduce-device-size 32M run from the initramfs while the root filesystem is unmounted, after resize2fs -M. It needs console access, a separate unencrypted /boot partition and a tested backup. Without a separate /boot, encrypt a data disk or file container instead, or reinstall with the installer's encrypted LVM option.
How does remote unlock of a LUKS root over SSH work?
The dropbear-initramfs package puts a small SSH server into the initramfs, which runs before the root filesystem is mounted. At boot it takes an address from the kernel's ip= parameter or DHCP; you log in with a public key and run cryptroot-unlock, which prompts "Please unlock disk root_crypt:" and hands the passphrase to cryptsetup, and the boot continues.
Does LUKS encryption slow down a VPS?
On our two-vCPU Debian 13 test machine, AES-XTS capped sequential throughput near 1.7 to 1.9 GB/s writing and 2.1 to 2.3 GB/s reading, and random 4 KB IOPS fell 15 to 20 percent. Most workloads never reach the sequential cap, so the IOPS figure is the one you may notice.
Can the hosting provider read my encrypted VPS?
Not from the disk image, a backup or a replaced drive while the volume is locked. While the server runs with the volume open, the key is in the virtual machine's memory, which whoever controls the hypervisor could in principle read. LUKS protects data at rest, not a running server from its own host.
Why does Debian ignore my /etc/crypttab at boot?
The generator that turns crypttab lines into systemd units is in the separate systemd-cryptsetup package, which apt install cryptsetup did not pull in on the Debian 13 cloud image we tested. Install it and run systemctl daemon-reload.
Why does cryptroot-unlock print a timeout?
Argon2id on a 2 GB server defaults to about 850 MB of memory and takes far longer at boot than on an idle system; we measured 19 to 24 seconds for a keyfile unlock during boot. cryptroot-unlock waits ten seconds before printing "Timeout reached while waiting for PID", although the unlock still completes. cryptsetup luksConvertKey --pbkdf-memory 262144 brought ours under two seconds.
Pick the disk, we will pick the rest with you
If the disk image is your worry, Path A on any plan gives you LUKS2 encryption at rest this afternoon, and in Prishtina the free weekly backups you restore from the client area are then ciphertext too. If it has to be the whole system, take a VPS in Kosovo on VPS Mini or larger and put /boot on its own partition before you encrypt; the same plans run in Amsterdam and Dublin. Then work through the Debian 13 hardening checklist, because an encrypted disk behind a weak SSH login protects nothing. Unsure whether your image has a separate /boot? Send us the output of lsblk on Telegram and we will say which path fits.