Files
slawekandClaude Sonnet 5 91f1abef8a Add Vault auto-unseal service and 1Password bootstrap script
Add vault-tpm2-unseal.sh/.service to unseal Vault once after boot using
the TPM-sealed key, and tpm-bootstrap-1password.sh to (re)seal that key
from a 1Password secret reference. Document PCR policy selection, the
Vault service, and the host-specific tpm2.env (gitignored, example in
README) in README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 14:19:35 +02:00

15 KiB

TPM 2.0 Password Management Scripts

Two pairs of scripts for storing and retrieving a password using the TPM 2.0 on Ubuntu 24.04.

Requirements

  • Ubuntu 24.04
  • tpm2-tools package (apt install tpm2-tools)
  • Access to /dev/tpmrm0, either as root or as a member of the tss group

Which pair to use

  • tpm-nv-store-password.sh / tpm-nv-read-password.sh : stores the password as plaintext inside the TPM's own NV (non-volatile) memory. Confidentiality comes only from the TPM enforcing access to the index; the data itself is not encrypted.
  • tpm-seal-password.sh / tpm-unseal-password.sh : seals the password as an encrypted TPM object and binds its release to a PCR policy, so it can only be unsealed when the current boot measurements match those recorded at seal time.

tpm-nv-store-password.sh

Stores a password directly in a TPM NV index.

Usage

tpm-nv-store-password.sh [-H handle] [-f]
  • -H handle : NV index to store the password at. Default: 0x1500016.
  • -f : Undefine any existing NV index at the target handle before storing the new password.

Behavior

  1. Checks that tpm2-tools is installed.
  2. Checks whether an NV index already exists at the target handle. If one exists and -f was not given, the script exits with an error.
  3. Prompts for the password twice (input is not echoed) and verifies both entries match.
  4. Defines an NV index sized to the exact byte length of the password, under the TPM owner hierarchy.
  5. Writes the password into that index.

Exit status

Non-zero if tpm2-tools is missing, if an NV index already exists at the handle without -f, or if the two password entries do not match.

tpm-nv-read-password.sh

Retrieves a password previously stored with tpm-nv-store-password.sh.

Usage

tpm-nv-read-password.sh [-H handle]
  • -H handle : NV index the password was stored at. Default: 0x1500016.

Behavior

  1. Checks that tpm2-tools is installed.
  2. Checks that an NV index exists at the given handle.
  3. Reads the full contents of the index and prints the password to stdout.

Exit status

Non-zero if tpm2-tools is missing or if no NV index exists at the given handle.

tpm-seal-password.sh

Seals a password into the TPM as an encrypted object and persists it at a TPM handle. The sealed object's authorization policy requires the current PCR values to match those read at seal time.

Usage

tpm-seal-password.sh [-H handle] [-l pcr-list] [-f]
  • -H handle : Persistent TPM handle to store the sealed password at. Default: 0x81010001.
  • -l pcr-list : PCR bank and indices to bind release to, in tpm2_createpolicy --policy-pcr -l format. Default: sha256:7 (Secure Boot state).
  • -f : Evict any existing object already stored at the target handle before sealing the new password.

Behavior

  1. Checks that tpm2-tools is installed.
  2. Checks whether an object already exists at the target handle. If one exists and -f was not given, the script exits with an error.
  3. Prompts for the password twice (input is not echoed) and verifies both entries match.
  4. Creates a primary key under the TPM owner hierarchy.
  5. Reads the current values of the given PCRs and builds a policy digest requiring those exact values.
  6. Seals the password as a TPM sealed data object under that primary key, with the PCR policy digest as its authorization policy.
  7. Loads the sealed object into the TPM and persists it at the target handle via tpm2_evictcontrol.
  8. Removes all temporary key material created during the process.

Exit status

Non-zero if tpm2-tools is missing, if a sealed object already exists at the handle without -f, or if the two password entries do not match.

tpm-unseal-password.sh

Retrieves a password previously sealed with tpm-seal-password.sh.

Usage

tpm-unseal-password.sh [-H handle] [-l pcr-list]
  • -H handle : Persistent TPM handle the password was sealed at. Default: 0x81010001.
  • -l pcr-list : PCR bank and indices the password was bound to at seal time. Default: sha256:7.

Behavior

  1. Checks that tpm2-tools is installed.
  2. Checks that a sealed object exists at the given handle.
  3. Unseals the object using a PCR policy session built from the current PCR values, and prints the password to stdout. The TPM refuses the operation if the current PCR values do not match those recorded at seal time.

Exit status

Non-zero if tpm2-tools is missing, if no sealed object exists at the given handle, or if the current PCR values do not satisfy the policy recorded at seal time.

Security properties

NV storage pair

The password is stored as plaintext inside the TPM's non-volatile memory. Confidentiality relies entirely on the TPM enforcing the NV index's access attributes (ownerread/ownerwrite under the owner hierarchy) for every read and write command. Anyone with access to the TPM device (/dev/tpmrm0) and the owner hierarchy authorization (empty by default, since none is set) can run tpm-nv-read-password.sh and retrieve the password, regardless of what booted the machine.

