You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ra...@apache.org on 2026/08/22 11:09:24 UTC

(ranger) branch master updated: RANGER-5723: Plugin SPIFFE outbound auth for audit-server destination (#1139)

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

ramackri pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/master by this push:
     new 8e7716cd3 RANGER-5723: Plugin SPIFFE outbound auth for audit-server destination (#1139)
8e7716cd3 is described below

commit 8e7716cd3c36da395b8edc8470e328f736fbd984
Author: Ramachandran Krishnan <ra...@gmail.com>
AuthorDate: Sat Aug 22 16:39:19 2026 +0530

    RANGER-5723: Plugin SPIFFE outbound auth for audit-server destination (#1139)
    
    * RANGER-5723: Plugin SPIFFE outbound auth for audit-server destination
    
    Add PluginHeaderAuthConfig, SpiffeIdentityResolver, and
    RangerRESTClient.setTrustedAuthHeaders(); wire SPIFFE headers into
    RangerAuditServerDestination when audit XML authn.header.enabled=true.
    
    * RANGER-5723: Remove unused SPIFFE prefix-discovery helpers
    
    Drop configPrefixForServiceType, resolveEnabledConfigPrefix, and
    RANGER_CONFIG_PREFIX; audit destination passes an explicit config prefix to
    buildSpiffeAuthHeaders.
    
    * RANGER-5723: Address PR review comments for SPIFFE outbound auth
    
    Remove redundant SPIFFE ID trim, add misconfiguration and REST client
    header tests, and document that SPIFFE header auth is additive to authn.type.
    
    * RANGER-5723: Fix Checkstyle import order in TestRangerRESTClient
    
    Add blank line between javax and java import groups required by
    dev-support/checkstyle.xml ImportOrder rule.
    
    * Update SpiffeIdentityResolver.java
    
    * Update SpiffeIdentityResolver.java
    
    * RANGER-5723: Fix checkstyle and address remaining PR review comments
    
    Fix SpiffeIdentityResolver checkstyle violations, rename buildSpiffeAuthHeaders
    to buildTrustedAuthHeaders, and add generic authn.header.headers slot-based
    configuration with file:/env: value resolution.
    
    * RANGER-5723: Document trusted-header config modes and value specs
    
    Expand PluginHeaderAuthConfig Javadoc for legacy SPIFFE and generic slot
    configuration, including file:/env:/literal value spec examples.
    
    * RANGER-5723: Use header-name-as-property for trusted auth config
    
    Address PR review: configure outbound trusted headers as
    authn.header.{Header-Name}=value specs (file:/env:/literal) instead
    of slot-based or legacy SPIFFE properties.
    
    * Update RangerAuditServerDestination.java
    
    * Update RangerAuditServerDestinationTest.java
    
    * Update TestRangerRESTClient.java
    
    * Update PluginHeaderAuthConfig.java
    
    * RANGER-5723: Drop outbound SPIFFE validation and SpiffeIdentityResolver
    
    Address PR review: trusted outbound headers pass resolved values
    through without SPIFFE format checks; remove SpiffeIdentityResolver
    and obsolete tests for the old authn.spiffe.* resolution model.
    
    * Update PluginHeaderAuthConfig.java
    
    * Update PluginHeaderAuthConfig.java
    
    * RANGER-5723: Remove unnecessary property-name sorting in header auth config.
    
    HTTP header order does not affect auth; iterate props directly per review feedback.
    
    ---------
    
    Co-authored-by: ramk <ra...@cloudera.com>
    Co-authored-by: Madhan Neethiraj <ma...@apache.org>
---
 agents-audit/dest-auditserver/pom.xml              |   6 +
 .../destination/RangerAuditServerDestination.java  |  11 ++
 .../RangerAuditServerDestinationTest.java          |  56 +++++++
 .../ranger/plugin/util/RangerRESTClient.java       |  24 +++
 .../ranger/plugin/util/TestRangerRESTClient.java   |  51 +++++++
 .../ranger/plugin/util/PluginHeaderAuthConfig.java | 166 +++++++++++++++++++++
 .../plugin/util/PluginHeaderAuthConfigTest.java    |  92 ++++++++++++
 7 files changed, 406 insertions(+)

diff --git a/agents-audit/dest-auditserver/pom.xml b/agents-audit/dest-auditserver/pom.xml
index 41aa1baee..d512ebbae 100644
--- a/agents-audit/dest-auditserver/pom.xml
+++ b/agents-audit/dest-auditserver/pom.xml
@@ -74,6 +74,12 @@
         </dependency>
 
         <!-- Test -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>log4j-over-slf4j</artifactId>
diff --git a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java
index a7eacb999..421ac0710 100644
--- a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java
+++ b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java
@@ -27,6 +27,7 @@
 import org.apache.ranger.audit.model.AuthzAuditEvent;
 import org.apache.ranger.audit.provider.MiscUtil;
 import org.apache.ranger.plugin.authn.DefaultJwtProvider;
+import org.apache.ranger.plugin.util.PluginHeaderAuthConfig;
 import org.apache.ranger.plugin.util.RangerRESTClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -98,6 +99,16 @@ public void init(Properties props, String propPrefix) {
         this.restClient.setMaxRetryAttempts(maxRetryAttempts);
         this.restClient.setRetryIntervalMs(retryIntervalMs);
 
+        // Trusted header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled,
+        // trusted headers are added in addition to whatever authType configured above.
+        Map<String, String> trustedHeaders = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, propPrefix);
+
+        if (!trustedHeaders.isEmpty()) {
+            this.restClient.setTrustedAuthHeaders(trustedHeaders);
+
+            LOG.debug("Trusted authentication headers added for audit-server destination");
+        }
+
         LOG.info("<== RangerAuditServerDestination:init()");
     }
 
diff --git a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java
new file mode 100644
index 000000000..4ba059e3f
--- /dev/null
+++ b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.audit.destination;
+
+import org.apache.ranger.plugin.util.PluginHeaderAuthConfig;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerAuditServerDestinationTest {
+    private static final String AUDIT_DEST_PREFIX = "xasecure.audit.destination.auditserver";
+
+    @Test
+    public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() {
+        Properties props = new Properties();
+
+        props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true");
+        props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive");
+
+        Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX);
+
+        assertEquals(1, headers.size());
+        assertEquals("spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive", headers.get("X-Spiffe-Id"));
+    }
+
+    @Test
+    public void buildTrustedAuthHeadersEmptyWhenAuditDestinationDisabled() {
+        Properties props = new Properties();
+
+        props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false");
+        props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive");
+
+        Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX);
+
+        assertTrue(headers.isEmpty());
+    }
+}
diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
index d4d49523b..94046a549 100644
--- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
+++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java
@@ -60,6 +60,8 @@
 import java.security.SecureRandom;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -143,6 +145,7 @@ public String getMethod() {
     private volatile Client       cookieAuthClient;
     private          JwtProvider  jwtProvider;
     private volatile String       authHeader;
+    private volatile Map<String, String> trustedAuthHeaders = Collections.emptyMap();
 
     public RangerRESTClient(String url, String sslConfigFileName, Configuration config) {
         this(url, sslConfigFileName, config, getPropertyPrefix(config));
@@ -215,6 +218,19 @@ public void setRetryIntervalMs(int retryIntervalMs) {
         this.retryIntervalMs = retryIntervalMs;
     }
 
+    /**
+     * Trusted HTTP headers for SPIFFE or other header-based auth.
+     * Applied to every REST request from this client.
+     */
+    public void setTrustedAuthHeaders(Map<String, String> headers) {
+        if (headers == null || headers.isEmpty()) {
+            trustedAuthHeaders = Collections.emptyMap();
+        } else {
+            trustedAuthHeaders = Collections.unmodifiableMap(new LinkedHashMap<>(headers));
+        }
+        resetClient();
+    }
+
     public void setBasicAuthInfo(String username, String password) {
         setBasicAuthFilter(username, password);
     }
@@ -494,9 +510,17 @@ private Invocation.Builder createInvocationBuilder(int currentIndex, String rela
             builder = builder.cookie(sessionId);
         }
 
+        applyTrustedAuthHeaders(builder);
+
         return builder;
     }
 
+    private void applyTrustedAuthHeaders(Invocation.Builder builder) {
+        for (Map.Entry<String, String> entry : trustedAuthHeaders.entrySet()) {
+            builder.header(entry.getKey(), entry.getValue());
+        }
+    }
+
     private Response performRequest(HttpMethod method, String relativeUrl, Map<String, String> params, Object requestBody, Cookie sessionId) throws Exception {
         Response finalResponse = null;
         int      startIndex    = this.lastKnownActiveUrlIndex;
diff --git a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java
index 9d837a2c4..2f5f6ca22 100644
--- a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java
+++ b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java
@@ -19,11 +19,23 @@
 
 package org.apache.ranger.plugin.util;
 
+import com.sun.net.httpserver.HttpServer;
+import org.apache.hadoop.conf.Configuration;
 import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig;
 import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions;
 import org.apache.ranger.plugin.service.RangerBasePlugin;
 import org.junit.jupiter.api.Test;
 
+import javax.ws.rs.core.Response;
+
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,6 +45,8 @@ public class TestRangerRESTClient {
     private static final String SERVICE_NAME = "test-service";
     private static final String APP_ID = "test-app";
     private static final String ERR_MESSAGE = "Ranger URL is null or empty.";
+    private static final String VALID_SPIFFE =
+            "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om";
 
     @Test
     public void testPluginInit_WithNoUrl_ThrowsException() {
@@ -50,4 +64,41 @@ public void testPluginInit_WithValidUrl_Succeeds() {
         plugin.init();
         assertNotNull(plugin, "RangerBasePlugin should be initialized successfully");
     }
+
+    @Test
+    public void setTrustedAuthHeadersAddsHeaderToOutboundRequest() throws Exception {
+        AtomicReference<String> capturedSpiffeHeader = new AtomicReference<>();
+        HttpServer              httpServer           = HttpServer.create(new InetSocketAddress(0), 0);
+
+        httpServer.createContext("/", exchange -> {
+            List<String> values = exchange.getRequestHeaders().get("X-Spiffe-Id");
+
+            if (values != null && !values.isEmpty()) {
+                capturedSpiffeHeader.set(values.get(0));
+            }
+
+            exchange.sendResponseHeaders(200, -1);
+            exchange.close();
+        });
+
+        httpServer.start();
+
+        try {
+            String              serverUrl = "http://localhost:" + httpServer.getAddress().getPort();
+            Configuration       conf      = new Configuration();
+            RangerRESTClient    client    = new RangerRESTClient(serverUrl, null, conf);
+            Map<String, String> headers   = new LinkedHashMap<>();
+
+            headers.put("X-Spiffe-Id", VALID_SPIFFE);
+            client.setTrustedAuthHeaders(headers);
+
+            try (Response response = client.get("/test", Collections.emptyMap())) {
+                assertEquals(200, response.getStatus());
+            }
+
+            assertEquals(VALID_SPIFFE, capturedSpiffeHeader.get());
+        } finally {
+            httpServer.stop(0);
+        }
+    }
 }
diff --git a/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java b/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java
new file mode 100644
index 000000000..a96a81fa9
--- /dev/null
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java
@@ -0,0 +1,166 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Outbound trusted-header auth for audit-server and other REST clients.
+ *
+ * <p>When {@code authn.header.enabled=true}, trusted HTTP headers are added to every
+ * outbound REST request. Each header is configured as a property whose name is the
+ * HTTP header name under {@code authn.header} (audit destination example):
+ * <pre>
+ * xasecure.audit.destination.auditserver.authn.header.enabled=true
+ * xasecure.audit.destination.auditserver.authn.header.X-Spiffe-Id=file:/path/to/spiffe-id.file
+ * # valid value specs:
+ * #   file:/path/to/spiffe-id.file
+ * #   env:SPIFFE_ID
+ * #   spiffe://trust-domain/ns/.../sa/...  (literal string)
+ * </pre>
+ */
+public final class PluginHeaderAuthConfig {
+    private static final Logger LOG = LoggerFactory.getLogger(PluginHeaderAuthConfig.class);
+
+    public static final String PROP_HEADER_PREFIX       = "authn.header.";
+    public static final String PROP_HEADER_AUTH_ENABLED = PROP_HEADER_PREFIX + "enabled";
+
+    private static final String VALUE_PREFIX_FILE = "file:";
+    private static final String VALUE_PREFIX_ENV  = "env:";
+
+    private PluginHeaderAuthConfig() {
+        // to block instantiation
+    }
+
+    /**
+     * Returns whether trusted header auth is enabled for the given config prefix.
+     *
+     * @param props        plugin or site configuration properties
+     * @param configPrefix property prefix for header-auth settings
+     * @return {@code true} when header auth is enabled
+     */
+    public static boolean isHeaderAuthEnabled(final Properties props, final String configPrefix) {
+        return props != null && StringUtils.isNotBlank(configPrefix) && Boolean.parseBoolean(props.getProperty(configPrefix + "." + PROP_HEADER_AUTH_ENABLED, "false"));
+    }
+
+    /**
+     * Builds trusted HTTP headers for outbound REST calls when header auth is enabled.
+     *
+     * @param props        plugin or site configuration properties
+     * @param configPrefix prefix such as {@code xasecure.audit.destination.auditserver}
+     * @return immutable header map; empty when auth is disabled or misconfigured
+     */
+    public static Map<String, String> buildTrustedAuthHeaders(final Properties props, final String configPrefix) {
+        final Map<String, String> ret;
+
+        if (isHeaderAuthEnabled(props, configPrefix)) {
+            String              propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX;
+            Map<String, String> headers        = new LinkedHashMap<>();
+
+            for (String propertyName : props.stringPropertyNames()) {
+                if (!propertyName.startsWith(propertyPrefix)) {
+                    continue;
+                }
+
+                String headerName = propertyName.substring(propertyPrefix.length());
+
+                if (StringUtils.isBlank(headerName) || "enabled".equals(headerName)) {
+                    continue;
+                }
+
+                String headerValue = resolveConfiguredValue(props.getProperty(propertyName));
+
+                addConfiguredHeader(headers, configPrefix, headerName, headerValue);
+            }
+
+            if (headers.isEmpty()) {
+                LOG.warn("Plugin header auth enabled for {} but no trusted headers could be resolved", configPrefix);
+            }
+
+            ret = Collections.unmodifiableMap(headers);
+        } else {
+            ret = Collections.emptyMap();
+        }
+
+        return ret;
+    }
+
+    private static void addConfiguredHeader(final Map<String, String> headers, final String configPrefix, final String headerName, final String headerValue) {
+        if (StringUtils.isBlank(headerValue)) {
+            LOG.warn("Plugin header auth enabled for {} but trusted header {} has no resolvable value", configPrefix, headerName);
+        } else {
+            headers.put(headerName, headerValue);
+        }
+    }
+
+    private static String resolveConfiguredValue(final String valueSpec) {
+        final String ret;
+
+        if (StringUtils.isBlank(valueSpec)) {
+            ret = null;
+        } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) {
+            ret = readFirstNonBlankLine(valueSpec.substring(VALUE_PREFIX_FILE.length()));
+        } else if (valueSpec.startsWith(VALUE_PREFIX_ENV)) {
+            ret = StringUtils.trimToNull(System.getenv(valueSpec.substring(VALUE_PREFIX_ENV.length())));
+        } else {
+            ret = StringUtils.trimToNull(valueSpec);
+        }
+
+        return ret;
+    }
+
+    private static String readFirstNonBlankLine(final String filePath) {
+        String ret = null;
+
+        if (StringUtils.isNotBlank(filePath)) {
+            try {
+                Path path = Paths.get(filePath.trim());
+
+                if (Files.isRegularFile(path)) {
+                    for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) {
+                        String trimmed = StringUtils.trimToNull(line);
+
+                        if (trimmed != null) {
+                            ret = trimmed;
+
+                            break;
+                        }
+                    }
+                }
+            } catch (IOException ex) {
+                LOG.debug("Unable to read trusted header value from file {}", filePath, ex);
+            }
+        }
+
+        return ret;
+    }
+}
diff --git a/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java
new file mode 100644
index 000000000..98286c39a
--- /dev/null
+++ b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PluginHeaderAuthConfigTest {
+    private static final String VALID_SPIFFE =
+            "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om";
+
+    @Test
+    public void buildTrustedAuthHeadersUsesLiteralHeaderValue() {
+        Properties props = new Properties();
+        props.setProperty("ranger.ozone.authn.header.enabled", "true");
+        props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE);
+
+        Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone");
+
+        assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id"));
+    }
+
+    @Test
+    public void buildTrustedAuthHeadersUsesFileValueSpec(@TempDir Path tempDir) throws Exception {
+        Path spiffeFile = tempDir.resolve("spiffe");
+        Files.writeString(spiffeFile, VALID_SPIFFE + "\n", StandardCharsets.UTF_8);
+
+        Properties props = new Properties();
+        props.setProperty("ranger.ozone.authn.header.enabled", "true");
+        props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "file:" + spiffeFile);
+
+        Map<String, String> headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone");
+
+        assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id"));
+    }
+
+    @Test
+    public void buildTrustedAuthHeadersEmptyWhenDisabled() {
+        Properties props = new Properties();
+        props.setProperty("ranger.ozone.authn.header.enabled", "false");
+        props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE);
+
+        assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+    }
+
+    @Test
+    public void isHeaderAuthEnabledFalseForMissingPrefix() {
+        assertFalse(PluginHeaderAuthConfig.isHeaderAuthEnabled(new Properties(), "ranger.ozone"));
+    }
+
+    @Test
+    public void buildTrustedAuthHeadersEmptyWhenNoHeadersConfigured() {
+        Properties props = new Properties();
+        props.setProperty("ranger.ozone.authn.header.enabled", "true");
+
+        assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+    }
+
+    @Test
+    public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() {
+        Properties props = new Properties();
+        props.setProperty("ranger.ozone.authn.header.enabled", "true");
+        props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "env:UNSET_SPIFFE_ID_VAR");
+
+        assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+    }
+}