fix(storage): harden the KVS-backed identity and wallet stores - #2408
AkramBitar wants to merge 2 commits into
Conversation
57ee536 to
97f9bec
Compare
Effi-S
left a comment
There was a problem hiding this comment.
Some Findings:
1. Backward-incompatible Vault storage format — Medium
Location: token/services/storage/db/kvs/hashicorp/vault_kvs.go — NormalizeID
Keys with / or % (identity hashes routinely contain /) now map to different Vault
paths, so existing deployments stop finding previously written identity/wallet data. Called
out in a doc WARNING but there is no migration tooling — upgraders must re-register affected
identities manually. Confirm this is acceptable / has a migration story.
2. KVS IdentityExists cannot surface read failures — Medium
Location: token/services/storage/db/kvs/walletdb.go — IdentityExists
The new (bool, error) return only ever carries a key-construction error; kvs.Exists still
swallows Vault read errors (FSC GetExisting), so a transient failure reads as "not bound"
for the KVS backend. The hardening goal (fault ≠ non-membership) is only actually met by the
SQL backend. Documented, but the KVS half of the change is effectively inert.
3. Context threaded but not used for Vault I/O — Low
Location: token/services/storage/db/kvs/hashicorp/vault_kvs.go — read (ignores ctx),
Get, iterator
ctx is now plumbed through but read calls client.Logical().Read(...), not
ReadWithContext. Cancellation/deadlines are not honored. Cosmetic threading; switch to the
*WithContext Vault calls to make it real.
4. StoreIdentity intermediate inconsistency window — Low
Location: token/services/storage/db/kvs/walletdb.go — StoreIdentity
The GetWalletID key [tmsID, roleID, idHash] is written before the IdentityExists key.
A failure between them leaves GetWalletID returning a wallet while IdentityExists reports
false. Retries converge (writes are idempotent), so low impact, but the two reads disagree
in the window. Acceptable if IdentityExists is the sole commit marker.
A composite key maps onto a Vault path by turning each of its components
into a path component, but the components were copied in verbatim. A
component that is empty, or that itself contains a separator, therefore
dropped or added path components, and two distinct composite keys could
resolve to the same secret: CreateCompositeKey("", []string{"1"}) and
CreateCompositeKey("1", nil) both reduce to <mount>/1. The empty case is
reachable by any caller, since an empty attribute passes
CreateCompositeKey's validation. The separator case is worse than latent:
identity hashes are base64-encoded and routinely contain "/", so the key
the iterator reports back splits into more attributes than were stored,
which is exactly what WalletStore.GetConfID matches against. A "." or ".."
component was resolved away by the path.Join the Vault client applies,
letting a component walk out of the configured mount point.
Each component is now percent-escaped, with an empty one encoded as "%"
(url.PathEscape never emits a bare "%") and the dots of "." and ".."
escaped, and deNormalizeID is the exact inverse. Components without "/"
or "%" are unchanged, so paths for well-formed keys stay as they were.
Entries whose components do contain those characters move: that is a
storage-format change for Vault-backed deployments, called out in the new
documentation.
The iterator returned by GetByPartialCompositeID lists keys and reads each
one's value lazily. A key deleted in between hit Get's "no value found"
branch, which returns a nil error without touching the destination, so the
iterator yielded a zero-valued record as if it were stored data -
IdentityConfigurationsIterator had no way to tell. The read now reports
missing separately from failed, happens in HasNext, and a vanished key is
skipped. Vault's KV v1 API has no multi-read endpoint, so the round trip
per key is inherent and is documented on the iterator.
Two more defects in the same function: it returned a nil kvs.Iterator when
the list found nothing, which panics inside
IdentityConfigurationsIterator.HasNext, and it asserted the stored value
was a string without checking, panicking on a malformed secret. Both are
fixed; three assertions in vault_kvs_test.go that encoded the nil-iterator
behaviour now assert an empty iterator.
WalletStore.StoreIdentity writes up to four keys with no atomicity. The
writes are now ordered so the entry IdentityExists reads is written last
and every entry is idempotent, so a failure part-way reports the identity
as not bound and a retry converges; the invariant is documented. The
previous order wrote that entry first, which reported a binding whose
wallet reference was missing.
TrackedKVS recorded the caller's pointer in its Put/Get history, so the
common "one destination reused across calls" idiom made every entry alias
the same object and retroactively show a later call's data. Pointers are
snapshotted into a fresh value of the same type.
fscKVS.Close() called kvs.KVS.Stop(), which only logs a failure to close
the underlying store, and then returned nil unconditionally - a close
failure was unobservable. It now closes the store it created directly and
returns the error. The Vault test helper also leaked the response body of
every successful health poll.
Tests: a table and a fuzz target (wired into nightly-fuzz.yml, which can
now run a target from a nested module) for the composite-key/Vault-path
round trip, a Vault-backed case covering "/" , empty, ".." and "%"
components plus a key deleted after the list, a tracker regression test
for the shared-destination idiom, and close tests for the in-memory
backend. New page docs/services/storage/kvs.md documents the key layout,
the path encoding and its migration note, the iterator semantics and the
partial-write ordering guarantee.
The issue also suggests reordering the wallet and identity keys to put the
identity hash first, to avoid the full-TMS scans in GetConfID and
ConfigurationsByID. That cannot be a pure reordering - GetWalletIDs needs
the existing prefix, so it has to be a secondary index, and existing data
has no index entries, so both lookups would have to keep the scan as a
fallback. It is left as a follow-up rather than bundled here.
Fixes #2041
Signed-off-by: AkramBitar <akram@il.ibm.com>
Signed-off-by: Effi-S <effi.szt@gmail.com>
Adopt the WalletStoreService.IdentityExists signature introduced in #2409, so a failed lookup is no longer indistinguishable from "the binding does not exist". Both implementations are updated. The KVS store previously swallowed the composite-key error and answered false; it now wraps it. Only key construction can be reported that way, because this backend's Exists is FSC's GetExisting, which drops the underlying store error - the same caveat already documented on GetWalletID. Registry.ContainsIdentity cannot grow an error return: it backs driver.Wallet.Contains, which returns a plain bool. It therefore logs the failure before reporting false, so a storage problem is at least visible. This duplicates the IdentityExists slice of #2409 verbatim so the two branches converge without conflict; whichever lands second drops the hunks as already applied. Signed-off-by: AkramBitar <akram@il.ibm.com> Signed-off-by: Effi-S <effi.szt@gmail.com>
97f9bec to
89f201d
Compare
Fixes #2041
Six fixes in
token/services/storage/db/kvs:/inside one (base64 identity hashes have them) or a./..can no longer make two different keys point at the same secret.StoreIdentity— the entryIdentityExistsreads is written last and every write is idempotent, so a partial failure reads as "not stored" and a retry converges.tracker.go— history entries are snapshots, not the caller's pointer.fscKVS.Close()— returns the store's close error instead of only logging it.Also fixed in the same code: a nil iterator for an empty list (panicked in
IdentityConfigurationsIterator), an uncheckedvalue.(string), and an unclosed iterator inGetWalletIDs.Warning
Storage-format change: Vault entries whose key components contain
/or%now live at a different path and are not found by the new code. Seedocs/services/storage/kvs.md.IdentityExistsreturns(bool, error)A second commit adopts the
WalletStoreService.IdentityExistssignature from #2409, so a failed lookup is no longer indistinguishable from "the binding does not exist". The KVS store previously swallowed the composite-key error and answeredfalse; it now wraps it. Only key construction can be reported that way, because this backend'sExistsis FSC'sGetExisting, which drops the underlying store error — the caveat already documented onGetWalletID.Registry.ContainsIdentitycannot grow an error return (it backsdriver.Wallet.Contains, a plainbool), so it logs the failure before reportingfalse.Note
This duplicates the
IdentityExistsslice of #2409 verbatim — #2409 already updateskvs/walletdb.goitself, so taking the signature here also means taking the interface,sql/common/wallet.go, the mock and the test callers. The overlapping hunks are byte-identical, so whichever PR merges second drops them as already-applied rather than conflicting. If #2409 lands first, this commit can simply be dropped.The issue's perf suggestion (hash-first keys) is left out — it needs a secondary index plus a scan fallback for existing data, so it belongs in its own change.
Tests: unit + fuzz for the key/path round trip (in the nightly fuzz matrix), a Vault-backed case for
/, empty,..and%components plus a key deleted after the list, and a tracker regression test. New pagedocs/services/storage/kvs.md.