fix(secrets): add encrypted Bitwarden stale cache - #65711
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for building a narrow, opt-in encrypted alternative to the plaintext stale-cache approach discussed on #41938. The stale-fallback premise is still present on current main: agent/secret_sources/bitwarden.py:410 directly calls _run_bws_list with no fallback.
Problems
agent/secret_sources/bitwarden.py:464validates expiry from unauthenticated outer JSON metadata before decrypting. AES-GCM binds onlyserialized_keyat line 472, so changing outerfetched_atcan extend valid stale ciphertext pastmax_stale_secondsduring a network failure.agent/secret_sources/bitwarden.py:586writes the encrypted cache without removing a prior plaintextcache/bws_cache.json; enabling the option does not clean an existing plaintext cache.bws_cache.enc.jsonis absent from the parallel credential protections inagent/file_safety.py:271-282,gateway/platforms/base.py:1174-1192, andhermes_cli/web_server.py:1342-1355.
Suggested changes
- Authenticate or decrypt-before-check the timestamp, and add a metadata-tampering regression test.
- Remove the legacy plaintext cache after a successful encrypted write and test that migration.
- Extend all credential guards and their tests; document the new config in the example and Bitwarden guide.
Automated hermes-sweeper review.
| fetched_at = payload.get("fetched_at") | ||
| if not isinstance(fetched_at, (int, float)): | ||
| return None | ||
| entry_age = time.time() - float(fetched_at) |
There was a problem hiding this comment.
This stale-window check trusts unauthenticated outer JSON metadata. AES-GCM below authenticates only serialized_key, so changing fetched_at to now makes old valid ciphertext acceptable during a network failure. Check the decrypted inner timestamp instead, or authenticate this metadata, and add a tampering regression test.
| _DISK_CACHE.write(cache_key, entry, cache_ttl_seconds, home_path) | ||
| if cache_ttl_seconds > 0: | ||
| _CACHE[cache_key] = entry | ||
| if encrypted_cache_enabled and encrypted_cache_max_stale_seconds > 0: |
There was a problem hiding this comment.
When a user enables this after normal caching, an existing plaintext cache/bws_cache.json is neither read nor removed, so the old secret payload remains on disk. Remove or migrate that legacy cache after a successful encrypted write and add a transition test.
fad3146 to
686652e
Compare
# Conflicts: # agent/secret_sources/bitwarden.py
|
Merged via #69251 — both your commits landed as-is (rebase-merged, authorship preserved). We reworked the fallback control flow onto the stale-cache path that merged in the meantime (#69051): one transport-only gate, encrypted tier replaces plaintext when enabled, plus one addition — |
What does this PR do?
Adds an opt-in encrypted last-good cache for Bitwarden Secrets Manager secrets so Hermes can still start during transient BWS/network outages without storing the cached secret payload in plaintext-equivalent JSON.
This is related to #41938, but intentionally different: #41938 serves the existing plaintext disk cache when BWS fails. This PR adds an encrypted cache path with explicit stale-window controls and keeps fallback limited to network/timeout failures so auth failures do not bypass revocation.
Changes made
bws_cache.enc.json, written atomically with mode0600under<hermes_home>/cache/.BWS_ACCESS_TOKENplus a random per-cache salt via HKDF-SHA256.cache_ttl_seconds, so operators can setcache_ttl_seconds: 0while still retaining an encrypted break-glass cache.NETWORK/TIMEOUTBWS failures.Security notes
This does not eliminate the bootstrap-token trust boundary: a local attacker who can read
BWS_ACCESS_TOKENcan derive the cache key and already has enough material to fetch secrets when the network is available. It does improve the common failure/theft case where a cache file is copied or exposed without the bootstrap token: the cached secret payload is no longer plaintext JSON.Tests
All passed locally.
Runtime smoke
Also verified on a live Hermes profile without printing secret values:
0600;bwsreturningnetwork is unreachablesuccessfully loaded secrets from the encrypted stale cache;Using stale encrypted Bitwarden cache after network fetching BWS secrets.AI Disclosure
This change was implemented with AI assistance.