Sealing pair

The password is encrypted under the primary key before being written to the TPM, and stays encrypted even while persisted inside the TPM's own non-volatile memory. Release additionally requires a policy session proving the current PCR values match those recorded at seal time, so unsealing fails if the machine was booted through a different firmware, bootloader, or Secure Boot configuration than the one in effect when the password was sealed.

This policy binding does not protect the password from a process running with sufficient privilege on a machine that has already booted through the expected boot chain, since at that point the PCR values satisfy the policy and tpm2_unseal succeeds for any caller with TPM device access. It also does not protect against bus-sniffing attacks on TPM implementations where the TPM chip is discrete (a separate chip on the LPC/SPI bus) and commands are not sent through an encrypted TPM session, since neither script establishes one.

Any firmware, bootloader, or Secure Boot configuration change that alters the measured value of the bound PCRs invalidates the existing seal. The password must be resealed against the new PCR values (rerun tpm-seal-password.sh -f) before it can be unsealed again.

tpm-bootstrap-1password.sh

Alternative to tpm-seal-password.sh for the sealing pair: reads the password from 1Password via the op CLI instead of an interactive prompt, and seals it with the same PCR-policy binding. Requires the 1Password CLI (op) in addition to tpm2-tools.

This still requires an operator present: op read needs either an active op signin session or approval through the 1Password desktop app's biometric unlock integration. It is meant for (re)initializing the seal used by vault-tpm2-unseal.service from a canonical copy of the Vault unseal key kept in 1Password, not for unattended use.

Usage

tpm-bootstrap-1password.sh [-c config] [-H handle] [-l pcr-list] [-f]
  • -c config : Environment file providing OP_SECRET_REFERENCE and, optionally, TPM_HANDLE/TPM_PCR_LIST. Default: /etc/vault.d/tpm2.env.
  • -H handle : Persistent TPM handle to store the sealed password at. Overrides TPM_HANDLE from the config file. Falls back to 0x81010001 if neither is set.
  • -l pcr-list : PCR bank and indices to bind release to. Overrides TPM_PCR_LIST from the config file. Falls back to sha256:7 if neither is set.
  • -f : Evict an existing object at that handle before sealing.

