Linux

Encrypt everything: VeraCrypt alternatives on Linux, tested on a cow’s secrets

Whole milk heavy stuff, digest slowly

LUKS, gocryptfs, age and GnuPG to encrypt disks, folders and files on Linux, plus scp, magic-wormhole and rclone crypt to send them, with every command really run on a cow’s most (un)important data.

cowsay

Ercolina the cow has data. Precious data. Data that, in the wrong hands, could bring down governments: a secret recipe, a password, a diary, a tiramisu, and her fantasy football lineup. Today we encrypt all of it, on Linux, without VeraCrypt, and then we send some of it to her friend Pina without the postman, the cloud or the Wi-Fi at the café being able to read a single word.

This is a whole-milk post: lots of terminal. Every command was really run, on Debian 13, with cryptsetup 2.7, gocryptfs 2.5, age 1.2, GnuPG 2.4, magic-wormhole 0.18 and rclone 1.60, on two machines called barn (Ercolina’s) and farm (Pina’s). The outputs are the real ones; we only cut a few lines of noise from our test containers, and the passphrases you see are for demonstration only. Please don’t use correct-horse-battery-moo. It is now the least secret passphrase on the internet.

First: what’s wrong with VeraCrypt?

Nothing, really. VeraCrypt is a fork of TrueCrypt, first released in June 2013 by Mounir Idrassi of IDRIX. A year later, on 28 May 2014, TrueCrypt was discontinued in mysterious circumstances, and VeraCrypt became the obvious heir. It was audited by QuarksLab in 2016, it fixed what the audit found, and it also runs on Linux.

But on Linux, VeraCrypt is a guest. The house already has its own tools, built into the kernel or packaged by your distribution, each one specialised: one for disks, one for folders, one for single files, one for sending. And as a bonus, cryptsetup, the first tool we’ll see, can even open your old TrueCrypt and VeraCrypt volumes (cryptsetup open --type tcrypt), so moving away doesn’t mean leaving anything behind.

🐄
The cow says: a good tool that does everything is fine. Four small tools that each do one thing well is Linux. Pick by job, not by habit.

What encryption protects you from (and what it doesn’t)

Before typing anything, the question nobody asks: who are you hiding from? Disk encryption protects data at rest: a laptop stolen while switched off, a USB stick forgotten on a train, a disk thrown away without wiping it. Whoever finds it sees noise.

It does not protect a computer that is switched on and unlocked. Once you’ve typed the passphrase, the data is readable by you, by your programs, and by any malware running as you. Encryption is a lock on the door, not a bodyguard in the living room.

🐄
Leaving the laptop unlocked at the café while you get a cappuccino, “because the disk is encrypted anyway”. Wrong. An unlocked encrypted disk is an open book with a very expensive cover.

LUKS: the safe built into Linux

LUKS (Linux Unified Key Setup) was created by Clemens Fruhwirth in 2004 and is the standard for disk encryption on Linux. The tool you use is cryptsetup; the actual encryption is done by the kernel’s dm-crypt. It’s the closest thing to a VeraCrypt volume, and since cryptsetup 2.1 the default format is LUKS2.

The easiest way: at install time

If you’re installing Linux on a laptop, the best encryption is the one you don’t have to think about. Debian’s installer, in guided partitioning, offers to set up the whole disk with encrypted LVM, and Ubuntu’s installer has an equivalent option. One passphrase at boot, and your data is encrypted.

🐄
Encrypting the laptop disk when you install the system. Right. It costs one tick in the installer and a passphrase at every boot. Doing it afterwards costs a weekend and a backup.

A safe in a file

For the VeraCrypt-style experience, a file that contains an encrypted disk, LUKS works on an ordinary file too. Everything in this section runs as root (or put sudo in front). First, we create a 64 MB file and format it as LUKS:

# fallocate -l 64M secrets.img
# cryptsetup luksFormat secrets.img

WARNING!
========
This will overwrite data on secrets.img irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for secrets.img:
Verify passphrase:

Then we open it (it appears as a virtual disk in /dev/mapper/vault), create a filesystem, mount it and put Ercolina’s most precious document inside: the secret recipe for milk.

