[
https://issues.apache.org/jira/browse/KNOX-3534?focusedWorklogId=1051853&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1051853
]
ASF GitHub Bot logged work on KNOX-3534:
----------------------------------------
Author: ASF GitHub Bot
Created on: 02/Nov/26 12:10
Start Date: 02/Nov/26 12:10
Worklog Time Spent: 10m
Work Description: smolnar82 commented on code in PR #1476:
URL: https://github.com/apache/knox/pull/1476#discussion_r4442067545
##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/K8sDestinationAudienceValidator.java:
##########
@@ -399,19 +526,84 @@ public void init(final FilterConfig filterConfig) throws
Exception {
enforceActSubIsServiceAccount =
Boolean.parseBoolean(filterConfig.getInitParameter(ENFORCE_ACT_SUB_IS_SERVICE_ACCOUNT_PARAM));
+ ingressSourceRoutingEnabled = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM));
+ ingressSourceRoutingSkipTrustDomainMatch = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_TRUST_DOMAIN_MATCH_PARAM));
+ ingressSourceRoutingSkipValidation = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_VALIDATION_PARAM));
+
+ // Parsed strictly whether or not routing is enabled, so a malformed entry
fails the
+ // topology at startup instead of sitting inert.
+ final String ingressSourceSpiffeIdsParam =
+
blankToNull(filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_PARAM));
+ ingressSourceSpiffeIds = new ArrayList<>();
+ if (ingressSourceSpiffeIdsParam != null) {
+ for (final String entry :
parseCommaSeparated(ingressSourceSpiffeIdsParam)) {
+ final Optional<SpiffeId> parsed = SpiffeId.parse(entry);
+ if (parsed.isEmpty()) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s contains an invalid SPIFFE id entry: %s",
INGRESS_SOURCE_SPIFFE_IDS_PARAM, entry));
+ }
+ ingressSourceSpiffeIds.add(lowerCased(parsed.get()));
+ }
+ }
+
if (enforceActSubMatchesSourceSpiffeId && sourceSpiffeIdHeader == null) {
throw new ServletException(String.format(Locale.ROOT,
"%s is true but %s is not configured",
ENFORCE_ACT_SUB_MATCHES_SOURCE_SPIFFE_ID_PARAM,
SOURCE_SPIFFE_ID_HEADER_PARAM));
}
+
+ if (ingressSourceRoutingEnabled) {
+ if (sourceSpiffeIdHeader == null) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s is true but %s is not configured",
INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM,
+ SOURCE_SPIFFE_ID_HEADER_PARAM));
+ }
+ if (ingressSourceSpiffeIds.isEmpty()) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s is true but %s is not configured",
INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM,
+ INGRESS_SOURCE_SPIFFE_IDS_PARAM));
+ }
+ }
}
@Override
public AudienceValidationResult validate(final HttpServletRequest request,
final JWT token,
final List<String> configuredAudiences) {
final boolean hasActClaim = token.getClaimAsObject(JWTToken.ACT_CLAIM) !=
null;
- if (!hasActClaim && !validateAudiencesWithoutActClaim) {
- return
AudienceValidationResult.of(AbstractJWTFilter.matchesConfiguredAudiences(token,
configuredAudiences));
+
+ // Resolved before the early returns below because the routing decision
needs it. An
+ // absent value is not rejected here: that rejection happens further down,
after the destination
+ // header and server-name checks, so a request whose source header is
missing or unparseable
+ // is reported only once those earlier checks have passed.
+ final Optional<SpiffeId> sourceSpiffeId = sourceSpiffeIdHeader == null
+ ? Optional.empty() : sourceSpiffeIdFromHeader(request);
+ final boolean fromIngressSource = ingressSourceRoutingEnabled
+ && isConfiguredIngressSource(sourceSpiffeId);
+
+ if (!hasActClaim) {
+ if (fromIngressSource) {
+ // A token with no act claim arriving through the ingress names no
in-cluster destination,
+ // so its audience is either not checked at all or checked against the
fixed configured
+ // list. Both decisions are made before the aud claim is required to
be present, because
+ // the fixed-list check has its own handling for a token that carries
no aud claim.
+ return ingressSourceRoutingSkipValidation
+ ? AudienceValidationResult.of(true)
+ : AudienceValidationResult.of(
+ AbstractJWTFilter.matchesConfiguredAudiences(token,
configuredAudiences));
+ }
+ if (!validateAudiencesWithoutActClaim) {
+ return AudienceValidationResult.of(
+ AbstractJWTFilter.matchesConfiguredAudiences(token,
configuredAudiences));
+ }
+ } else if (fromIngressSource) {
+ // An act claim means the token authorizes one workload to call another
inside the cluster.
+ // Arriving from an ingress identity means it was presented from
outside, which no such call
+ // does.
+ return new AudienceValidationResult(false, "Token has an act claim but
its source SPIFFE id in header "
+ + sourceSpiffeIdHeader + " is a configured ingress source: " +
request.getHeader(sourceSpiffeIdHeader));
Review Comment:
**Diverges from the JIRA.** KNOX-3534: *"Delegation tokens are not allowed
on that path, so even if they come from an ingress, still treat them as they
are now."* I read that as "don't send `act` tokens down the ingress route;
destination-validate them as today", not "reject them".
With routing enabled, an RFC 8693-exchanged token presented through the
ingress used to be destination-validated. Now it always gets a 400. That breaks
clients outside the cluster that re-exchange a delegated token through the
public ingress.
Could we either:
- drop this branch, so `act` tokens fall through to destination validation
as before, or
- put the rejection behind its own flag that defaults to `false` (e.g.
`request.audience.k8s.ingress.routing.reject.delegation.tokens`)?
If the stricter behavior is intended, please update the JIRA so the two
agree.
##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/K8sDestinationAudienceValidator.java:
##########
@@ -399,19 +526,84 @@ public void init(final FilterConfig filterConfig) throws
Exception {
enforceActSubIsServiceAccount =
Boolean.parseBoolean(filterConfig.getInitParameter(ENFORCE_ACT_SUB_IS_SERVICE_ACCOUNT_PARAM));
+ ingressSourceRoutingEnabled = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM));
+ ingressSourceRoutingSkipTrustDomainMatch = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_TRUST_DOMAIN_MATCH_PARAM));
+ ingressSourceRoutingSkipValidation = Boolean.parseBoolean(
+
filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_VALIDATION_PARAM));
+
+ // Parsed strictly whether or not routing is enabled, so a malformed entry
fails the
+ // topology at startup instead of sitting inert.
+ final String ingressSourceSpiffeIdsParam =
+
blankToNull(filterConfig.getInitParameter(INGRESS_SOURCE_SPIFFE_IDS_PARAM));
+ ingressSourceSpiffeIds = new ArrayList<>();
+ if (ingressSourceSpiffeIdsParam != null) {
+ for (final String entry :
parseCommaSeparated(ingressSourceSpiffeIdsParam)) {
+ final Optional<SpiffeId> parsed = SpiffeId.parse(entry);
+ if (parsed.isEmpty()) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s contains an invalid SPIFFE id entry: %s",
INGRESS_SOURCE_SPIFFE_IDS_PARAM, entry));
+ }
+ ingressSourceSpiffeIds.add(lowerCased(parsed.get()));
+ }
+ }
+
if (enforceActSubMatchesSourceSpiffeId && sourceSpiffeIdHeader == null) {
throw new ServletException(String.format(Locale.ROOT,
"%s is true but %s is not configured",
ENFORCE_ACT_SUB_MATCHES_SOURCE_SPIFFE_ID_PARAM,
SOURCE_SPIFFE_ID_HEADER_PARAM));
}
+
+ if (ingressSourceRoutingEnabled) {
+ if (sourceSpiffeIdHeader == null) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s is true but %s is not configured",
INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM,
+ SOURCE_SPIFFE_ID_HEADER_PARAM));
+ }
+ if (ingressSourceSpiffeIds.isEmpty()) {
+ throw new ServletException(String.format(Locale.ROOT,
+ "%s is true but %s is not configured",
INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM,
+ INGRESS_SOURCE_SPIFFE_IDS_PARAM));
+ }
+ }
}
@Override
public AudienceValidationResult validate(final HttpServletRequest request,
final JWT token,
final List<String> configuredAudiences) {
final boolean hasActClaim = token.getClaimAsObject(JWTToken.ACT_CLAIM) !=
null;
- if (!hasActClaim && !validateAudiencesWithoutActClaim) {
- return
AudienceValidationResult.of(AbstractJWTFilter.matchesConfiguredAudiences(token,
configuredAudiences));
+
+ // Resolved before the early returns below because the routing decision
needs it. An
+ // absent value is not rejected here: that rejection happens further down,
after the destination
+ // header and server-name checks, so a request whose source header is
missing or unparseable
+ // is reported only once those earlier checks have passed.
+ final Optional<SpiffeId> sourceSpiffeId = sourceSpiffeIdHeader == null
+ ? Optional.empty() : sourceSpiffeIdFromHeader(request);
+ final boolean fromIngressSource = ingressSourceRoutingEnabled
+ && isConfiguredIngressSource(sourceSpiffeId);
+
+ if (!hasActClaim) {
+ if (fromIngressSource) {
+ // A token with no act claim arriving through the ingress names no
in-cluster destination,
+ // so its audience is either not checked at all or checked against the
fixed configured
+ // list. Both decisions are made before the aud claim is required to
be present, because
+ // the fixed-list check has its own handling for a token that carries
no aud claim.
+ return ingressSourceRoutingSkipValidation
+ ? AudienceValidationResult.of(true)
+ : AudienceValidationResult.of(
+ AbstractJWTFilter.matchesConfiguredAudiences(token,
configuredAudiences));
Review Comment:
`matchesConfiguredAudiences(token, null)` returns `true`, so with the
default `skip.validation=false` and no `knox.token.audiences`, this route
behaves exactly like `skip.validation=true`. The docs mention it, but nothing
at runtime does. Could we log a WARN at startup when routing is enabled and the
provider has no `knox.token.audiences`? (`AbstractJWTFilter` sees both.)
Alternatively, consider a dedicated `ingress.source.audiences` list. Reusing
`knox.token.audiences` also changes what mesh tokens without `act` accept when
`validate.audiences.without.act.claim=false`, which makes it hard to restrict
ingress tokens without affecting the rest.
##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/K8sDestinationAudienceValidator.java:
##########
@@ -331,6 +345,115 @@ public class K8sDestinationAudienceValidator implements
RequestAudienceValidator
public static final String ENFORCE_ACT_SUB_IS_SERVICE_ACCOUNT_PARAM =
PARAM_PREFIX + "enforce.act.sub.is.service.account";
+ /**
+ * Whether a request presented by an ingress gateway is routed to a
different audience check
+ * than validation against the request destination. A request is from an
ingress gateway when
+ * the SPIFFE id in the header named by {@link
#SOURCE_SPIFFE_ID_HEADER_PARAM} matches an entry
+ * of {@link #INGRESS_SOURCE_SPIFFE_IDS_PARAM}. Defaults to {@code false},
the fail-closed
+ * setting: with routing off, a request from an ingress gateway is handled
like a request from
+ * any other source.
+ *
+ * <p>When {@code true}, a request from an ingress gateway is routed by
whether its token
+ * carries an {@code act} claim.
+ *
+ * <p>A token with no {@code act} claim is not destination-validated. What
it is checked
+ * against depends on {@link
#INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_VALIDATION_PARAM}: either
+ * no audience validation at all, or the fixed configured allowed-audience
list. Either way the
+ * token does not reach destination validation, so the requirement that
every configured header
+ * be present and parseable does not apply to it.
+ *
+ * <p>A token with an {@code act} claim is rejected, because a delegation
token must not
+ * arrive through the ingress. Routing never lets such a token through,
whatever the skip setting.
+ *
+ * <p>This parameter reads {@link #SOURCE_SPIFFE_ID_HEADER_PARAM} and
+ * {@link #INGRESS_SOURCE_SPIFFE_IDS_PARAM}, neither of which has a default.
Setting this to
+ * {@code true} without both of them is a configuration error:
initialization fails, rather than
+ * leaving routing a deployment asked for silently inactive.
+ *
+ * <p>Routing is driven by the value of the header named by
+ * {@link #SOURCE_SPIFFE_ID_HEADER_PARAM}, which is trusted at face value. A
caller able to set
+ * that header can have its request routed as an ingress request, so it must
be set or overwritten by a trusted
+ * component before the request reaches Knox. This matters most with
+ * {@link #INGRESS_SOURCE_SPIFFE_IDS_ROUTING_SKIP_VALIDATION_PARAM}, which
disables audience
+ * checking for the requests it routes.
+ *
+ * <p>A request whose configured source SPIFFE id header is missing or
unparseable does not name
+ * an ingress source, so it is not routed; it continues to the ordinary
flow. There it is
+ * rejected by the existing requirement that a configured header be present
and parseable,
+ * unless {@link #VALIDATE_AUDIENCES_WITHOUT_ACT_CLAIM_PARAM} is {@code
false}, in which case a
+ * token with no {@code act} claim is validated against the fixed configured
allowed-audience
+ * list.
+ */
+ public static final String INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM =
+ PARAM_PREFIX + "ingress.source.spiffe-id.routing.enabled";
Review Comment:
Naming nit, but these names become public config once released: this one
says `spiffe-id` (singular) while the others say `spiffe-ids`. The names are
also quite long. Something like this might be clearer: (current -> suggested):
- `ingress.source.spiffe-id.routing.enabled` -> `ingress.routing.enabled`
- `ingress.source.spiffe-ids` -> `ingress.source.spiffe-ids`
- `ingress.source.spiffe-ids.routing.skip.trust.domain.match` ->
`ingress.routing.skip.trust.domain.match`
- `ingress.source.spiffe-ids.routing.skip.validation` ->
`ingress.routing.skip.audience.validation`
##########
gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/K8sDestinationAudienceValidator.java:
##########
@@ -49,14 +49,21 @@
* {@link AudienceResource#parseCustomForm(String, String)}.
* <p>The first form is tried at all only when {@link
#DNS_FORMAT_ENABLED_PARAM} is {@code true},
* its default; when it is {@code false}, every entry is evaluated only as the
custom form.
- * <p>A token carrying a delegation {@code act} claim is always routed through
this validator; see
- * {@link #validate(HttpServletRequest, JWT, List)}. A token with no {@code
act} claim is too,
- * unless {@link #VALIDATE_AUDIENCES_WITHOUT_ACT_CLAIM_PARAM} is {@code false}.
+ * <p>A token carrying a delegation {@code act} claim is always
destination-validated; see
+ * {@link #validate(HttpServletRequest, JWT, List)}. The one departure is
ingress source routing:
+ * when {@link #INGRESS_SOURCE_SPIFFE_ID_ROUTING_ENABLED_PARAM} is {@code
true} and the request's
+ * source SPIFFE id names one of the {@link #INGRESS_SOURCE_SPIFFE_IDS_PARAM},
such a token is
+ * rejected outright, because a delegation token must not arrive through the
ingress.
+ * A token with no {@code act} claim is destination-validated too, unless
ingress source routing
+ * applies to it or {@link #VALIDATE_AUDIENCES_WITHOUT_ACT_CLAIM_PARAM} is
{@code false}.
Review Comment:
If the `act`-token rejection changes (see the comment on `validate()`), this
paragraph needs updating too.
Issue Time Tracking
-------------------
Worklog Id: (was: 1051853)
Time Spent: 0.5h (was: 20m)
> Add k8s request audience validator bypass for JWTs from an ingress
> ------------------------------------------------------------------
>
> Key: KNOX-3534
> URL: https://issues.apache.org/jira/browse/KNOX-3534
> Project: Apache Knox
> Issue Type: Sub-task
> Components: JWT
> Reporter: Harrison Sheinblatt
> Assignee: Harrison Sheinblatt
> Priority: Major
> Time Spent: 0.5h
> Remaining Estimate: 0h
>
> The existing K8sDestinationAudienceValidator feature focus on service to
> service audience validation. Enable using the validator for the use case
> where a single topology is to validate all jwts, both service to service and
> ingress traffic JWTs.
> Add flags to enable a feature to match JWTs coming from an ingress gateway
> and treat them differently: either validate the audience against the same
> static whitelist used for the rest of the traffic, or skip audience
> validation. This path would be split and evaluated first. Delegation tokens
> are not allowed on that path, so even if they come from an ingress, still
> treat them as they are now.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)