This is an automated email from the ASF dual-hosted git repository.

kumaab pushed a commit to branch ranger-2.10
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.10 by this push:
     new dbb54f55b RANGER-5700: Support SPIFFE-based authn via HTTP headers 
(#1096)
dbb54f55b is described below

commit dbb54f55b137c837f35c393940a498c2fd82aed6
Author: Abhishek Kumar <[email protected]>
AuthorDate: Tue Jul 21 20:00:17 2026 -0700

    RANGER-5700: Support SPIFFE-based authn via HTTP headers (#1096)
    
    (cherry picked from commit 22fbc813d576b2d386f7baaf51a1895c96b1bece)
---
 .../apache/ranger/plugin/util/SpiffeIdUtil.java    | 130 ++++++++++++++
 .../ranger/plugin/util/SpiffeIdUtilTest.java       | 159 ++++++++++++++++++
 .../ranger-docker/scripts/pdp/ranger-pdp-site.xml  |   6 +
 pdp/conf.dist/README-k8s.md                        |   1 +
 pdp/conf.dist/ranger-pdp-site.xml                  |   7 +
 .../org/apache/ranger/pdp/RangerPdpServer.java     |   1 +
 .../apache/ranger/pdp/config/RangerPdpConfig.java  |   4 +
 .../ranger/pdp/config/RangerPdpConstants.java      |   1 +
 .../pdp/security/HttpHeaderAuthNHandler.java       |  34 +++-
 .../ranger/pdp/security/RangerPdpAuthNFilter.java  |   3 +-
 pdp/src/main/resources/ranger-pdp-default.xml      |   7 +
 .../pdp/security/HttpHeaderAuthNHandlerTest.java   | 121 +++++++++++++-
 .../web/filter/RangerHeaderPreAuthFilter.java      |  44 ++++-
 .../main/resources/conf.dist/ranger-admin-site.xml |   5 +
 .../web/filter/TestRangerHeaderPreAuthFilter.java  | 186 ++++++++++++++++++++-
 .../TestRangerSecurityContextFormationFilter.java  |   6 +-
 16 files changed, 694 insertions(+), 21 deletions(-)

diff --git 
a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java 
b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
new file mode 100644
index 000000000..5c303364b
--- /dev/null
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.plugin.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper for validating a SPIFFE ID and extracting the service-account 
identity from it.
+ *
+ * <p>A SPIFFE ID is expected in the exact form {@code 
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>};
+ * for service-to-service authentication the receiving server trusts the 
workload
+ * identity carried in a header (e.g. {@code x-service-spiffe-id}) and uses
+ * the trailing service-account segment as the authenticated principal.
+ */
+public final class SpiffeIdUtil {
+    private static final String  HEADER_NAMES_SEP  = ",";
+
+    /**
+     * Matches {@code 
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>} using the character
+     * sets from the SPIFFE-ID specification:
+     * <ul>
+     *   <li>trust-domain: lowercase letters, digits, {@code .}, {@code -}, 
{@code _} (the spec requires
+     *       the trust domain to be lowercase);
+     *   <li>namespace and service-account: ASCII letters (any case), digits, 
{@code .}, {@code -}, {@code _}
+     *       (SPIFFE path segments are case-sensitive).
+     * </ul>
+     * All segments must be non-empty. This is an allow-list of safe 
characters, so whitespace, control
+     * characters and {@code /} are excluded, keeping the extracted principal 
well-formed. Per the spec,
+     * the namespace and service-account path segments must not be the 
relative modifiers {@code .} or
+     * {@code ..} (enforced by the leading negative look-aheads). The 
service-account is exposed via the
+     * named group {@code sa}.
+     */
+    private static final Pattern SPIFFE_ID_PATTERN = Pattern.compile(
+            
"^spiffe://[a-z0-9._-]+/ns/(?!\\.{1,2}/)[A-Za-z0-9._-]+/sa/(?<sa>(?!\\.{1,2}$)[A-Za-z0-9._-]+)$");
+
+    private SpiffeIdUtil() {
+        // to block instantiation
+    }
+
+    /**
+     * Parses a configured, comma-separated list of SPIFFE header names into 
an ordered list of
+     * trimmed, non-empty names. Supports a single header name as a list of 
length one.
+     *
+     * @param configValue the raw config value (e.g. {@code X-Spiffe-Id, 
X-Workload-Id})
+     * @return an ordered, immutable list of header names; empty when the 
input is blank
+     */
+    public static List<String> parseHeaderNames(String configValue) {
+        if (StringUtils.isBlank(configValue)) {
+            return Collections.emptyList();
+        }
+
+        List<String> ret = new ArrayList<>();
+
+        for (String name : configValue.split(HEADER_NAMES_SEP)) {
+            String trimmed = StringUtils.trimToNull(name);
+
+            if (trimmed != null) {
+                ret.add(trimmed);
+            }
+        }
+
+        return Collections.unmodifiableList(ret);
+    }
+
+    /**
+     * Validates that the given value is a well-formed SPIFFE ID in the exact 
form
+     * {@code spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>} 
with non-empty segments.
+     *
+     * @param spiffeId the raw SPIFFE ID value from the request header
+     * @return {@code true} if the value matches the expected SPIFFE ID 
format; {@code false} otherwise
+     */
+    public static boolean isValidSpiffeId(String spiffeId) {
+        return matchSpiffeId(spiffeId) != null;
+    }
+
+    /**
+     * Extracts the service-account name from a SPIFFE ID such as
+     * {@code spiffe://my-cluster/ns/service-namespace/sa/service-sa}. The 
value must match the exact
+     * SPIFFE ID format (see {@link #isValidSpiffeId(String)}); otherwise 
{@code null} is returned.
+     *
+     * @param spiffeId the raw SPIFFE ID value from the request header
+     * @return the service-account segment (e.g. {@code service-sa}), or
+     *         {@code null} if the input is blank or not a well-formed SPIFFE 
ID
+     */
+    public static String extractServiceAccount(String spiffeId) {
+        Matcher matcher = matchSpiffeId(spiffeId);
+
+        return matcher != null ? matcher.group("sa") : null;
+    }
+
+    /**
+     * Matches the given value against the expected SPIFFE ID format, 
computing the {@link Matcher} once.
+     *
+     * @param spiffeId the raw SPIFFE ID value from the request header
+     * @return a matched {@link Matcher} (with groups available) if the value 
is a well-formed SPIFFE ID;
+     *         {@code null} if the value is blank or does not match
+     */
+    private static Matcher matchSpiffeId(String spiffeId) {
+        if (StringUtils.isBlank(spiffeId)) {
+            return null;
+        }
+
+        Matcher matcher = SPIFFE_ID_PATTERN.matcher(spiffeId.trim());
+
+        return matcher.matches() ? matcher : null;
+    }
+}
diff --git 
a/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
 
b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
new file mode 100644
index 000000000..acda94d30
--- /dev/null
+++ 
b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.plugin.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SpiffeIdUtilTest {
+    private static final String VALID = 
"spiffe://my-cluster/ns/service-namespace/sa/service-sa";
+
+    @Test
+    void isValidSpiffeId_acceptsExactFormat() {
+        assertTrue(SpiffeIdUtil.isValidSpiffeId(VALID));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("  " + VALID + "  ")); // 
trimmed
+    }
+
+    @Test
+    void isValidSpiffeId_rejectsMalformed() {
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(null));
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(""));
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("   "));
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("not-a-spiffe-id"));
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("https://my-cluster/ns/n/sa/s";));   // 
wrong scheme
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/service-sa")); 
// missing /ns/
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n")); 
      // missing /sa/
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns//sa/s"));   // 
empty namespace
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe:///ns/n/sa/s"));      
      // empty trust domain
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n/sa/"));   // 
empty service account
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/"));                
      // trailing slash
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/extra"));           
      // extra segment
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/s/ns/n"));  // 
wrong order
+    }
+
+    @Test
+    void isValidSpiffeId_rejectsUnsafeOrIllegalCharacters() {
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service
 sa"));   // embedded space
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service\tsa"));
  // embedded tab
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service\nsa"));
  // embedded newline
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc@admin"));
    // illegal char '@'
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://My-Cluster/ns/prod/sa/service-sa"));
   // trust domain must be lowercase per spec
+
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service
 sa"));
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service\nsa"));
+    }
+
+    @Test
+    void isValidSpiffeId_rejectsRelativePathModifierSegments() {
+        // Per SPIFFE spec 2.2, path segments must not be the relative 
modifiers '.' or '..'.
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/./sa/service-sa"));
   // namespace '.'
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/../sa/service-sa"));
  // namespace '..'
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/."));  
       // service-account '.'
+        
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/..")); 
       // service-account '..'
+
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/."));
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/../sa/service-sa"));
+
+        // Dots inside a segment (not a bare '.'/'..') remain valid.
+        
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc.v1"));
+        assertEquals("svc.v1", 
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/..prod/sa/svc.v1"));
+    }
+
+    @Test
+    void isValidSpiffeId_acceptsSpecAllowedCharacters() {
+        // SPIFFE path segments are case-sensitive and allow letters (any 
case), digits, '.', '-', '_'.
+        
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster.example.com/ns/service-namespace/sa/service-sa"));
+        assertEquals("Service-SA", 
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/Prod/sa/Service-SA"));
 // mixed case
+        assertEquals("svc_01", 
SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns_2/sa/svc_01"));     
      // underscore
+        assertEquals("svc-01", 
SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns-2/sa/svc-01"));     
      // hyphen/digits
+    }
+
+    @Test
+    void extractServiceAccount_returnsSaOnlyForValidId() {
+        assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount(VALID));
+        assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount("  " + 
VALID + "  "));
+    }
+
+    @Test
+    void extractServiceAccount_returnsNullForMalformedId() {
+        assertNull(SpiffeIdUtil.extractServiceAccount(null));
+        assertNull(SpiffeIdUtil.extractServiceAccount(""));
+        assertNull(SpiffeIdUtil.extractServiceAccount("not-a-spiffe-id"));
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/sa/service-sa"));
+        
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/n/sa/"));
+        assertNull(SpiffeIdUtil.extractServiceAccount(VALID + "/extra"));
+    }
+
+    @Test
+    void extractServiceAccount_handlesRealisticProductionIds() {
+        // Kubernetes trust domain with a DNS-style cluster name and typical 
namespace/service-account names.
+        assertEquals("nginx-ingress",
+                
SpiffeIdUtil.extractServiceAccount("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
+
+        // Cloud/mesh style trust domain (e.g. the header value from the PR 
description).
+        assertEquals("service-sa",
+                
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/service-namespace/sa/service-sa"));
+
+        // Istio-style default service account in an application namespace.
+        assertEquals("default",
+                
SpiffeIdUtil.extractServiceAccount("spiffe://cluster.local/ns/payments/sa/default"));
+
+        // Namespace and service-account with digits and hyphens, as commonly 
generated by platforms.
+        assertEquals("spark-driver-01",
+                
SpiffeIdUtil.extractServiceAccount("spiffe://data-plane.internal/ns/team-analytics-42/sa/spark-driver-01"));
+
+        
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
+        
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster.local/ns/payments/sa/default"));
+    }
+
+    @Test
+    void rejectsSpecValidButNonConformingSpiffeId() {
+        // These are well-formed SPIFFE IDs per the SPIFFE spec (valid scheme 
+ trust domain + path),
+        // but they do not follow the expected 
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
+        // layout, so Ranger must reject them.
+        String workloadPath = "spiffe://example.org/workload/frontend"; // 
arbitrary path, no /ns//sa/
+        String nsOnly       = "spiffe://example.org/ns/prod";           // has 
/ns/ but no /sa/ segment
+
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(workloadPath));
+        assertFalse(SpiffeIdUtil.isValidSpiffeId(nsOnly));
+        assertNull(SpiffeIdUtil.extractServiceAccount(workloadPath));
+        assertNull(SpiffeIdUtil.extractServiceAccount(nsOnly));
+    }
+
+    @Test
+    void parseHeaderNames_handlesSingleAndMultiple() {
+        assertTrue(SpiffeIdUtil.parseHeaderNames(null).isEmpty());
+        assertTrue(SpiffeIdUtil.parseHeaderNames("   ").isEmpty());
+
+        assertEquals(Collections.singletonList("x-spiffe"), 
SpiffeIdUtil.parseHeaderNames("x-spiffe"));
+
+        List<String> names = SpiffeIdUtil.parseHeaderNames(" x-source , 
x-upstream ,");
+
+        assertEquals(Arrays.asList("x-source", "x-upstream"), names);
+    }
+}
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml 
b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
index 47fa64d13..2d097db17 100644
--- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
@@ -115,6 +115,12 @@
     <value>X-Forwarded-User</value>
   </property>
 
+  <!-- service-to-service authentication via SPIFFE ID (comma-separated header 
names) -->
+  <property>
+    <name>ranger.pdp.authn.header.spiffe</name>
+    <value>X-Spiffe-Id</value>
+  </property>
+
   <!-- JWT bearer token authentication for incoming REST requests -->
   <property>
     <name>ranger.pdp.authn.jwt.enabled</name>
diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md
index b630bec7d..95d7727ec 100644
--- a/pdp/conf.dist/README-k8s.md
+++ b/pdp/conf.dist/README-k8s.md
@@ -82,6 +82,7 @@ When configuring inbound PDP authentication in 
`ranger-pdp-site.xml`, use the
 - `ranger.pdp.authn.types`
 - `ranger.pdp.authn.header.enabled`
 - `ranger.pdp.authn.header.username`
+- `ranger.pdp.authn.header.spiffe`
 - `ranger.pdp.authn.jwt.enabled`
 - `ranger.pdp.authn.jwt.provider.url`
 - `ranger.pdp.authn.jwt.public.key`
diff --git a/pdp/conf.dist/ranger-pdp-site.xml 
b/pdp/conf.dist/ranger-pdp-site.xml
index 953584692..9d67b8eab 100644
--- a/pdp/conf.dist/ranger-pdp-site.xml
+++ b/pdp/conf.dist/ranger-pdp-site.xml
@@ -145,6 +145,13 @@
     <description>HTTP header name from which the authenticated username is 
read.</description>
   </property>
 
+  <!-- optional; enables service-to-service authentication via SPIFFE ID -->
+  <property>
+    <name>ranger.pdp.authn.header.spiffe</name>
+    <value></value>
+    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE 
ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single 
header name is also valid. The trailing service-account segment is used as the 
authenticated principal. When multiple headers are configured, the first one 
carrying a well-formed SPIFFE ID is used. Consulted only when the username 
header is absent.</description>
+  </property>
+
   <!-- JWT bearer token authentication for incoming REST requests -->
   <property>
     <name>ranger.pdp.authn.jwt.enabled</name>
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java 
b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
index dc74e0716..2f7ba67b1 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
@@ -225,6 +225,7 @@ private void addAuthFilter(Context ctx) {
         // HTTP Header auth
         
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, 
Boolean.toString(config.isHeaderAuthnEnabled()));
         
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, 
config.getHeaderAuthnUsername());
+        
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
config.getHeaderAuthnSpiffe());
 
         // JWT bearer token auth
         
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, 
Boolean.toString(config.isJwtAuthnEnabled()));
diff --git 
a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java 
b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
index 25faa5ef1..d541d4268 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
@@ -143,6 +143,10 @@ public String getHeaderAuthnUsername() {
         return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "");
     }
 
+    public String getHeaderAuthnSpiffe() {
+        return get(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, "");
+    }
+
     // --- JWT bearer token auth ---
     public boolean isJwtAuthnEnabled() {
         return getBoolean(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, false);
diff --git 
a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java 
b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
index c19518540..a0c40f290 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
@@ -65,6 +65,7 @@ private RangerPdpConstants() {
     public static final String PROP_AUTHN_HEADER_PREFIX   = PROP_AUTHN_PREFIX 
+ "header.";
     public static final String PROP_AUTHN_HEADER_ENABLED  = 
PROP_AUTHN_HEADER_PREFIX + "enabled";
     public static final String PROP_AUTHN_HEADER_USERNAME = 
PROP_AUTHN_HEADER_PREFIX + "username";
+    public static final String PROP_AUTHN_HEADER_SPIFFE   = 
PROP_AUTHN_HEADER_PREFIX + "spiffe";
 
     // JWT auth
     public static final String PROP_AUTHN_JWT_PREFIX       = PROP_AUTHN_PREFIX 
+ "jwt.";
diff --git 
a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java 
b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
index 0d8f8aacd..1761a0938 100644
--- 
a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
+++ 
b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
@@ -21,12 +21,14 @@
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.plugin.util.SpiffeIdUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import java.util.List;
 import java.util.Properties;
 
 /**
@@ -44,24 +46,44 @@
 public class HttpHeaderAuthNHandler implements PdpAuthNHandler {
     private static final Logger LOG = 
LoggerFactory.getLogger(HttpHeaderAuthNHandler.class);
 
-    public static final String AUTH_TYPE = "HEADER";
+    public static final String AUTH_TYPE        = "HEADER";
+    public static final String AUTH_TYPE_SPIFFE = "SPIFFE";
 
-    private String usernameHeader;
+    private String       usernameHeader;
+    private List<String> spiffeHeaders;
 
     @Override
     public void init(Properties config) {
         usernameHeader = 
config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME);
+        spiffeHeaders  = 
SpiffeIdUtil.parseHeaderNames(config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE));
 
-        LOG.info("HttpHeaderAuthHandler initialized; username header={}", 
usernameHeader);
+        LOG.info("HttpHeaderAuthHandler initialized; username header={}, 
spiffe headers={}", usernameHeader, spiffeHeaders);
     }
 
     @Override
     public Result authenticate(HttpServletRequest request, HttpServletResponse 
response) {
-        String userName = request.getHeader(usernameHeader);
+        String userName = StringUtils.isNotBlank(usernameHeader) ? 
request.getHeader(usernameHeader) : null;
 
-        LOG.debug("authenticate(): user={} (from header {})", userName, 
usernameHeader);
+        if (StringUtils.isNotBlank(userName)) {
+            LOG.debug("authenticate(): user={} (from header {})", userName, 
usernameHeader);
 
-        return StringUtils.isBlank(userName) ? Result.skip() : 
Result.authenticated(userName.trim(), AUTH_TYPE);
+            return Result.authenticated(userName.trim(), AUTH_TYPE);
+        }
+
+        for (String spiffeHeader : spiffeHeaders) {
+            String spiffeId       = request.getHeader(spiffeHeader);
+            String serviceAccount = 
SpiffeIdUtil.extractServiceAccount(spiffeId);
+
+            if (StringUtils.isNotBlank(serviceAccount)) {
+                LOG.debug("authenticate(): service-account={} (from SPIFFE 
header {})", serviceAccount, spiffeHeader);
+
+                return Result.authenticated(serviceAccount, AUTH_TYPE_SPIFFE);
+            } else if (StringUtils.isNotBlank(spiffeId)) {
+                LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE 
ID", spiffeHeader);
+            }
+        }
+
+        return Result.skip();
     }
 
     @Override
diff --git 
a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java 
b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
index e425464a7..00b8f4c70 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
@@ -151,7 +151,8 @@ private PdpAuthNHandler createHandler(String type, 
FilterConfig filterConfig) {
         switch (type) {
             case "header":
                 ret = getBoolean(filterConfig, 
RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED) &&
-                        
StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME))
+                        
(StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME))
 ||
+                                
StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE)))
                         ? new HttpHeaderAuthNHandler() : null;
                 break;
             case "jwt":
diff --git a/pdp/src/main/resources/ranger-pdp-default.xml 
b/pdp/src/main/resources/ranger-pdp-default.xml
index 953584692..9d67b8eab 100644
--- a/pdp/src/main/resources/ranger-pdp-default.xml
+++ b/pdp/src/main/resources/ranger-pdp-default.xml
@@ -145,6 +145,13 @@
     <description>HTTP header name from which the authenticated username is 
read.</description>
   </property>
 
+  <!-- optional; enables service-to-service authentication via SPIFFE ID -->
+  <property>
+    <name>ranger.pdp.authn.header.spiffe</name>
+    <value></value>
+    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE 
ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single 
header name is also valid. The trailing service-account segment is used as the 
authenticated principal. When multiple headers are configured, the first one 
carrying a well-formed SPIFFE ID is used. Consulted only when the username 
header is absent.</description>
+  </property>
+
   <!-- JWT bearer token authentication for incoming REST requests -->
   <property>
     <name>ranger.pdp.authn.jwt.enabled</name>
diff --git 
a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
 
b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
index 1bd2a5746..e122516bd 100644
--- 
a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
+++ 
b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
@@ -26,6 +26,9 @@
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Properties;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -63,12 +66,128 @@ public void testAuthenticate_usesConfiguredHeaderName() {
         assertEquals("bob", result.getUserName());
     }
 
+    @Test
+    void testAuthenticate_usesSpiffeHeaderWhenUsernameAbsent() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, 
"X-Authenticated-User");
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id");
+
+        handler.init(config);
+
+        // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust 
domain + namespace/service-account.
+        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", 
"spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, 
result.getStatus());
+        assertEquals("nginx-ingress", result.getUserName());
+        assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE, 
result.getAuthType());
+    }
+
+    @Test
+    void testAuthenticate_usernameTakesPrecedenceOverSpiffe() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, 
"X-Authenticated-User");
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id");
+
+        handler.init(config);
+
+        Map<String, String> headers = new HashMap<>();
+
+        headers.put("X-Authenticated-User", "bob");
+        headers.put("X-Spiffe-Id", 
"spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+        PdpAuthNHandler.Result result = 
handler.authenticate(requestWithHeaders(headers), null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, 
result.getStatus());
+        assertEquals("bob", result.getUserName());
+        assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType());
+    }
+
+    @Test
+    void testAuthenticate_multipleSpiffeHeadersUsesFirstValid() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id, X-Workload-Id");
+
+        handler.init(config);
+
+        Map<String, String> headers = new HashMap<>();
+
+        headers.put("X-Spiffe-Id", "not-a-spiffe-id");
+        headers.put("X-Workload-Id", 
"spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+        PdpAuthNHandler.Result result = 
handler.authenticate(requestWithHeaders(headers), null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, 
result.getStatus());
+        assertEquals("service-sa", result.getUserName());
+        assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE, 
result.getAuthType());
+    }
+
+    @Test
+    void testAuthenticate_malformedSpiffeHeaderSkips() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id");
+
+        handler.init(config);
+
+        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", 
"not-a-spiffe-id");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+        assertNull(result.getUserName());
+    }
+
+    @Test
+    void testAuthenticate_specValidButNonConformingSpiffeHeaderSkips() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id");
+
+        handler.init(config);
+
+        // Valid SPIFFE ID per the SPIFFE spec, but not in the expected 
/ns/<ns>/sa/<sa> layout.
+        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", 
"spiffe://example.org/workload/frontend");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+        assertNull(result.getUserName());
+    }
+
+    @Test
+    void testAuthenticate_spiffeHeaderWithIllegalCharsSkips() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, 
"X-Spiffe-Id");
+
+        handler.init(config);
+
+        // Correct layout but the service-account contains whitespace, which 
is not an allowed SPIFFE character.
+        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", 
"spiffe://my-cluster/ns/prod/sa/service sa");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+        assertNull(result.getUserName());
+    }
+
     private static HttpServletRequest requestWithHeader(String expectedHeader, 
String headerValue) {
+        return requestWithHeaders(Collections.singletonMap(expectedHeader, 
headerValue));
+    }
+
+    private static HttpServletRequest requestWithHeaders(Map<String, String> 
headers) {
         InvocationHandler invocationHandler = (proxy, method, args) -> {
             String methodName = method.getName();
 
             if ("getHeader".equals(methodName)) {
-                return expectedHeader.equals(args[0]) ? headerValue : null;
+                return headers.get(args[0]);
             } else if ("getHeaders".equals(methodName) || 
"getHeaderNames".equals(methodName)) {
                 return java.util.Collections.emptyEnumeration();
             } else if ("getMethod".equals(methodName)) {
diff --git 
a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
 
b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
index 920f2ad8a..3cf984f7d 100644
--- 
a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
+++ 
b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
@@ -22,6 +22,7 @@
 import org.apache.ranger.biz.UserMgr;
 import org.apache.ranger.common.PropertiesUtil;
 import org.apache.ranger.entity.XXAuthSession;
+import org.apache.ranger.plugin.util.SpiffeIdUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -51,10 +52,12 @@ public class RangerHeaderPreAuthFilter extends 
GenericFilterBean {
 
     public static final String PROP_HEADER_AUTH_ENABLED    = 
"ranger.admin.authn.header.enabled";
     public static final String PROP_USERNAME_HEADER_NAME   = 
"ranger.admin.authn.header.username";
+    public static final String PROP_SPIFFE_HEADER_NAME     = 
"ranger.admin.authn.header.spiffe";
     public static final String PROP_REQUEST_ID_HEADER_NAME = 
"ranger.admin.authn.header.requestid";
 
-    private boolean headerAuthEnabled;
-    private String  userNameHeaderName;
+    private boolean      headerAuthEnabled;
+    private String       userNameHeaderName;
+    private List<String> spiffeHeaderNames;
 
     @Autowired
     UserMgr userMgr;
@@ -65,9 +68,10 @@ protected void initialize() {
 
         if (headerAuthEnabled) {
             userNameHeaderName = 
PropertiesUtil.getProperty(PROP_USERNAME_HEADER_NAME);
+            spiffeHeaderNames  = 
SpiffeIdUtil.parseHeaderNames(PropertiesUtil.getProperty(PROP_SPIFFE_HEADER_NAME));
 
-            if (StringUtils.isBlank(userNameHeaderName)) {
-                LOG.warn("Disabling header-based authentication, as 
configuration {} is not set", PROP_USERNAME_HEADER_NAME);
+            if (StringUtils.isBlank(userNameHeaderName) && 
spiffeHeaderNames.isEmpty()) {
+                LOG.warn("Disabling header-based authentication, as neither {} 
nor {} is set", PROP_USERNAME_HEADER_NAME, PROP_SPIFFE_HEADER_NAME);
 
                 headerAuthEnabled = false;
             }
@@ -81,7 +85,7 @@ public void doFilter(ServletRequest request, ServletResponse 
response, FilterCha
 
             if (existingAuthn == null || !existingAuthn.isAuthenticated()) {
                 HttpServletRequest  httpRequest = (HttpServletRequest) request;
-                String              username    = 
StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName));
+                String              username    = 
resolvePrincipal(httpRequest);
 
                 if (StringUtils.isNotBlank(username)) {
                     List<GrantedAuthority>    grantedAuthorities = 
getAuthoritiesFromRanger(username);
@@ -94,7 +98,7 @@ public void doFilter(ServletRequest request, ServletResponse 
response, FilterCha
 
                     LOG.debug("Authenticated request using trusted headers for 
user={}", username);
                 } else {
-                    LOG.debug("Username header '{}' is missing or empty in the 
request!", userNameHeaderName);
+                    LOG.debug("No trusted identity header found in the 
request!");
                 }
             }
         } else {
@@ -104,6 +108,34 @@ public void doFilter(ServletRequest request, 
ServletResponse response, FilterCha
         chain.doFilter(request, response);
     }
 
+    /**
+     * Resolves the principal from trusted headers. The username header (user 
identity) takes
+     * precedence; when it is absent, the SPIFFE header (service identity) is 
used and
+     * the trailing service-account segment of the SPIFFE ID becomes the 
principal.
+     */
+    private String resolvePrincipal(HttpServletRequest httpRequest) {
+        String username = StringUtils.isNotBlank(userNameHeaderName) ? 
StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)) : null;
+
+        if (StringUtils.isNotBlank(username)) {
+            return username;
+        }
+
+        for (String spiffeHeaderName : spiffeHeaderNames) {
+            String spiffeId       = 
StringUtils.trimToNull(httpRequest.getHeader(spiffeHeaderName));
+            String serviceAccount = 
SpiffeIdUtil.extractServiceAccount(spiffeId);
+
+            if (StringUtils.isNotBlank(serviceAccount)) {
+                LOG.debug("Resolved service-account '{}' from SPIFFE header 
'{}'", serviceAccount, spiffeHeaderName);
+
+                return serviceAccount;
+            } else if (StringUtils.isNotBlank(spiffeId)) {
+                LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE 
ID", spiffeHeaderName);
+            }
+        }
+
+        return null;
+    }
+
     /**
      * Loads authorities from Ranger DB
      */
diff --git a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml 
b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
index 6c0e8b2d8..5493b69ba 100644
--- a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
+++ b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
@@ -323,6 +323,11 @@
                <name>ranger.admin.authn.header.username</name>
                <value></value>
        </property>
+       <property>
+               <name>ranger.admin.authn.header.spiffe</name>
+               <value></value>
+               <description>Comma-separated list of HTTP header name(s) 
carrying a SPIFFE ID (e.g. 
spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header 
name is also valid. The trailing service-account segment is used as the 
authenticated principal. When multiple headers are configured, the first one 
carrying a well-formed SPIFFE ID is used. Consulted only when the username 
header is absent.</description>
+       </property>
        <property>
                <name>ranger.admin.authn.header.requestid</name>
                <value></value>
diff --git 
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
 
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
index c721c30f0..675970dd2 100644
--- 
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
+++ 
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
@@ -66,6 +66,7 @@ public void tearDown() {
 
         
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED);
         
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME);
+        
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME);
         
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME);
     }
 
@@ -91,7 +92,7 @@ public void testDoFilter_disabled_passesThrough() throws 
Exception {
     @Test
     public void testDoFilter_enabled_missingUsername_passesThrough() throws 
Exception {
         
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
-        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "x-awc-username");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "X-Forwarded-User");
 
         RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
         UserMgr                   userMgr = mock(UserMgr.class);
@@ -115,7 +116,7 @@ public void 
testDoFilter_enabled_missingUsername_passesThrough() throws Exceptio
     @Test
     public void 
testDoFilter_enabled_withUsername_setsAuthenticationFromRangerDbRoles() throws 
Exception {
         
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
-        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "x-awc-username");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "X-Forwarded-User");
 
         RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
         UserMgr                   userMgr = mock(UserMgr.class);
@@ -128,7 +129,7 @@ public void 
testDoFilter_enabled_withUsername_setsAuthenticationFromRangerDbRole
         HttpServletRequest  request  = mock(HttpServletRequest.class);
         HttpServletResponse response = mock(HttpServletResponse.class);
 
-        when(request.getHeader("x-awc-username")).thenReturn("joeuser");
+        when(request.getHeader("X-Forwarded-User")).thenReturn("joeuser");
 
         FilterChain chain = new FilterChain() {
             @Override
@@ -151,10 +152,187 @@ public void doFilter(ServletRequest req, ServletResponse 
res) {
         filter.doFilter(request, response, chain);
     }
 
+    @Test
+    public void 
testDoFilter_enabled_withSpiffeHeader_setsServiceAccountAuthentication() throws 
Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "X-Forwarded-User");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        
when(userMgr.getRolesByLoginId("nginx-ingress")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+
+        when(request.getHeader("X-Forwarded-User")).thenReturn(null);
+        // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust 
domain + namespace/service-account.
+        
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
+
+        FilterChain chain = new FilterChain() {
+            @Override
+            public void doFilter(ServletRequest req, ServletResponse res) {
+                org.springframework.security.core.Authentication auth = 
SecurityContextHolder.getContext().getAuthentication();
+
+                assertNotNull(auth);
+                assertTrue(auth instanceof RangerAuthenticationToken);
+                RangerAuthenticationToken rangerAuth = 
(RangerAuthenticationToken) auth;
+                assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, 
rangerAuth.getAuthType());
+                assertEquals("nginx-ingress", auth.getName());
+            }
+        };
+
+        filter.doFilter(request, response, chain);
+    }
+
+    @Test
+    public void testDoFilter_enabled_usernameHeaderTakesPrecedenceOverSpiffe() 
throws Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "X-Forwarded-User");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        
when(userMgr.getRolesByLoginId("joeuser")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+
+        when(request.getHeader("X-Forwarded-User")).thenReturn("joeuser");
+
+        FilterChain chain = new FilterChain() {
+            @Override
+            public void doFilter(ServletRequest req, ServletResponse res) {
+                org.springframework.security.core.Authentication auth = 
SecurityContextHolder.getContext().getAuthentication();
+
+                assertNotNull(auth);
+                assertEquals("joeuser", auth.getName());
+            }
+        };
+
+        filter.doFilter(request, response, chain);
+
+        verify(userMgr, never()).getRolesByLoginId("service-sa");
+    }
+
+    @Test
+    public void testDoFilter_enabled_multipleSpiffeHeaders_usesFirstValid() 
throws Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id, X-Workload-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        
when(userMgr.getRolesByLoginId("service-sa")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+
+        when(request.getHeader("X-Spiffe-Id")).thenReturn("not-a-spiffe-id");
+        
when(request.getHeader("X-Workload-Id")).thenReturn("spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+        FilterChain chain = new FilterChain() {
+            @Override
+            public void doFilter(ServletRequest req, ServletResponse res) {
+                org.springframework.security.core.Authentication auth = 
SecurityContextHolder.getContext().getAuthentication();
+
+                assertNotNull(auth);
+                assertEquals("service-sa", auth.getName());
+            }
+        };
+
+        filter.doFilter(request, response, chain);
+    }
+
+    @Test
+    public void testDoFilter_enabled_malformedSpiffeHeader_passesThrough() 
throws Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        FilterChain         chain    = mock(FilterChain.class);
+
+        when(request.getHeader("X-Spiffe-Id")).thenReturn("not-a-spiffe-id");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(request, response);
+        verify(userMgr, never()).getRolesByLoginId(anyString());
+        assertNull(SecurityContextHolder.getContext().getAuthentication());
+    }
+
+    @Test
+    public void 
testDoFilter_enabled_specValidButNonConformingSpiffeHeader_passesThrough() 
throws Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        FilterChain         chain    = mock(FilterChain.class);
+
+        // Valid SPIFFE ID per the SPIFFE spec, but not in the expected 
/ns/<ns>/sa/<sa> layout.
+        
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://example.org/workload/frontend");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(request, response);
+        verify(userMgr, never()).getRolesByLoginId(anyString());
+        assertNull(SecurityContextHolder.getContext().getAuthentication());
+    }
+
+    @Test
+    public void 
testDoFilter_enabled_spiffeHeaderWithIllegalChars_passesThrough() throws 
Exception {
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
 "X-Spiffe-Id");
