A small Fedora-focused systemd + dracut helper that temporarily allows a LUKS2-encrypted system to unlock itself with TPM2 for the next reboot, then automatically removes the TPM unlock state after boot. Designed for unattended update/maintenance reboots without permanently enabling TPM auto-unlock.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-23 21:14:23 +02:00
dracut/91luks-reboot-once Creating project. 2026-08-26 01:39:07 +02:00
systemd Turning copr. 2026-09-22 16:53:29 +02:00
tests Fix RPM systemd scriptlets 2026-09-23 21:14:23 +02:00
.gitignore Creating project. 2026-08-26 01:39:07 +02:00
COMMERCIAL-LICENSE Added COMMERCIAL-LICENSE 2026-08-26 01:33:04 +02:00
install.sh Turning copr. 2026-09-22 16:53:29 +02:00
LICENSE Initial commit 2026-08-26 01:25:42 +02:00
luks-reboot-once Fix RPM systemd scriptlets 2026-09-23 21:14:23 +02:00
luks-reboot-once.spec Fix RPM systemd scriptlets 2026-09-23 21:14:23 +02:00
README.md Target Fedora 45 2026-09-22 22:47:19 +02:00
uninstall.sh Automatically purge managed state on uninstall 2026-09-22 17:19:01 +02:00
VERSION Fix RPM systemd scriptlets 2026-09-23 21:14:23 +02:00

luks-reboot-once

luks-reboot-once is a small Fedora-oriented systemd + dracut helper for temporarily allowing an encrypted LUKS2 system to unlock itself with TPM2 for the next boot, then removing that TPM copy again after the system starts.

It is meant for the boring case: install updates on a machine with an encrypted root filesystem, arm one unattended reboot, reboot, and go back to normal passphrase-only boot afterward.

There is no daemon and no permanent TPM auto-unlock. The normal state is deliberately boring:

  • your existing LUKS passphrase keyslot(s) remain unchanged;
  • one random managed LUKS keyslot exists permanently;
  • the matching random key file exists only inside the encrypted filesystem;
  • the TPM normally contains nothing for this project.

When armed, the managed key is copied temporarily into TPM2 NV storage. The initramfs consumes it and read-locks it for that TPM startup. A normal userspace systemd service then undefines the TPM NV index.

Status and scope

Use this at your own risk. This software modifies LUKS metadata, TPM NV state, systemd units, and initramfs images. A bug or an unusual storage/boot layout can leave a machine requiring manual recovery. Keep a working LUKS passphrase and recovery access before testing it.

This project targets Fedora 45 and was tested on Fedora Linux 45 KDE Plasma Desktop Edition Prerelease with the Fedora cryptsetup/tpm2-tools/dracut stack. It uses mechanisms common to Fedora/RHEL systems and may be adaptable to RHEL-family distributions, but Fedora 45 is the supported environment. Other distributions and initramfs systems are not supported by this repository.

The initial tested environment included:

  • Fedora Linux 45 KDE Plasma Desktop Edition Prerelease
  • cryptsetup 2.8.8
  • tpm2-tools 5.8
  • dracut 111
  • systemd 262~rc3
  • LUKS2 encrypted root
  • TPM 2.0 exposed through /dev/tpmrm0

Treat the current version as experimental even on Fedora. In particular, perform the first reboot with the normal LUKS passphrase and console/recovery access available.

Why this exists

I made this because I needed the equivalent of "allow one unattended reboot" on a Fedora machine with LUKS full-disk encryption.

Windows/BitLocker has a straightforward suspend-for-reboot workflow. I expected the same job to be easier to compose from Linux tooling, and I was surprised that I could not find a small Fedora-native utility that did exactly this without either permanently enabling TPM unlock or adding/removing a LUKS keyslot every time.

Linux already had all of the pieces: LUKS2, systemd, dracut, TPM2 NV storage, and cryptsetup-keys.d. This project is mostly glue around those existing tools.

It intentionally stays small. It is just a helper script, three templated systemd services, and a tiny dracut module.

Important: "once" is operational, not cryptographic

The name describes the intended workflow, not a hard TPM monotonic counter.

