Hardware-backed secrets management using TPM 2.0 and LUKS encryption.
Unlike cloud-based secrets managers, Vault-TPM stores your master key in a TPM chip - physically bound to your hardware. Even if someone steals your disk, they can't decrypt your secrets without the original TPM.
- Hardware-bound security - Master key sealed to TPM 2.0, bound to boot state (PCR 0+7)
- Encryption at rest - LUKS2 encrypted volume with AES-XTS-256
- Double encryption - Secrets encrypted again with AES-256-GCM inside the vault
- Pluggable backends - SQLite (default), PostgreSQL, or simple JSON file
- Full audit trail - Every access logged with timestamps
- Zero cloud dependencies - Everything runs on your hardware
# Requires root and TPM 2.0 hardware
sudo ./scripts/install.shThis creates:
- LUKS encrypted volume at
/opt/vault-tpm/secrets.img - Master key sealed to TPM at handle
0x81000002 - Auto-unlock systemd service
- Recovery passphrase (backup unlock method if TPM fails)
pip install vault-tpm
# With PostgreSQL support
pip install vault-tpm[postgres]
# Everything
pip install vault-tpm[all]# Store a secret
vault-tpm set API_KEY "sk_live_xxx" --type api_key
# Retrieve it
vault-tpm get API_KEY
# List all secrets
vault-tpm list
# Check status
vault-tpm status# Basic operations
vault-tpm set <KEY> <VALUE>
vault-tpm get <KEY>
vault-tpm delete <KEY>
vault-tpm list
# With secret types
vault-tpm set DB_PASSWORD "hunter2" --type password
vault-tpm set STRIPE_KEY "sk_live_xxx" --type api_key
vault-tpm set AES_KEY "base64..." --type encryption_key
# Read from stdin (for scripts)
echo "secret" | vault-tpm set MY_SECRET --stdin
cat private.pem | vault-tpm set PRIVATE_KEY --type private_key --stdin
# Different backends
vault-tpm --backend sqlite --db ./vault.db set KEY value
vault-tpm --backend postgres --db "postgresql://localhost/vault" set KEY value
vault-tpm --backend file --db ./secrets.json set KEY value
# Export/Import (for backup)
vault-tpm export -o backup.txt
vault-tpm import backup.txt --overwritefrom vault_tpm import Vault, SecretType
from vault_tpm.backends import SqliteBackend, PostgresBackend
# SQLite (default, zero config)
vault = Vault(SqliteBackend())
# PostgreSQL (production)
vault = Vault(PostgresBackend("postgresql://localhost/vault"))
# Store secrets
vault.set("API_KEY", "sk_live_xxx", SecretType.API_KEY)
vault.set("DB_PASSWORD", "hunter2", SecretType.PASSWORD)
# Retrieve secrets
api_key = vault.get("API_KEY")
# Safe get with default
optional = vault.get_or_default("MAYBE_EXISTS", default=None)
# List and check
keys = vault.list()
exists = vault.exists("API_KEY")
# Rotate a secret
vault.rotate("API_KEY", "sk_live_new_value")
# Metadata
vault.set("KEY", "value", metadata={"purpose": "Production API"})
meta = vault.get_metadata("KEY")export VAULT_TPM_BACKEND=sqlite # sqlite, postgres, file
export VAULT_TPM_DB=/path/to/vault.db # Database path/connection string
export VAULT_TPM_MASTER_KEY=/path/to/key # Master key locationThe master key is searched in this order:
VAULT_TPM_MASTER_KEYenvironment variable/opt/vault-tpm/secrets/master.key(default LUKS mount)/run/secrets/master_key(Docker/Kubernetes secrets)~/.vault-tpm/master.key(user-local fallback)
PCR Values Used:
- PCR 0: Firmware measurement (BIOS/UEFI code)
- PCR 7: Secure Boot state (ensures boot chain integrity)
flowchart TB
subgraph TPM["TPM 2.0 Chip"]
sealed["Sealed Object @ 0x81000002\n(Bound to PCR 0+7)"]
end
subgraph LUKS["LUKS2 Encrypted Volume"]
volume["/opt/vault-tpm/secrets.img\nAES-XTS-256"]
masterkey["master.key (32 bytes)"]
volume --> masterkey
end
subgraph Backend["Storage Backend"]
db[("SQLite / PostgreSQL / JSON")]
secrets["Encrypted Secrets\nAES-256-GCM\n(ciphertext + nonce + tag)"]
db --- secrets
end
boot{{"System Boot"}} -->|"Valid firmware +\nSecure Boot state"| TPM
TPM -->|"Unseals LUKS key"| LUKS
LUKS -->|"Master key decrypts"| Backend
user(["vault-tpm CLI"])
user -->|"get / set / delete"| Backend
sequenceDiagram
participant User
participant CLI as vault-tpm
participant Enc as Encryption Engine
participant DB as Storage Backend
participant LUKS as LUKS Volume
participant TPM as TPM 2.0
Note over TPM,LUKS: Boot-time (once)
TPM->>LUKS: Unseal LUKS key (PCR 0+7 valid)
LUKS->>Enc: Load master.key
Note over User,DB: Runtime operations
User->>CLI: vault-tpm set KEY "secret"
CLI->>Enc: Encrypt with master key
Enc->>Enc: AES-256-GCM + random nonce
Enc->>DB: Store (ciphertext, nonce, tag)
DB-->>CLI: Success
CLI-->>User: Secret stored
User->>CLI: vault-tpm get KEY
CLI->>DB: Fetch encrypted data
DB-->>Enc: (ciphertext, nonce, tag)
Enc->>Enc: Decrypt with master key
Enc-->>CLI: Plaintext
CLI-->>User: secret
| Backend | Use Case | Concurrency | Audit Log |
|---|---|---|---|
| SQLite | Single machine, dev | Single writer | ✓ |
| PostgreSQL | Production, multi-service | Full | ✓ |
| File | Minimal/embedded | None | ✗ |
- Linux (TPM support required)
- TPM 2.0 hardware (or virtual TPM for VMs)
- Python 3.11+
- Root access for initial setup (TPM + LUKS)
- System packages:
tpm2-tools,cryptsetup(installed by setup script)
- At rest: Master key in LUKS volume (AES-XTS-256), secrets in DB (AES-256-GCM)
- Hardware binding: Master key only accessible when TPM unseals (correct boot state)
- Key derivation: PBKDF2-SHA256 with 100k iterations
- Recovery: Manual passphrase if TPM fails or PCR values change
- Memory: Decrypted secrets live in RAM during use
- Root access: Root on running system can read mounted secrets
- Physical access + boot: If attacker can boot your OS, they can read secrets
| Threat | Protected? |
|---|---|
| Disk theft (cold) | ✓ LUKS + TPM seal |
| Database dump | ✓ AES-256-GCM encrypted |
| Network sniffing | ✓ Never leaves machine |
| Root on running system | ✗ Can read mounted volume |
| Evil maid (boot tampering) | ✓ PCR binding detects |
| Physical TPM extraction | ✗ Possible with lab equipment |
| Feature | Vault-TPM | HashiCorp Vault | AWS Secrets Manager |
|---|---|---|---|
| Hardware-backed | ✓ TPM | Optional (HSM $$$) | ✗ |
| Self-hosted | ✓ | ✓ | ✗ |
| Zero cloud | ✓ | ✓ | ✗ |
| Complexity | Low | High | Low |
| Cost | Free | Free/$$ | $$$ |
| HA/Clustering | ✗ | ✓ | ✓ |
Apache 2.0
PRs welcome. Please:
- Add tests for new features
- Run
ruff checkandmypy - Update docs if needed