Skip to content

Support one-way SPIFFE compatibility with legacy identities - #149

Open
bellatrix007 wants to merge 15 commits into
linkedin:branch-3.6from
bellatrix007:bellatrix007/spiffe-urn-fallback
Open

bellatrix007 wants to merge 15 commits into
linkedin:branch-3.6from
bellatrix007:bellatrix007/spiffe-urn-fallback

Conversation

@bellatrix007

Copy link
Copy Markdown

Description

Depends on #142. This branch is built on Sanay's SPIFFE branch and targets branch-3.6. Merge #142 first, then rebase this branch if needed.

The current upstream diff includes Sanay's unmerged SPIFFE implementation (18 files total). The description below covers the 16-file compatibility increment on top of it. Review that increment separately in the compatibility-only diff; this PR uses the same head commit, c7dbf32495c5ceb1274fd6e3a558b1f61dc6c270.

Preserve certificate-derived client ID strings while supporting one-way SPIFFE-to-legacy application-name compatibility in URI-domain mappings, direct znode ACLs, and opt-in legacy superuser-ID selection. Eligible SPIFFE applications can use bare application names or formatted service-principal targets; legacy SAN/DN clients retain their original pre-PR matching behavior.

getClientId(...) returns an immutable ClientIdentity with the original ID and its extraction type: SPIFFE_V1_WL, SPIFFE_V1_WORKLOAD, SPIFFE_V2, LEGACY_SAN, or SUBJECT_DN. The original SPIFFE/configured SAN/Subject DN extraction order and strings are unchanged. The earlier automatic certificate-side URN normalization is removed.

Successful authentication retains this pair on the connection, separately from AuthInfo. Authentication failure clears it. Direct ACL checks therefore use authenticated type information without reparsing certificates per request or changing stored identity strings.

URI-domain matching

Lookup order:

  1. Return an exact client-ID mapping if present.
  2. Otherwise, collect domains from the existing slash-aligned prefix mappings. Any matching prefix takes precedence over legacy compatibility, including an explicitly empty prefix mapping.
  3. Only when both lookups miss, compare an eligible SPIFFE application's name with bare application names or names parsed from legacy service-principal mapping keys.

Eligible identities:

Extraction type Original client ID Name used only for compatibility
SPIFFE_V1_WL kafka kafka
SPIFFE_V1_WORKLOAD application/example-mp/kafka kafka
SPIFFE_V2 application/example-mp/kafka kafka
Either application type, with a tag application/example-mp/kafka/cluster-a kafka

Application paths must be exactly application/<mp>/<app>[/<tag>], with nonempty segments. Product-only paths such as application/example-mp, missing app segments, arbitrary v2 paths, and user/group/airflow/workload paths do not gain legacy service-principal compatibility. The product or tag is never used in place of the app name.