+
+        RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
+        UserMgr                   userMgr = mock(UserMgr.class);
+
+        filter.userMgr = userMgr;
+        filter.initialize();
+
+        HttpServletRequest  request  = mock(HttpServletRequest.class);
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        FilterChain         chain    = mock(FilterChain.class);
+
+        // Correct layout but the service-account contains whitespace, which 
is not an allowed SPIFFE character.
+        
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://my-cluster/ns/prod/sa/service
 sa");
+
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(request, response);
+        verify(userMgr, never()).getRolesByLoginId(anyString());
+        assertNull(SecurityContextHolder.getContext().getAuthentication());
+    }
+
     @Test
     public void 
testDoFilter_enabled_existingAuthenticatedContext_doesNotOverrideAuthentication()
 throws Exception {
         
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
 "true");
-        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "x-awc-username");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
 "X-Forwarded-User");
 
         RangerHeaderPreAuthFilter filter  = new RangerHeaderPreAuthFilter();
         UserMgr                   userMgr = mock(UserMgr.class);
diff --git 
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
 
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
index ff9f059bc..e468de58f 100644
--- 
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
+++ 
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
@@ -184,7 +184,7 @@ public void testGetAuthType_reflectionVariants() throws 
Exception {
 
     @Test
     public void 
testDoFilter_authenticated_createsSecurityContextAndUserSession() throws 
Exception {
-        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME,
 "x-awc-requestid");
+        
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME,
 "X-Request-Id");
 
         try {
             RangerSecurityContextFormationFilter filter = new 
RangerSecurityContextFormationFilter();
@@ -212,7 +212,7 @@ public void 
testDoFilter_authenticated_createsSecurityContextAndUserSession() th
             Mockito.when(req.getSession(false)).thenReturn(session);
             
Mockito.when(session.getAttribute(RangerSecurityContextFormationFilter.AKA_SC_SESSION_KEY)).thenReturn(null);
             
Mockito.when(req.getHeader(RangerSecurityContextFormationFilter.USER_AGENT)).thenReturn("Mozilla/5.0");
-            
Mockito.when(req.getHeader("x-awc-requestid")).thenReturn("awc-request-1");
+            
Mockito.when(req.getHeader("X-Request-Id")).thenReturn("request-1");
             Mockito.when(req.getRequestURI()).thenReturn("/secure");
             
Mockito.when(httpUtil.getDeviceType(req)).thenReturn(RangerCommonEnums.DEVICE_BROWSER);
 
@@ -229,7 +229,7 @@ public void doFilter(ServletRequest servletRequest, 
ServletResponse servletRespo
 
                     assertNotNull(ctx);
                     assertNotNull(ctx.getRequestContext());
-                    assertEquals("awc-request-1", 
ctx.getRequestContext().getServerRequestId());
+                    assertEquals("request-1", 
ctx.getRequestContext().getServerRequestId());
                     assertSame(userSession, ctx.getUserSession());
                 }
             };

Reply via email to