Skip to content

KNOX-3520: Support multi-hop RFC 8693 delegation via policy-bound subject audiences - #1460

Open
smolnar82 wants to merge 1 commit into
apache:masterfrom
smolnar82:KNOX-3520
Open

smolnar82 wants to merge 1 commit into
apache:masterfrom
smolnar82:KNOX-3520

Conversation

@smolnar82

Copy link
Copy Markdown
Contributor

KNOX-3520 - Support multi-hop RFC 8693 delegation via policy-bound subject audiences

What changes were proposed in this pull request?

A token-exchange topology validates the subject_token and the actor_token against the same knox.token.audiences list. A token minted by a delegated exchange carries only the requested target audience, so it can never be presented again as a subject_token, and on-behalf-of chains stop after the first hop, even though delegation.max.actor.chain.depth allows deeper chains. The workarounds either break recipient binding (adding the token-service audience to every downstream token) or open the gate (dropping knox.token.audiences, which lets any stolen SA token, minted for any audience, act as a delegation actor).

This PR adds one narrow relaxation that is bound to the actor: a re-exchanged subject_token is accepted only by the service it was addressed to.

New delegation policy field: subjectAudiences

  • The audiences that identify the actor as a recipient. This is the inbound counterpart of resourcePolicy, which lists the services the actor may mint tokens for.
  • Optional, defaults to empty. An empty set never matches, so existing policies behave exactly as before.
  • Stored in a new DELEGATION_POLICY_SUBJECT_AUDIENCES child table, added to both the default and the Oracle DDL scripts, with ON DELETE CASCADE. createTablesIfNotExist creates missing tables at startup, so existing databases pick up the new table.
  • Exposed through the KNOXIDF_ADMIN delegation-policy API (request and response). Blank values are rejected with 400 invalid_request.
  • JdbcDelegationPolicyService#evaluate denies with the new audit reason subject_audience_not_allowed when the check is required and the subject token's aud does not intersect subjectAudiences. The client still gets the generic 400 invalid_request "rejected by policy".

New JWTProvider parameters (both off by default)

Parameter Effect
token.exchange.subject.token.policy.audiences.issuers Comma-separated issuers. On a delegated exchange (actor_token present), a subject_token from one of these issuers that fails the knox.token.audiences check is accepted only if its aud intersects the actor policy's subjectAudiences. Never applies to actor tokens, headless or same-subject exchanges. Unset: today's behavior.
delegation.headless.reject.subject.with.act When true, a headless subject_token (the acting party) that carries an act claim is rejected with 400 invalid_request (audit reason headless_subject_has_act).

Resulting audience rules

# Token Rule Status
1 actor_token, and the headless subject_token aud must contain a knox.token.audiences value Unchanged
2 First-hop delegated subject_token aud must contain a knox.token.audiences value Unchanged
3 Re-exchanged delegated subject_token Without a knox.token.audiences value, accepted only if an actor_token is present, iss is listed, and aud ∩ subjectAudiences ≠ ∅ New
4 Same-subject subject_token aud must contain a knox.token.audiences value Unchanged
5 Headless subject_token Must not carry act (when enabled) New

Implementation

  • PolicyBoundSubjectAudienceValidator (new) wraps the topology's RequestAudienceValidator. When the knox.token.audiences check fails for a token from a listed issuer, it defers the decision to the policy instead of rejecting. Signature, issuer, expiry and not-before checks are unchanged and still enforced.
  • TokenExchangeHandler uses it only for delegated exchanges (createSubjectAudienceValidator), and passes the subject token's audiences, plus whether the policy match is required, to the policy check through PolicyCheckRequest.
  • JWTFederationFilter reads the two new parameters.

Docs
knox-site/docs/knoxidf/:

  • token_exchange.md: the new subjectAudiences field, evaluation order, a new "Multi-hop delegation" section, and the configuration table.
  • configuration.md: the 2 new parameters.
  • endpoints.md: the admin API field and validation.
  • security.md: 2 new bullets.
  • operations.md: the new deny reason.

How was this patch tested?

  • mvn -pl gateway-spi,gateway-server,gateway-provider-security-jwt,gateway-service-knoxidf verify. All modules pass, including checkstyle, PMD, SpotBugs and forbiddenapis.
  • New and updated unit and integration tests:
    • PolicyBoundSubjectAudienceValidatorTest (new): a delegate pass returns no policy match; a delegate failure is accepted provisionally only for a listed issuer (not for unlisted or null issuers); the configured audiences are passed to the delegate; the flag is reset on each validation.
    • TokenExchangeHandlerTest:
      • rule 3: a policy match is required only for a listed issuer whose token fails the configured audience check; a mismatch is rejected with invalid_request and audited;
      • rule 3 never applies without an actor token, to headless exchanges, when no issuers are configured, or to the actor token;
      • rule 5: enabled and disabled, with and without act, and not applied to delegated exchanges.
    • JWTFederationFilterDelegationConfigTest: parsing of the new parameters.
    • JWTFederationFilterTokenExchangeTest, full filter path:
      • a re-exchanged token from a listed issuer requires the policy audience match;
      • a bad signature is still rejected (401) even when the issuer is listed;
      • with no issuers configured, the token is rejected with "missing required token audience";
      • actor tokens and same-subject exchanges are not relaxed.
    • JdbcDelegationPolicyServiceTest:
      • subject_audience_not_allowed cases: match, mismatch, empty policy set, token without aud, ignored when not required;
      • checked before the subject and resource checks;
      • register, update, registerOrUpdate and delete of subjectAudiences.
    • DelegationPolicySchemaTest and KnoxDatabaseTest: the new table is queryable, enforces its FK and cascades on delete; a pre-KNOX-3520 schema gets the table on upgrade.
    • DelegationPolicyResourceTest: field omitted, persisted, null elements filtered; empty or whitespace values rejected on register, update and registerOrUpdate.
    • DelegationPolicyDtoImmutabilityTest: defensive copies in the request and response DTOs.
    • DelegationPolicyTest and PolicyCheckRequestTest: new fields, defaults, immutability.
  • Docker Compose integration suite run locally: 149 passed.

