From c694b9889a8f3aa49a8ced835630d0d002d828f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sun, 1 Feb 2026 23:39:11 +0100 Subject: [PATCH] vault: add auto-unseal --- docs/vault/auto-unseal.md | 178 +++++++++++++++++++++++++++++++++++++ services/vault/README.md | 38 ++++++++ services/vault/default.nix | 87 +++++++++++++++++- 3 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 docs/vault/auto-unseal.md create mode 100644 services/vault/README.md diff --git a/docs/vault/auto-unseal.md b/docs/vault/auto-unseal.md new file mode 100644 index 0000000..eeed239 --- /dev/null +++ b/docs/vault/auto-unseal.md @@ -0,0 +1,178 @@ +# OpenBao TPM2 Auto-Unseal Setup + +This document describes the one-time setup process for enabling TPM2-based auto-unsealing on vault01. + +## Overview + +The auto-unseal feature uses systemd's `LoadCredentialEncrypted` with TPM2 to securely store and retrieve an unseal key. On service start, systemd automatically decrypts the credential using the VM's TPM, and the service unseals OpenBao. + +## Prerequisites + +- OpenBao must be initialized (`bao operator init` completed) +- You must have at least one unseal key from the initialization +- vault01 must have a TPM2 device (virtual TPM for Proxmox VMs) + +## Initial Setup + +Perform these steps on vault01 after deploying the service configuration: + +### 1. Save Unseal Key + +```bash +# Create temporary file with one of your unseal keys +echo "paste-your-unseal-key-here" > /tmp/unseal-key.txt +``` + +### 2. Encrypt with TPM2 + +```bash +# Encrypt the key using TPM2 binding +systemd-creds encrypt \ + --with-key=tpm2 \ + --name=unseal-key \ + /tmp/unseal-key.txt \ + /var/lib/openbao/unseal-key.cred + +# Set proper ownership and permissions +chown openbao:openbao /var/lib/openbao/unseal-key.cred +chmod 600 /var/lib/openbao/unseal-key.cred +``` + +### 3. Cleanup + +```bash +# Securely delete the plaintext key +shred -u /tmp/unseal-key.txt +``` + +### 4. Test Auto-Unseal + +```bash +# Restart the service - it should auto-unseal +systemctl restart openbao + +# Verify it's unsealed +bao status +# Should show: Sealed = false +``` + +## TPM PCR Binding + +The default `--with-key=tpm2` binds the credential to PCR 7 (Secure Boot state). For stricter binding that includes firmware and boot state: + +```bash +systemd-creds encrypt \ + --with-key=tpm2 \ + --tpm2-pcrs=0+7+14 \ + --name=unseal-key \ + /tmp/unseal-key.txt \ + /var/lib/openbao/unseal-key.cred +``` + +PCR meanings: +- **PCR 0**: BIOS/UEFI firmware measurements +- **PCR 7**: Secure Boot state (UEFI variables) +- **PCR 14**: MOK (Machine Owner Key) state + +**Trade-off**: Stricter PCR binding improves security but may require re-encrypting the credential after firmware updates or kernel changes. + +## Re-provisioning + +If you need to reprovision vault01 from scratch: + +1. **Before destroying**: Back up your root token and all unseal keys (stored securely offline) +2. **After recreating the VM**: + - Initialize OpenBao: `bao operator init` + - Follow the setup steps above to encrypt a new unseal key with TPM2 +3. **Restore data** (if migrating): Copy `/var/lib/openbao` from backup + +## Handling System Changes + +**After firmware updates, kernel updates, or boot configuration changes**, PCR values may change, causing TPM decryption to fail. + +### Symptoms +- Service fails to start +- Logs show: `Failed to decrypt credentials` +- OpenBao remains sealed after reboot + +### Fix +1. Unseal manually with one of your offline unseal keys: + ```bash + bao operator unseal + ``` + +2. Re-encrypt the credential with updated PCR values: + ```bash + echo "your-unseal-key" > /tmp/unseal-key.txt + systemd-creds encrypt \ + --with-key=tpm2 \ + --name=unseal-key \ + /tmp/unseal-key.txt \ + /var/lib/openbao/unseal-key.cred + chown openbao:openbao /var/lib/openbao/unseal-key.cred + chmod 600 /var/lib/openbao/unseal-key.cred + shred -u /tmp/unseal-key.txt + ``` + +3. Restart the service: + ```bash + systemctl restart openbao + ``` + +## Security Considerations + +### What This Protects Against +- **Data at rest**: Vault data is encrypted and cannot be accessed without unsealing +- **VM snapshot theft**: An attacker with a VM snapshot cannot decrypt the unseal key without the TPM state +- **TPM binding**: The key can only be decrypted by the same VM with matching PCR values + +### What This Does NOT Protect Against +- **Compromised host**: If an attacker gains root access to vault01 while running, they can access unsealed data +- **Boot-time attacks**: If an attacker can modify the boot process to match PCR values, they may retrieve the key +- **VM console access**: An attacker with VM console access during boot could potentially access the unsealed vault + +### Recommendations +- **Keep offline backups** of root token and all unseal keys in a secure location (password manager, encrypted USB, etc.) +- **Use Shamir secret sharing**: The default 5-key threshold means even if the TPM key is compromised, an attacker needs the other keys +- **Monitor access**: Use OpenBao's audit logging to detect unauthorized access +- **Consider stricter PCR binding** (PCR 0+7+14) for production, accepting the maintenance overhead + +## Troubleshooting + +### Check if credential exists +```bash +ls -la /var/lib/openbao/unseal-key.cred +``` + +### Test credential decryption manually +```bash +# Should output your unseal key if TPM decryption works +systemd-creds decrypt /var/lib/openbao/unseal-key.cred - +``` + +### View service logs +```bash +journalctl -u openbao -n 50 +``` + +### Manual unseal +```bash +bao operator unseal +# Enter one of your offline unseal keys when prompted +``` + +### Check TPM status +```bash +# Check if TPM2 is available +ls /dev/tpm* + +# View TPM PCR values +tpm2_pcrread +``` + +## References + +- [systemd.exec - Credentials](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Credentials) +- [systemd-creds man page](https://www.freedesktop.org/software/systemd/man/systemd-creds.html) +- [TPM2 PCR Documentation](https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/) +- [OpenBao Documentation](https://openbao.org/docs/) diff --git a/services/vault/README.md b/services/vault/README.md new file mode 100644 index 0000000..e1b7d93 --- /dev/null +++ b/services/vault/README.md @@ -0,0 +1,38 @@ +# OpenBao Service Module + +NixOS service module for OpenBao (open-source Vault fork) with TPM2-based auto-unsealing. + +## Features + +- TLS-enabled TCP listener on `0.0.0.0:8200` +- Unix socket listener at `/run/openbao/openbao.sock` +- File-based storage at `/var/lib/openbao` +- TPM2 auto-unseal on service start + +## Configuration + +The module expects: +- TLS certificate: `/var/lib/openbao/cert.pem` +- TLS private key: `/var/lib/openbao/key.pem` +- TPM2-encrypted unseal key: `/var/lib/openbao/unseal-key.cred` + +Certificates are loaded via systemd `LoadCredential`, and the unseal key via `LoadCredentialEncrypted`. + +## Setup + +For initial setup and configuration instructions, see: +- **Auto-unseal setup**: `/docs/vault/auto-unseal.md` +- **Terraform configuration**: `/terraform/vault/README.md` + +## Usage + +```bash +# Check seal status +bao status + +# Manually seal (for maintenance) +bao operator seal + +# Service will auto-unseal on restart +systemctl restart openbao +``` diff --git a/services/vault/default.nix b/services/vault/default.nix index 17a7236..bb30d60 100644 --- a/services/vault/default.nix +++ b/services/vault/default.nix @@ -1,4 +1,83 @@ -{ ... }: +{ pkgs, ... }: +let + unsealScript = pkgs.writeShellApplication { + name = "openbao-unseal"; + runtimeInputs = with pkgs; [ + openbao + coreutils + gnugrep + getent + ]; + text = '' + # Set environment to use Unix socket + export BAO_ADDR='unix:///run/openbao/openbao.sock' + SOCKET_PATH="/run/openbao/openbao.sock" + CREDS_DIR="''${CREDENTIALS_DIRECTORY:-}" + + # Wait for socket to exist + echo "Waiting for OpenBao socket..." + for _ in {1..30}; do + if [ -S "$SOCKET_PATH" ]; then + echo "Socket exists" + break + fi + sleep 1 + done + + # Wait for OpenBao to accept connections + echo "Waiting for OpenBao to be ready..." + for _ in {1..30}; do + output=$(timeout 2 bao status 2>&1 || true) + + if echo "$output" | grep -q "Sealed.*false"; then + # Already unsealed + echo "OpenBao is already unsealed" + exit 0 + elif echo "$output" | grep -qE "(Sealed|Initialized)"; then + # Got a valid response, OpenBao is ready (sealed) + echo "OpenBao is ready" + break + fi + + sleep 1 + done + + # Check if already unsealed + if output=$(timeout 2 bao status 2>&1 || true); then + if echo "$output" | grep -q "Sealed.*false"; then + echo "OpenBao is already unsealed" + exit 0 + fi + fi + + # Unseal using the TPM-decrypted keys (one per line) + if [ -n "$CREDS_DIR" ] && [ -f "$CREDS_DIR/unseal-key" ]; then + echo "Unsealing OpenBao..." + while IFS= read -r key; do + # Skip empty lines + [ -z "$key" ] && continue + + echo "Applying unseal key..." + bao operator unseal "$key" + + # Check if unsealed after each key + if output=$(timeout 2 bao status 2>&1 || true); then + if echo "$output" | grep -q "Sealed.*false"; then + echo "OpenBao unsealed successfully" + exit 0 + fi + fi + done < "$CREDS_DIR/unseal-key" + + echo "WARNING: Applied all keys but OpenBao is still sealed" + exit 0 + else + echo "WARNING: Unseal key credential not found, OpenBao remains sealed" + exit 0 + fi + ''; + }; +in { services.openbao = { enable = true; @@ -25,5 +104,11 @@ "key.pem:/var/lib/openbao/key.pem" "cert.pem:/var/lib/openbao/cert.pem" ]; + # TPM2-encrypted unseal key (created manually, see setup instructions) + LoadCredentialEncrypted = [ + "unseal-key:/var/lib/openbao/unseal-key.cred" + ]; + # Auto-unseal on service start + ExecStartPost = "${unsealScript}/bin/openbao-unseal"; }; }