You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a registration entry's federatesWith references a trust domain with no
corresponding bundles row, SPIRE Server rejects the entry create/update
entirely:
unable to find federated bundle "spiffe://some-trust-domain"
This is enforced in makeFederatesWith
(pkg/server/datastore/sqlstore/sqlstore.go), which requires every trust
domain listed in federatesWith to resolve to an existing bundles row
before the entry write succeeds.
Real-world impact
We run federation from several per-cluster trust domains into a central
cluster, managed by an operator reconciling ClusterFederatedTrustDomain
resources. During an incident, one federated cluster's bundle failed to
publish to its storage backend (S3 404 / NoSuchKey). SPIRE Server had a FederatedTrustDomain relationship for that trust domain registered, but no bundles row. Our default ClusterSPIFFEID's federatesWith included that
trust domain among several others, so every entry generated from that CSID
failed to create — surfacing to workloads as PermissionDenied: no identity issued — even though only one of several federated trust domains was
actually unhealthy.
(To be clear: this isn't "the whole batch call fails" — BatchCreateEntry/ BatchUpdateEntry already process and report per-entry status correctly. The
blast radius here is topological: one broad federatesWith fans out to many
entries, and each independently hits the same existence check.)
History
Creating entries with a FederatesWith for an unknown trust domain should succeed #1256 (2019): proposed the same fix. Discussion converged on a "sentinel
bundle" design (@azdagron's
comment):
auto-create an empty bundle row (trust domain set, data column NULL) as
a placeholder so the federated_registration_entries many-to-many join
always resolves, with fetch/list operations filtering out NULL-data rows
so placeholders are never mistaken for real bundles. This was chosen
specifically to avoid the schema/data migration a "proper" fix would
otherwise need (a decoupled table mapping entry ID -> trust domain
strings, discussed here). @dfeldman implemented an auto-create-on-missing-bundle approach in If a bundle doesn't exist yet when creating a federation reg. entry, create it #1448
(dfeldman:empty-bundle-federation). Note that this always created a real bundles row (never left a literal dangling foreign key — every entry's federatesWith still pointed at something that existed), but left two
things unresolved by his own admission: placeholder bundles showed up in bundle list operations indistinguishable from real ones, and nothing
cleaned them up when the owning relationship was removed.
The issue and PR were ultimately closed won't-fix, but not over literal
referential integrity (that was never actually at risk in If a bundle doesn't exist yet when creating a federation reg. entry, create it #1448's
implementation) — @evan2645's closing rationale points back to the original 2019 intent behind the strict check: "we did this consciously
with the intention of signaling to the registrant that something is up."
Auto-creating a bundle on any federatesWith value removes that
typo/misconfiguration signal, so a mistyped or stale trust domain string
would succeed silently and leave a permanent, unintentional bundle row
behind, with nothing to prompt an operator to notice. That's a real,
distinct cost from FK-integrity risk, and it's the main thing an opt-in
(rather than default-on) design needs to weigh.
Make this opt-in and server-wide (no per-relationship granularity, no CRD
or spire-api-sdk changes, no schema migration), addressing the concerns
that sank #1256/#1448:
Sentinel bundle, never a dropped reference. When enabled via a
server-wide config flag, and an entry references a trust domain with no bundles row, create a placeholder row (trust_domain set, data left NULL) instead of failing, inside makeFederatesWith. The entry keeps
its federatesWith reference — nothing is silently dropped — so anything
validating a peer against that trust domain's bundle still fails closed
for that one relationship (zero authorities to validate against),
instead of the workload getting no identity at all. No entry ever
references a nonexistent row, only a temporarily-empty one, preserving
the invariant Creating entries with a FederatesWith for an unknown trust domain should succeed #1256 was protecting.
NULLdata is already legal today — no schema change needed. Bundle.Data (pkg/server/datastore/sqlstore/models.go) has no not null constraint. A real bundle — even one that happens to have zero
authorities, which is a legitimate state a federated endpoint can publish
({} / {"keys":[]} is valid per pkg/common/bundleutil/unmarshal.go)
— always has non-NULLdata, because bundleToModel
(sqlstore.go) always marshals at least TrustDomainId before writing
it. So data IS NULL cleanly and unambiguously distinguishes "never
resolved placeholder" from "resolved, happens to be empty" — a
distinction decoded-authority-count alone cannot make.
Filter placeholders from FetchBundle/ListBundles, per @azdagron's original proposal, so they don't show up in spire-server bundle list/bundle show (or via GetFederatedBundle/ ListFederatedBundles) as if they were real (if empty) published
bundles.
No new cleanup mechanism on relationship deletion. Placeholder
bundles are ordinary bundles rows. Deleting a FederationRelationship
doesn't touch bundles today either, and shouldn't start now — spire-server bundle delete already defaults to RESTRICT, so any real
entry dependency blocks accidental removal. Building bespoke cleanup here
would risk recreating the exact dangling-reference bug this proposal
avoids.
Operator UX parity. Losing the hard failure removes the original
typo/misconfiguration signal (see "History"). Getting equivalent
visibility back isn't a single change: today the error reaches CLI/API
callers as an immediate, hard failure, and reaches spire-controller-manager
as both a specific log line and a bump to the existing ClusterSPIFFEID status stats. Matching that means work in a few
places: SPIRE Server surfacing a non-error warning on the affected
create/update result, the spire-server CLI actually displaying it
(today it only reads the result message on failure, not success), and
spire-controller-manager surfacing the same signal somewhere operators
already look (e.g. ClusterSPIFFEID/ClusterFederatedTrustDomain
status). None of this comes for free just by adding the server-side
flag.
Open questions for maintainers
Trigger point: proactively in CreateFederationRelationship, or lazily in makeFederatesWith only when actually referenced by an entry? Leaning
lazy, to avoid creating rows for trust domains nobody references yet.
Happy to put together a WIP PR if this direction seems reasonable to
maintainers.
Summary
When a registration entry's
federatesWithreferences a trust domain with nocorresponding
bundlesrow, SPIRE Server rejects the entry create/updateentirely:
This is enforced in
makeFederatesWith(
pkg/server/datastore/sqlstore/sqlstore.go), which requires every trustdomain listed in
federatesWithto resolve to an existingbundlesrowbefore the entry write succeeds.
Real-world impact
We run federation from several per-cluster trust domains into a central
cluster, managed by an operator reconciling
ClusterFederatedTrustDomainresources. During an incident, one federated cluster's bundle failed to
publish to its storage backend (S3 404 /
NoSuchKey). SPIRE Server had aFederatedTrustDomainrelationship for that trust domain registered, but nobundlesrow. Our defaultClusterSPIFFEID'sfederatesWithincluded thattrust domain among several others, so every entry generated from that CSID
failed to create — surfacing to workloads as
PermissionDenied: no identity issued— even though only one of several federated trust domains wasactually unhealthy.
(To be clear: this isn't "the whole batch call fails" —
BatchCreateEntry/BatchUpdateEntryalready process and report per-entry status correctly. Theblast radius here is topological: one broad
federatesWithfans out to manyentries, and each independently hits the same existence check.)
History
Creating entries with a FederatesWith for an unknown trust domain should succeed #1256 (2019): proposed the same fix. Discussion converged on a "sentinel
bundle" design (@azdagron's
comment):
auto-create an empty bundle row (trust domain set,
datacolumnNULL) asa placeholder so the
federated_registration_entriesmany-to-many joinalways resolves, with fetch/list operations filtering out
NULL-data rowsso placeholders are never mistaken for real bundles. This was chosen
specifically to avoid the schema/data migration a "proper" fix would
otherwise need (a decoupled table mapping entry ID -> trust domain
strings, discussed
here).
@dfeldman implemented an auto-create-on-missing-bundle approach in If a bundle doesn't exist yet when creating a federation reg. entry, create it #1448
(
dfeldman:empty-bundle-federation). Note that this always created a realbundlesrow (never left a literal dangling foreign key — every entry'sfederatesWithstill pointed at something that existed), but left twothings unresolved by his own admission: placeholder bundles showed up in
bundle listoperations indistinguishable from real ones, and nothingcleaned them up when the owning relationship was removed.
The issue and PR were ultimately closed won't-fix, but not over literal
referential integrity (that was never actually at risk in If a bundle doesn't exist yet when creating a federation reg. entry, create it #1448's
implementation) — @evan2645's closing rationale points back to the
original 2019 intent behind the strict check: "we did this consciously
with the intention of signaling to the registrant that something is up."
Auto-creating a bundle on any
federatesWithvalue removes thattypo/misconfiguration signal, so a mistyped or stale trust domain string
would succeed silently and leave a permanent, unintentional bundle row
behind, with nothing to prompt an operator to notice. That's a real,
distinct cost from FK-integrity risk, and it's the main thing an opt-in
(rather than default-on) design needs to weigh.
spire-oidc does not start if federatesWith contains a down federated endpoint #4445 (2023): same failure recurred via spire-controller-manager /
ClusterSPIFFEID. @evan2645 called it "pretty tough to fix" and asked fora volunteer; none appeared, closed without a fix.
Proposal
Make this opt-in and server-wide (no per-relationship granularity, no CRD
or
spire-api-sdkchanges, no schema migration), addressing the concernsthat sank #1256/#1448:
server-wide config flag, and an entry references a trust domain with no
bundlesrow, create a placeholder row (trust_domainset,dataleftNULL) instead of failing, insidemakeFederatesWith. The entry keepsits
federatesWithreference — nothing is silently dropped — so anythingvalidating a peer against that trust domain's bundle still fails closed
for that one relationship (zero authorities to validate against),
instead of the workload getting no identity at all. No entry ever
references a nonexistent row, only a temporarily-empty one, preserving
the invariant Creating entries with a FederatesWith for an unknown trust domain should succeed #1256 was protecting.
NULLdatais already legal today — no schema change needed.Bundle.Data(pkg/server/datastore/sqlstore/models.go) has nonot nullconstraint. A real bundle — even one that happens to have zeroauthorities, which is a legitimate state a federated endpoint can publish
(
{}/{"keys":[]}is valid perpkg/common/bundleutil/unmarshal.go)— always has non-
NULLdata, becausebundleToModel(
sqlstore.go) always marshals at leastTrustDomainIdbefore writingit. So
data IS NULLcleanly and unambiguously distinguishes "neverresolved placeholder" from "resolved, happens to be empty" — a
distinction decoded-authority-count alone cannot make.
FetchBundle/ListBundles, per@azdagron's original proposal, so they don't show up in
spire-server bundle list/bundle show(or viaGetFederatedBundle/ListFederatedBundles) as if they were real (if empty) publishedbundles.
bundles are ordinary
bundlesrows. Deleting aFederationRelationshipdoesn't touch
bundlestoday either, and shouldn't start now —spire-server bundle deletealready defaults toRESTRICT, so any realentry dependency blocks accidental removal. Building bespoke cleanup here
would risk recreating the exact dangling-reference bug this proposal
avoids.
typo/misconfiguration signal (see "History"). Getting equivalent
visibility back isn't a single change: today the error reaches CLI/API
callers as an immediate, hard failure, and reaches spire-controller-manager
as both a specific log line and a bump to the existing
ClusterSPIFFEIDstatus stats. Matching that means work in a fewplaces: SPIRE Server surfacing a non-error warning on the affected
create/update result, the
spire-serverCLI actually displaying it(today it only reads the result message on failure, not success), and
spire-controller-manager surfacing the same signal somewhere operators
already look (e.g.
ClusterSPIFFEID/ClusterFederatedTrustDomainstatus). None of this comes for free just by adding the server-side
flag.
Open questions for maintainers
CreateFederationRelationship, or lazily inmakeFederatesWithonly when actually referenced by an entry? Leaninglazy, to avoid creating rows for trust domains nobody references yet.
Happy to put together a WIP PR if this direction seems reasonable to
maintainers.