# cryptsetup open secrets.img vault
Enter passphrase for secrets.img:
# mkfs.ext4 -q /dev/mapper/vault
# mkdir -p /mnt/vault && mount /dev/mapper/vault /mnt/vault
# echo '1. Eat grass. 2. Wait. 3. Milk.' > /mnt/vault/secret_recipe.txt
# ls /mnt/vault
lost+found  secret_recipe.txt
# umount /mnt/vault
# cryptsetup close vault

Yes, the recipe for milk is “eat grass, wait”. Every cow knows it. It is still the most sensitive document in the barn, and it deserves military-grade encryption. Now let’s check that the file really doesn’t contain it in clear text:

# grep -c 'Eat grass' secrets.img
0
# xxd -l 16 secrets.img
00000000: 4c55 4b53 babe 0002 0000 0000 0000 4000  LUKS..........@.

Zero matches. The only readable thing is the magic signature LUKS at the beginning: the file openly says it’s encrypted, but not what it contains. Let’s look inside the header:

# cryptsetup luksDump secrets.img
LUKS header information
Version:        2
[...]
Data segments:
  0: crypt
        offset: 16777216 [bytes]
        length: (whole device)
        cipher: aes-xts-plain64
        sector: 4096 [bytes]

Keyslots:
  0: luks2
        Key:        512 bits
        Priority:   normal
        Cipher:     aes-xts-plain64
        Cipher key: 512 bits
        PBKDF:      argon2id
        Time cost:  21
        Memory:     238814
        Threads:    4
[...]

Two things worth knowing. The data is encrypted with AES in XTS mode, with a 512-bit key (two 256-bit AES keys, as XTS requires). And your passphrase doesn’t encrypt the data directly: it goes through Argon2id, a deliberately slow and memory-hungry function, to unlock a keyslot that contains the real key. cryptsetup asks Argon2id for up to 1 GiB of memory and about two seconds of work for each attempt; our small test server had less free memory, so it settled for about 233 MB. That’s what makes guessing passphrases painful for an attacker: every single attempt costs them the same.

Spare keys and the header

Since the real key is separate from the passphrase, you can have several passphrases (LUKS2 has up to 32 keyslots), for example a spare one to keep in a safe place. And you should back up the header:

# cryptsetup luksAddKey secrets.img
Enter any existing passphrase:
Enter new passphrase for key slot:
Verify passphrase:
# cryptsetup luksDump secrets.img | grep -E '^  [0-9]: luks2'
  0: luks2
  1: luks2
# cryptsetup luksHeaderBackup secrets.img --header-backup-file secrets-header.img
# du -h secrets-header.img
16M     secrets-header.img

The cryptsetup manual is blunt: if the header of a LUKS volume gets damaged, all data is permanently lost unless you have a header backup. A few corrupted kilobytes at the start of the disk, and the rest is noise forever. Keep the backup somewhere else, and remember it opens with the passphrases that were valid when you made it.

🐄
Backing up the LUKS header and keeping it outside the encrypted disk. Right. It’s 16 MB that stands between you and a disk full of perfectly encrypted, perfectly useless random bytes.

gocryptfs: an encrypted folder, file by file

LUKS has a problem with the cloud. If you put a 1 GB encrypted container in Dropbox and change one line in one file, the sync tool sees a modified 1 GB blob. gocryptfs solves this by encrypting each file separately, names included. It was inspired by EncFS and set out to fix its security issues; it reached version 1.0 in July 2016, and its cryptographic design was audited by Taylor Hornby in 2017. It runs in user space via FUSE: no root needed.

$ mkdir vault.enc vault
$ gocryptfs -init vault.enc
Choose a password for protecting your files.
Password:
Repeat:

Your master key is:

    f04b5a2c-e9d3d500-d8069602-af81a998-
    9ae78829-586598a1-205edcf1-8bd6df41

If the gocryptfs.conf file becomes corrupted or you ever forget your password,
there is only one hope for recovery: The master key. Print it to a piece of
paper and store it in a drawer. This message is only printed once.
The gocryptfs filesystem has been created successfully.
You can now mount it using: gocryptfs vault.enc MOUNTPOINT

