hsheinblatt opened a new pull request, #1448: URL: https://github.com/apache/knox/pull/1448
KNOX-3509 - Add k8s service DNS audience form, non-delegation audience validation, any-path matching, and actor-subject binding to the k8s destination audience validator ## What changes were proposed in this pull request? Extends K8sDestinationAudienceValidator with a second accepted aud format, extends destination validation to tokens with no act claim, allows an empty path to mean service-only matching (any destination path), and adds an opt-in check binding a delegation token's actor to the service account presenting it. Also adds a parameter to disable the new format for a deployment that cannot use it safely. A second accepted aud entry shape, the "DNS form", is https://<service>[.<namespace>[.svc[.<cluster-domain>]]][:port] [/resource-path] -- the k8s service DNS name a client already calls, shortened from the right the way a DNS resolver search path allows. The previously-supported shape, now called the "custom form", is unchanged: https://cluster-domain[:port]/[skipped-segments/]namespace /service-name[/resource-path]. Each aud entry is tried as the DNS form first and the custom form second, under the new AudienceResource record shared by both parse methods (replacing the single custom-form parser), and is accepted if either matches; the two forms produce the same AudienceResource fields, so the rest of the validator's matching logic does not need to know which form a candidate came from. A host of three or more labels must have "svc" as its third label, and any labels past it must be a label-boundary prefix of the configured request.audience.k8s.dns.format.cluster.domain (default cluster.local) taken from the left; a trailing dot on the host is rejected, as a deliberate restriction rather than an oversight -- no normal client request produces one. A one-label (service-only) DNS-form host has no namespace of its own; its namespace is instead taken from the source workload's SPIFFE id (see below) and must equal the destination namespace, which is the one case in this validator where a segment with no configured source is still compared against rather than skipped. The custom form's cluster-domain allow-list, request.audience.k8s.cluster-domains, now applies only to custom-form entries; the DNS form's cluster domain is checked separately, against the new single-value request.audience.k8s.dns.format.cluster.domain, named apart from cluster-domains specifically so the two are not mistaken for each other. An aud entry with no resource path, or a resource path of exactly "/", now matches any request path for either form, even when the path header is configured and present -- it is a statement about which service a token may be presented to, not about which operation on that service it may invoke. request.audience.k8s.dns.format.enabled (default true) controls whether the DNS form is tried at all. A deployment fronting more than one Kubernetes cluster or trust domain behind a single validator instance should set it false: a DNS-form host carries no cluster or trust-domain identifier of its own, unlike the custom form's explicit cluster-domain allow-list, so there is no way to tell which cluster a bare <service>.<namespace> host belongs to. When false, every entry is evaluated only as the custom form and dns.format.cluster.domain is unused. request.audience.k8s.validate.audiences.without.act.claim (default true) extends destination validation to a token with no act claim, i.e. one that is not a delegation token: its aud claim is now checked against the request's actual destination the same way a delegation token's is, and the request is rejected if it carries no aud claim or one that is not a valid destination for this request. Setting it to false sends such a token to AbstractJWTFilter.matchesConfiguredAudiences instead, the same handling every token had before this change. This parameter has no effect on a token that does carry an act claim; that token is unconditionally destination-validated regardless of its value. request.audience.k8s.enforce.act.sub.service-account.matches.source.spiffeid (default false) binds a delegation token's actor to the service account presenting it: when true, the most recent actor in the token's act chain, read via the existing TokenUtils.extractActorChain, must be a Kubernetes service-account subject (system:serviceaccount:<namespace> :<service-account-name>, parsed by the new ServiceAccountSubject record, which ActorIdentity now also uses for its own prefix constant) matching the namespace and service-account name in the SPIFFE id named by the new request.audience.k8s.source.spiffe-id.header.name. That header also supplies the namespace for a namespace-less DNS-form host, as above. Enabling this parameter without configuring that header fails topology startup rather than leaving the check silently inactive. request.audience.k8s.enforce.act.sub.is.service.account (default false), consulted only when the parameter above is true, controls whether an actor subject that cannot be read in that form fails validation (true) or passes unchecked (false). An act claim present but not a JSON object -- so no actor subject can be read from it at all -- fails validation when the matches-source-spiffeid check is enabled, the same as any other rejected actor subject; this is distinct from a token with no act claim at all, which the check does not apply to. Elsewhere, parsing a comma-separated list (currently only cluster-domains) no longer silently drops an entry that trims to empty: a stray ",," or trailing "," now produces an empty entry that is passed through to parsing and rejected there, the same as any other malformed entry, since this is operator-supplied configuration and not request data. knox-site/docs/config_request_audience_validator.md is rewritten to describe both aud formats, the dns.format.cluster.domain vs. cluster-domains split, the path-wildcard behavior, non-delegation validation, and actor-subject binding, including a worked provider-descriptor example and an item-by-item table of what that example configuration enforces. What changed: - gateway-provider-security-jwt: AudienceResource (DNS-form parsing added alongside the existing custom-form parsing, both producing the same record); K8sDestinationAudienceValidator (dns.format.enabled, dns.format.cluster.domain, source.spiffe-id.header.name, validate.audiences.without.act.claim, enforce.act.sub.service-account.matches.source.spiffeid, enforce.act.sub.is.service.account params; dual-format resolution; path-wildcard matching; non-delegation validation; actor-subject binding; stricter comma-separated-list parsing); ServiceAccountSubject (new record); ActorIdentity (reuses ServiceAccountSubject's prefix constant instead of its own copy, no behavior change). - knox-site: config_request_audience_validator.md rewritten for the above. ## How was this patch tested? - AudienceResourceTest (63 cases, up from 19): all prior custom-form cases retained; new DNS-form cases covering one/two/three/four/five label hosts, the literal "svc" requirement and its absence, the label-boundary cluster-domain prefix rule and rejecting a non-prefix or lookalike suffix or more labels than the cluster domain has, a configured cluster-domain override, a trailing dot and an empty label, case-insensitivity, port parsed but discarded, path-is-always-the-whole-path (including the "/" and no-path wildcard cases), query/fragment stripping, dot/dot-dot/empty path segments, wrong scheme or userinfo present, null/blank input, and a two-label base domain that also happens to parse as DNS form. - ServiceAccountSubjectTest (7 cases, new): valid subject parsing, null input, wrong prefix, missing separator, empty namespace or name, an extra colon in the name, and a bare prefix with nothing after it. - ActorIdentityTest (6 cases, new): a service-account subject tagged K8S_SA, a non-service-account subject and a null subject both tagged USER, a prefixed subject with no second colon still tagged K8S_SA, a bare prefix still tagged K8S_SA, and resourceName combining authority and id. - K8sDnsAudienceFormatTest (30 cases, new): end-to-end validator-level coverage of the DNS form across one/two/three/four/five label hosts, trailing-dot rejection with custom-form fallback also failing, the DNS form never consulting the custom form's cluster-domains list and vice versa, dns.format.cluster.domain being independent of server.name.cluster-suffix, dual-format fallback in both directions (a value parsing under both forms at once, a base domain that happens to also be DNS-shaped), the combined failure message naming both formats, require-all-audiences-match across a mix of DNS and custom-form entries, and dns.format.enabled false disabling DNS-form matching, falling back to custom form, and unwrapping the failure message to mention only the custom form. - K8sActorSubjectBindingTest (22 cases, new): the check disabled (default) passing regardless of actor/source mismatch; enabled and matching passes; enabled and a namespace or service-account-name mismatch fails; case-insensitive comparison; is.service.account true/false behavior for a non-service-account actor subject, and that is.service.account alone without the enabling parameter has no effect; the source header missing or unparseable failing regardless of whether the check itself is enabled; only the most recent actor in a multi-actor chain being checked; an act claim that is not a JSON object failing when the check is enabled and passing when it is not; a non-delegation token never running this check regardless of either flag; and several malformed-subject shapes (missing sub, sub not a string, missing colon separator, wrong prefix). - K8sAudiencePathMatchingTest (18 cases, new): the no-path and explicit-root-path wildcard for both forms; exact match and mismatch; raw vs. percent-encoded request-path candidates; the audience.path.prefix search applying to the custom form and never to the DNS form; the path header missing, empty, or (with path.header.from.url) malformed; and path.header.from.url extracting the path component and stripping query/fragment in both its true and (default) false modes. - K8sDestinationAudienceValidatorTest (72 cases, up from 51): all prior cases retained, now covering both aud forms where applicable; new cases for the non-delegation token being validated against the destination by default and the init-time checks for enforce.act.sub.service-account.matches.source.spiffeid requiring the source SPIFFE header, and for cluster-domains entries with a stray interior comma failing init. ## Integration Tests None were added, they will follow on. ## UI changes N/A -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
