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>
This commit is contained in:
2026-08-29 14:19:35 +02:00
co-authored by Claude Sonnet 5
parent c5ed8b77ac
commit 91f1abef8a
5 changed files with 305 additions and 0 deletions
+119
View File
@@ -126,6 +126,125 @@ This policy binding does not protect the password from a process running with su
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`).