Yes, we just published our master key on the internet. It protects a cow’s diary, and we are prepared to live with the consequences. You, on the other hand, should do exactly what it says: print it and put it in a drawer, because if you forget the password, it’s the only way back.

Now we mount it and move in Ercolina’s second most precious data: her only password, her diary and a photo album.

$ gocryptfs vault.enc vault
Password:
Decrypting master key
Filesystem mounted and ready.
$ echo 'moo123' > vault/passwords.txt
$ echo 'Dear diary, today I ate grass.' > vault/diary.txt
$ mkdir vault/photos && echo jpeg > vault/photos/me_in_the_meadow.jpg
$ ls vault
diary.txt  passwords.txt  photos
$ ls vault.enc
IOH1OaNoHynzsrt6kGTruQ  gocryptfs.conf  s-en7POhG6H0wPSm0d2v_Q
gaJDrpg_xigkGQ4N-KTB-A  gocryptfs.diriv
$ fusermount -u vault
$ ls vault
$

On the left, in vault, you work normally. On the right, in vault.enc, there’s what you sync to the cloud: unreadable names, encrypted content, plus a configuration file. Unmount, and vault is an empty folder again.

Note that the photo me_in_the_meadow.jpg is five bytes long and contains the word “jpeg”. Ercolina is not a great photographer. It is encrypted anyway, because encryption doesn’t judge.

🐄
Choosing moo123 as a password and then protecting it with AES-256. Wrong. The strongest lock in the world, with the key under the doormat. A passphrase of several random words, the famous “correct horse battery staple” from xkcd, is easier to remember and much harder to guess.

gocryptfs is also honest about what it doesn’t hide, and it says so in its threat model: whoever holds the encrypted folder can compute the size of each file from its encrypted size, and a cloud provider can see which parts of a file change when you write. The content is safe; the shape isn’t completely.

🐄
gocryptfs on Dropbox, Google Drive or Nextcloud. It depends. The content is protected, but the number of files, their sizes and when you change them are visible. For a diary, fine. If the mere existence of a known set of files is the secret, use something else.

age: one file, zero drama

Sometimes you don’t want a disk or a folder: you want to encrypt one file. Here VeraCrypt has nothing to offer, and on Linux there’s a small modern tool that does exactly this: age, designed by Ben Cartwright-Cox and Filippo Valsorda. Its motto is small explicit keys and no config options. With a passphrase:

$ echo 'Milk: 2 litres. Grass: unlimited. Fantasy football: bench Ercolina.' > shopping_list.txt
$ age -p -o shopping_list.txt.age shopping_list.txt
Enter passphrase (leave empty to autogenerate a secure one):
Confirm passphrase:
$ head -3 shopping_list.txt.age
age-encryption.org/v1
-> scrypt 389PovIwgB3dwl239zZMeQ 18
8TZe4/qJXGrBcpEFCB6g1UGMe/HROQa4MrQ1BrXmanc
$ age -d shopping_list.txt.age
Enter passphrase:
Milk: 2 litres. Grass: unlimited. Fantasy football: bench Ercolina.

The shopping list contains milk (an obvious conflict of interest), grass, and a fantasy football decision that could destroy a friendship. Now it’s safe. If you leave the passphrase empty, age generates a good one for you:

$ age -p -o diary.age shopping_list.txt
Enter passphrase (leave empty to autogenerate a secure one):
age: using autogenerated passphrase "decade-lawsuit-hair-devote-stumble-never-sister-health-menu-bicycle"

Ten random words, much better than anything a cow can invent. But the real strength of age is keys: a public key you can give to anyone, and a private key you keep:

$ age-keygen -o key.txt
Public key: age18cvftg9ppuq7nsgs3axwpmds4xp90qhh6sx6qunnyqp5gxwyp4hq4v8cmw

Whoever has your public key can encrypt files that only you can open. Which brings us to the most interesting part.

Sending encrypted data

Encrypting data for yourself is half the job. The other half is sending it to someone, through channels that by definition you don’t control: email, chat, cloud, a USB stick in an envelope. The idea is always the same: encrypt before sending, with a key that only the recipient has, and then the channel doesn’t matter.

age + SSH keys + scp

Here’s the trick that makes age delightful: it can encrypt to an SSH public key. Many people already have one, and if they use GitHub it’s even public: age -R accepts the list at https://github.com/USERNAME.keys. Pina gives Ercolina her ~/.ssh/id_ed25519.pub. Ercolina encrypts the tiramisu recipe and sends it with scp:

ercolina@barn:~$ cat pina.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJhPpBdu1+7R6ChAMwCWGpz5cuXvt5BTnG6xBu3clAJC pina@farm
ercolina@barn:~$ age -R pina.pub -o tiramisu.txt.age tiramisu.txt
ercolina@barn:~$ scp tiramisu.txt.age pina@farm:
tiramisu.txt.age                              100%  299   881.7KB/s   00:00

Pina, on farm, decrypts it with her SSH private key (which, being well brought up, is protected by a passphrase):

pina@farm:~$ age -d -i .ssh/id_ed25519 tiramisu.txt.age
Enter passphrase for ".ssh/id_ed25519":
TOP SECRET tiramisu
- 500 g mascarpone
- 3 eggs
- coffee
- the milk is mine, obviously

Note that scp was already encrypting the transfer. But with age the file stays encrypted after arriving: on Pina’s disk, in her backups, in an email attachment. Encrypting the channel protects the journey; encrypting the file protects it forever.

GnuPG: the grandfather, still in service

GnuPG was started by Werner Koch; version 1.0 came out in September 1999. It implements OpenPGP, the standard for encrypted and signed email. It’s more complicated than age, but it does one thing age doesn’t: signatures, to prove who wrote something. Pina creates her key pair (today’s defaults: Ed25519 for signing, Curve25519 for encryption, expiry in three years) and exports the public part:

pina@farm:~$ gpg --quick-generate-key "Pina <pina@example.com>"
About to create a key for:
    "Pina <pina@example.com>"

Continue? (Y/n) Y
[...]
public and secret key created and signed.

pub   ed25519 2025-10-08 [SC] [expires: 2028-10-07]
      FF1C6348AFD6287C041CD576DD673AF363FF991F
uid                      Pina <pina@example.com>
sub   cv25519 2025-10-08 [E]

pina@farm:~$ gpg --armor --export pina@example.com > pina.asc
pina@farm:~$ head -3 pina.asc
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEaOY48BYJKwYBBAHaRw8BAQdAujNgL3QSzKzyqnsYNFsYBzIh7bjoDTaXPyQ1

Ercolina imports it. And here comes the most important moment of the whole article:

ercolina@barn:~$ gpg --import pina.asc
gpg: key DD673AF363FF991F: public key "Pina <pina@example.com>" imported
gpg: Total number processed: 1
gpg:               imported: 1
ercolina@barn:~$ gpg --fingerprint pina@example.com
pub   ed25519 2025-10-08 [SC] [expires: 2028-10-07]
      FF1C 6348 AFD6 287C 041C  D576 DD67 3AF3 63FF 991F
uid           [ unknown] Pina <pina@example.com>
sub   cv25519 2025-10-08 [E]

See that [ unknown]? GnuPG is saying: “I have a key that says it’s Pina’s. I have no idea if it really is.” Anyone can create a key with the name Pina in it. If Ercolina tries to encrypt anyway, GnuPG stops her:

ercolina@barn:~$ gpg --encrypt --armor -r pina@example.com tiramisu.txt
gpg: DAC0CC3B54FFFB3D: There is no assurance this key belongs to the named user
[...]
It is NOT certain that the key belongs to the person named
in the user ID.  If you *really* know what you are doing,
you may answer the next question with yes.

Use this key anyway? (y/N)

The right answer is not “y”. The right answer is to call Pina, and have her read out the fingerprint of her key (FF1C 6348 …). If it matches, Ercolina certifies the key locally, and from then on GnuPG trusts it. Then she encrypts and signs, with --armor so the result is text you can paste into an email:

ercolina@barn:~$ gpg --quick-lsign-key FF1C6348AFD6287C041CD576DD673AF363FF991F
[...]
The signature will be marked as non-exportable.
ercolina@barn:~$ gpg --fingerprint pina@example.com | grep uid
uid           [  full  ] Pina <pina@example.com>
ercolina@barn:~$ gpg --encrypt --sign --armor -r pina@example.com tiramisu.txt
ercolina@barn:~$ head -3 tiramisu.txt.asc
-----BEGIN PGP MESSAGE-----

hF4D2sDMO1T/+z0SAQdAzID0bafJXcqn2KUtcvXbOc+DbaYRBWPBFMBL1yMPwxow

Pina, who in turn had verified and certified Ercolina’s key, decrypts and checks the signature:

pina@farm:~$ gpg --decrypt tiramisu.txt.asc
gpg: encrypted with cv25519 key, ID DAC0CC3B54FFFB3D, created 2025-10-08
      "Pina <pina@example.com>"
Passphrase:
TOP SECRET tiramisu
- 500 g mascarpone
- 3 eggs
- coffee
- the milk is mine, obviously
gpg: Signature made Wed Oct  8 10:20:00 2025 UTC
gpg:                using EDDSA key 554769C545F4065054DC37485531F5071B2CF778
gpg: Good signature from "Ercolina <ercolina@example.com>" [full]

“Good signature”: the tiramisu really comes from Ercolina, and nobody changed it on the way. It’s also, legally, a written statement that the milk is hers. Pina may want to consult a lawyer.

🐄
Checking the fingerprint of a key by phone or in person before trusting it. Right. Encryption with the wrong key is a beautifully sealed envelope delivered to the con artist.
🐄
Sending the encrypted file and its password in the same email. Wrong. It’s like locking the safe and taping the combination to the door. The password goes by another route: a phone call, a message, a carrier pigeon.

magic-wormhole: from computer to computer, with a code

And if you just need to send a file to someone, right now, with no keys to exchange? magic-wormhole, by Brian Warner, uses a short code you can read out over the phone:

ercolina@barn:~$ wormhole send lineup.txt
Sending 61 Bytes file named 'lineup.txt'
Wormhole code is: 2-midsummer-ahead

On the other computer, please run:

wormhole receive 2-midsummer-ahead

Sending (->relay:tcp:magic-wormhole-transit.debian.net:4001)..
100% 61.0/61.0 [00:00<00:00, 115kB/s]
File sent.. waiting for confirmation
Confirmation received. Transfer complete.

(It also prints a QR code, which we’ve spared you.) Pina types the code, with tab completion for the words:

pina@farm:~$ wormhole receive
Enter receive wormhole code: 2-midsummer-ahead
 (note: you can use <Tab> to complete words)
Receiving file (61 Bytes) into: 'lineup.txt'
ok? (Y/n): y
Receiving (->relay:tcp:magic-wormhole-transit.debian.net:4001)..
100% 61.0/61.0 [00:00<00:00, 258kB/s]
Received file written to lineup.txt
pina@farm:~$ cat lineup.txt
Fantasy football lineup: Ercolina captain. Do NOT tell Pina.

Behind the scenes the two computers use a PAKE (SPAKE2): the short code becomes a strong shared key, and everything that passes through the relay servers is end-to-end encrypted. The code works only once, and an attacker gets a single attempt with a 1 in 65,536 chance; a failed guess shows up as an error, so you notice.

The file arrived safely, encrypted end to end, unreadable by anyone in between. Unfortunately it says “Do NOT tell Pina”, and it was sent to Pina. Encryption protects the message from everyone except the recipient. Choosing the recipient is still up to you.

🐄
Reading the wormhole code out over the phone, or saying it to the person sitting next to you. Right. Short, single-use, and useless five minutes later. The cow still recommends checking who you’re sending things to.

rclone crypt: the cloud sees only noise

Last case: backups on a cloud you don’t trust (which, as a rule, is every cloud). rclone talks to dozens of storage services, and its crypt remote encrypts everything on your side before uploading. To try it without a cloud account, we use Pina’s machine over SFTP as the “cloud”:

