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. 



-- 
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]

Reply via email to