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>
255 lines
15 KiB
Markdown
255 lines
15 KiB
Markdown
# 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](#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.
|