ercolina@barn:~$ rclone config create farm sftp host=farm user=pina key_file=~/.ssh/id_ed25519
[farm]
type = sftp
host = farm
user = pina
key_file = /home/ercolina/.ssh/id_ed25519
ercolina@barn:~$ rclone config create secretfarm crypt remote=farm:cloud password=correct-horse-battery-moo --obscure
[secretfarm]
type = crypt
remote = farm:cloud
password = *** ENCRYPTED ***
ercolina@barn:~$ rclone copy diary secretfarm:diary
ercolina@barn:~$ rclone ls secretfarm:
       15 diary/monday.txt
        7 diary/tax_return_2025.pdf
       46 diary/tuesday.txt

Through secretfarm: Ercolina sees her diary and her tax return in clear. Here’s what farm, the “cloud”, actually stores:

ercolina@barn:~$ rclone ls farm:cloud
       94 qju1hn84i7nm8tqd5k73oaqvtg/5a13sotkaq3tdqcrafhluamiqc
       55 qju1hn84i7nm8tqd5k73oaqvtg/94uo7ujqoc3rel61oi194s2uhuplk2gn36ev8bu67s8ok00gbjr0
       63 qju1hn84i7nm8tqd5k73oaqvtg/s1oq8lj5hhci3o9jgua565pgl0
pina@farm:~$ xxd -l 16 cloud/qju1hn84i7nm8tqd5k73oaqvtg/5a13sotkaq3tdqcrafhluamiqc
00000000: 5243 4c4f 4e45 0000 5c45 40c8 0a1d f7f0  RCLONE..\E@.....

Encrypted names, encrypted content (XSalsa20-Poly1305 for the data, AES-256 for the names), and a header that starts with RCLONE. Each file is exactly 48 bytes bigger than the original: a 32-byte header plus a 16-byte authenticator for every 64 KiB block. And for Ercolina, everything works as usual:

ercolina@barn:~$ rclone cat secretfarm:diary/tuesday.txt
Tuesday: more grass. Pina looked at me funny.

The documentation is clear about the limits: sizes and modification times are visible, and so is the directory structure. Also, the password in rclone.conf is only obscured, not encrypted: anyone who reads that file can recover it. rclone can encrypt its own configuration with a password; use that.

🐄
Thinking that password = *** ENCRYPTED *** in the output means the configuration file is safe. Wrong. It’s obscured, which in password language means “wearing sunglasses”. Protect rclone.conf, or encrypt the configuration.

Which tool for which job

You want to…UseLike VeraCrypt?
Encrypt the whole laptop diskLUKS, at install timeYes, system encryption
Carry a “safe” file or a USB stickLUKS on a file or deviceYes, a volume
Encrypt a folder that syncs to the cloudgocryptfsBetter: file by file
Encrypt one file, for yourself or someone elseage (or GnuPG)No, it doesn’t do this
Encrypt and sign an emailGnuPGNo
Send a file to someone, right nowmagic-wormholeNo
Back up to a cloud you don’t trustrclone cryptNo

And one rule that applies to all of them: encrypted data without the key is indistinguishable from random noise, including for you. Before encrypting anything important, decide where the passphrase, the spare key, the gocryptfs master key and the LUKS header backup will live. Not inside the thing you’re encrypting.

🐄
Keeping the only copy of your passwords in a file inside the encrypted volume. Wrong. Ercolina did it, with passwords.txt. It’s a safe that contains its own combination. Very secure. Forever.
🐄
Moral: encrypt the laptop at install time, use gocryptfs for cloud folders and age for single files, check fingerprints before trusting a key, never send the password with the parcel, and keep a backup of every key outside the safe. The cow has spoken, and the cow is law.

Moo. 🐄

Sources

All commands were run on Debian 13 with cryptsetup 2.7.5, gocryptfs 2.5.1, age 1.2.1, GnuPG 2.4.7, magic-wormhole 0.18.0 and rclone 1.60.1; the outputs shown are the real ones, with a few lines of container noise removed.

Enjoyed it? Follow the RSS feed and never miss a post. No email, no spam: cow’s honour.

RSS feed