After the initramfs reads the TPM NV secret it applies READ_STCLEAR, which prevents another read during the same TPM startup. However, if the machine resets or loses power before the real-root remover service undefines the NV index, the TPM restarts, the read lock clears, and the key may be readable again on the next boot.

So the guarantee is approximately:

arm an unattended reboot; after a successful normal boot, automatically disarm it.

It is not:

this key can be cryptographically consumed exactly once regardless of crashes, resets, or power loss.

A future design using a TPM NV counter/policy could provide stronger one-shot semantics. This version deliberately does not attempt that complexity.

Security model

Normal/disarmed state

The project creates one 32-byte random key and enrolls it into a LUKS2 keyslot. The key file is stored at:

/var/lib/luks-reboot-once/<LUKS-UUID>.key

with root:root ownership and mode 0600.

The important protection is not merely the Unix permissions: that file must live inside the encrypted filesystem that it unlocks. When the machine is off and LUKS is locked, the key is therefore inaccessible from the disk itself.

The corresponding LUKS keyslot remains present, but a random 256-bit keyslot is not useful without the key material.

Armed state

luks-reboot-add@UUID.service copies the managed key into a TPM2 NV index using owner-authorized read/write access and READ_STCLEAR/WRITE_STCLEAR behavior. It is deliberately not PCR-bound.

That is intentional for maintenance/update reboots: kernel, shim, Secure Boot database, firmware, and boot-chain changes are exactly the events that can invalidate PCR-bound secrets.

The tradeoff is significant: while armed, the machine is less protected against an attacker who controls the whole boot environment and can access the TPM. A malicious early boot environment may be able to read the TPM NV secret. If an attacker ever copies the managed key while armed, that key remains a valid LUKS unlock key until you rotate/purge it.

This is conceptually similar to intentionally suspending a measured-boot protector for maintenance. Do not leave the system armed longer than necessary.

TPM restart caveat

The initramfs performs:

  1. tpm2_nvread
  2. tpm2_nvreadlock
  3. exposes the key under /run/cryptsetup-keys.d/
  4. lets systemd/cryptsetup unlock the volume
  5. deletes the temporary runtime key file

The userspace remover later performs tpm2_nvundefine.

READ_STCLEAR only lasts for the current TPM startup. A reset before nvundefine means the NV secret may become readable again.

TPM NV endurance

TPM NV storage has finite write endurance. This project is intended for occasional update/maintenance reboots, not for arming/disarming continuously or on every normal boot.

Storage assumptions

Before using this project, verify all of the following:

  • the target is LUKS2;
  • the target volume is the encrypted volume you actually want to auto-unlock;
  • /var/lib/luks-reboot-once resides inside encrypted storage;
  • the encrypted mapping is handled by systemd/dracut;
  • the matching /etc/crypttab entry does not specify some other explicit key file;
  • TPM owner hierarchy authorization is empty/default, as on many normal Fedora installations;
  • you have at least one known-good independent LUKS passphrase/recovery key.

If /var is a separate unencrypted filesystem, the default key location is unsafe. Do not use the project unchanged. Move STATE_DIR to storage protected by the same encryption boundary first.

Custom initramfs systems, highly customized /etc/crypttab, UKI/signing pipelines, unusual device-mapper stacks, remote-unlock setups, and multiple encrypted-root arrangements may need adaptation. Multiple UUID instances are structurally supported but have not been a primary test target.

What changes on disk

First add

The first arm for a LUKS UUID:

  1. generates a 32-byte random managed key file;
  2. finds a free LUKS2 keyslot;
  3. asks for an existing LUKS passphrase;
  4. adds the managed key to that free keyslot;
  5. proves that the file unlocks that exact keyslot with cryptsetup --test-passphrase --key-slot;
  6. creates a TPM NV object and writes/verifies the same key;
  7. stores non-secret configuration under /etc/luks-reboot-once/;
  8. regenerates initramfs so the dracut hook knows the mapper and TPM NV handle;
  9. permanently enables the remover service for that UUID.

Subsequent add

Normal subsequent arms do not add a new LUKS key and do not modify the LUKS header. They verify the existing key file against its keyslot and temporarily recreate/write the TPM NV object.

Initramfs is regenerated only when the persisted per-volume configuration or installed dracut module needs it.

remove

remove/disarm only removes the TPM NV object. It leaves the managed LUKS keyslot, key file, and configuration alone.

It is intentionally safe to leave the remover service enabled on every boot. If the TPM object is absent, it does nothing.

purge

purge is intentionally destructive. It:

  • removes the TPM NV object if present;
  • removes the managed LUKS keyslot;
  • deletes the managed key file;
  • deletes the per-volume config;
  • disables the per-volume remover;
  • rebuilds initramfs to remove the configuration from future images.

Use purge before uninstalling, or to rotate to a fresh managed key.

Which LUKS keyslot is used?

There is no fixed slot number.

LUKS2 exposes slots 0 through 31. On initial setup the helper searches downward from slot 31 and chooses the highest free slot. On a typical Fedora installation with only slot 0 already occupied, this means the managed key normally lands in slot 31.

If slot 31 is already occupied, it tries 30, then 29, and so on. Existing active slots are not intentionally overwritten.

The selected slot is recorded in:

/etc/luks-reboot-once/<LUKS-UUID>.conf

When the key file exists, purge does not blindly trust that number: it tests the key file against active keyslots and removes the key by its actual key material.

If the key file is missing, purge has no way to reconstruct or fingerprint the old LUKS passphrase from the keyslot. In that recovery case it can fall back to the recorded slot number, but only after asking for and verifying another valid LUKS credential in a different active slot.

Do not manually copy/edit per-volume config files between disks or UUIDs. A stale or intentionally falsified config can make destructive recovery operations target the wrong slot.

Installation

The repository contains luks-reboot-once.spec. Each release needs an immutable v<VERSION> Git tag matching VERSION, the helper script, and the spec file. Create the source release with:

VERSION=$(cat VERSION)
git tag -a "v$VERSION" -m "luks-reboot-once $VERSION"
git push origin "v$VERSION"

To publish with COPR, create a package using the SCM source type, set the clone URL to this repository, and set the spec path to luks-reboot-once.spec. Enable the Fedora chroots you intend to support. COPR will build a signed DNF repository from the tagged source archive referenced by the spec.

Install from a configured COPR repository with:

sudo dnf install luks-reboot-once

The RPM does not arm a volume, rebuild initramfs, or alter LUKS metadata during installation. Removing the package automatically purges every healthy managed volume, including its managed LUKS keyslot, TPM state, key file, configuration, and initramfs integration. Removal is refused only when the state is inconsistent and cannot be purged safely without administrator recovery.

Installing over an older direct source installation migrates the original unmodified units out of /etc/systemd/system so they cannot shadow the packaged units. Locally customized unit overrides are preserved and reported; reconcile those overrides manually after installation.

Direct source installation

Install the dependencies:

sudo dnf install cryptsetup jq tpm2-tools dracut

Install the project:

sudo ./install.sh

The installer places:

/usr/libexec/luks-reboot-once
/usr/lib/systemd/system/luks-reboot-add@.service
/usr/lib/systemd/system/luks-reboot-remove@.service
/usr/lib/systemd/system/luks-reboot-purge@.service
/usr/lib/dracut/modules.d/91luks-reboot-once/

It deliberately does not arm any disk during installation.

Back up the LUKS header first

The first add and every purge modify LUKS metadata. Before experimenting, make a LUKS header backup and store it somewhere secure off the encrypted disk:

UUID=<your-luks-uuid>
DEV="/dev/disk/by-uuid/$UUID"

sudo cryptsetup luksHeaderBackup "$DEV" \
  --header-backup-file "luks-header-$UUID.img"

Protect that backup carefully. A LUKS header backup contains security-sensitive keyslot metadata and can reintroduce historical keyslot state if restored later.

Find the correct LUKS UUID

Use the UUID of the LUKS container, not the filesystem UUID inside it:

sudo cryptsetup luksUUID /dev/nvme0n1p3

Example:

UUID=78c6db60-6a0b-4f84-b859-16e0e44067c3

Sanity-check it:

ls -l "/dev/disk/by-uuid/$UUID"
sudo cryptsetup luksDump --dump-json-metadata "/dev/disk/by-uuid/$UUID" |
  jq '{keyslots: (.keyslots | keys), tokens: (.tokens | keys)}'

Arm the next reboot

sudo systemctl start "luks-reboot-add@$UUID.service"