Behavior

  1. Checks that tpm2-tools and op are installed.
  2. Reads the config file and requires OP_SECRET_REFERENCE to be set in it.
  3. Checks whether an object already exists at the target handle. If one exists and -f was not given, the script exits with an error.
  4. Runs op read against OP_SECRET_REFERENCE (format op://vault/item/field) to retrieve the password.
  5. Creates a primary key under the TPM owner hierarchy, builds a policy digest from the current values of the given PCRs, and seals the password under that policy, following the same steps as tpm-seal-password.sh.
  6. Persists the sealed object at the target handle and removes all temporary key material.

Exit status

Non-zero if tpm2-tools or op is missing, if the config file is unreadable or missing OP_SECRET_REFERENCE, if a sealed object already exists at the handle without -f, or if op read returns an empty value.

PCR policies

tpm-seal-password.sh/tpm-unseal-password.sh default to -l sha256:7. PCR 7 records the UEFI Secure Boot policy state: the platform key, key exchange key, signature database, and forbidden signature database in effect, plus whether Secure Boot enforcement is on. Firmware vendors differ in how PCR 7 behaves when Secure Boot is disabled in the UEFI setup: some still extend it with a fixed "disabled" measurement on every boot, others leave it at its unextended reset value, and some populate it inconsistently across boots. On hardware where Secure Boot is off, PCR 7 is not a reliable anchor to seal against, and tpm-unseal-password.sh can fail even though nothing relevant to the boot chain changed.

Check what a given PCR actually reads on the target hardware before choosing a policy:

tpm2_pcrread sha256:0,1,2,3,4,5,6,7

Run this twice, after two separate reboots, and compare. A PCR whose value changes between otherwise-identical boots is not usable for a policy.

Other commonly available PCRs, per the TCG PC Client Platform Firmware Profile:

  • PCR 0 : firmware/BIOS code (CRTM, POST code, embedded option ROMs). Changes on a firmware update.
  • PCR 1 : firmware/BIOS configuration data (UEFI setup settings).
  • PCR 2 : option ROM code (add-in card firmware, e.g. RAID or NIC controllers).
  • PCR 3 : option ROM configuration and data.
  • PCR 4 : boot manager/IPL code, the actual bootloader binary handed control by firmware (e.g. shimx64.efi/grubx64.efi). Changes whenever the bootloader package is upgraded.
  • PCR 5 : boot manager/IPL configuration data (partition table).
  • PCR 6 : platform state transition and wake events.
  • PCR 7 : Secure Boot policy state, as above.

PCR 4 is measured by firmware regardless of whether Secure Boot enforcement is on, because it is part of TCG measured boot rather than Secure Boot specifically: firmware records the hash of whatever bootloader binary it loads, whether or not it also verifies a signature on it. This makes PCR 4, alone or combined with PCR 0, a more direct fit than PCR 7 for a machine that does not use Secure Boot: -l sha256:0,4 binds release to "the same firmware, loading the same bootloader binary" without relying on a Secure Boot certificate database that is not in use.

The tradeoff is reseal frequency: PCR 0 changes on a firmware/BIOS update, and PCR 4 changes on every bootloader package upgrade (grub-efi-amd64 on Ubuntu), which happens more often than firmware updates. Each such change requires rerunning tpm-seal-password.sh -f with the new PCR values before tpm-unseal-password.sh will succeed again, and Vault stays sealed in the meantime unless a fallback (manual unseal) is available.

Vault auto-unseal service

vault-tpm2-unseal.sh, vault-tpm2-unseal.service, and /etc/vault.d/tpm2.env automatically unseal a HashiCorp Vault instance once, right after vault.service starts, using a key sealed with tpm-seal-password.sh.

tpm2.env is host-specific (it names the local Vault address and the local TPM handle) and is not stored in this repository. Create it on each host from the example below.

Prerequisites

The Vault unseal key must already be sealed into the TPM with tpm-seal-password.sh, at the handle and PCR list referenced in tpm2.env.

Example tpm2.env

# Create at /etc/vault.d/tpm2.env, mode 600, owned by root.

VAULT_ADDR=https://127.0.0.1:8200
TPM_UNSEAL_SCRIPT=/usr/local/sbin/tpm-unseal-password.sh
TPM_HANDLE=0x81010001
TPM_PCR_LIST=sha256:7

# 1Password secret reference to the Vault unseal key, used only by
# tpm-bootstrap-1password.sh, e.g. op://Homelab/Vault Unseal Key/password
OP_SECRET_REFERENCE=
  • VAULT_ADDR : Vault API address, e.g. https://127.0.0.1:8200.
  • TPM_UNSEAL_SCRIPT : path to the installed tpm-unseal-password.sh. Default: /usr/local/sbin/tpm-unseal-password.sh.
  • TPM_HANDLE : persistent TPM handle the key was sealed at. Default: 0x81010001.
  • TPM_PCR_LIST : PCR bank/indices the key was bound to. Default: sha256:7.
  • OP_SECRET_REFERENCE : 1Password secret reference to the Vault unseal key, read only by tpm-bootstrap-1password.sh. Format: op://vault/item/field.

Installation

install -m 0755 -o root -g root tpm-unseal-password.sh /usr/local/sbin/tpm-unseal-password.sh
install -m 0755 -o root -g root tpm-bootstrap-1password.sh /usr/local/sbin/tpm-bootstrap-1password.sh
install -m 0755 -o root -g root vault-tpm2-unseal.sh /usr/local/sbin/vault-tpm2-unseal.sh
install -m 0644 -o root -g root vault-tpm2-unseal.service /etc/systemd/system/vault-tpm2-unseal.service
systemctl daemon-reload
systemctl enable vault-tpm2-unseal.service

Create /etc/vault.d/tpm2.env from the example above, with mode 600 owned by root, and fill in the values for the target host.

Behavior

vault-tpm2-unseal.service is a Type=oneshot unit ordered After=/Requires= vault.service. Since the packaged vault.service is Type=notify, systemd starts this unit only once Vault has signaled it is ready (its HTTP listener is up), whether or not it is still sealed. [Install] WantedBy=vault.service means that once enabled, starting vault.service also starts this unit; it does not need to be in any target's default dependency chain.

vault-tpm2-unseal.sh then:

  1. Checks that the vault CLI and the configured TPM_UNSEAL_SCRIPT are present.
  2. Runs vault status against VAULT_ADDR. If Vault is already unsealed, the script exits successfully without doing anything further, making the service safe to run on every Vault start regardless of how Vault was last sealed.
  3. If Vault is sealed, runs tpm-unseal-password.sh to retrieve the key from the TPM and passes it to vault operator unseal.

Exit status

Non-zero if the vault CLI or the configured TPM unseal script is missing, if vault status fails for a reason other than being sealed, or if tpm-unseal-password.sh fails (for example because the current PCR values no longer satisfy the seal policy — see PCR policies).

Security note

The unseal key is passed to vault operator unseal as a command-line argument, since the Vault CLI does not document a way to supply it via stdin. This makes the key briefly visible to anything on the same host inspecting the process list (ps) while the command runs. This is a tradeoff inherent to scripting Vault's unseal command, not specific to sourcing the key from the TPM.

Handles

tpm-nv-store-password.sh/tpm-nv-read-password.sh default to NV index 0x1500016, in the NV index handle range (0x01000000-0x01FFFFFF).

tpm-seal-password.sh/tpm-unseal-password.sh default to persistent handle 0x81010001, in the TPM's owner-hierarchy persistent handle range (0x81000000-0x81FFFFFF).

Use -H with the same value on both scripts of a pair if a different handle is required, or to manage multiple stored passwords at distinct handles.