Integration Tests

  • New topology .github/workflows/build/conf/topologies/knoxidf-token-multi-hop.xml, added to the Dockerfile. It sets knox.token.audiences=https://recipient1, its own issuer (listed in both jwt.expected.issuer and token.exchange.subject.token.policy.audiences.issuers), delegation.headless.reject.subject.with.act=true, delegation.max.actor.chain.depth=3, nested act claims, and the passthrough audience validator.
  • New suite .github/workflows/tests/test_multi_hop_delegation.py (11 tests). Policies are seeded through the KNOXIDF_ADMIN API. tom is the subject; recursiveUser and sam alternate as actors.
    • 2-hop and 3-hop chains succeed. Each minted token has sub=tom, the multi-hop issuer, only the requested aud, and the full nested act chain.
    • A 4th hop is rejected by delegation.max.actor.chain.depth.
    • A re-exchange is rejected by policy when the actor's subjectAudiences do not match, or are empty.
    • A delegated token from an unlisted issuer is rejected for the missing audience, even though it is addressed to the actor.
    • A re-exchanged token is rejected in a same-subject exchange and as a headless subject.
    • A delegated token that does carry the token-service audience is still rejected as a headless subject (rule 5).
    • Actor tokens and first-hop subject tokens without the token-service audience are rejected.
  • delegation_helpers.py:
    • DelegationPolicyAdmin.register()/update() accept subject_audiences.
    • New shared assert_issued_jwt() helper.
tests-1  | ------------------------------------
tests-1  | Your code has been rated at 10.00/10
tests-1  | 
tests-1  | Waiting for knox...
tests-1  | ============================= test session starts ==============================
tests-1  | platform linux -- Python 3.10.20, pytest-9.0.3, pluggy-1.7.0
tests-1  | rootdir: /tests
tests-1  | plugins: platformdirs-4.12.4
tests-1  | collected 149 items
tests-1  | 
tests-1  | test_clientid_credentials.py .......                                     [  4%]
tests-1  | test_delegation.py .........                                             [ 10%]
tests-1  | test_health.py .....                                                     [ 14%]
tests-1  | test_k8s_delegation.py ........                                          [ 19%]
tests-1  | test_k8s_jwks_ca.py ...........                                          [ 26%]
tests-1  | test_k8s_serviceaccount_validation.py ......                             [ 30%]
tests-1  | test_knox_admin_path_traversal.py ...                                    [ 32%]
tests-1  | test_knox_auth_service_and_ldap.py ...                                   [ 34%]
tests-1  | test_knox_configs.py .                                                   [ 35%]
tests-1  | test_knox_ldap_backend_password_alias_retry.py ..                        [ 36%]
tests-1  | test_knox_ldap_cache.py ...                                              [ 38%]
tests-1  | test_knox_ldap_injection.py .......                                      [ 43%]
tests-1  | test_knox_ldap_proxy_search.py .........                                 [ 49%]
tests-1  | test_knoxauth_preauth_and_paths.py ......                                [ 53%]
tests-1  | test_knoxauth_token_forwarding.py ...........                            [ 61%]
tests-1  | test_knoxidf.py .......                                                  [ 65%]
tests-1  | test_knoxsso_redirect.py .                                               [ 66%]
tests-1  | test_knoxtoken_jwt.py ....................                               [ 79%]
tests-1  | test_multi_hop_delegation.py ...........                                 [ 87%]
tests-1  | test_remote_auth.py ...                                                  [ 89%]
tests-1  | test_remoteauth_extauthz_additional_path.py ....                         [ 91%]
tests-1  | test_token_exchange.py ............                                      [100%]
tests-1  | 
tests-1  | =============================== warnings summary ===============================
...
tests-1  | ----------------- generated xml file: /tests/test-results.xml ------------------
tests-1  | ================= 149 passed, 139 warnings in 83.73s (0:01:23) =================
tests-1 exited with code 0
Aborting on container exit...
Container compose-tests-1 Stopping 
Container compose-tests-1 Stopped Config   w Enable Watch   d Detach

…ject audiences

- New delegation policy field subjectAudiences (schema, admin API, policy evaluation,
  deny reason subject_audience_not_allowed)
- token.exchange.subject.token.policy.audiences.issuers: on a delegated exchange, accept a
  subject_token from a listed issuer that lacks knox.token.audiences only if its aud
  intersects the actor policy's subjectAudiences
- delegation.headless.reject.subject.with.act: reject a delegated token as a headless subject
- Unit, integration and E2E tests (knoxidf-token-multi-hop topology); docs

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Test Results

  4 files    4 suites   1m 40s ⏱️
156 tests 156 ✅ 0 💤 0 ❌
180 runs  180 ✅ 0 💤 0 ❌

Results for commit 6f71e00.

This branch has not been deployed

No deployments
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.

1 participant