On first use, systemd will ask for an existing LUKS passphrase so the random managed key can be enrolled.

On later uses, the helper verifies the existing managed key and should not require the LUKS passphrase again.

The remover service is enabled automatically after a successful arm. You may also enable it explicitly:

sudo systemctl enable "luks-reboot-remove@$UUID.service"

Leaving it enabled is the intended configuration.

Disarm without rebooting

sudo systemctl start "luks-reboot-remove@$UUID.service"

This removes only the TPM NV object. The permanent managed LUKS key and local key file remain so a future add is cheap and does not touch the LUKS header.

Purge / rotate the managed key

To completely remove this mechanism from one LUKS volume:

sudo systemctl start "luks-reboot-purge@$UUID.service"

To rotate to a brand-new managed key:

sudo systemctl start "luks-reboot-purge@$UUID.service"
sudo systemctl start "luks-reboot-add@$UUID.service"

The second command will prompt for an existing LUKS passphrase again because it is creating a fresh managed keyslot.

Uninstall

Remove whichever installation type you used:

sudo ./uninstall.sh
# or
sudo dnf remove luks-reboot-once

Uninstall automatically runs the equivalent of purge for every managed volume before deleting the software. This modifies LUKS metadata, removes the managed keyslot and TPM NV object, deletes local state, and rebuilds initramfs.

All managed volumes are checked before the first one is changed. If a key and configuration do not match structurally—for example, one is missing or is a symbolic link—uninstall stops without purging any volume. Repair that state and retry removal; the package is not silently removed while an unexplained managed keyslot may remain.

Verify before the first reboot

After add, inspect the non-secret config:

sudo cat "/etc/luks-reboot-once/$UUID.conf"

Example:

SLOT=31
NV_INDEX=0x1000000
MAPPER=luks-78c6db60-6a0b-4f84-b859-16e0e44067c3

Check the TPM NV public metadata:

NV_INDEX=$(sudo awk -F= '/^NV_INDEX=/{print $2}' "/etc/luks-reboot-once/$UUID.conf")
sudo tpm2_nvreadpublic "$NV_INDEX"

The managed index should be 32 bytes and, after arming, should show written and writelocked among its attributes.

Check that the dracut module is in the initramfs:

sudo lsinitrd -m | grep -i luks-reboot
sudo lsinitrd | grep -E 'luks-reboot-once|tpm2_nv(read|readlock|readpublic)'

Check the active mapper name:

sudo cryptsetup status "$(sudo awk -F= '/^MAPPER=/{print $2}' "/etc/luks-reboot-once/$UUID.conf")"

And verify the cleanup service is enabled:

systemctl is-enabled "luks-reboot-remove@$UUID.service"

Keep the ordinary LUKS passphrase available for the first reboot.

Expected boot flow

systemctl start luks-reboot-add@UUID
              |
              v
verify permanent key <-> managed LUKS keyslot
              |
              v
create TPM NV index + write managed key
              |
              v
            reboot
              |
              v
          initramfs
              |
              +--> tpm2_nvread
              +--> tpm2_nvreadlock
              |
              v
/run/cryptsetup-keys.d/<mapper>.key
              |
              v
      systemd-cryptsetup opens LUKS
              |
              v
delete temporary /run key
              |
              v
        switch to real root
              |
              v
luks-reboot-remove@UUID.service
              |
              v
        tpm2_nvundefine
              |
              v
       normal disarmed state

Files and secrets

Secret

/var/lib/luks-reboot-once/<UUID>.key

This is an actual LUKS unlock credential. Do not print, upload, log, back it up casually, or put it in a repository.

While armed, the same secret exists in the configured TPM NV index.

Commands such as the following can expose it and should not be used for routine diagnostics:

cat /var/lib/luks-reboot-once/<UUID>.key
tpm2_nvread <index>
cryptsetup luksDump --dump-volume-key ...

Not secret by itself

The following are metadata and are normally safe to inspect/share when debugging:

  • LUKS UUID
  • LUKS keyslot numbers
  • TPM NV handles
  • tpm2_nvreadpublic output
  • /etc/luks-reboot-once/<UUID>.conf
  • mapper name

What can go wrong / what can be destructive

