Recipe 20: Time-Bound Secret Vault
Situation
You need secrets to exist only for a bounded time window. Operationally, “remember to revoke” is weak.
The lease model gives you a natural tool:
- secrets live in short TTL key leases
- ciphertext can be durable
This recipe is the “unexpectedly cool” security consequence of leases: temporal scoping becomes an enforceable property of the runtime, not a policy checkbox.
What You Build
A vault that:
- manages encryption key epochs via
KeyEpochManager - encrypts/decrypts blobs via
EncryptedBlobStore - rotates keys with automatic old-epoch expiry
- enforces fail-closed semantics: expired epoch = no decryption
Building Blocks
grafos_securestore::{KeyEpochManager, EncryptedBlobStore, EpochStatus}— sourcegrafos_securestore::{EpochId, BlobId, BlobInfo, CryptoBackend}— source
Related API docs:
Design
KeyEpochManager Manages the Epoch Lifecycle
KeyEpochManager holds encryption keys scoped to epochs. Each epoch has:
- a TTL (key material dies when the epoch expires)
- a status:
Active->Rotating->Expired - a
MemRegionLocatordescribing where the key lives in leased memory
Only one epoch is Active at a time. Creating a new epoch transitions the previous one to Rotating.
Calling expire_old(now) zeroes key material for expired epochs — fail closed, enforced by zeroize.
EncryptedBlobStore for Encrypt/Decrypt
EncryptedBlobStore uses the active epoch’s key to encrypt blobs. Each blob records its epoch ID,
nonce, and AAD. At decryption time, the store resolves the key by epoch ID. If the epoch is expired
or missing, decryption fails with SecureStoreError.
Access Control (Capabilities)
If you integrate capability tokens:
- every read requires a token scoped to the vault resource id and operation
- tokens should be audience-bound (to the client identity) and time-bounded
- revocation broadcasts can kill tokens early
Even with transport-level authentication, keep capability checks as defense-in-depth.
Auditability
Emit structured events:
- epoch created / rotated / expired
- epoch renewal failed
- decrypt attempts failed due to expired epoch
Walkthrough
1. Initialize the Vault
use grafos_securestore::{ KeyEpochManager, EncryptedBlobStore, MockCryptoBackend, BlobId,};
let crypto = Box::new(MockCryptoBackend::new()); // Use AesGcmBackend in productionlet mut key_mgr = KeyEpochManager::new(crypto);
// Create the first epoch with a 1-hour TTLlet epoch_id = key_mgr.create_epoch(now, 3600)?;
// Wrap in a blob storelet mut vault = EncryptedBlobStore::new(key_mgr);2. Encrypt a Secret
let blob_info = vault.put(BlobId(1), b"my secret data")?;// blob_info contains epoch_id, nonce, AAD — needed for decryption3. Decrypt While Key is Active
let plaintext = vault.get(&blob_info)?;assert_eq!(plaintext, b"my secret data");4. Rotate Keys
// Create a new epoch — old one becomes Rotatinglet new_epoch = vault.key_manager_mut().rotate(now, 3600)?;
// Old epoch keys still accessible for decryption during rotation windowlet plaintext = vault.get(&blob_info)?;assert_eq!(plaintext, b"my secret data");5. Expire Old Epoch
// Time passes... old epoch TTL expiresvault.key_manager_mut().expire_old(now + 7200);
// Old epoch key material is zeroed. Decryption fails closed:let result = vault.get(&blob_info);assert!(result.is_err()); // SecureStoreError::EpochExpired6. Check Epoch Status
use grafos_securestore::EpochStatus;
let info = vault.key_manager().get_epoch(epoch_id);if let Some(info) = info { match info.status { EpochStatus::Active => { /* current epoch */ } EpochStatus::Rotating => { /* old epoch, still decryptable */ } EpochStatus::Expired => { /* key zeroed, no decryption */ } }}Failure Modes
- Key epoch expires unexpectedly: fail closed on reads.
get()returnsEpochExpired. Disconnected: if you cannot verify/renew key lease, assume expiry is imminent and fail closed.- Partial rotation: entries encrypted under old epoch remain decryptable until
expire_old()is called. Keep epoch metadata until all active entries are rewrapped or expired.
Variations
- split DEK material across multiple key leases (requires quorum to decrypt)
- per-tenant epochs for multi-tenant isolation
- durable “vault index” in block storage mapping entry id -> ciphertext locator + epoch
- use
renew_active(duration_secs)to extend the active epoch’s TTL before it expires