Some private data is large, but a surprising amount of it is not. Configuration files, recovery notes, certificates, scans, source code and personal records may fit in a few megabytes. The awkward part is carrying those files safely. I may want copies on several disks, a USB stick, cloud storage or, when the size permits, an email attachment. I do not want every place holding a copy to become a place I must trust.
My solution is efs, a small Python tool that creates an ext4 filesystem inside one LUKS2-encrypted file. The container is an ordinary file while it is closed. It can be renamed, copied and uploaded like any other file, but its contents are not readable without the passphrase. I open it only on a Linux computer I trust.
The source and current instructions are available at github.com/lonezor/efs.
The Basic Idea
The workflow has two distinct states.
Closed: private.efs is an encrypted binary file. This is the state used for transport and storage.
Open: Linux unlocks the container and mounts its ext4 filesystem at a local directory. Files can be read and changed normally until the container is closed again.
The decrypted filesystem is provided through Linux device mapper. The tool does not create a second plaintext image beside the encrypted file. This matters when the container itself lives on removable or synchronized storage.
Install the Requirements
On Debian, Ubuntu and Raspberry Pi OS, install the required tools:
sudo apt update
sudo apt install git python3 cryptsetup e2fsprogs util-linux zstd
Download efs:
git clone https://github.com/lonezor/efs.git
cd efs
The script uses sudo for the operations that create a device-mapper mapping or mount a filesystem. It is best to run the script from a normal account that has sudo access rather than starting a root shell.
Create a Small Container
This creates an 8 MiB container in the current directory:
./efs.py create private.efs 8
Eight MiB is the minimum size supported by the tool. Around 2 MiB is used by the compact LUKS2 header, leaving the remainder for ext4. That is enough for many small collections of text files, keys or documents. Use a larger number when needed; for example, 1024 creates a 1 GiB container.
For containers up to and including 64 MiB, cryptsetup warns that the keyslots area is very small. This is expected: efs deliberately reserves a compact 1 MiB keyslot area so that tiny containers remain useful. It limits the number of additional passphrases that can be added later, but it does not reduce the encryption or Argon2id strength of the passphrase in use. Containers larger than 64 MiB use cryptsetup’s normal keyslot area.
The command asks for a new passphrase and verifies it. Use a long, unique passphrase. LUKS2 protects it with Argon2id, which makes each password guess deliberately expensive, but it cannot turn a short or reused password into a strong one.
To create the container directly on a mounted USB drive, give its complete path:
./efs.py create /media/alice/USB/private.efs 8
The resulting private.efs is the file to copy or upload. Its size is fixed when created, which makes storage requirements predictable.
Open It on a Trusted Linux Computer
Open the container by passing its current location:
./efs.py open /media/alice/USB/private.efs
Enter the passphrase when prompted. The command prints the local mountpoint, which has a form such as:
/mnt/efs-0123456789abcdef0123456789abcdef
That mountpoint is deliberately outside the USB or cloud-sync directory. Otherwise, a synchronization program might see the mounted plaintext files and upload them. The long name comes from the container’s LUKS UUID, so moving or renaming the encrypted file does not change its identity.
An easier name can be supplied explicitly. Create a local directory outside synchronized storage, then use it as the mountpoint:
mkdir -p "$HOME/mnt/private"
./efs.py open /media/alice/USB/private.efs "$HOME/mnt/private"
Files can now be managed with the file manager, editor or ordinary shell commands:
cp important-notes.txt "$HOME/mnt/private/"
ls -la "$HOME/mnt/private"
While the container is open, applications on that computer can access its files according to normal Linux permissions. This is why I only open it on systems I trust.
Close It Before Moving or Copying It
Close the container using the encrypted file’s path:
./efs.py close /media/alice/USB/private.efs
The command unmounts the filesystem and removes its device-mapper mapping. After it reports success, the USB drive can be unmounted or the container can be copied elsewhere.
Always close the container before copying it, uploading it or disconnecting its storage. Copying a live filesystem image can capture file data and filesystem metadata at different moments, producing an inconsistent copy. For the same reason, pause cloud synchronization while the container is open and let synchronization complete after it is closed.
A simple routine is therefore:
copy or download → open → read and update → close → copy or upload
Keep one authoritative working copy at a time. Byte-for-byte copies carry the same LUKS UUID and should not be opened simultaneously on the same Linux computer. More broadly, editing separate copies creates the same conflict problem as editing any file independently in several places.
Pack a Small Container for Transport
A newly created container can compress surprisingly well. In one test, a new 8 MiB container packed to about 1.2 MiB with gzip. The exact result varies, but the minimum-size 8 MiB case is where this saving is usually most useful—for example, when fitting the container into an email attachment or keeping many portable copies.
The LUKS header and every filesystem block that has been written contain ciphertext, which is effectively incompressible, but the unused part of the outer file is still mostly zero-filled. As more filesystem blocks are used, the packed file grows and the saving becomes smaller.
First close the container, then pack it:
./efs.py close private.efs
./efs.py pack private.efs
When Zstandard is installed, this creates private.efs.zst. If it is unavailable, efs falls back to gzip and creates private.efs.gz. The original .efs file is removed only after the archive passes an integrity check. The packed file remains encrypted, so it is the file I can put on USB sticks, upload to cloud storage or attach to email.
Before opening the container on a trusted Linux computer, restore it:
./efs.py unpack private.efs.zst
./efs.py open private.efs
The packed archive is removed only after the restored file has been recognized as a valid LUKS container. The complete packed workflow is:
copy or download → unpack → open → update → close → pack → copy or upload
Automatic packing is limited to containers of 64 MiB or less. Larger containers take longer to process and usually save less space as they fill with encrypted data. ./efs.py pack private.efs --force-large overrides the limit when compression is still worthwhile. --format gzip can be used when the receiving Linux computer does not have Zstandard.
A packed archive cannot be opened or grown directly. Unpack it first. Packing also reveals something through the resulting archive size: an observer may be able to estimate roughly how much of the container has ever been written, although they still cannot read its contents. Email services add their own attachment encoding overhead, so check the final message size.
Growing the Container
If the collection outgrows its original capacity, first close the container. Then give grow the new total size in MiB:
./efs.py grow private.efs 64
This expands both the outer LUKS2 file and the ext4 filesystem inside it. Growing is an offline operation: the tool refuses to proceed while the container is open. It also refuses to shrink a container, because safe shrinking requires a more complicated sequence and is much easier to get wrong.
If growth is interrupted after the outer file has already expanded, run the same command again with the same size. The operation checks ext4 and completes the filesystem expansion.
What This Protects
The container uses LUKS2, AES-XTS and an Argon2id-protected passphrase. Its main purpose is confidentiality at rest: someone who finds a USB stick, receives an attachment accidentally or gains access to a cloud-storage copy should not be able to read the stored files without guessing the passphrase.
It does not hide the container’s size or timestamps. Regular LUKS2 storage also does not authenticate every encrypted disk sector, so it is not intended to prove that a hostile party has never modified the binary container. It cannot protect data from malware, an administrator or another sufficiently privileged process while the filesystem is open.
Encryption is not a backup either. A damaged LUKS header, failed drive, accidental deletion or overwritten cloud copy can still destroy the data. I keep several closed copies and make sure at least one recent copy is outside the device I am currently using.
Within those limits, the format is deliberately ordinary: one encrypted file, a strong passphrase and standard Linux storage tools. That makes it useful even when the private dataset is tiny, and it lets the untrusted places holding the closed container remain uninteresting.