add modifies the LUKS header on first use

The first add enrolls a new key. Power loss, storage errors, or bugs while changing LUKS metadata are reasons to have a header backup and known-good recovery credential.

Normal later add operations do not intentionally change LUKS metadata.

purge removes a LUKS keyslot

This is the intentionally destructive command. The helper includes checks to avoid removing the last active keyslot and prefers proving the managed slot using the actual key file. Still, do not run purge against configs you have manually edited, copied from another machine, or otherwise made inconsistent.

A strange /var layout can expose the permanent key

If /var/lib/luks-reboot-once is on unencrypted storage, the whole design is broken: the key that unlocks LUKS would be sitting outside LUKS. Verify your storage layout.

A strange crypttab can prevent auto-unlock

The initramfs uses systemd's /run/cryptsetup-keys.d/<mapper>.key convention. If the relevant crypttab line explicitly forces another key file, or your boot path bypasses systemd-cryptsetup, the helper may not work. The arm command rejects an explicit key-file column when it can identify the matching crypttab entry.

A failed boot can leave the TPM armed

If LUKS unlock succeeds but the machine crashes before the remover service runs, the TPM NV object can survive. After a TPM restart its READ_STCLEAR lock clears, so another unattended unlock may be possible.

Once you regain userspace, manually disarm with:

sudo systemctl start "luks-reboot-remove@$UUID.service"

An attacker who extracts the armed key gets a persistent credential

The TPM copy is temporary; the LUKS keyslot is not. If somebody obtains the 32-byte managed key while the machine is armed, simply removing the TPM NV object does not revoke the copy they stole. Use purge and add to rotate the managed key after any suspected exposure.

TPM owner authorization is assumed to be available

The current implementation uses the TPM owner hierarchy (-C o) and assumes the common Fedora-style setup where the needed owner authorization is available without a custom secret. Machines with custom TPM hierarchy authorization require changes.

Firmware/TPM behavior varies

TPM NV capacity, policy, firmware bugs, provisioning, and platform-created NV indexes vary by machine. The helper allocates a free NV index and never intentionally deletes unrelated indexes, but inspect TPM state carefully if allocation behaves unexpectedly.

Recovery behavior

The project is intentionally conservative when its state stops making sense.

The initramfs freshness stamp covers the per-volume configuration and every installed file in the dracut module. Upgrading the RPM or a source installation therefore makes the next add rebuild initramfs automatically when the module content has changed.

If the config says a managed slot exists but the key file has disappeared, add refuses to silently invent another key. LUKS cannot recover the original passphrase/key from a keyslot, and creating another one would risk leaving an unknown orphan slot.

If the key file exists, the helper can test it against every active LUKS keyslot without opening a new mapping:

cryptsetup open --type luks2 --test-passphrase \
  --key-file <keyfile> --key-slot <slot> <device>

purge uses this proof whenever possible.

If the key file is gone but a config remains, explicit purge asks for another valid LUKS credential and verifies that credential against a different active slot before deleting the recorded managed slot.

Why not just use permanent TPM auto-unlock?

Permanent TPM enrollment with Clevis or systemd-cryptenroll is the right solution when you actually want the machine to unlock itself on every boot.

This project is for the opposite policy:

  • normally require a human LUKS credential;
  • temporarily allow an unattended maintenance reboot;
  • automatically return to the normal state afterward.

The permanent dormant keyslot avoids modifying the LUKS header every maintenance cycle, while the temporary TPM NV copy is the thing that is armed/disarmed.

Project layout

luks-reboot-once
├── install.sh
├── uninstall.sh
├── luks-reboot-once
├── systemd/
│   ├── luks-reboot-add@.service
│   ├── luks-reboot-remove@.service
│   └── luks-reboot-purge@.service
└── dracut/
    └── 91luks-reboot-once/
        ├── module-setup.sh
        ├── luks-reboot-once-initrd
        ├── luks-reboot-once-initrd-clean
        ├── luks-reboot-once-initrd.service
        └── luks-reboot-once-initrd-clean.service

References

The implementation is built from standard components rather than a custom crypto format:

License

luks-reboot-once is available under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). Alternative commercial licensing is available for users who require terms incompatible with the AGPLv3; see COMMERCIAL-LICENSE.