Every Linux account is really three things wearing one name: a line in /etc/passwd, a directory under /home, and — if you were careful — an encrypted volume that must be unlocked before either of those is useful.
Keeping those three in sync is a manual job, and manual jobs drift. useradd creates the account but not the encryption. A copied home directory arrives with the wrong numeric owner. A laptop gets stolen and the disk was never encrypted because "we'll do it later". And when you move a machine, the account row and the data travel separately, with a README as the integration layer.
systemd-homed takes a different position: the account and the home directory are the same object. One signed JSON record describes the user; one self-contained home area holds the data. Copy the object, and the account travels with it. Encrypt it with LUKS2, and the encryption key is the login password — no second secret, no /etc/crypttab entry, no initramfs hook.
This is a practical tour of that model, based on the documented interface in the current upstream man pages (systemd 258 and newer). Version-gated features are called out; check yours with homectl --version.
What systemd-homed is — and what it is not
systemd-homed.service manages regular, human accounts only. It explicitly refuses classic accounts created by useradd, and it is not for system accounts (UID below 1000) or root.
The core ideas:
-
The user record is authoritative and signed. It lives inside the home area as
~/.identityand contains everything NSS normally spreads across/etc/passwdand/etc/shadow: name, UID, groups, shell, environment, resource limits, SSH keys. - The home area is self-contained. For the LUKS2 backend that means a GPT-partitioned image file, a LUKS2 volume inside it, a filesystem inside that, and one directory holding the user's files. The LUKS2 header also carries an encrypted copy of the user record, so the image can be validated before it is mounted.
-
Accounts are synthesized at runtime. Users do not appear in
/etc/passwd. They are exposed through the user/group record API and glibc NSS, sogetent passwd waldoworks,id waldoworks, anduserdbctl user waldoworks — but a script that greps/etc/passwdwill not see them. -
Activation is a login-time operation. PAM unlocks the home when the user authenticates and mounts it at
/home/$USER; it is unmounted after the last session ends.
That last point is the mental model shift. An inactive home is not "a directory you forgot to mount" — it is ciphertext on disk. Root cannot read it either. That is the feature, and also the operational constraint.
Pick a storage backend
homectl create --storage= accepts five values. The trade-offs are not subtle:
| Backend | What it is | Encryption | Notes |
|---|---|---|---|
luks |
LUKS2 volume in a loopback file (or on a USB stick), filesystem inside | Strong; password is the LUKS passphrase | Recommended. Default on unencrypted /home
|
fscrypt |
Directory encrypted with fscrypt, /home/user.homedir
|
Weaker; filesystem metadata is unprotected | Primarily ext4; password cannot be changed after creation |
subvolume |
btrfs subvolume per user | None | Good quota support |
directory |
Plain directory /home/user.homedir
|
None | Fallback everywhere |
cifs |
Windows/Samba share mounted at login | Depends on the server | Portable across machines via SMB |
If you do not specify a backend, homed.conf(5) decides. The documented default logic: if the system is not a container and /home is not itself encrypted, use luks; otherwise use subvolume on btrfs, else directory. The default filesystem inside a LUKS home is btrfs, configurable with DefaultFileSystemType=.
You can set site defaults in /etc/systemd/homed.conf:
[Home]
DefaultStorage=luks
DefaultFileSystemType=btrfs
Drop-ins under /etc/systemd/homed.conf.d/*.conf override it. Keep the main file vendor-clean and put local policy in a drop-in — the same rule as every other systemd configuration file.
Enable the service and create the first account
On many distributions systemd-homed ships installed but disabled, because it changes how human accounts work. Check before assuming:
systemctl status systemd-homed.service
command -v homectl
If it is not running:
sudo systemctl enable --now systemd-homed.service
Now create a user. The important flag here is --disk-size: for the LUKS backend the default is 85% of the available space on the backing filesystem, which is almost never what you want on a shared machine.
sudo homectl create waldo \
--real-name="Waldo McWaldo" \
--storage=luks \
--fs-type=btrfs \
--disk-size=20G \
--member-of=wheel
Replace wheel with your distribution's admin group (sudo on Debian/Ubuntu, wheel on Fedora/Arch). Groups must already exist; homed does not create auxiliary groups. It does always create a matching group with the same name and GID as the user's UID.
homectl will prompt for the new password twice, enforce the system password-quality policy, and use that same password as the LUKS passphrase. There is no --password-file option — user creation is intentionally interactive. For image-based provisioning, the documented automation path is homectl firstboot with home.create.* service credentials (systemd 256+).
Verify the result from four angles, because each one exercises a different layer:
sudo homectl list # homed's own view
sudo homectl inspect waldo # record + runtime state
getent passwd waldo # NSS synthesis
userdbctl user waldo # the JSON record behind it
userdbctl services | grep -i home # should include io.systemd.Home
If getent returns nothing, NSS is not wired up — usually nss-systemd missing from /etc/nsswitch.conf. Without it, logins may work while every other tool insists the user does not exist.
Look at what actually landed on disk
This is where homed stops being magic. Deactivate the home first so you are inspecting a cold image, not one already attached by the service:
sudo homectl deactivate waldo
ls -lh /home/waldo.home
sudo losetup -Pf --show /home/waldo.home
# prints e.g. /dev/loop12
lsblk -f /dev/loop12
sudo cryptsetup luksDump /dev/loop12p1
sudo losetup -d /dev/loop12
The documented layout is specific, and worth knowing when something goes wrong:
- A GPT partition table with a single partition whose type UUID is
773f91ef-66d4-49b5-bd83-d683bf40ad16and whose label is the username. - Inside it, a LUKS2 volume labelled with the username, carrying a LUKS2 token of type
systemd-homed. The token holds the encrypted JSON user record and its IV. - Inside that, ext4/btrfs/xfs labelled with the username, containing one directory named after the user. That directory becomes
/home/waldowhen active.
The .home suffix is dropped at activation: /home/waldo.home is the container, /home/waldo is the mount point. That is why you must not already have a /home/waldo directory when you create the account.
The GPT wrapper is not decoration. It makes the image recognizable to partition tools and to other machines, and it lets the same file be written directly to a USB stick and used as a portable home.
What happens when the user logs in
The flow, as documented:
- PAM (
pam_systemd_home) authenticates the user. For LUKS homes, the login password derives the key that unlocks the volume. - homed compares the user record on the host, the copy in the LUKS2 header, and
~/.identityinside the home. They must describe the same user and be signed by a recognized key. IflastChangeUSecdiffers, the newest record wins and is propagated to the other two. - The filesystem checker runs before mount.
- The filesystem is mounted at
/home/$USER, withnodevandnosuidby default (noexecis off by default). - If the on-disk ownership does not match the locally assigned UID, the tree is recursively
chown()ed before login completes. - On the last logout, the home is unmounted and trimmed or shrunk according to the record's settings.
Check that PAM is actually configured before you rely on any of this:
grep -R pam_systemd_home /etc/pam.d/ 2>/dev/null
If that returns nothing, your PAM stack will never call homed — and the failure mode is confusing: the account resolves via NSS, authentication fails, and nothing obviously points at PAM. Most distributions that ship homed also ship the PAM wiring; if yours does not, copy the exact fragment from pam_systemd_home(8) rather than improvising. Test on a second TTY or in a VM, and keep a root shell open: a broken PAM stack can lock you out of every login path at once.
For a quick test without logging out, activate manually. It prompts for the password and leaves the home mounted until you deactivate it:
sudo homectl activate waldo
ls -la /home/waldo
sudo homectl deactivate waldo
Day-two operations
Change the password. For LUKS homes this rewrites the LUKS keyslot, so it is genuinely a re-key, not just an /etc/shadow edit:
sudo homectl passwd waldo
Add a recovery key before you need it. This is the difference between "forgot password" and "lost data":
sudo homectl update waldo --recovery-key=yes
The key is printed once. Print it, store it offline, and treat it like the LUKS recovery key it is. There is no server-side escrow.
Add a hardware token. FIDO2 support requires the hmac-secret extension (most current YubiKeys qualify). Client PIN and user presence default to on; user verification defaults to off and is often unnecessary:
sudo homectl update waldo --fido2-device=auto
sudo homectl update waldo --fido2-device=list # if auto cannot decide
PKCS#11/PIV tokens are supported through --pkcs11-token-uri=, including the list and auto shortcuts. Both mechanisms can coexist with the password.
Resize the home. Growth works while the user is logged in; shrinking depends on the filesystem:
sudo homectl resize waldo 30G
sudo homectl resize waldo min
With ext4 inside LUKS, you must deactivate before shrinking. btrfs can grow and shrink online. xfs cannot shrink at all, which is why the man page calls it "not recommended" for this backend. On LUKS+btrfs, --auto-resize-mode=shrink-and-grow is the default: the home expands to the configured size at login and compacts at logout.
Cap resources per user. These map onto the user's slice unit, not onto a shell profile:
sudo homectl update waldo --memory-max=8G --cpu-weight=100 --tasks-max=4096
--tasks-max controls TasksMax= on user-$UID.slice; --memory-high/--memory-max and --cpu-weight/--io-weight map to the corresponding cgroup settings. --rlimit= and --nice= are also available. This is the cleanest way to stop one interactive user from starving a build agent on the same box.
Lock without logging out. Useful around suspend, and only defined for LUKS homes:
sudo homectl lock waldo
sudo homectl unlock waldo # re-authenticates
sudo homectl lock-all # typically invoked on system suspend
To get automatic locking at suspend for graphical sessions, set suspend=yes on the PAM module. It only works if the session can re-authenticate on resume — a lock screen that runs outside the user's context. TTY sessions cannot, and will appear hung until the home is unlocked elsewhere.
Run a command against an inactive home. This is the supported backup hook:
sudo homectl with waldo -- tar -C /home/waldo -czf /backup/waldo-$(date +%F).tgz .
The command runs under your identity, not the user's, but the home is activated for its duration and deactivated afterwards. It requires the user's authentication, by design.
Moving a home area to another machine
The data move is anticlimactic: the image is self-describing.
sudo homectl deactivate waldo
sudo rsync -a /home/waldo.home root@newhost:/home/
What does not travel automatically is trust. User records are signed, and a host only accepts records signed by keys it knows. Two documented options:
# On the old host, authorize its signing key on the new one
sudo homectl get-signing-key | \
ssh root@newhost 'homectl add-signing-key --key-name="oldhost.public"'
Or copy the public key directly. The key directory is /var/lib/systemd/home/, and any *.public file there is accepted:
sudo scp /var/lib/systemd/home/local.public \
root@newhost:/var/lib/systemd/home/oldhost.public
To replicate the account definition onto another host without moving data (for example, a CIFS-backed account), export a stripped record and create from stdin:
homectl inspect -EE waldo | ssh root@newhost homectl create -i-
-E strips binding and runtime fields; -EE also strips the signature, so the record is re-signed by the destination. That command creates a new, empty home with the same settings. It is not a data migration — move the .home file for that.
Gotchas that will bite you
-
The 85% default. A new LUKS home can claim most of the free space on
/home. Always pass--disk-size, and use--rebalance-weight=if several users share a pool. -
Utilities that read
/etc/passwddirectly will not see homed users. Shells,getent,id, and PAM work. Hand-rolled scripts that parse the file do not. This includes some installers, monitoring agents, and container images that bake in the host's passwd file. -
The UID is a local binding, not an identity. The same home can receive a different UID on a different machine, and the tree is
chown()ed at activation. Numeric UIDs in ACLs, NFS exports, or backups are therefore not portable. - fscrypt is the weak option. It encrypts file contents and names but not most metadata, primarily targets ext4, and cannot change the password after creation. Use it when LUKS is impossible, not because it is easier.
-
Discard on loopback files can overcommit.
--luks-discard=yeslets the inner filesystem punch holes in the sparse image. If the backing filesystem fills, the upper filesystem gets I/O errors. The default is off for loopback files and on for real block devices — leave it alone unless you understand the trade. - xfs cannot shrink. Growing is fine; shrinking is not supported. If you want elastic homes, use btrfs (or ext4 with the deactivate-first rule).
-
Do not mix classic and homed accounts with the same name. homed will not adopt a
useraddaccount, and an existing/home/waldodirectory blocks creation. Migrating a real user means creating the homed account under a clean name, copying data in, and removing the old account deliberately. -
SSH needs PAM. If
sshd_confighasUsePAM no,pam_systemd_homenever runs, and password logins against a homed account fail even though the account resolves. Also note that theuser%arealogin syntax is not understood by ssh. -
Encryption is not backup.
homectl remove waldodeletes the record and the home area. There is no undelete, and root cannot decrypt a lost-password home without a recovery key or token.
When not to use it
systemd-homed is a workstation and laptop tool. It is a poor fit for:
- servers driven by configuration management that expects
/etc/passwdto be the source of truth; - environments with LDAP/AD/SSSD, where identity already comes from a directory;
- NFS-mounted home directories, shared service accounts, and CI runners;
- containers, where the host's user database is not the container's.
For a fleet of developer laptops, though, it solves a real problem: the account is the encrypted home, it survives a reinstall, and it does not depend on a provisioning script remembering to run chown -R.
Verify, then adopt
If you want to try this without risking your daily driver, the safe sequence is: a VM, one throwaway account, --storage=luks with an explicit small --disk-size, a recovery key, and a login test on a second TTY. Then inspect the image with losetup and cryptsetup luksDump so the abstraction stops being opaque.
The payoff is not fewer commands. It is that the account, the encryption, and the data stop being three things that can disagree.
Sources
-
homectl(1)— https://man7.org/linux/man-pages/man1/homectl.1.html -
systemd-homed.service(8)— https://man7.org/linux/man-pages/man8/systemd-homed.service.8.html -
homed.conf(5)— https://man7.org/linux/man-pages/man5/homed.conf.5.html -
pam_systemd_home(8)— https://man7.org/linux/man-pages/man8/pam_systemd_home.8.html -
userdbctl(1)— https://man7.org/linux/man-pages/man1/userdbctl.1.html -
nss-systemd(8)— https://man7.org/linux/man-pages/man8/nss-systemd.8.html - Home directory format — https://systemd.io/HOME_DIRECTORY/
- JSON user records — https://systemd.io/USER_RECORD/
- User/group record API — https://systemd.io/USER_GROUP_API/
Top comments (0)