Bare names (kafka), truncated names (servicePrincipal(kafka), closed names (servicePrincipal(kafka)), and complete service-principal URNs are supported. Compatible legacy keys contribute a union of their domain sets only on fallback. Legacy SAN/DN and string-only lookup retain their existing behavior.

New bare-name aliases must match ^[A-Za-z0-9][A-Za-z0-9._-]*$ and are compared literally. This is a conservative compatibility grammar, not a change to certificate extraction or an assertion about every valid application name. For example, application/example-mp/CN=admin cannot acquire an alias to CN=admin; the same applies to urn:example:admin and other structured-looking targets. Existing exact matching and formatted service-principal parsing remain unchanged. A full application-path target is never shortened.

The existing znode can remain:

/zookeeper/uri-domain-map/kafka/servicePrincipal(kafka

A legacy SAN ID servicePrincipal(kafka matches its exact key. A SPIFFE v1/wl ID kafka, or a v1/v2 ID application/example-mp/kafka, can reach the same domain through the compatibility fallback. A bare mapping such as /zookeeper/uri-domain-map/broker-access/kafka is also eligible for a full SPIFFE application identity's fallback; neither mapping needs renaming.

Legacy compatibility compares app names only; it does not distinguish products or tags. Full-path or prefix mappings take precedence when product/tag-specific domain selection is configured. The full authenticated identity is retained.

Direct znode ACL matching

Authenticated client identity Matching direct ACL
(SPIFFE_V1_WL, "kafka") x509:kafka (exact), or x509:servicePrincipal(kafka
SPIFFE v1/v2 application/example-mp/kafka[/cluster-a] x509:kafka or x509:servicePrincipal(kafka
(LEGACY_SAN, "kafka") x509:kafka (existing exact match)
(LEGACY_SAN, "servicePrincipal(kafka") x509:servicePrincipal(kafka (existing exact match)

Closed and full-URN service-principal ACL forms are supported as well. An application identity is not rewritten into a bare ID. The reverse legacy-service-principal-to-bare-name alias added in an earlier iteration of this PR is removed, restoring original pre-PR legacy-client behavior. Legacy IDs do not gain new aliases to bare or full product/tag-qualified application ACLs.

A bare ACL ID can also denote a domain. This intentionally allows an eligible SPIFFE application to match a same-named bare domain ACL; app-name compatibility does not distinguish products or tags.

LegacyServicePrincipalMatcher shares the rules between domain lookup and direct ACL checks. The optional connection-aware AuthenticationProvider.matches(...) overload is forwarded by the existing provider wrapper; other providers retain their original string matcher. Both X509 providers use the authenticated identity pair.

The candidate AuthInfo ID must equal the original certificate-derived ID. A mapped domain cannot be reinterpreted as another service principal. Ordinary x509 identities and creator ACL expansion rules are unchanged; no synthetic x509 aliases are added. Superuser role markers are described below.

User/group/metadata principal names, untyped identities, unrelated application names, and Subject DN identities do not gain compatibility access. Matching is case-sensitive, and normal ACL permission checks remain enforced.

Opt-in legacy superuser-ID compatibility

New shared Java system property, disabled by default:

-Dzookeeper.ssl.x509.legacySuperUserCompatibilityEnabled=true

This applies to zookeeper.X509AuthenticationProvider.superUser and zookeeper.X509ZNodeGroupAclProvider.superUserId selection. Exact configured client-ID matches always take precedence and do not require the option. When enabled, eligible SPIFFE v1/wl or complete v1/v2 application identities can instead match a configured bare application name or formatted legacy service principal using the same compatibility rules. Multiple compatible configured IDs select a stable marker.

The authenticated certificate-derived identity is unchanged. The super AuthInfo entry uses the matched configured ID:

Authenticated ID:       application/example-mp/kafka
Configured superuser:   servicePrincipal(kafka
Super AuthInfo marker:  super:servicePrincipal(kafka

Keeping the configured marker is important: PrepRequestProcessor.fixupACL() still recognizes the connection as an explicit superuser rather than a cross-domain component, so it does not accidentally replace that superuser's requested ACLs under the group provider's automatic ACL policy.

Only forward SPIFFE-to-legacy compatibility is added for superuser selection, just as for domain and direct-ACL fallback. User/group/airflow/arbitrary-v2/DN identities do not gain superuser aliases, and mapped domain names are not substituted for the authenticated identity. Existing exact legacy SAN/DN superuser settings remain valid, including structured ID strings excluded from the new bare-name alias grammar.

This flag does not gate existing URI-domain or direct-ACL compatibility and does not change cross-domain grants. A mapping into a configured cross-domain domain continues to produce its existing super role regardless of the new flag.

Privilege scope: legacy app names do not distinguish products or tags. Enable this only when all eligible trusted identities with that app name should have full superuser privileges. Treat it as a startup setting and restart servers or reconnect clients when changing it; it is not an immediate revocation switch for already authenticated connections.

No workflow, dependency, quorum-TLS, or unrelated refactoring changes are included.

Tests

  • Local code review completed
  • Original identity strings and extraction types covered with real certificates and existing SAN/DN cases
  • Complete application identities, optional tags, missing app segments, and non-application types covered
  • Exact/prefix precedence, prefix-domain union, empty prefix mappings, and legacy fallback covered
  • Real-certificate authentication exercises mapping deletion/recreation while retaining the full ID
  • Direct ACL parity exercised through ZooKeeperServer.checkACL and both X509 providers' matching paths
  • Permission denials, principal-type boundaries, mapped-domain isolation, missing authentication context, failed-authentication clearing, unchanged creator ACLs, and superuser behavior covered
  • Both providers' default-off and enabled superuser compatibility, exact precedence, stable configured markers, type/name denials, and original identity preservation covered
  • Explicit-superuser ACL handling distinguished from cross-domain ACL replacement; certificate trust rejection and mapped-domain non-promotion retained
  • Bare-name grammar boundaries, literal DN/URN-shaped SPIFFE app segments, unchanged formatted service-principal parsing, and original exact legacy SAN/DN behavior covered

The mapping fixture uses the existing temporary-directory helper to isolate its test database.

Latest focused run:

mvn -q -pl zookeeper-server -am \
  -Dtest=X509DirectAclTest,X509AuthTest,X509AuthFailureTest,X509SpiffeAuthIntegrationTest,ZkClientUriDomainMappingHelperTest,X509ZNodeGroupAclProviderTest,AuthUtilTest,ClientSSLTest,SSLAuthTest \
  -DfailIfNoTests=false -Dsurefire-forkcount=1 test

Results: 96 tests passed (19 + 10 + 1 + 19 + 23 + 14 + 2 + 6 + 2), with no failures, errors, or skipped tests. The single-fork setting avoids the existing provider fixture's intermittent parallel port-binding conflicts.

Known inherited gaps

The broader review reproduced two behaviors in code inherited from the base: deleting the last nested mapping leaf can cause its parent to become a broader prefix grant, and the basic X509 provider's DN-only ACL validator rejects explicit non-DN ACL authoring (including bare names, service principals, and application paths) even though existing ACL matching succeeds. The passing suites are not evidence that these cases are fixed. This focused compatibility change does not resolve either issue; they remain rollout considerations.

Changes that Break Backward Compatibility (Optional)

Both public getClientId(...) overloads return ClientIdentity rather than String. In-repository callers are updated; external callers must recompile and use .getId() for the previous string result.

The connection-aware matches(...) method is a default interface overload, preserving existing authentication-provider implementations. String-only domain lookup retains its original behavior.

The intended authorization change is one-way compatibility from eligible SPIFFE application identities to equivalent bare or formatted legacy application names. Certificate validation, raw extracted IDs, stored AuthInfo/ACL formats, normal ACL permission checks, and quorum configuration are unchanged. Superuser-ID compatibility is separately opt-in; existing superuser settings and exact matching remain supported. Original pre-PR legacy-client matching is preserved; the reverse alias introduced earlier in this PR is not retained.

For the domain provider, direct principal ACL compatibility applies when the original client ID is present in AuthInfo under the existing configuration; domain-only AuthInfo is not promoted to a principal.

Documentation (Optional)

The typed identity result, lookup order, application-name requirements, direct-ACL rules, and compatibility boundaries are documented above and in the implementation. The new superuser option, its scope, marker semantics, and activation caveats are documented in zookeeperAdmin.md.

Generated with GitHub Copilot CLI

Sanju98 and others added 15 commits May 14, 2026 14:39
Adds server-side support for SPIFFE-issued client certificates in X509 auth.
The principal is the ILM UID (path-after-/v2/), aligned with the LinkedIn ILM
v2 design: trust-domain stripped, segment-prefix matching for ACL lookup.

Key changes:
- X509AuthenticationUtil.matchAndExtractSpiffeSAN extracts the ILM UID from
  v2 SPIFFE URIs. v1 SPIFFE URIs and user-identity URIs (/v<N>/user/...) fall
  through to existing URN / Subject-DN extraction.
- X509AuthenticationConfig adds spiffe.sanMatchRegex as the operator-
  controlled trust-domain gate.
- ZkClientUriDomainMappingHelper recursively walks the znode subtree below
  each domain. Only leaf znodes are registered as keys (path joined by '/'),
  letting multi-segment SPIFFE UIDs be expressed as nested znodes (whose
  names can't contain '/'). getDomains does exact-match then segment-prefix
  walk-up. clientUriToDomainNames is volatile with in-method snapshot.
- Defense-in-depth: SPIFFE path extraction uses URI.getRawPath() and rejects
  any path containing '%' to prevent URL-decoding bypass of identity checks.

Tests: 29/29 pass (X509AuthTest 13, X509SpiffeAuthIntegrationTest 6,
ZkClientUriDomainMappingHelperTest 10; includes end-to-end SPIFFE-cert
through X509ZNodeGroupAclProvider with real znode mapping).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oduction path

Two fixes addressing the code review on PR linkedin#142:

1. X509AuthenticationConfig.getSpiffeSanMatchPattern now uses
   double-checked locking with volatile fields and a dedicated lock
   object, matching the pattern already used by allowedClientIdAsAclDomains
   and other lazy-loaded fields in the same class. The previous lazy-init
   was a data race on the per-handshake hot path.

2. X509ZNodeGroupAclProvider's setDomainAuthUpdater lambda now calls
   helper.getDomains(clientId) instead of the raw map's getOrDefault,
   so the segment-prefix walk-up added to ZkClientUriDomainMappingHelper
   is reachable from the production authentication path. This is the
   znode-tree analogue of LinkedIn's documented acl-tool wildcard idiom
   (`--spiffe "application/<mp>/*"`); without this fix, MP-level prefix
   grants documented in the class javadoc would silently no-op.

Adds testA4_SpiffeCertResolvesViaPrefixWalkUpToDomainAuthInfo: real
SPIFFE cert with a 4-segment principal resolves to an MP-level leaf
grant through the full provider->helper.getDomains path. This test
would have failed before fix linkedin#2 (the exact-match lookup misses the
4-segment principal against a 2-segment registered prefix).

All 30 SPIFFE-related tests pass (X509AuthTest 13 +
X509SpiffeAuthIntegrationTest 6 + ZkClientUriDomainMappingHelperTest 11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per @rgodha's review comment, the extractor now accepts both SPIFFE
versions instead of rejecting v1 outright:

- v2 (existing): spiffe://<td>/v2/<path> → principal is the full path
  after /v2/ (the ILM UID, e.g. "application/foo-mp/bar-app").
- v1 workload (new): spiffe://<td>/v1/wl/<app-name> → strip the "wl/"
  type prefix; principal is just the app-name. This matches how legacy
  authZ handled v1 identities. Other v1 paths (e.g. v1/wf/ workflow)
  still fall through to URN/DN.

User-identity URIs (/v<N>/user/...) continue to be rejected for both
versions — they must never be promoted to a service principal.

The test match regex broadens to ^spiffe://.*/v[12]/.*$ so existing
test scaffolding exercises both versions. testSpiffeV1FallsBackToDn
becomes testSpiffeV1WlAuth (now asserts extraction). The integration
test for v1 likewise flips from fall-back-to-DN to v1/wl extraction.

LISPIFFE-ID spec reference:
https://github.com/linkedin-multiproduct/gopki/blob/master/LISPIFFE-ID.md#2-uri-path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… and add regression tests for a real Grestin cert's dual urn:li: SAN scenario.
Previously SPIFFE detection was gated behind the opt-in
ssl.x509.spiffe.sanMatchRegex system property and was a no-op unless an
operator explicitly configured it. This removes that config knob entirely:
X509AuthenticationUtil#getClientId now checks for a spiffe:// URI SAN
unconditionally, before the clientCertIdType-gated legacy URN fallback,
regardless of how (or whether) clientCertIdType is configured.

- Remove SSL_X509_SPIFFE_SAN_MATCH_REGEX and its lazy-loaded Pattern
  field/getter/setter from X509AuthenticationConfig.
- X509AuthenticationUtil: replace the configurable SPIFFE match pattern
  with an unconditional constant and run SPIFFE detection first,
  unconditionally, in getClientId().
- Update SpiffeAuthTestUtil and existing SPIFFE tests to drop references
  to the removed config property.
- Add regression tests proving SPIFFE v1/v2 extraction and SPIFFE
  user-identity rejection work with zero clientCertIdType configuration
  (X509AuthTest, X509SpiffeAuthIntegrationTest).
…tion

LISPIFFE-ID spec section 2.A lists application/<...> and airflow/<....>
as valid v1 workload sub-types alongside wl/, which were not previously
recognized. Per rgodha's review comment on PR linkedin#142, extend the v1
extractor to accept these forms, retaining the type prefix in the
principal (matching v2 semantics), while wl/ keeps its existing
bare-app-name behavior. v1/wf/ (Flyte workflow) remains out of scope.

Adds 6 new tests across X509AuthTest and X509SpiffeAuthIntegrationTest.
X509AuthTest previously duplicated most SPIFFE URI-SAN extraction
scenarios using TestCertificate, a hand-rolled X509Certificate whose
getSubjectAlternativeNames() just returns a canned list -- it never
exercises real ASN.1/SAN encoding or the JDK's certificate parsing.

X509SpiffeAuthIntegrationTest already covered several of the same
scenarios using real BouncyCastle-signed certs, but had a few gaps.

Changes:
- Added 8 real-cert tests to X509SpiffeAuthIntegrationTest to close
  the coverage gaps: v1/wl zero-config, v1/wl multi-segment fallback,
  v1/user rejection, v2/workload extraction, v2/user zero-config
  rejection, non-SPIFFE-SAN-falls-back-to-URN (with URN configured),
  multiple-SPIFFE-SANs fallback, and SPIFFE-wins-over-URN precedence.
- Removed the now-redundant SPIFFE-specific mock tests from
  X509AuthTest (17 tests across ~230 lines), along with the
  SPIFFE_V1_URI/SPIFFE_V2_URI fixtures and now-unused imports.
  X509AuthTest keeps only the generic (non-SPIFFE) auth/SAN-regex
  tests, which still use the lightweight fake certificate since they
  don't need real certificate parsing.
- X509SpiffeAuthIntegrationTest is now the authoritative, real-cert
  suite for SPIFFE certificate validation and principal extraction.

Verified: X509AuthTest (6 tests) + X509SpiffeAuthIntegrationTest
(18 tests, up from 10) all pass.
DEPEND-89163 follow-up: X509AuthenticationUtil.getClientId() now
recognizes LinkedIn's legacy Grestin-issued
'urn:li:servicePrincipal(<name>;...)' URI SAN automatically and
resolves it to the bare <name>, mirroring how the SPIFFE v1/wl
extraction strips its 'wl/' type prefix. This removes the need for
every cluster to hand-author a clientCertIdSanExtractRegex just to
get parity between legacy Grestin certs and SPIFFE certs during
migration (e.g. Kafka's 'servicePrincipal(kafka' vs SPIFFE's 'kafka'
znode-name mismatch).

The new pattern only runs when clientCertIdType=SAN was never
configured, so clusters that already tuned a SAN extractRegex (e.g.
one that intentionally keeps the 'servicePrincipal(' prefix, or one
that falls back to Subject DN on a misconfigured regex) keep their
existing, unchanged behavior.

Adds 4 unit tests to X509AuthTest covering: auto-extraction with no
config, correct behavior with a sibling servicePrincipalMetadata(...)
SAN present, non-interference with an already-configured
clientCertIdType=SAN cluster, and the existing no-match fallback to
Subject DN.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove company and internal-ticket references from the added comments and use generic certificate fixtures. Clarify that configured SAN extraction failures retain the Subject DN fallback; extraction behavior is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace automatic certificate-side URN normalization with type-aware domain lookup. Return an immutable certificate-type/client-ID pair while preserving the original SPIFFE, configured SAN and Subject DN strings. Only SPIFFE v1/wl identities may fall back to exact application names parsed from legacy service-principal mapping keys; explicit mappings and full-path matching retain precedence and behavior.

Cover typed identities, legacy mapping compatibility, exact-match precedence, principal/type isolation and mapping refresh. Use independent temporary databases for the mapping fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retain the authenticated certificate-type/client-ID pair on each connection and forward connection context through a backward-compatible authentication-provider matcher overload. Share legacy service-principal parsing between domain lookup and direct ACL checks.

Allow SPIFFE v1/wl identities to match legacy service-principal ACLs and typed legacy service principals to match bare application ACLs. Bind compatibility to the original authenticated ID so mapped domains, other principal kinds and v2 paths are not reinterpreted. Preserve AuthInfo, creator ACLs, permission checks and exact superuser handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Try exact and segment-prefix domain mappings before legacy service-principal fallback. Require an explicit application/product/app path with an optional tag before comparing the application segment; retain existing v1/wl support.

Reuse the same rule for direct ACLs without changing certificate extraction, stored identities, permissions or authenticated-ID binding. Preserve legacy-to-bare-name compatibility without inferring product or tag context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep exact superuser matches first and share typed SPIFFE-to-legacy service-principal selection between both X509 providers. The new zookeeper.ssl.x509.legacySuperUserCompatibilityEnabled setting defaults to false.

Preserve the original certificate identity and use the matched configured ID as the super marker, retaining explicit-superuser ACL handling without changing cross-domain policy. Document activation and privilege scope and add focused regression coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allow eligible SPIFFE application identities to match bare or formatted legacy targets without changing certificate-derived IDs. Restrict new bare aliases to conservative ASCII application names and remove the reverse alias introduced by this branch, preserving original legacy matching.

Retain authenticated-ID binding, exact-match precedence and default-off superuser compatibility. Document the boundaries and cover structured targets and legacy exact matches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants