A non-interactive systemd password agent for encrypted disk passphrases written in Rust.
This agent watches /run/systemd/ask-password/ for password requests from systemd and automatically responds to cryptsetup requests (encrypted disk passphrases) with a configured passphrase. All other password requests are ignored.
This is useful for systems with encrypted disks that need to be unlocked automatically without user interaction, such as headless servers or embedded systems.
src/
├── main.rs # Entry point and main loop
├── parse.rs # Parsing utilities for systemd config values
├── password_request.rs # PasswordRequest parsing and PasswordRequestWatcher iterator
├── cryptsetup.rs # CryptsetupPassphraseRequest for handling cryptsetup requests
├── passphrase.rs # Passphrase derivation from device ID
└── rpifwcrypto.rs # FFI bindings to librpifwcrypto
debian/
├── control # Package metadata
├── rules # Build rules
├── changelog # Package changelog
├── copyright # Licence information
├── gbp.conf # git-buildpackage configuration
├── cargo-checksum.json # Cargo checksum placeholder
├── salsa-ci.yml # Salsa CI configuration
├── source/format # Source package format
├── cryptsetup-passphrase-agent.service # systemd service unit
├── cryptsetup-passphrase-agent.path # systemd path unit
└── cryptsetup-passphrase-agent.install # Installation paths
This project is packaged as a native Debian package and uses Debian's librust-* packages for dependencies rather than downloading crates from crates.io, ensuring an isolated and reproducible build environment.
Install build dependencies:
sudo mk-build-deps --install --removeBuild the package:
debuild -b -uc -usThe built .deb package will be created in the parent directory.
Cargo may be used directly in support of incremental builds by setting the following environment variables:
export CARGO_HOME=debian/cargo_home
export CARGO_REGISTRY=debian/cargo_registryThen use cargo as normal:
cargo build --release
cargo testThe binary will be at target/release/cryptsetup-passphrase-agent.
The agent must be run as root (or with appropriate privileges) to access /run/systemd/ask-password/ and respond to password requests.
sudo ./target/release/cryptsetup-passphrase-agentThe agent will:
- Watch
/run/systemd/ask-password/for new password request files - Parse each
ask.*file that appears - For
cryptsetup:requests only, send the configured passphrase - Ignore all other password requests
The passphrase for each encrypted device is derived using:
- Device Innate ID - A unique identifier for the block device (e.g., the CID for eMMC/SD cards)
- HMAC-SHA256 - The ID is hashed using a firmware OTP key via
rpifwcrypto
This ensures that:
- Each device gets a unique passphrase
- The passphrase cannot be derived without access to the firmware crypto service
- The raw key material never leaves the firmware
The device innate ID is retrieved using the block-device-id crate, which reads hardware identifiers from sysfs (e.g., MMC CID from /sys/block/mmcblk0/device/cid or NVMe serial numbers).
The package installs two systemd units:
A path unit that watches /run/systemd/ask-password/ and triggers the service when the directory becomes non-empty. Configured to run before cryptsetup.target:
[Unit]
DefaultDependencies=no
Before=paths.target cryptsetup.target
Conflicts=emergency.service shutdown.target
[Path]
DirectoryNotEmpty=/run/systemd/ask-password
MakeDirectory=yes
[Install]
WantedBy=sysinit.targetThe service unit that runs the agent:
[Service]
Type=simple
ExecStart=/usr/bin/cryptsetup-passphrase-agentAfter installing the Debian package:
sudo systemctl enable cryptsetup-passphrase-agent.pathThe path unit will automatically start the service when password requests appear.
The agent implements the systemd Password Agents protocol:
- Uses
inotifyto watch forIN_CLOSE_WRITEandIN_MOVED_TOevents in/run/systemd/ask-password/ - Parses
.iniformat ask files usingrust-ini - Validates requests:
- Checks if the requesting process is still alive
- Checks if the request has expired (
NotAfterfield) - Only handles
cryptsetup:requests (based onIdfield)
- Sends the passphrase prefixed with
+to the Unix datagram socket specified inSocket=
With the environment variables set (see above):
cargo testTo test the full passphrase derivation and disk unlock flow, you need a Raspberry Pi with an OTP key provisioned and a spare block device.
Ensure an OTP key is provisioned:
rpi-fw-crypto get-key-status 1Format the block device and create a single partition. In this example, we use mmcblk0 (the SD card) whilst booting from NVMe:
# Save the device's CID for passphrase derivation
cp /sys/block/mmcblk0/device/cid ./tmp_cidDerive the disk encryption passphrase using the firmware HMAC:
rpi-fw-crypto hmac --in ./tmp_cid --key-id 1 --outform hexThis prints the passphrase to stdout. Use this when setting up LUKS.
Format the partition with LUKS using the derived passphrase:
sudo cryptsetup luksFormat /dev/mmcblk0p1Enter the passphrase from step 3 when prompted.
The password agent only responds to requests from systemd services, not interactive terminal sessions. In production, /etc/crypttab entries are processed by systemd-cryptsetup-generator to create unlock services. For testing, create a service manually:
Create /etc/systemd/system/systemd-cryptsetup@test.service:
[Unit]
Description=Cryptography Setup for %I
Documentation=man:crypttab(5) man:systemd-cryptsetup-generator(8) man:systemd-cryptsetup@.service(8)
DefaultDependencies=no
After=cryptsetup-pre.target systemd-udevd-kernel.socket
Before=blockdev@dev-mapper-%i.target
Wants=blockdev@dev-mapper-%i.target
IgnoreOnIsolate=true
Conflicts=umount.target
Before=umount.target
Before=cryptsetup.target
BindsTo=dev-mmcblk0p1
After=dev-mmcblk0p1
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutSec=infinity
KeyringMode=shared
OOMScoreAdjust=500
ExecStart=/usr/bin/systemd-cryptsetup attach 'test' '/dev/mmcblk0p1' 'none' 'luks,discard'
ExecStop=/usr/bin/systemd-cryptsetup detach 'test'Reload systemd to pick up the new service, then start it:
sudo systemctl daemon-reload
sudo systemctl start systemd-cryptsetup@test.serviceCheck the agent responded successfully:
systemctl status cryptsetup-passphrase-agent.serviceThe encrypted device should now be available at /dev/mapper/test.
- Passphrases are derived at runtime using hardware-bound cryptography:
- The device's innate ID (e.g., MMC CID) is read from sysfs
- HMAC-SHA256 is computed using a firmware OTP key that never leaves the hardware
- The agent runs with root privileges to access block device information and password sockets
- The passphrase is sent over a Unix socket with restricted permissions
- The systemd service unit includes security hardening options
All Rust dependencies are satisfied by Debian's librust-* packages as specified in debian/control. This ensures the build is completely isolated from external package registries and uses only packages that have been reviewed and packaged by Debian maintainers.
block-device-id- Retrieve innate unique identifiers from block devicesnix- Safe Rust bindings to Unix APIsrust-ini- INI file parser
librpifwcrypto- Raspberry Pi firmware cryptography service
BSD-3-Clause - See LICENSE file for details.