#!/usr/bin/env bash # # Bootstrap the TPM 2.0 seal used by vault-tpm2-unseal.service: reads the # Vault unseal key from 1Password via the op CLI, using the secret # reference configured in the environment file, and seals it with the same # PCR-policy binding as tpm-seal-password.sh. # # This replaces interactive password entry with an op read, but op read # itself still requires an operator present to approve the request (device # biometric unlock or an active `op signin` session) - this script does not # run unattended. # # Requires: tpm2-tools, the 1Password CLI (op), access to /dev/tpmrm0. set -euo pipefail CONFIG="/etc/vault.d/tpm2.env" HANDLE="" PCR_LIST="" FORCE=0 usage() { cat <<-EOF Usage: $(basename "$0") [-c config] [-H handle] [-l pcr-list] [-f] -c config Environment file providing OP_SECRET_REFERENCE and, optionally, TPM_HANDLE/TPM_PCR_LIST (default: ${CONFIG}) -H handle Persistent TPM handle to store the sealed password at, overrides TPM_HANDLE from the config file -l pcr-list PCR bank and indices to bind release to, overrides TPM_PCR_LIST from the config file -f Evict an existing object at that handle before sealing EOF } while getopts "c:H:l:fh" opt; do case "$opt" in c) CONFIG="$OPTARG" ;; H) HANDLE="$OPTARG" ;; l) PCR_LIST="$OPTARG" ;; f) FORCE=1 ;; h) usage; exit 0 ;; *) usage; exit 1 ;; esac done [ -r "$CONFIG" ] || { echo "Cannot read configuration file: ${CONFIG}" >&2 exit 1 } # shellcheck disable=SC1090 source "$CONFIG" : "${OP_SECRET_REFERENCE:?OP_SECRET_REFERENCE must be set in ${CONFIG}}" HANDLE="${HANDLE:-${TPM_HANDLE:-0x81010001}}" PCR_LIST="${PCR_LIST:-${TPM_PCR_LIST:-sha256:7}}" command -v tpm2_createprimary >/dev/null || { echo "tpm2-tools is required (apt install tpm2-tools)" >&2 exit 1 } command -v op >/dev/null || { echo "1Password CLI (op) is required" >&2 exit 1 } if tpm2_readpublic -c "$HANDLE" >/dev/null 2>&1; then if [ "$FORCE" -eq 1 ]; then tpm2_evictcontrol -C o -c "$HANDLE" >/dev/null else echo "A sealed object already exists at ${HANDLE}; rerun with -f to replace it" >&2 exit 1 fi fi WORKDIR="$(mktemp -d)" trap 'rm -rf "$WORKDIR"' EXIT umask 077 echo "Reading ${OP_SECRET_REFERENCE} from 1Password; approve the request if prompted." >&2 PASSWORD="$(op read "$OP_SECRET_REFERENCE")" [ -n "$PASSWORD" ] || { echo "op read returned an empty value for ${OP_SECRET_REFERENCE}" >&2 exit 1 } tpm2_createprimary -C o -c "$WORKDIR/primary.ctx" -Q tpm2_createpolicy -Q \ --policy-pcr \ -l "$PCR_LIST" \ -L "$WORKDIR/policy.digest" printf '%s' "$PASSWORD" | tpm2_create \ -C "$WORKDIR/primary.ctx" \ -L "$WORKDIR/policy.digest" \ -i - \ -u "$WORKDIR/seal.pub" \ -r "$WORKDIR/seal.priv" \ -Q unset PASSWORD tpm2_load \ -C "$WORKDIR/primary.ctx" \ -u "$WORKDIR/seal.pub" \ -r "$WORKDIR/seal.priv" \ -c "$WORKDIR/seal.ctx" \ -Q tpm2_evictcontrol -C o -c "$WORKDIR/seal.ctx" "$HANDLE" >/dev/null echo "Password from ${OP_SECRET_REFERENCE} sealed and persisted at handle ${HANDLE}, bound to PCRs ${PCR_LIST}"