You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ma...@apache.org on 2026/04/01 18:52:37 UTC

(ranger) branch master updated: RANGER-5371: pdp server implementation (#899)

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

madhan 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 e4c0a6628 RANGER-5371: pdp server implementation (#899)
e4c0a6628 is described below

commit e4c0a6628d79e917f177071fcc465e789f08e382
Author: Madhan Neethiraj <ma...@apache.org>
AuthorDate: Wed Apr 1 11:52:32 2026 -0700

    RANGER-5371: pdp server implementation (#899)
    
    * RANGER-5371: pdp server implementation
    * RANGER-5531: update Python client to support Ranger PDP server APIs
---
 .../ranger/authz/model/RangerAuthzResult.java      |  13 +-
 .../ranger/authz/model/RangerMultiAuthzResult.java |  13 +-
 .../ranger/authz/embedded/RangerAuthzPlugin.java   |   8 +-
 .../authz/embedded/RangerEmbeddedAuthorizer.java   |  32 +-
 dev-support/ranger-docker/.dockerignore            |   1 +
 dev-support/ranger-docker/.env                     |   4 +
 dev-support/ranger-docker/Dockerfile.ranger-pdp    |  36 ++
 dev-support/ranger-docker/README.md                |   8 +-
 .../ranger-docker/docker-compose.ranger-pdp.yml    |  61 +++
 .../ranger-docker/scripts/kdc/entrypoint.sh        |   5 +-
 dev-support/ranger-docker/scripts/pdp/logback.xml  |  47 ++
 .../ranger-docker/scripts/pdp/ranger-pdp-site.xml  | 240 ++++++++++
 .../ranger-docker/scripts/pdp/ranger-pdp.sh        |  39 ++
 distro/pom.xml                                     |   8 +
 distro/src/main/assembly/pdp.xml                   | 207 +++++++++
 intg/src/main/python/README.md                     | 470 +++++++++++++++-----
 .../main/python/apache_ranger/client/__init__.py   |  31 ++
 .../apache_ranger/client/ranger_pdp_client.py      |  84 ++++
 intg/src/main/python/apache_ranger/exceptions.py   |  22 +-
 .../main/python/apache_ranger/model/__init__.py    |  19 +
 .../python/apache_ranger/model/ranger_authz.py     | 260 +++++++++++
 pdp/conf.dist/README-k8s.md                        | 103 +++++
 pdp/conf.dist/logback.xml                          |  36 ++
 pdp/conf.dist/ranger-pdp-site.xml                  | 364 +++++++++++++++
 pdp/pom.xml                                        | 200 +++++++++
 pdp/scripts/ranger-pdp-services.sh                 | 223 ++++++++++
 pdp/scripts/ranger-pdp.sh                          |  76 ++++
 .../org/apache/ranger/pdp/RangerPdpServer.java     | 295 +++++++++++++
 .../java/org/apache/ranger/pdp/RangerPdpStats.java | 113 +++++
 .../apache/ranger/pdp/RangerPdpStatusServlet.java  | 129 ++++++
 .../apache/ranger/pdp/config/RangerPdpConfig.java  | 281 ++++++++++++
 .../ranger/pdp/config/RangerPdpConstants.java      |  94 ++++
 .../org/apache/ranger/pdp/model/ErrorResponse.java |  45 ++
 .../ranger/pdp/rest/RangerPdpApplication.java      |  62 +++
 .../org/apache/ranger/pdp/rest/RangerPdpREST.java  | 491 +++++++++++++++++++++
 .../pdp/security/HttpHeaderAuthNHandler.java       |  71 +++
 .../ranger/pdp/security/JwtAuthNHandler.java       | 105 +++++
 .../ranger/pdp/security/KerberosAuthNHandler.java  | 275 ++++++++++++
 .../ranger/pdp/security/PdpAuthNHandler.java       | 111 +++++
 .../ranger/pdp/security/RangerPdpAuthNFilter.java  | 189 ++++++++
 .../security/RangerPdpRequestContextFilter.java    |  73 +++
 pdp/src/main/resources/ranger-pdp-default.xml      | 364 +++++++++++++++
 .../org/apache/ranger/pdp/RangerPdpStatsTest.java  |  54 +++
 .../ranger/pdp/RangerPdpStatusServletTest.java     | 108 +++++
 .../ranger/pdp/config/RangerPdpConfigTest.java     |  51 +++
 .../apache/ranger/pdp/rest/RangerPdpRESTTest.java  | 360 +++++++++++++++
 .../pdp/security/HttpHeaderAuthNHandlerTest.java   |  84 ++++
 .../pdp/security/KerberosAuthHandlerTest.java      |  69 +++
 .../pdp/security/RangerPdpAuthNFilterTest.java     |  99 +++++
 .../RangerPdpRequestContextFilterTest.java         | 132 ++++++
 pom.xml                                            |   3 +
 .../src/main/python/sample_kms_client.py           |  86 ++++
 .../src/main/python/sample_pdp_client.py           | 101 +++++
 53 files changed, 6311 insertions(+), 144 deletions(-)

diff --git a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
index 4c6470660..0dbf6b4cb 100644
--- a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
+++ b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
@@ -41,7 +41,12 @@ public RangerAuthzResult() {
     }
 
     public RangerAuthzResult(String requestId) {
-        this(requestId, null);
+        this.requestId = requestId;
+    }
+
+    public RangerAuthzResult(String requestId, AccessDecision decision) {
+        this.requestId = requestId;
+        this.decision  = decision;
     }
 
     public RangerAuthzResult(String requestId, Map<String, PermissionResult> permissions) {
@@ -49,6 +54,12 @@ public RangerAuthzResult(String requestId, Map<String, PermissionResult> permiss
         this.permissions = permissions;
     }
 
+    public RangerAuthzResult(String requestId, AccessDecision decision, Map<String, PermissionResult> permissions) {
+        this.requestId   = requestId;
+        this.decision    = decision;
+        this.permissions = permissions;
+    }
+
     public String getRequestId() {
         return requestId;
     }
diff --git a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
index 5c15c2fd6..cae5d213e 100644
--- a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
+++ b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
@@ -39,7 +39,12 @@ public RangerMultiAuthzResult() {
     }
 
     public RangerMultiAuthzResult(String requestId) {
-        this(requestId, null);
+        this.requestId = requestId;
+    }
+
+    public RangerMultiAuthzResult(String requestId, AccessDecision decision) {
+        this.requestId = requestId;
+        this.decision  = decision;
     }
 
     public RangerMultiAuthzResult(String requestId, List<RangerAuthzResult> accesses) {
@@ -47,6 +52,12 @@ public RangerMultiAuthzResult(String requestId, List<RangerAuthzResult> accesses
         this.accesses  = accesses;
     }
 
+    public RangerMultiAuthzResult(String requestId, AccessDecision decision, List<RangerAuthzResult> accesses) {
+        this.requestId = requestId;
+        this.decision  = decision;
+        this.accesses  = accesses;
+    }
+
     public String getRequestId() {
         return requestId;
     }
diff --git a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
index 3fd84a033..4f0be2a5d 100644
--- a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
+++ b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
@@ -70,8 +70,8 @@ class RangerAuthzPlugin {
     private final RangerBasePlugin                      plugin;
     private final Map<String, RangerResourceNameParser> rrnTemplates = new HashMap<>();
 
-    public RangerAuthzPlugin(String serviceType, String serviceName, Properties properties) {
-        plugin = new RangerBasePlugin(getPluginConfig(serviceType, serviceName, properties)) {
+    public RangerAuthzPlugin(String serviceType, String serviceName, String appId, Properties properties) {
+        plugin = new RangerBasePlugin(getPluginConfig(serviceType, serviceName, appId, properties)) {
             @Override
             public void setPolicies(ServicePolicies policies) {
                 super.setPolicies(policies);
@@ -407,7 +407,7 @@ private void updateResourceTemplates() {
         }
     }
 
-    private static RangerPluginConfig getPluginConfig(String serviceType, String serviceName, Properties properties) {
-        return new RangerPluginConfig(serviceType, serviceName, null, properties);
+    private static RangerPluginConfig getPluginConfig(String serviceType, String serviceName, String appId, Properties properties) {
+        return new RangerPluginConfig(serviceType, serviceName, appId, properties);
     }
 }
diff --git a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
index 650c16187..d2d8d570b 100644
--- a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
+++ b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
@@ -36,9 +36,11 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import static org.apache.ranger.authz.api.RangerAuthzApiErrorCode.INVALID_REQUEST_SERVICE_NAME_OR_TYPE_MANDATORY;
 import static org.apache.ranger.authz.embedded.RangerEmbeddedAuthzErrorCode.NO_DEFAULT_SERVICE_FOR_SERVICE_TYPE;
@@ -49,7 +51,7 @@ public class RangerEmbeddedAuthorizer extends RangerAuthorizer {
 
     private final RangerAuthzConfig              config;
     private final String                         appType;
-    private final Map<String, RangerAuthzPlugin> plugins = new HashMap<>();
+    private final Map<String, RangerAuthzPlugin> plugins = new ConcurrentHashMap<>();
 
     public RangerEmbeddedAuthorizer(Properties properties) {
         super(properties);
@@ -131,6 +133,10 @@ public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request, RangerA
         return authorize(request, plugin, auditHandler);
     }
 
+    public Set<String> getLoadedServices() {
+        return new HashSet<>(this.plugins.keySet());
+    }
+
     @Override
     protected void validateAccessContext(RangerAccessContext context) throws RangerAuthzException {
         super.validateAccessContext(context);
@@ -206,24 +212,18 @@ private RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request, Ranger
         return result;
     }
 
-    private RangerAuthzPlugin getOrCreatePlugin(String serviceName, String serviceType) throws RangerAuthzException {
-        RangerAuthzPlugin ret = plugins.get(serviceName);
-
-        if (ret == null) {
-            synchronized (plugins) {
-                ret = plugins.get(serviceName);
+    private RangerAuthzPlugin getOrCreatePlugin(String serviceName, String serviceType) {
+        return plugins.computeIfAbsent(serviceName, k -> createPlugin(serviceName, serviceType));
+    }
 
-                if (ret == null) {
-                    Properties pluginProperties = this.config.getServiceProperties(serviceName, serviceType);
+    private RangerAuthzPlugin createPlugin(String serviceName, String serviceType) {
+        Properties pluginProperties = config.getServiceProperties(serviceName, serviceType);
 
-                    LOG.debug("properties for service {}: {}", serviceName, pluginProperties);
+        LOG.debug("properties for service {}: {}", serviceName, pluginProperties);
 
-                    ret = new RangerAuthzPlugin(serviceType, serviceName, pluginProperties);
+        RangerAuthzPlugin ret = new RangerAuthzPlugin(serviceType, serviceName, appType, pluginProperties);
 
-                    plugins.put(serviceName, ret);
-                }
-            }
-        }
+        LOG.info("Created plugin: serviceName={}, serviceType={}", serviceName, serviceType);
 
         return ret;
     }
diff --git a/dev-support/ranger-docker/.dockerignore b/dev-support/ranger-docker/.dockerignore
index 22e3ff3d9..6e66a678e 100644
--- a/dev-support/ranger-docker/.dockerignore
+++ b/dev-support/ranger-docker/.dockerignore
@@ -5,6 +5,7 @@
 !dist/ranger-*-kms.tar.gz
 !dist/ranger-*-usersync.tar.gz
 !dist/ranger-*-tagsync.tar.gz
+!dist/ranger-*-pdp.tar.gz
 !dist/ranger-*-audit-server.tar.gz
 !dist/ranger-*-audit-consumer-solr.tar.gz
 !dist/ranger-*-audit-consumer-hdfs.tar.gz
diff --git a/dev-support/ranger-docker/.env b/dev-support/ranger-docker/.env
index 03a222ec7..ddaa76588 100644
--- a/dev-support/ranger-docker/.env
+++ b/dev-support/ranger-docker/.env
@@ -47,6 +47,9 @@ USERSYNC_VERSION=3.0.0-SNAPSHOT
 # Tagsync Configuration
 TAGSYNC_VERSION=3.0.0-SNAPSHOT
 
+# PDP Configuration
+PDP_VERSION=3.0.0-SNAPSHOT
+
 # Solr Configuration
 SOLR_VERSION=8.11.3
 SOLR_PLUGIN_VERSION=3.0.0-SNAPSHOT
@@ -84,4 +87,5 @@ OPENSEARCH_VERSION=1.3.19
 DEBUG_ADMIN=false
 DEBUG_USERSYNC=false
 DEBUG_TAGSYNC=false
+DEBUG_PDP=false
 ENABLE_FILE_SYNC_SOURCE=false
diff --git a/dev-support/ranger-docker/Dockerfile.ranger-pdp b/dev-support/ranger-docker/Dockerfile.ranger-pdp
new file mode 100644
index 000000000..0f74b3162
--- /dev/null
+++ b/dev-support/ranger-docker/Dockerfile.ranger-pdp
@@ -0,0 +1,36 @@
+# 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.
+
+ARG RANGER_BASE_IMAGE=apache/ranger-base
+ARG RANGER_BASE_VERSION=20260123-2-8
+
+FROM ${RANGER_BASE_IMAGE}:${RANGER_BASE_VERSION}
+
+ARG PDP_VERSION
+
+COPY ./dist/ranger-${PDP_VERSION}-pdp.tar.gz /home/ranger/dist/
+COPY ./scripts/pdp/ranger-pdp.sh             ${RANGER_SCRIPTS}/
+
+RUN    tar xvfz /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz --directory=${RANGER_HOME} \
+    && ln -s ${RANGER_HOME}/ranger-${PDP_VERSION}-pdp ${RANGER_HOME}/pdp \
+    && rm -f /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz \
+    && mkdir -p /var/log/ranger/pdp /var/run/ranger /etc/ranger/cache \
+    && ln -s ${RANGER_HOME}/pdp/ranger-pdp-services.sh /usr/bin/ranger-pdp-services.sh \
+    && chown -R ranger:ranger ${RANGER_HOME}/pdp/ ${RANGER_SCRIPTS}/ /var/log/ranger/ /var/run/ranger /etc/ranger/ \
+    && chmod 744 ${RANGER_SCRIPTS}/ranger-pdp.sh
+
+USER ranger
+ENTRYPOINT [ "/home/ranger/scripts/ranger-pdp.sh" ]
diff --git a/dev-support/ranger-docker/README.md b/dev-support/ranger-docker/README.md
index 63f206830..b0156d61c 100644
--- a/dev-support/ranger-docker/README.md
+++ b/dev-support/ranger-docker/README.md
@@ -74,14 +74,14 @@ cd dev-support/ranger-docker
 
 ### Run Ranger Services in Containers
 
-#### Bring up ranger-core services: ranger, usersync, tagsync and ranger-kms in containers
+#### Bring up ranger-core services: ranger, usersync, tagsync, pdp and kms in containers
 ~~~
 # To enable file based sync source for usersync do:
 # export ENABLE_FILE_SYNC_SOURCE=true
 
 # valid values for RANGER_DB_TYPE: mysql/postgres/oracle
 
-docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml up -d
+docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml up -d
 
 # Ranger Admin can be accessed at http://localhost:6080 (admin/rangerR0cks!)
 ~~~
@@ -111,7 +111,7 @@ Similarly, check the `depends` section of the `docker-compose.ranger-service.yam
 #### Bring up all containers
 ~~~
 ./scripts/ozone/ozone-plugin-docker-setup.sh
-docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d
+docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d
 ~~~
           
 #### To rebuild specific images and start containers with the new image:
@@ -122,4 +122,4 @@ docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.ym
 #### To bring up audit server, solr and hdfs consumer. Make sure kafka,solr and hdfs containers are running before bring up audit server.
 ~~~
 docker compose  -f docker-compose.ranger.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-audit-server.yml  up -d
-~~~
\ No newline at end of file
+~~~
diff --git a/dev-support/ranger-docker/docker-compose.ranger-pdp.yml b/dev-support/ranger-docker/docker-compose.ranger-pdp.yml
new file mode 100644
index 000000000..78015f88a
--- /dev/null
+++ b/dev-support/ranger-docker/docker-compose.ranger-pdp.yml
@@ -0,0 +1,61 @@
+# 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.
+
+services:
+  ranger-pdp:
+    build:
+      context: .
+      dockerfile: Dockerfile.ranger-pdp
+      args:
+        - RANGER_BASE_IMAGE=${RANGER_BASE_IMAGE}
+        - RANGER_BASE_VERSION=${RANGER_BASE_VERSION}
+        - PDP_VERSION=${PDP_VERSION}
+    image: ranger-pdp
+    container_name: ranger-pdp
+    hostname: ranger-pdp.rangernw
+    volumes:
+      - ./dist/keytabs/ranger-pdp:/etc/keytabs
+      - ./scripts/kdc/krb5.conf:/etc/krb5.conf
+      - ./scripts/hadoop/core-site.xml:/home/ranger/scripts/core-site.xml:ro
+      - ./dist/version:/home/ranger/dist/version:ro
+      - ./scripts/pdp/logback.xml:/opt/ranger/pdp/conf/logback.xml
+      - ./scripts/pdp/ranger-pdp-site.xml:/opt/ranger/pdp/conf/ranger-pdp-site.xml
+    stdin_open: true
+    tty: true
+    networks:
+      - ranger
+    ports:
+      - "6500:6500"
+    healthcheck:
+      test: ["CMD-SHELL", "curl -sf http://localhost:6500/health/ready >/dev/null || exit 1"]
+      interval: 20s
+      timeout: 10s
+      retries: 20
+      start_period: 30s
+    depends_on:
+      ranger:
+        condition: service_started
+      ranger-solr:
+        condition: service_started
+    environment:
+      - PDP_VERSION
+      - KERBEROS_ENABLED
+      - DEBUG_PDP=${DEBUG_PDP:-false}
+
+networks:
+  ranger:
+    name: rangernw
+    external: true
diff --git a/dev-support/ranger-docker/scripts/kdc/entrypoint.sh b/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
index e920f413e..998bad441 100644
--- a/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
+++ b/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
@@ -90,6 +90,9 @@ function create_keytabs() {
 
   create_principal_and_keytab rangerkms ranger-kms
 
+  create_principal_and_keytab HTTP      ranger-pdp
+  create_principal_and_keytab rangerpdp ranger-pdp
+
   create_principal_and_keytab dn          ranger-hadoop
   create_principal_and_keytab hdfs        ranger-hadoop
   create_principal_and_keytab healthcheck ranger-hadoop
@@ -142,7 +145,7 @@ if [ ! -f $DB_DIR/principal ]; then
   echo "Database initialized"
 
   create_keytabs
-  create_testusers ranger ranger-usersync ranger-tagsync ranger-audit-server ranger-audit-consumer-solr ranger-audit-consumer-hdfs ranger-hadoop ranger-hive ranger-hbase ranger-kafka ranger-solr ranger-knox ranger-kms ranger-ozone ranger-trino ranger-opensearch
+  create_testusers ranger ranger-usersync ranger-tagsync ranger-pdp ranger-audit-server ranger-audit-consumer-solr ranger-audit-consumer-hdfs ranger-hadoop ranger-hive ranger-hbase ranger-kafka ranger-solr ranger-knox ranger-kms ranger-ozone ranger-trino ranger-opensearch
 else
   echo "KDC DB already exists; skipping create"
 fi
diff --git a/dev-support/ranger-docker/scripts/pdp/logback.xml b/dev-support/ranger-docker/scripts/pdp/logback.xml
new file mode 100644
index 000000000..769a3e032
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/logback.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<configuration>
+  <property name="pdpLogDir" value="${logdir:-/var/log/ranger/pdp}"/>
+  <property name="pdpHostname" value="${hostname:-unknown-host}"/>
+
+  <appender name="file" class="ch.qos.logback.core.FileAppender">
+    <file>${pdpLogDir}/ranger-pdp-${pdpHostname}.log</file>
+    <append>true</append>
+    <encoder>
+      <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+    <target>System.out</target>
+    <encoder>
+      <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <logger name="com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator" level="OFF"/>
+  <logger name="org.apache.directory.server.core" level="OFF"/>
+  <logger name="org.apache.hadoop.security" level="OFF"/>
+  <logger name="org.apache.hadoop.conf" level="ERROR"/>
+  <logger name="org.apache.hadoop.util.NativeCodeLoader" level="OFF"/>
+
+  <root level="INFO">
+    <appender-ref ref="file"/>
+    <appender-ref ref="stdout"/>
+  </root>
+</configuration>
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
new file mode 100644
index 000000000..3fc4e6245
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  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.
+-->
+<!-- Ranger PDP Server - Docker Configuration -->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value>ranger</value>
+  </property>
+
+  <!-- Only delegation users are allowed call on behalf of other users -->
+  <property>
+    <name>ranger.pdp.service.dev_hdfs.delegation.users</name>
+    <value>hdfs</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.dev_hive.delegation.users</name>
+    <value>hive</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.dev_hbase.delegation.users</name>
+    <value>hbase</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.dev_kafka.delegation.users</name>
+    <value>kafka</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.dev_trino.delegation.users</name>
+    <value>trino</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.dev_polaris.delegation.users</name>
+    <value>polaris</value>
+  </property>
+
+  <!-- SSL/TLS: disabled for Docker dev environment -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.types</name>
+    <value>header,jwt,kerberos</value>
+  </property>
+
+  <!-- HTTP Header authentication -->
+  <property>
+    <name>ranger.pdp.authn.header.enabled</name>
+    <value>false</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.header.username</name>
+    <value>X-Forwarded-User</value>
+  </property>
+
+  <!-- JWT bearer token authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.jwt.enabled</name>
+    <value>false</value>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.jwt.provider.url</name>
+    <value/>
+  </property>
+
+  <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.kerberos.enabled</name>
+    <value>true</value>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+    <value>HTTP/ranger-pdp.rangernw@EXAMPLE.COM</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+    <value>/etc/keytabs/HTTP.keytab</value>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.name.rules</name>
+    <value>DEFAULT</value>
+  </property>
+
+  <!-- Connection to Ranger Admin Server -->
+  <property>
+    <name>ranger.authz.default.policy.rest.url</name>
+    <value>http://ranger:6080</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.username</name>
+    <value>admin</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.password</name>
+    <value>rangerR0cks!</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.cache.dir</name>
+    <value>/etc/ranger/cache</value>
+  </property>
+
+  <!-- Audit logs -->
+  <property>
+    <name>ranger.authz.audit.is.enabled</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.log.status.log.enabled</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr.urls</name>
+    <value>http://ranger-solr:8983/solr/ranger_audits</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr.batch.filespool.dir</name>
+    <value>/var/log/ranger/pdp/audit/solr/spool</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr.force.use.inmemory.jaas.config</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.loginModuleName</name>
+    <value>com.sun.security.auth.module.Krb5LoginModule</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.loginModuleControlFlag</name>
+    <value>required</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.serviceName</name>
+    <value>HTTP</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.useKeyTab</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.storeKey</name>
+    <value>false</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.useTicketCache</name>
+    <value>true</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.keyTab</name>
+    <value>/etc/keytabs/rangerpdp.keytab</value>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.jaas.Client.option.principal</name>
+    <value>rangerpdp/ranger-pdp.rangernw@EXAMPLE.COM</value>
+  </property>
+</configuration>
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh b/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh
new file mode 100644
index 000000000..89e2f3c09
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# 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.
+
+if [ ! -e ${RANGER_HOME}/.setupDone ]
+then
+  SETUP_RANGER=true
+else
+  SETUP_RANGER=false
+fi
+
+if [ "${SETUP_RANGER}" == "true" ]
+then
+  if [ "${KERBEROS_ENABLED}" == "true" ]
+  then
+    ${RANGER_SCRIPTS}/wait_for_keytab.sh HTTP.keytab
+    ${RANGER_SCRIPTS}/wait_for_testusers_keytab.sh
+    cp ${RANGER_SCRIPTS}/core-site.xml ${RANGER_HOME}/pdp/conf/core-site.xml
+  fi
+
+  touch "${RANGER_HOME}"/.setupDone
+fi
+
+cd ${RANGER_HOME}/pdp || exit 1
+exec ./ranger-pdp-services.sh run
diff --git a/distro/pom.xml b/distro/pom.xml
index cb3c09a8c..be243be0c 100644
--- a/distro/pom.xml
+++ b/distro/pom.xml
@@ -297,6 +297,12 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-pdp</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.ranger</groupId>
             <artifactId>ranger-plugin-classloader</artifactId>
@@ -489,6 +495,7 @@
                                         <descriptor>src/main/assembly/tagsync.xml</descriptor>
                                         <descriptor>src/main/assembly/migration-util.xml</descriptor>
                                         <descriptor>src/main/assembly/kms.xml</descriptor>
+                                        <descriptor>src/main/assembly/pdp.xml</descriptor>
                                         <descriptor>src/main/assembly/ranger-tools.xml</descriptor>
                                         <descriptor>src/main/assembly/ranger-src.xml</descriptor>
                                         <descriptor>src/main/assembly/plugin-atlas.xml</descriptor>
@@ -1077,6 +1084,7 @@
                                         <descriptor>src/main/assembly/tagsync.xml</descriptor>
                                         <descriptor>src/main/assembly/migration-util.xml</descriptor>
                                         <descriptor>src/main/assembly/kms.xml</descriptor>
+                                        <descriptor>src/main/assembly/pdp.xml</descriptor>
                                         <descriptor>src/main/assembly/ranger-tools.xml</descriptor>
                                         <descriptor>src/main/assembly/ranger-src.xml</descriptor>
                                         <descriptor>src/main/assembly/plugin-atlas.xml</descriptor>
diff --git a/distro/src/main/assembly/pdp.xml b/distro/src/main/assembly/pdp.xml
new file mode 100644
index 000000000..f995fa37e
--- /dev/null
+++ b/distro/src/main/assembly/pdp.xml
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<assembly>
+    <id>pdp</id>
+    <formats>
+        <format>tar.gz</format>
+    </formats>
+    <baseDirectory>${project.parent.name}-${project.version}-pdp</baseDirectory>
+    <includeBaseDirectory>true</includeBaseDirectory>
+
+    <moduleSets>
+        <!-- ranger-pdp main JAR → dist/; selected runtime dependencies → lib/ -->
+        <moduleSet>
+            <useAllReactorProjects>true</useAllReactorProjects>
+            <includes>
+                <include>org.apache.ranger:ranger-pdp</include>
+                <include>org.apache.ranger:credentialbuilder</include>
+                <include>org.apache.ranger:authz-embedded</include>
+                <include>org.apache.ranger:ranger-audit-core</include>
+                <include>org.apache.ranger:ranger-audit-dest-hdfs</include>
+                <include>org.apache.ranger:ranger-audit-dest-solr</include>
+                <include>org.apache.ranger:ranger-authn</include>
+                <include>org.apache.ranger:ranger-authz-api</include>
+                <include>org.apache.ranger:ranger-common-utils</include>
+                <include>org.apache.ranger:ranger-plugin-classloader</include>
+                <include>org.apache.ranger:ranger-plugins-common</include>
+                <include>org.apache.ranger:ranger-plugins-cred</include>
+                <include>org.apache.ranger:ugsync-util</include>
+            </includes>
+            <binaries>
+                <outputDirectory>lib</outputDirectory>
+                <includeDependencies>true</includeDependencies>
+                <unpack>false</unpack>
+                <directoryMode>755</directoryMode>
+                <fileMode>644</fileMode>
+                <includes>
+                    <!-- Embedded Tomcat (HTTP/1.1 + HTTP/2) -->
+                    <include>org.apache.tomcat.embed:tomcat-embed-core:jar:${tomcat.embed.version}</include>
+                    <include>org.apache.tomcat.embed:tomcat-embed-jasper:jar:${tomcat.embed.version}</include>
+                    <include>org.apache.tomcat.embed:tomcat-embed-el:jar:${tomcat.embed.version}</include>
+                    <include>org.apache.tomcat:tomcat-annotations-api:jar:${tomcat.embed.version}</include>
+
+                    <!-- Jersey 2.x (JAX-RS server + Jackson media type) -->
+                    <include>org.glassfish.jersey.core:jersey-server</include>
+                    <include>org.glassfish.jersey.core:jersey-common</include>
+                    <include>org.glassfish.jersey.core:jersey-client</include>
+                    <include>org.glassfish.jersey.containers:jersey-container-servlet-core</include>
+                    <include>org.glassfish.jersey.ext:jersey-entity-filtering</include>
+                    <include>org.glassfish.jersey.inject:jersey-hk2</include>
+                    <include>org.glassfish.jersey.media:jersey-media-json-jackson</include>
+                    <include>jakarta.ws.rs:jakarta.ws.rs-api</include>
+                    <include>javax.inject:javax.inject</include>
+                    <include>javax.validation:validation-api</include>
+
+                    <!--
+                      Jersey-1 client runtime for ranger-plugins-common.
+                      Use client/core only; avoid jersey-bundle to prevent
+                      ASM package clashes with Jersey-2 server scanner.
+                    -->
+                    <include>com.sun.jersey:jersey-client:jar:${jersey-bundle.version}</include>
+                    <include>com.sun.jersey:jersey-core:jar:${jersey-bundle.version}</include>
+
+                    <!-- HK2 (Jersey DI framework) -->
+                    <include>org.glassfish.hk2:hk2-api</include>
+                    <include>org.glassfish.hk2:hk2-locator</include>
+                    <include>org.glassfish.hk2:hk2-utils</include>
+                    <include>org.glassfish.hk2.external:aopalliance-repackaged</include>
+                    <include>org.glassfish.hk2.external:javax.inject</include>
+                    <include>org.glassfish:osgi-resource-locator</include>
+
+                    <!-- Jackson (JSON serialization for authz-api model classes) -->
+                    <include>com.fasterxml.jackson.core:jackson-databind:jar:${fasterxml.jackson.databind.version}</include>
+                    <include>com.fasterxml.jackson.core:jackson-core:jar:${fasterxml.jackson.version}</include>
+                    <include>com.fasterxml.jackson.core:jackson-annotations:jar:${fasterxml.jackson.version}</include>
+                    <include>com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:${fasterxml.jackson.version}</include>
+                    <include>com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:${fasterxml.jackson.version}</include>
+                    <include>com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:${fasterxml.jackson.version}</include>
+
+                    <!-- JWT bearer token authentication -->
+                    <include>com.nimbusds:nimbus-jose-jwt:jar:${nimbus-jose-jwt.version}</include>
+                    <include>net.minidev:json-smart</include>
+                    <include>com.nimbusds:content-type</include>
+
+                    <!-- Commons -->
+                    <include>org.apache.commons:commons-lang3:jar:${commons.lang3.version}</include>
+                    <include>commons-codec:commons-codec:jar:${commons.codec.version}</include>
+                    <include>org.apache.commons:commons-collections4:jar:${commons.collections4.version}</include>
+                    <include>commons-collections:commons-collections:jar:${commons.collections.version}</include>
+                    <include>org.apache.commons:commons-configuration2:jar:${commons.configuration.version}</include>
+                    <include>org.apache.commons:commons-compress:jar:${commons.compress.version}</include>
+                    <include>commons-io:commons-io:jar:${commons.io.version}</include>
+                    <include>commons-logging:commons-logging:jar:${commons.logging.version}</include>
+                    <include>org.apache.commons:commons-text:jar:${commons.text.version}</include>
+                    <include>org.eclipse.jetty:jetty-client:jar:${jetty-client.version}</include>
+
+                    <!-- Hadoop client (audit, Kerberos utilities) -->
+                    <include>org.apache.hadoop:hadoop-auth:jar:${hadoop.version}</include>
+                    <include>org.apache.hadoop:hadoop-client-api:jar:${hadoop.version}</include>
+                    <include>org.apache.hadoop:hadoop-client-runtime:jar:${hadoop.version}</include>
+                    <include>org.apache.hadoop.thirdparty:hadoop-shaded-guava</include>
+
+                    <!-- XML / StAX -->
+                    <include>org.codehaus.woodstox:stax2-api:jar:${codehaus.woodstox.stax2api.version}</include>
+                    <include>com.fasterxml.woodstox:woodstox-core:jar:${fasterxml.woodstox.version}</include>
+
+                    <!-- Audit sinks -->
+                    <include>org.apache.solr:solr-solrj</include>
+                    <include>org.apache.httpcomponents:httpmime:jar:${httpcomponents.httpmime.version}</include>
+                    <include>org.apache.httpcomponents:httpclient:jar:${httpcomponents.httpclient.version}</include>
+                    <include>org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version}</include>
+                    <include>org.apache.zookeeper:zookeeper:jar:${zookeeper.version}</include>
+                    <include>org.noggit:noggit</include>
+
+                    <!-- Native libs (hostname lookup) -->
+                    <include>com.kstruct:gethostname4j</include>
+                    <include>net.java.dev.jna:jna</include>
+                    <include>net.java.dev.jna:jna-platform</include>
+
+                    <!-- Logging -->
+                    <include>org.slf4j:slf4j-api:jar:${slf4j.version}</include>
+                    <include>ch.qos.logback:logback-classic:jar:${logback.version}</include>
+                    <include>ch.qos.logback:logback-core:jar:${logback.version}</include>
+                </includes>
+                <excludes>
+                    <!-- avoid jersey-bundle because it conflicts with Jersey-2 -->
+                    <exclude>com.sun.jersey:jersey-bundle</exclude>
+                </excludes>
+            </binaries>
+        </moduleSet>
+
+    </moduleSets>
+
+    <fileSets>
+        <!-- Default config template (copy to conf/ and customise) -->
+        <fileSet>
+            <directoryMode>755</directoryMode>
+            <fileMode>600</fileMode>
+            <outputDirectory>conf.dist</outputDirectory>
+            <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+        </fileSet>
+
+        <!-- Empty runtime config dir (populated by installer / operator) -->
+        <fileSet>
+            <directoryMode>750</directoryMode>
+            <fileMode>640</fileMode>
+            <outputDirectory>conf</outputDirectory>
+            <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+        </fileSet>
+
+        <!-- Empty logs dir -->
+        <fileSet>
+            <directoryMode>755</directoryMode>
+            <outputDirectory>logs</outputDirectory>
+            <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+            <excludes>
+                <exclude>**/*</exclude>
+            </excludes>
+        </fileSet>
+
+        <!-- Version file (generated by distro build) -->
+        <fileSet>
+            <directoryMode>755</directoryMode>
+            <outputDirectory></outputDirectory>
+            <directory>${project.build.directory}</directory>
+            <includes>
+                <include>version</include>
+            </includes>
+            <fileMode>444</fileMode>
+        </fileSet>
+    </fileSets>
+
+    <files>
+        <!-- Service control script (placed at root; symlinked to /usr/bin during install) -->
+        <file>
+            <source>${project.parent.basedir}/pdp/scripts/ranger-pdp-services.sh</source>
+            <outputDirectory></outputDirectory>
+            <destName>ranger-pdp-services.sh</destName>
+            <fileMode>755</fileMode>
+        </file>
+
+        <!-- Init-style wrapper (delegates to ranger-pdp-services.sh via su) -->
+        <file>
+            <source>${project.parent.basedir}/pdp/scripts/ranger-pdp.sh</source>
+            <outputDirectory></outputDirectory>
+            <destName>ranger-pdp</destName>
+            <fileMode>755</fileMode>
+        </file>
+    </files>
+</assembly>
diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md
index 90e8ff6b8..a8c0c2671 100644
--- a/intg/src/main/python/README.md
+++ b/intg/src/main/python/README.md
@@ -39,6 +39,173 @@ Package      Version
 apache-ranger 0.0.12
 ```
 
+## Python API clients
+
+### Ranger Admin (`RangerClient`)
+
+Base URL: `http(s)://<ranger-admin-host>:<port>`
+
+`RangerClient` wraps APIs under `service/public/v2/api`, including:
+
+- service-def CRUD
+- service CRUD
+- policy CRUD/apply/search
+- roles
+- security-zones
+- plugin-info and policy-delta maintenance
+
+Authentication options:
+
+- Basic auth tuple, for example `("admin", "password")`
+- Kerberos/SPNEGO (`requests-kerberos`)
+- custom headers/query params via constructor
+
+Example:
+
+```python
+from apache_ranger.client.ranger_client import RangerClient
+
+ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!"))
+services = ranger.find_services()
+print(len(services.list))
+```
+
+### Ranger User Management (`RangerUserMgmtClient`)
+
+`RangerUserMgmtClient` builds on `RangerClient` and covers user/group/group-user operations, including:
+
+- create/get/update/delete user
+- create/get/update/delete group
+- add/remove/list group-user mappings
+- list groups for user, list users in group
+
+Example:
+
+```python
+from apache_ranger.client.ranger_client import RangerClient
+from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient
+
+ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!"))
+user_mgmt = RangerUserMgmtClient(ranger)
+
+users = user_mgmt.find_users()
+print(len(users.list))
+```
+
+### Ranger KMS (`RangerKMSClient`)
+
+Base URL: `http(s)://<kms-host>:<port>`
+
+`RangerKMSClient` wraps KMS APIs such as:
+
+- create/delete key
+- rollover/get metadata/current version
+- generate/decrypt/reencrypt encrypted keys
+- status and key-name listing
+
+Authentication options:
+
+- Hadoop simple auth (`HadoopSimpleAuth("user")`)
+- Kerberos/SPNEGO (`requests-kerberos`)
+- Basic auth tuple (when enabled)
+
+Example:
+
+```python
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client import HadoopSimpleAuth
+
+kms = RangerKMSClient("http://localhost:9292", HadoopSimpleAuth("keyadmin"))
+print(kms.kms_status())
+```
+
+### Ranger PDP (`RangerPDPClient`)
+
+`RangerPDPClient` is a thin Python wrapper for the PDP REST APIs exposed at `http(s)://<pdp-host>:<pdp-port>/authz/v1`.
+
+Endpoints:
+
+- `POST /authz/v1/authorize` -> single access evaluation
+- `POST /authz/v1/authorizeMulti` -> batch access evaluation
+- `POST /authz/v1/permissions` -> effective permissions for a resource
+
+Request context requirements:
+
+- `context.serviceType` (for example: `hive`, `hdfs`, `kafka`)
+- `context.serviceName` (Ranger service name, for example: `dev_hive`)
+- for `authorize` and `authorizeMulti`, `user.name` must be present
+
+Authentication options:
+
+- **Kerberos/SPNEGO**
+  - install dependency: `pip install requests-kerberos`
+  - use `HTTPKerberosAuth()` as `auth` in `RangerPDPClient`
+- **Trusted header**
+  - pass caller header (default `X-Forwarded-User`, configurable by `ranger.pdp.authn.header.username`)
+  - recommended only behind a trusted proxy
+- **JWT bearer**
+  - pass `Authorization: Bearer <token>` in request headers
+
+Delegation behavior:
+
+- if caller differs from `request.user.name`, delegation permission is required
+- delegation users are configured with `ranger.pdp.service.<service-name>.delegation.users`
+  (or wildcard `ranger.pdp.service.*.delegation.users`)
+- without delegation permission, PDP returns `403 FORBIDDEN`
+
+`RangerPDPClient` example (Kerberos):
+
+```python
+from requests_kerberos import HTTPKerberosAuth
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz import (
+    RangerAccessContext, RangerAccessInfo, RangerAuthzRequest,
+    RangerResourceInfo, RangerUserInfo
+)
+
+pdp = RangerPDPClient("http://localhost:6500", HTTPKerberosAuth())
+
+req = RangerAuthzRequest({
+    'requestId': 'req-1',
+    'user': RangerUserInfo({'name': 'alice'}),
+    'access': RangerAccessInfo({
+        'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}),
+        'action': 'QUERY',
+        'permissions': ['select']
+    }),
+    'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+print(res.decision)
+```
+
+Raw REST example using `requests` (JWT bearer):
+
+```python
+import requests
+
+url = "http://localhost:6500/authz/v1/authorize"
+headers = {
+    "Authorization": "Bearer <jwt-token>",
+    "Content-Type": "application/json"
+}
+payload = {
+    "requestId": "req-1",
+    "user": {"name": "alice"},
+    "access": {
+        "resource": {"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]},
+        "action": "QUERY",
+        "permissions": ["select"]
+    },
+    "context": {"serviceType": "hive", "serviceName": "dev_hive"}
+}
+
+resp = requests.post(url, headers=headers, json=payload, timeout=30)
+resp.raise_for_status()
+print(resp.json())
+```
+
 ## Usage
 
 ```python test_ranger.py```
@@ -122,113 +289,6 @@ print('    deleted service: id=' + str(created_service.id))
 
 ```
 
-```python test_ranger_kms.py```
-```python
-# test_ranger_kms.py
-from apache_ranger.client.ranger_kms_client import RangerKMSClient
-from apache_ranger.client.ranger_client     import HadoopSimpleAuth
-from apache_ranger.model.ranger_kms         import RangerKey
-import time
-
-
-##
-## Step 1: create a client to connect to Apache Ranger KMS
-##
-kms_url  = 'http://localhost:9292'
-kms_auth = HadoopSimpleAuth('keyadmin')
-
-# For Kerberos authentication
-#
-# from requests_kerberos import HTTPKerberosAuth
-#
-# kms_auth = HTTPKerberosAuth()
-#
-# For HTTP Basic authentication
-#
-# kms_auth = ('keyadmin', 'rangerR0cks!')
-
-kms_client = RangerKMSClient(kms_url, kms_auth)
-
-
-
-##
-## Step 2: Let's call KMS APIs
-##
-
-kms_status = kms_client.kms_status()
-print('kms_status():', kms_status)
-print()
-
-key_name = 'test_' + str(int(time.time() * 1000))
-
-key = kms_client.create_key(RangerKey({'name':key_name}))
-print('create_key(' + key_name + '):', key)
-print()
-
-rollover_key = kms_client.rollover_key(key_name, key.material)
-print('rollover_key(' + key_name + '):', rollover_key)
-print()
-
-kms_client.invalidate_cache_for_key(key_name)
-print('invalidate_cache_for_key(' + key_name + ')')
-print()
-
-key_metadata = kms_client.get_key_metadata(key_name)
-print('get_key_metadata(' + key_name + '):', key_metadata)
-print()
-
-current_key = kms_client.get_current_key(key_name)
-print('get_current_key(' + key_name + '):', current_key)
-print()
-
-encrypted_keys = kms_client.generate_encrypted_key(key_name, 6)
-print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):')
-for i in range(len(encrypted_keys)):
-  encrypted_key   = encrypted_keys[i]
-  decrypted_key   = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
-  reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
-  print('  encrypted_keys[' + str(i) + ']: ', encrypted_key)
-  print('  decrypted_key[' + str(i) + ']:  ', decrypted_key)
-  print('  reencrypted_key[' + str(i) + ']:', reencrypted_key)
-print()
-
-reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys)
-print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):')
-for i in range(len(reencrypted_keys)):
-  print('  batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i])
-print()
-
-key_versions = kms_client.get_key_versions(key_name)
-print('get_key_versions(' + key_name + '):', len(key_versions))
-for i in range(len(key_versions)):
-  print('  key_versions[' + str(i) + ']:', key_versions[i])
-print()
-
-for i in range(len(key_versions)):
-  key_version = kms_client.get_key_version(key_versions[i].versionName)
-  print('get_key_version(' + str(i) + '):', key_version)
-print()
-
-key_names = kms_client.get_key_names()
-print('get_key_names():', len(key_names))
-for i in range(len(key_names)):
-  print('  key_name[' + str(i) + ']:', key_names[i])
-print()
-
-keys_metadata = kms_client.get_keys_metadata(key_names)
-print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata))
-for i in range(len(keys_metadata)):
-  print('  key_metadata[' + str(i) + ']:', keys_metadata[i])
-print()
-
-key = kms_client.get_key(key_name)
-print('get_key(' + key_name + '):', key)
-print()
-
-kms_client.delete_key(key_name)
-print('delete_key(' + key_name + ')')
-```
-
 ```python test_ranger_user_mgmt.py```
 ```python
 # test_ranger_user_mgmt.py
@@ -365,4 +425,198 @@ print(f'\nDeleting user {user.name}')
 user_mgmt.delete_user_by_id(created_user.id, True)
 ```
 
-For more examples, checkout `sample-client` python  project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module.
+```python test_ranger_kms.py```
+```python
+# test_ranger_kms.py
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client     import HadoopSimpleAuth
+from apache_ranger.model.ranger_kms         import RangerKey
+import time
+
+
+##
+## Step 1: create a client to connect to Apache Ranger KMS
+##
+kms_url  = 'http://localhost:9292'
+kms_auth = HadoopSimpleAuth('keyadmin')
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# kms_auth = HTTPKerberosAuth()
+#
+# For HTTP Basic authentication
+#
+# kms_auth = ('keyadmin', 'rangerR0cks!')
+
+kms_client = RangerKMSClient(kms_url, kms_auth)
+
+
+
+##
+## Step 2: Let's call KMS APIs
+##
+
+kms_status = kms_client.kms_status()
+print('kms_status():', kms_status)
+print()
+
+key_name = 'test_' + str(int(time.time() * 1000))
+
+key = kms_client.create_key(RangerKey({'name':key_name}))
+print('create_key(' + key_name + '):', key)
+print()
+
+rollover_key = kms_client.rollover_key(key_name, key.material)
+print('rollover_key(' + key_name + '):', rollover_key)
+print()
+
+kms_client.invalidate_cache_for_key(key_name)
+print('invalidate_cache_for_key(' + key_name + ')')
+print()
+
+key_metadata = kms_client.get_key_metadata(key_name)
+print('get_key_metadata(' + key_name + '):', key_metadata)
+print()
+
+current_key = kms_client.get_current_key(key_name)
+print('get_current_key(' + key_name + '):', current_key)
+print()
+
+encrypted_keys = kms_client.generate_encrypted_key(key_name, 6)
+print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):')
+for i in range(len(encrypted_keys)):
+  encrypted_key   = encrypted_keys[i]
+  decrypted_key   = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+  reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+  print('  encrypted_keys[' + str(i) + ']: ', encrypted_key)
+  print('  decrypted_key[' + str(i) + ']:  ', decrypted_key)
+  print('  reencrypted_key[' + str(i) + ']:', reencrypted_key)
+print()
+
+reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys)
+print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):')
+for i in range(len(reencrypted_keys)):
+  print('  batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i])
+print()
+
+key_versions = kms_client.get_key_versions(key_name)
+print('get_key_versions(' + key_name + '):', len(key_versions))
+for i in range(len(key_versions)):
+  print('  key_versions[' + str(i) + ']:', key_versions[i])
+print()
+
+for i in range(len(key_versions)):
+  key_version = kms_client.get_key_version(key_versions[i].versionName)
+  print('get_key_version(' + str(i) + '):', key_version)
+print()
+
+key_names = kms_client.get_key_names()
+print('get_key_names():', len(key_names))
+for i in range(len(key_names)):
+  print('  key_name[' + str(i) + ']:', key_names[i])
+print()
+
+keys_metadata = kms_client.get_keys_metadata(key_names)
+print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata))
+for i in range(len(keys_metadata)):
+  print('  key_metadata[' + str(i) + ']:', keys_metadata[i])
+print()
+
+key = kms_client.get_key(key_name)
+print('get_key(' + key_name + '):', key)
+print()
+
+kms_client.delete_key(key_name)
+print('delete_key(' + key_name + ')')
+```
+
+```python test_ranger_pdp.py```
+```python
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz       import RangerAccessContext, RangerAccessInfo
+from apache_ranger.model.ranger_authz       import RangerAuthzRequest, RangerMultiAuthzRequest
+from apache_ranger.model.ranger_authz       import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo
+
+##
+## Step 1: create a client to connect to Ranger PDP
+##
+pdp_url  = 'http://localhost:6500'
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth())
+
+# For trusted-header authN with PDP (example only):
+#
+pdp = RangerPDPClient(pdp_url, auth=None, headers={ 'X-Forwarded-User': 'hive' })
+
+##
+## Step 2: call PDP authorization APIs
+##
+req = RangerAuthzRequest({
+    'requestId': 'req-1',
+    'user':      RangerUserInfo({'name': 'alice'}),
+    'access':    RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), 'permissions': ['create']}),
+    'context':   RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+
+print('authorize():')
+print(f'    {req}')
+print(f'    {res}')
+print('\n')
+
+
+req = RangerAuthzRequest({
+    'requestId': 'req-2',
+    'user':      RangerUserInfo({'name': 'alice'}),
+    'access':    RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}), 'permissions': ['select']}),
+    'context':   RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+
+print('authorize():')
+print(f'    {req}')
+print(f'    {res}')
+print('\n')
+
+
+req = RangerMultiAuthzRequest({
+    'requestId': 'req-3',
+    'user':      RangerUserInfo({'name': 'alice'}),
+    'accesses': [
+        RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email'], 'attributes': {'OWNER': 'alice'}}), 'permissions': ['select']}),
+        RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_vw1'}), 'permissions': ['create']})
+    ],
+    'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize_multi(req)
+
+print('authorize_multi():')
+print(f'    {req}')
+print(f'    {res}')
+print('\n')
+
+
+req = RangerResourcePermissionsRequest({
+    'requestId': 'req-4',
+    'resource':  RangerResourceInfo({'name': 'table:default/test_tbl1'}),
+    'context':   RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.get_resource_permissions(req)
+
+print('get_resource_permissions():')
+print(f'    {req}')
+print(f'    {res}')
+print('\n')
+```
+
+For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module (including `sample_client.py`, `user_mgmt.py`, `sample_kms_client.py`, and `sample_pdp_client.py`).
diff --git a/intg/src/main/python/apache_ranger/client/__init__.py b/intg/src/main/python/apache_ranger/client/__init__.py
index ed9d0b3c4..e4f911e7e 100644
--- a/intg/src/main/python/apache_ranger/client/__init__.py
+++ b/intg/src/main/python/apache_ranger/client/__init__.py
@@ -15,3 +15,34 @@
 # 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.
+
+from apache_ranger.client.ranger_client import RangerClient, RangerClientPrivate, HadoopSimpleAuth
+from apache_ranger.client.ranger_gds_client import RangerGdsClient
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz import RangerPermissionResult, RangerResourceInfo
+from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest, RangerUserInfo
+
+__all__ = [
+    "RangerClient",
+    "RangerClientPrivate",
+    "RangerGdsClient",
+    "RangerKMSClient",
+    "RangerPDPClient",
+    "RangerUserMgmtClient",
+    "RangerAccessContext",
+    "RangerAccessInfo",
+    "RangerAuthzRequest",
+    "RangerAuthzResult",
+    "RangerMultiAuthzRequest",
+    "RangerMultiAuthzResult",
+    "RangerPermissionResult",
+    "RangerResourceInfo",
+    "RangerResourcePermissions",
+    "RangerResourcePermissionsRequest",
+    "RangerUserInfo",
+    "HadoopSimpleAuth",
+]
diff --git a/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py
new file mode 100644
index 000000000..9268a2f4c
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+import logging
+from apache_ranger.client.ranger_client import RangerClientHttp
+from apache_ranger.model.ranger_authz   import RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz   import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz   import RangerResourcePermissions, RangerResourcePermissionsRequest
+from apache_ranger.utils                import API, HttpMethod, HTTPStatus
+from apache_ranger.utils                import type_coerce
+
+LOG = logging.getLogger(__name__)
+
+
+class RangerPDPClient:
+    """
+    Python client for Ranger PDP authorization APIs.
+
+    Base URL should point to PDP server endpoint, for example:
+      http://localhost:6500
+    """
+
+    def __init__(self, url, auth, query_params=None, headers=None):
+        self.client_http = RangerClientHttp(url, auth, query_params, headers)
+        self.session     = self.client_http.session
+        logging.getLogger("requests").setLevel(logging.WARNING)
+
+    def authorize(self, authz_request):
+        """
+        Call POST /authz/v1/authorize
+        :param authz_request: dict-like or RangerAuthzRequest
+        :return: RangerAuthzResult
+        """
+        req  = type_coerce(authz_request, RangerAuthzRequest)
+        resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE, request_data=req)
+        return type_coerce(resp, RangerAuthzResult)
+
+    def authorize_multi(self, multi_authz_request):
+        """
+        Call POST /authz/v1/authorizeMulti
+        :param multi_authz_request: dict-like or RangerMultiAuthzRequest
+        :return: RangerMultiAuthzResult
+        """
+        req  = type_coerce(multi_authz_request, RangerMultiAuthzRequest)
+        resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE_MULTI, request_data=req)
+        return type_coerce(resp, RangerMultiAuthzResult)
+
+    def get_resource_permissions(self, resource_perm_request):
+        """
+        Call POST /authz/v1/permissions
+        :param resource_perm_request: dict-like OR RangerResourcePermissionsRequest
+        :return: RangerResourcePermissions
+        """
+        req  = type_coerce(resource_perm_request, RangerResourcePermissionsRequest)
+        resp = self.client_http.call_api(RangerPDPClient.GET_RESOURCE_PERMISSIONS, request_data=req)
+
+        return type_coerce(resp, RangerResourcePermissions)
+
+    # URIs
+    URI_BASE                     = "authz/v1"
+    URI_AUTHORIZE                = URI_BASE + "/authorize"
+    URI_AUTHORIZE_MULTI          = URI_BASE + "/authorizeMulti"
+    URI_RESOURCE_PERMISSIONS     = URI_BASE + "/permissions"
+
+    # APIs
+    AUTHORIZE                = API(URI_AUTHORIZE, HttpMethod.POST, HTTPStatus.OK)
+    AUTHORIZE_MULTI          = API(URI_AUTHORIZE_MULTI, HttpMethod.POST, HTTPStatus.OK)
+    GET_RESOURCE_PERMISSIONS = API(URI_RESOURCE_PERMISSIONS, HttpMethod.POST, HTTPStatus.OK)
+
diff --git a/intg/src/main/python/apache_ranger/exceptions.py b/intg/src/main/python/apache_ranger/exceptions.py
index 9f19a59ac..347468a83 100644
--- a/intg/src/main/python/apache_ranger/exceptions.py
+++ b/intg/src/main/python/apache_ranger/exceptions.py
@@ -33,17 +33,21 @@ def __init__(self, api, response):
         self.msgDesc         = None
         self.messageList     = None
 
-        print(response)
-
         if api is not None and response is not None:
             if response.content:
-              try:
-                respJson         = response.json()
-                self.msgDesc     = respJson['msgDesc']     if respJson is not None and 'msgDesc'     in respJson else None
-                self.messageList = respJson['messageList'] if respJson is not None and 'messageList' in respJson else None
-              except Exception:
-                self.msgDesc     = response.content
-                self.messageList = [ response.content ]
+                try:
+                    respJson = response.json()
+
+                    if respJson:
+                        self.msgDesc     = respJson['msgDesc']     if 'msgDesc'     in respJson else None
+                        self.messageList = respJson['messageList'] if 'messageList' in respJson else None
+
+                        if self.msgDesc is None:
+                            self.msgDesc = respJson['message'] if 'message' in respJson else None
+
+                except Exception:
+                    self.msgDesc     = response.content
+                    self.messageList = [ response.content ]
 
             self.statusCode  = response.status_code
 
diff --git a/intg/src/main/python/apache_ranger/model/__init__.py b/intg/src/main/python/apache_ranger/model/__init__.py
index ed9d0b3c4..ca8a51a7e 100644
--- a/intg/src/main/python/apache_ranger/model/__init__.py
+++ b/intg/src/main/python/apache_ranger/model/__init__.py
@@ -15,3 +15,22 @@
 # 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.
+
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz import RangerPermissionResult, RangerResourceInfo
+from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest, RangerUserInfo
+
+__all__ = [
+    "RangerAccessContext",
+    "RangerAccessInfo",
+    "RangerAuthzRequest",
+    "RangerAuthzResult",
+    "RangerMultiAuthzRequest",
+    "RangerMultiAuthzResult",
+    "RangerPermissionResult",
+    "RangerResourceInfo",
+    "RangerResourcePermissions",
+    "RangerResourcePermissionsRequest",
+    "RangerUserInfo",
+]
diff --git a/intg/src/main/python/apache_ranger/model/ranger_authz.py b/intg/src/main/python/apache_ranger/model/ranger_authz.py
new file mode 100644
index 000000000..ca4b90613
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/model/ranger_authz.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.model.ranger_base import RangerBase
+from apache_ranger.utils             import non_null, type_coerce, type_coerce_dict, type_coerce_list
+
+
+class RangerUserInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.name       = attrs.get("name")
+        self.attributes = attrs.get("attributes")
+        self.groups     = attrs.get("groups")
+        self.roles      = attrs.get("roles")
+
+
+class RangerResourceInfo(RangerBase):
+    SCOPE_SELF                   = "SELF"
+    SCOPE_SELF_OR_ANY_CHILD      = "SELF_OR_ANY_CHILD"
+    SCOPE_SELF_OR_ANY_DESCENDANT = "SELF_OR_ANY_DESCENDANT"
+
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.name           = attrs.get("name")
+        self.subResources   = attrs.get("subResources")
+        self.nameMatchScope = attrs.get("nameMatchScope")
+        self.attributes     = attrs.get("attributes")
+
+
+class RangerAccessInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.resource    = attrs.get("resource")
+        self.action      = attrs.get("action")
+        self.permissions = attrs.get("permissions")
+
+    def type_coerce_attrs(self):
+        super(RangerAccessInfo, self).type_coerce_attrs()
+        self.resource = type_coerce(self.resource, RangerResourceInfo)
+
+
+class RangerAccessContext(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.serviceType          = attrs.get("serviceType")
+        self.serviceName          = attrs.get("serviceName")
+        self.accessTime           = attrs.get("accessTime")
+        self.clientIpAddress      = attrs.get("clientIpAddress")
+        self.forwardedIpAddresses = attrs.get("forwardedIpAddresses")
+        self.additionalInfo       = attrs.get("additionalInfo")
+
+
+class RangerAuthzRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.requestId = attrs.get("requestId")
+        self.user      = attrs.get("user")
+        self.access    = attrs.get("access")
+        self.context   = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerAuthzRequest, self).type_coerce_attrs()
+        self.user    = type_coerce(self.user, RangerUserInfo)
+        self.access  = type_coerce(self.access, RangerAccessInfo)
+        self.context = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerMultiAuthzRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.requestId = attrs.get("requestId")
+        self.user      = attrs.get("user")
+        self.accesses  = attrs.get("accesses")
+        self.context   = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerMultiAuthzRequest, self).type_coerce_attrs()
+        self.user     = type_coerce(self.user, RangerUserInfo)
+        self.accesses = type_coerce_list(self.accesses, RangerAccessInfo)
+        self.context  = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerPolicyInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.id      = attrs.get("id")
+        self.version = attrs.get("version")
+
+
+class RangerAccessResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.decision = attrs.get("decision")
+        self.policy   = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerAccessResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerDataMaskResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.maskType    = attrs.get("maskType")
+        self.maskedValue = attrs.get("maskedValue")
+        self.policy      = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerDataMaskResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerRowFilterResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.filterExpr = attrs.get("filterExpr")
+        self.policy     = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerRowFilterResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerResultInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.access         = attrs.get("access")
+        self.dataMask       = attrs.get("dataMask")
+        self.rowFilter      = attrs.get("rowFilter")
+        self.additionalInfo = attrs.get("additionalInfo")
+
+    def type_coerce_attrs(self):
+        super(RangerResultInfo, self).type_coerce_attrs()
+        self.access    = type_coerce(self.access, RangerAccessResult)
+        self.dataMask  = type_coerce(self.dataMask, RangerDataMaskResult)
+        self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult)
+
+
+class RangerPermissionResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.permission     = attrs.get("permission")
+        self.access         = attrs.get("access")
+        self.dataMask       = attrs.get("dataMask")
+        self.rowFilter      = attrs.get("rowFilter")
+        self.additionalInfo = attrs.get("additionalInfo")
+        self.subResources   = attrs.get("subResources")
+
+    def type_coerce_attrs(self):
+        super(RangerPermissionResult, self).type_coerce_attrs()
+        self.access       = type_coerce(self.access, RangerAccessResult)
+        self.dataMask     = type_coerce(self.dataMask, RangerDataMaskResult)
+        self.rowFilter    = type_coerce(self.rowFilter, RangerRowFilterResult)
+        self.subResources = type_coerce_dict(self.subResources, RangerResultInfo)
+
+
+class RangerAuthzResult(RangerBase):
+    DECISION_ALLOW          = "ALLOW"
+    DECISION_DENY           = "DENY"
+    DECISION_NOT_DETERMINED = "NOT_DETERMINED"
+    DECISION_PARTIAL        = "PARTIAL"
+
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.requestId   = attrs.get("requestId")
+        self.decision    = attrs.get("decision")
+        self.permissions = attrs.get("permissions")
+
+    def type_coerce_attrs(self):
+        super(RangerAuthzResult, self).type_coerce_attrs()
+        self.permissions = type_coerce_dict(self.permissions, RangerPermissionResult)
+
+
+class RangerMultiAuthzResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.requestId = attrs.get("requestId")
+        self.decision  = attrs.get("decision")
+        self.accesses  = attrs.get("accesses")
+
+    def type_coerce_attrs(self):
+        super(RangerMultiAuthzResult, self).type_coerce_attrs()
+        self.accesses = type_coerce_list(self.accesses, RangerAuthzResult)
+
+
+class RangerResourcePermissionsRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.requestId = attrs.get("requestId")
+        self.resource  = attrs.get("resource")
+        self.context   = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerResourcePermissionsRequest, self).type_coerce_attrs()
+        self.resource = type_coerce(self.resource, RangerResourceInfo)
+        self.context  = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerResourcePermissions(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.resource = attrs.get("resource")
+        self.users    = attrs.get("users")
+        self.groups   = attrs.get("groups")
+        self.roles    = attrs.get("roles")
+
+    def type_coerce_attrs(self):
+        super(RangerResourcePermissions, self).type_coerce_attrs()
+        self.resource = type_coerce(self.resource, RangerResourceInfo)
+        self.users    = _coerce_principal_permissions(self.users)
+        self.groups   = _coerce_principal_permissions(self.groups)
+        self.roles    = _coerce_principal_permissions(self.roles)
+
+
+def _coerce_principal_permissions(value):
+    if not isinstance(value, dict):
+        return None
+
+    ret = {}
+    for principal, permissions in value.items():
+        ret[principal] = type_coerce_dict(permissions, RangerPermissionResult)
+    return ret
+
diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md
new file mode 100644
index 000000000..b630bec7d
--- /dev/null
+++ b/pdp/conf.dist/README-k8s.md
@@ -0,0 +1,103 @@
+<!---
+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.
+-->
+# Ranger PDP Kubernetes Notes
+
+This document captures Kubernetes-oriented runtime recommendations for `ranger-pdp`.
+
+## Health Probes
+
+Use:
+
+- Liveness: `GET /health/live`
+- Readiness: `GET /health/ready`
+
+The readiness endpoint reports NOT_READY until:
+
+- server is started
+- authorizer is initialized
+- PDP is accepting requests
+
+## Metrics
+
+PDP exposes Prometheus-style text metrics at:
+
+- `GET /metrics`
+
+Current metrics include request counts, auth failures, average latency and loaded services count.
+
+## Runtime Tuning
+
+Thread/connection controls can be set in `ranger-pdp-site.xml` or via `JAVA_OPTS -D...`:
+
+- `ranger.pdp.http.connector.maxThreads`
+- `ranger.pdp.http.connector.minSpareThreads`
+- `ranger.pdp.http.connector.acceptCount`
+- `ranger.pdp.http.connector.maxConnections`
+
+## Logging
+
+For container-native logging, logback can emit to stdout.
+If file logging is needed, pass:
+
+`-Dlogdir=/path/to/log/dir`
+
+and use a file appender that resolves `${logdir}`.
+
+## Security Context
+
+Recommended:
+
+- run as non-root user
+- readOnlyRootFilesystem: true
+- mount writable volumes only for required paths (cache/log/temp if file logging is enabled)
+
+## Config/Secrets
+
+Recommended:
+
+- `ranger-pdp-site.xml` from ConfigMap
+- keytabs/JWT keys/credentials from Secrets
+
+## Authentication Config Keys
+
+When configuring inbound PDP authentication in `ranger-pdp-site.xml`, use the
+`ranger.pdp.authn.*` property names:
+
+- `ranger.pdp.authn.types`
+- `ranger.pdp.authn.header.enabled`
+- `ranger.pdp.authn.header.username`
+- `ranger.pdp.authn.jwt.enabled`
+- `ranger.pdp.authn.jwt.provider.url`
+- `ranger.pdp.authn.jwt.public.key`
+- `ranger.pdp.authn.jwt.cookie.name`
+- `ranger.pdp.authn.jwt.audiences`
+- `ranger.pdp.authn.kerberos.enabled`
+- `ranger.pdp.authn.kerberos.spnego.principal`
+- `ranger.pdp.authn.kerberos.spnego.keytab`
+- `ranger.pdp.authn.kerberos.token.valid.seconds`
+- `ranger.pdp.authn.kerberos.name.rules`
+
+## Network Policy
+
+Allow egress only to dependencies:
+
+- Ranger Admin
+- Solr (if audit destination enabled)
+- KDC (if Kerberos is used)
+
diff --git a/pdp/conf.dist/logback.xml b/pdp/conf.dist/logback.xml
new file mode 100644
index 000000000..a52ffde14
--- /dev/null
+++ b/pdp/conf.dist/logback.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<configuration>
+  <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+    <target>System.out</target>
+    <encoder>
+      <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <logger name="com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator" level="OFF"/>
+  <logger name="org.apache.directory.server.core" level="OFF"/>
+  <logger name="org.apache.hadoop.security" level="OFF"/>
+  <logger name="org.apache.hadoop.conf" level="ERROR"/>
+  <logger name="org.apache.hadoop.util.NativeCodeLoader" level="OFF"/>
+
+  <root level="INFO">
+    <appender-ref ref="stdout"/>
+  </root>
+</configuration>
diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml
new file mode 100644
index 000000000..ddf2ff934
--- /dev/null
+++ b/pdp/conf.dist/ranger-pdp-site.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  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.
+-->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+    <description>Port the PDP server listens on.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+    <description>Directory for PDP server log files.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value/>
+    <description>Comma-separated users, allowed to call on behalf of other users in all services</description>
+  </property>
+
+  <!-- SSL/TLS -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+    <description>Set to true to enable HTTPS.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.file</name>
+    <value/>
+    <description>Path to the keystore file (required when SSL is enabled).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.password</name>
+    <value/>
+    <description>Keystore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.type</name>
+    <value>JKS</value>
+    <description>Keystore type (JKS, PKCS12).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.enabled</name>
+    <value>false</value>
+    <description>Set to true to require client certificate authentication.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.file</name>
+    <value/>
+    <description>Path to the truststore file.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.password</name>
+    <value/>
+    <description>Truststore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.type</name>
+    <value>JKS</value>
+    <description>Truststore type (JKS, PKCS12).</description>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+    <description>
+      Enable HTTP/2 via upgrade on the connector.
+      Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1.
+    </description>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+    <description>Maximum number of worker threads handling simultaneous requests.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+    <description>Minimum number of spare worker threads kept ready.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+    <description>Queued connection backlog when all worker threads are busy.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+    <description>Maximum concurrent TCP connections accepted by the connector.</description>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.types</name>
+    <value>header,jwt,kerberos</value>
+    <description>
+      Comma-separated list of authentication methods for incoming REST requests,
+      tried in listed order. Supported values: header, jwt, kerberos.
+    </description>
+  </property>
+
+  <!-- HTTP Header authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.header.enabled</name>
+    <value>false</value>
+    <description>
+      Enable trusted HTTP header authentication. Use only behind a trusted proxy.
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.header.username</name>
+    <value>X-Forwarded-User</value>
+    <description>HTTP header name from which the authenticated username is read.</description>
+  </property>
+
+  <!-- JWT bearer token authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.jwt.enabled</name>
+    <value>false</value>
+    <description>Enable JWT authentication.</description>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.jwt.provider.url</name>
+    <value/>
+    <description>URL of the JWT provider (used to fetch the public key).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.public.key</name>
+    <value/>
+    <description>PEM-encoded public key for verifying JWT signatures.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.cookie.name</name>
+    <value>hadoop-jwt</value>
+    <description>Cookie name from which a JWT bearer token may be read.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.audiences</name>
+    <value/>
+    <description>Comma-separated list of accepted JWT audiences. Empty means any audience is accepted.</description>
+  </property>
+
+  <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.kerberos.enabled</name>
+    <value>false</value>
+    <description>Enable Kerberos authentication.</description>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+    <value/>
+    <description>Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+    <value/>
+    <description>Path to the keytab file for the SPNEGO service principal.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.token.valid.seconds</name>
+    <value>3600</value>
+    <description>Validity period (seconds) for Kerberos authentication tokens.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.name.rules</name>
+    <value>DEFAULT</value>
+    <description>Rules for mapping Kerberos principal names to short usernames.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.app.type</name>
+    <value>ranger-pdp</value>
+    <description>Application type reported in audit records.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.init.services</name>
+    <value/>
+    <description>
+      Comma-separated list of Ranger service names to eagerly initialize at startup.
+      Leave empty for lazy initialization on first access.
+      Example: dev_hive,dev_hdfs
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.source.impl</name>
+    <value>org.apache.ranger.admin.client.RangerAdminRESTClient</value>
+    <description>Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.url</name>
+    <value>http://localhost:6080</value>
+    <description>
+      URL of the Ranger Admin server. Comma-separated for HA.
+      Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.ssl.config.file</name>
+    <value/>
+    <description>
+      Path to the XML file containing SSL keystore/truststore settings for the
+      connection to Ranger Admin (required when the Admin URL uses https://).
+      The file uses the standard Ranger policymgr SSL property names:
+        xasecure.policymgr.clientssl.truststore
+        xasecure.policymgr.clientssl.truststore.credential.file
+        xasecure.policymgr.clientssl.keystore             (for mutual TLS)
+        xasecure.policymgr.clientssl.keystore.credential.file
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.connection.timeoutMs</name>
+    <value>120000</value>
+    <description>HTTP connection timeout (ms) for calls to Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.read.timeoutMs</name>
+    <value>30000</value>
+    <description>HTTP read timeout (ms) for calls to Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.pollIntervalMs</name>
+    <value>30000</value>
+    <description>How often (ms) to poll Ranger Admin for updated policies and roles.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.cache.dir</name>
+    <value>/var/ranger/cache/pdp</value>
+    <description>
+      Directory for local JSON cache files (policies, tags, roles, userstore, GDS).
+      The PDP can serve authorization requests from cache while Ranger Admin is unavailable.
+    </description>
+  </property>
+
+  <!-- Basic Auth authentication for connecting to Ranger Admin -->
+  <property>
+    <name>ranger.authz.default.policy.rest.client.username</name>
+    <value>admin</value>
+    <description>Username for connecting to Ranger Admin (basic auth).</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.password</name>
+    <value>admin</value>
+    <description>Password for connecting to Ranger Admin (basic auth).</description>
+  </property>
+
+  <!-- Kerberos authentication for connecting to Ranger Admin-->
+  <property>
+    <name>ranger.authz.default.ugi.initialize</name>
+    <value>false</value>
+    <description>Set to true to enable Kerberos login for the Ranger Admin connection.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.login.type</name>
+    <value/>
+    <description>Kerberos login type: keytab or jaas.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.keytab.principal</name>
+    <value/>
+    <description>Kerberos principal for keytab-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.keytab.file</name>
+    <value/>
+    <description>Path to the keytab file for keytab-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.jaas.appconfig</name>
+    <value/>
+    <description>JAAS application configuration name for jaas-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.use.rangerGroups</name>
+    <value>false</value>
+    <description>
+      When true, group membership is resolved from Ranger's internal userstore
+      (downloaded from Ranger Admin) rather than the local OS.
+    </description>
+  </property>
+
+  <!-- Audit -->
+  <property>
+    <name>ranger.authz.audit.is.enabled</name>
+    <value>true</value>
+    <description>Master switch for audit logging.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr</name>
+    <value>false</value>
+    <description>Enable Solr as an audit destination.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr.urls</name>
+    <value/>
+    <description>Solr URL for audit logging.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.hdfs</name>
+    <value>false</value>
+    <description>Enable HDFS as an audit destination.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.hdfs.dir</name>
+    <value/>
+    <description>HDFS directory path for audit log files.</description>
+  </property>
+</configuration>
diff --git a/pdp/pom.xml b/pdp/pom.xml
new file mode 100644
index 000000000..48da6dd21
--- /dev/null
+++ b/pdp/pom.xml
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ranger</groupId>
+        <artifactId>ranger</artifactId>
+        <version>3.0.0-SNAPSHOT</version>
+        <relativePath>..</relativePath>
+    </parent>
+
+    <artifactId>ranger-pdp</artifactId>
+    <description>Ranger Policy Decision Point (PDP) Server</description>
+
+    <properties>
+        <jersey2.version>2.35</jersey2.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${fasterxml.jackson.databind.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.module</groupId>
+            <artifactId>jackson-module-jaxb-annotations</artifactId>
+            <version>${fasterxml.jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <version>${nimbus-jose-jwt.version}</version>
+        </dependency>
+        <!-- Jersey-1 client pieces needed by ranger-plugins-common -->
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>${jersey-core.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-core</artifactId>
+            <version>${jersey-core.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>${commons.codec.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.ws.rs</groupId>
+            <artifactId>jakarta.ws.rs-api</artifactId>
+            <version>2.1.6</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>${javax-inject.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+            <version>${javax.validation}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>${commons.lang3.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>authz-embedded</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-authn</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-plugin-classloader</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ranger-plugins-common</artifactId>
+            <version>${project.version}</version>
+            <exclusions>
+                <!--
+                  Avoid bringing jersey-bundle (Jersey-1 aggregate JAR) because it
+                  conflicts with Jersey-2 server runtime used by PDP.
+                -->
+                <exclusion>
+                    <groupId>com.sun.jersey</groupId>
+                    <artifactId>jersey-bundle</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.ranger</groupId>
+            <artifactId>ugsync-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-core</artifactId>
+            <version>${tomcat.embed.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-jasper</artifactId>
+            <version>${tomcat.embed.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet-core</artifactId>
+            <version>${jersey2.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-server</artifactId>
+            <version>${jersey2.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+            <version>${jersey2.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <version>${jersey2.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>${javax.servlet.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <mainClass>org.apache.ranger.pdp.RangerPdpServer</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/pdp/scripts/ranger-pdp-services.sh b/pdp/scripts/ranger-pdp-services.sh
new file mode 100644
index 000000000..d3fba5398
--- /dev/null
+++ b/pdp/scripts/ranger-pdp-services.sh
@@ -0,0 +1,223 @@
+#!/bin/bash
+
+# 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.
+
+if [[ -z $1 ]]; then
+    echo "No argument provided."
+    echo "Usage: $0 {start | run | stop | restart | version}"
+    exit 1
+fi
+
+action=$1
+action=$(echo "$action" | tr '[:lower:]' '[:upper:]')
+
+realScriptPath=$(readlink -f "$0")
+realScriptDir=$(dirname "$realScriptPath")
+cd "$realScriptDir" || exit 1
+cdir=$(pwd)
+
+ranger_pdp_max_heap_size=${RANGER_PDP_MAX_HEAP_SIZE:-1g}
+
+for custom_env_script in $(find "${cdir}/conf/" -name "ranger-pdp-env*" 2>/dev/null); do
+    if [ -f "$custom_env_script" ]; then
+        . "$custom_env_script"
+    fi
+done
+
+if [ -z "${RANGER_PDP_PID_DIR_PATH}" ]; then
+    RANGER_PDP_PID_DIR_PATH=/var/run/ranger
+fi
+
+if [ -z "${RANGER_PDP_PID_NAME}" ]; then
+    RANGER_PDP_PID_NAME=pdp.pid
+fi
+
+pidf="${RANGER_PDP_PID_DIR_PATH}/${RANGER_PDP_PID_NAME}"
+
+if [ -z "${UNIX_PDP_USER}" ]; then
+    UNIX_PDP_USER=ranger
+fi
+
+JAVA_OPTS=" ${JAVA_OPTS} -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=200m -Xmx${ranger_pdp_max_heap_size} -Xms256m"
+
+if [ "${action}" == "START" ]; then
+
+    if [ -f "${cdir}/conf/java_home.sh" ]; then
+        . "${cdir}/conf/java_home.sh"
+    fi
+
+    for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do
+        if [ -f "$custom_env_script" ]; then
+            . "$custom_env_script"
+        fi
+    done
+
+    if [ "$JAVA_HOME" != "" ]; then
+        export PATH=$JAVA_HOME/bin:$PATH
+    fi
+
+    if [ -z "${RANGER_PDP_LOG_DIR}" ]; then
+        RANGER_PDP_LOG_DIR=/var/log/ranger/pdp
+    fi
+
+    if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then
+        mkdir -p "$RANGER_PDP_LOG_DIR"
+        chmod 755 "$RANGER_PDP_LOG_DIR"
+    fi
+
+    cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*"
+
+    if [ -f "$pidf" ]; then
+        pid=$(cat "$pidf")
+        if ps -p "$pid" > /dev/null 2>&1; then
+            echo "Ranger PDP Service is already running [pid=${pid}]"
+            exit 0
+        else
+            rm -f "$pidf"
+        fi
+    fi
+
+    if [ -z "${RANGER_PDP_CONF_DIR}" ]; then
+        RANGER_PDP_CONF_DIR=${cdir}/conf
+    fi
+
+    mkdir -p "${RANGER_PDP_PID_DIR_PATH}"
+
+    SLEEP_TIME_AFTER_START=5
+    nohup java -Dproc_rangerpdp ${JAVA_OPTS} \
+        -Dlogdir="${RANGER_PDP_LOG_DIR}" \
+        -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \
+        -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \
+        -Duser="${USER}" \
+        -Dhostname="${HOSTNAME}" \
+        -cp "${cp}" \
+        org.apache.ranger.pdp.RangerPdpServer \
+        > "${RANGER_PDP_LOG_DIR}/pdp.out" 2>&1 &
+
+    VALUE_OF_PID=$!
+    echo "Starting Ranger PDP Service"
+    sleep $SLEEP_TIME_AFTER_START
+
+    if ps -p $VALUE_OF_PID > /dev/null 2>&1; then
+        echo $VALUE_OF_PID > "${pidf}"
+        chown "${UNIX_PDP_USER}" "${pidf}" 2>/dev/null || true
+        chmod 660 "${pidf}"
+        pid=$(cat "$pidf")
+        echo "Ranger PDP Service with pid ${pid} has started."
+    else
+        echo "Ranger PDP Service failed to start!"
+        exit 1
+    fi
+    exit 0
+
+elif [ "${action}" == "RUN" ]; then
+    if [ -f "${cdir}/conf/java_home.sh" ]; then
+        . "${cdir}/conf/java_home.sh"
+    fi
+
+    for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do
+        if [ -f "$custom_env_script" ]; then
+            . "$custom_env_script"
+        fi
+    done
+
+    if [ "$JAVA_HOME" != "" ]; then
+        export PATH=$JAVA_HOME/bin:$PATH
+    fi
+
+    if [ -z "${RANGER_PDP_LOG_DIR}" ]; then
+        RANGER_PDP_LOG_DIR=/var/log/ranger/pdp
+    fi
+
+    if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then
+        mkdir -p "$RANGER_PDP_LOG_DIR"
+        chmod 755 "$RANGER_PDP_LOG_DIR"
+    fi
+
+    if [ -z "${RANGER_PDP_CONF_DIR}" ]; then
+        RANGER_PDP_CONF_DIR=${cdir}/conf
+    fi
+
+    cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*"
+    exec java -Dproc_rangerpdp ${JAVA_OPTS} \
+        -Dlogdir="${RANGER_PDP_LOG_DIR}" \
+        -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \
+        -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \
+        -Duser="${USER}" \
+        -Dhostname="${HOSTNAME}" \
+        -cp "${cp}" \
+        org.apache.ranger.pdp.RangerPdpServer
+
+elif [ "${action}" == "STOP" ]; then
+
+    WAIT_TIME_FOR_SHUTDOWN=2
+    NR_ITER_FOR_SHUTDOWN_CHECK=15
+
+    if [ -f "$pidf" ]; then
+        pid=$(cat "$pidf")
+    else
+        pid=$(ps -ef | grep java | grep -- '-Dproc_rangerpdp' | grep -v grep | awk '{ print $2 }')
+        if [ "$pid" != "" ]; then
+            echo "pid file (${pidf}) not found; taking pid from 'ps' output."
+        else
+            echo "Ranger PDP Service is not running."
+            exit 0
+        fi
+    fi
+
+    echo "Stopping Ranger PDP Service (pid=${pid})..."
+    kill -15 "$pid"
+
+    for ((i=0; i<NR_ITER_FOR_SHUTDOWN_CHECK; i++)); do
+        sleep $WAIT_TIME_FOR_SHUTDOWN
+        if ps -p "$pid" > /dev/null 2>&1; then
+            echo "Shutdown in progress. Checking again in ${WAIT_TIME_FOR_SHUTDOWN}s..."
+        else
+            break
+        fi
+    done
+
+    if ps -p "$pid" > /dev/null 2>&1; then
+        echo "Graceful stop failed; sending SIGKILL..."
+        kill -9 "$pid"
+    fi
+
+    sleep 1
+    if ps -p "$pid" > /dev/null 2>&1; then
+        echo "kill -9 failed. Process still running. Giving up."
+        exit 1
+    else
+        rm -f "$pidf"
+        echo "Ranger PDP Service with pid ${pid} has been stopped."
+    fi
+    exit 0
+
+elif [ "${action}" == "RESTART" ]; then
+    echo "Restarting Ranger PDP Service..."
+    "${cdir}/ranger-pdp-services.sh" stop
+    "${cdir}/ranger-pdp-services.sh" start
+    exit 0
+
+elif [ "${action}" == "VERSION" ]; then
+    cd "${cdir}/lib" || exit 1
+    java -cp "ranger-util-*.jar" org.apache.ranger.common.RangerVersionInfo
+    exit 0
+
+else
+    echo "Invalid argument [${action}]"
+    echo "Usage: $0 {start | run | stop | restart | version}"
+    exit 1
+fi
diff --git a/pdp/scripts/ranger-pdp.sh b/pdp/scripts/ranger-pdp.sh
new file mode 100644
index 000000000..15d9ebbb0
--- /dev/null
+++ b/pdp/scripts/ranger-pdp.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# 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.
+
+### BEGIN INIT INFO
+# Provides:        ranger-pdp
+# Required-Start:  $local_fs $remote_fs $network $named $syslog $time
+# Required-Stop:   $local_fs $remote_fs $network $named $syslog $time
+# Default-Start:   2 3 4 5
+# Default-Stop:
+# Short-Description: Start/Stop Ranger PDP Server
+### END INIT INFO
+
+LINUX_USER=ranger
+BIN_PATH=/usr/bin
+MOD_NAME=ranger-pdp-services.sh
+pidf=/var/run/ranger/pdp.pid
+pid=""
+
+if [ -f "${pidf}" ]; then
+    pid=$(cat "$pidf")
+fi
+
+case $1 in
+    start)
+        if [ "${pid}" != "" ]; then
+            echo "Ranger PDP Service is already running [pid=${pid}]"
+            exit 1
+        else
+            echo "Starting Ranger PDP Service."
+            /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} start"
+        fi
+        ;;
+    stop)
+        if [ "${pid}" != "" ]; then
+            echo "Stopping Ranger PDP Service."
+            /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} stop"
+        else
+            echo "Ranger PDP Service is NOT running."
+            exit 1
+        fi
+        ;;
+    restart)
+        if [ "${pid}" != "" ]; then
+            echo "Stopping Ranger PDP Service."
+            /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} stop"
+            sleep 10
+        fi
+        echo "Starting Ranger PDP Service."
+        /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} start"
+        ;;
+    status)
+        if [ "${pid}" != "" ]; then
+            echo "Ranger PDP Service is running [pid=${pid}]"
+        else
+            echo "Ranger PDP Service is NOT running."
+        fi
+        ;;
+    *)
+        echo "Invalid argument [$1]; supported: start | stop | restart | status"
+        exit 1
+        ;;
+esac
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
new file mode 100644
index 000000000..a3027184f
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
@@ -0,0 +1,295 @@
+/*
+ * 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.pdp;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.valves.AccessLogValve;
+import org.apache.coyote.http2.Http2Protocol;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.pdp.rest.RangerPdpApplication;
+import org.apache.ranger.pdp.security.RangerPdpAuthNFilter;
+import org.apache.ranger.pdp.security.RangerPdpRequestContextFilter;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+
+import java.io.File;
+
+/**
+ * Main entry point for the Ranger Policy Decision Point (PDP) server.
+ *
+ * <p>Starts an embedded Apache Tomcat instance that:
+ * <ul>
+ *   <li>Creates and initialises a {@link RangerEmbeddedAuthorizer} singleton
+ *   <li>Exposes the three authorizer methods as REST endpoints under {@code /authz/v1/}
+ *   <li>Enforces authentication via {@link RangerPdpAuthNFilter} (Kerberos/JWT/HTTP-Header)
+ *   <li>Optionally enables HTTP/2 ({@code Http2Protocol} upgrade on the connector)
+ * </ul>
+ *
+ * <p><b>Startup:</b> {@code java -jar ranger-pdp.jar}
+ * <br>Override config with: {@code -Dranger.pdp.conf.dir=/etc/ranger/pdp}
+ */
+public class RangerPdpServer {
+    private static final Logger LOG = LoggerFactory.getLogger(RangerPdpServer.class);
+
+    private final RangerPdpConfig  config;
+    private final RangerPdpStats   runtimeStats = new RangerPdpStats();
+    private       Tomcat           tomcat;
+    private       RangerAuthorizer authorizer;
+
+    public RangerPdpServer() {
+        this.config = new RangerPdpConfig();
+    }
+
+    public static void main(String[] args) throws Exception {
+        new RangerPdpServer().start();
+    }
+
+    public void start() throws Exception {
+        LOG.info("Starting Ranger PDP server");
+
+        initAuthorizer();
+        startTomcat();
+    }
+
+    public void stop() {
+        LOG.info("Stopping Ranger PDP server");
+
+        runtimeStats.setAcceptingRequests(false);
+        runtimeStats.setServerStarted(false);
+
+        if (tomcat != null) {
+            try {
+                if (tomcat.getConnector() != null) {
+                    tomcat.getConnector().pause();
+                }
+
+                tomcat.stop();
+                tomcat.destroy();
+            } catch (LifecycleException e) {
+                LOG.warn("Error stopping Tomcat", e);
+            }
+        }
+
+        if (authorizer != null) {
+            try {
+                authorizer.close();
+            } catch (RangerAuthzException e) {
+                LOG.warn("Error closing authorizer", e);
+            }
+        }
+    }
+
+    private void initAuthorizer() throws RangerAuthzException {
+        RangerAuthorizer authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties());
+
+        authorizer.init();
+
+        this.authorizer = authorizer;
+
+        runtimeStats.setAuthorizerInitialized(true);
+
+        LOG.info("RangerEmbeddedAuthorizer initialised");
+    }
+
+    private void startTomcat() throws Exception {
+        tomcat = new Tomcat();
+
+        tomcat.setConnector(createConnector());
+
+        String docBase = new File(System.getProperty("java.io.tmpdir"), "ranger-pdp-webapps").getAbsolutePath();
+
+        new File(docBase).mkdirs();
+
+        Context ctx = tomcat.addContext("", docBase);
+
+        ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER, authorizer);
+        ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_CONFIG, config);
+        ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE, runtimeStats);
+
+        addAuthFilter(ctx);
+        addJerseyServlet(ctx);
+        addStatusEndpoints(ctx);
+        addAccessLogValve();
+
+        Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "ranger-pdp-shutdown"));
+
+        tomcat.start();
+
+        runtimeStats.setServerStarted(true);
+        runtimeStats.setAcceptingRequests(true);
+
+        LOG.info("Ranger PDP server listening on port {} (SSL={}, HTTP/2={})", config.getPort(), config.isSslEnabled(), config.isHttp2Enabled());
+
+        tomcat.getServer().await();
+    }
+
+    /**
+     * Builds the Tomcat connector.
+     *
+     * <p>When SSL is enabled the connector is configured as an HTTPS endpoint.
+     * When HTTP/2 is enabled an {@link Http2Protocol} upgrade protocol is added,
+     * supporting both {@code h2} (over TLS) and {@code h2c} (cleartext upgrade) depending
+     * on whether SSL is enabled.
+     */
+    private Connector createConnector() {
+        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+
+        connector.setPort(config.getPort());
+        connector.setProperty("maxThreads", String.valueOf(config.getHttpConnectorMaxThreads()));
+        connector.setProperty("minSpareThreads", String.valueOf(config.getHttpConnectorMinSpareThreads()));
+        connector.setProperty("acceptCount", String.valueOf(config.getHttpConnectorAcceptCount()));
+        connector.setProperty("maxConnections", String.valueOf(config.getHttpConnectorMaxConnections()));
+
+        LOG.info("Configured HTTP connector limits: maxThreads={}, minSpareThreads={}, acceptCount={}, maxConnections={}",
+                config.getHttpConnectorMaxThreads(), config.getHttpConnectorMinSpareThreads(),
+                config.getHttpConnectorAcceptCount(), config.getHttpConnectorMaxConnections());
+
+        if (config.isSslEnabled()) {
+            connector.setSecure(true);
+            connector.setScheme("https");
+            connector.setProperty("SSLEnabled", "true");
+            connector.setProperty("protocol", "org.apache.coyote.http11.Http11NioProtocol");
+            connector.setProperty("keystoreFile", config.getKeystoreFile());
+            connector.setProperty("keystorePass", config.getKeystorePassword());
+            connector.setProperty("keystoreType", config.getKeystoreType());
+            connector.setProperty("sslProtocol", "TLS");
+
+            if (config.isTruststoreEnabled()) {
+                connector.setProperty("truststoreFile",  config.getTruststoreFile());
+                connector.setProperty("truststorePass",  config.getTruststorePassword());
+                connector.setProperty("truststoreType",  config.getTruststoreType());
+                connector.setProperty("clientAuth",      "true");
+            }
+        }
+
+        if (config.isHttp2Enabled()) {
+            connector.addUpgradeProtocol(new Http2Protocol());
+
+            LOG.info("HTTP/2 upgrade protocol registered on connector (port={})", config.getPort());
+        }
+
+        return connector;
+    }
+
+    /**
+     * Registers {@link RangerPdpAuthNFilter} on all {@code /authz/*} paths.
+     * Init parameters are forwarded from the server config so the filter can
+     * instantiate and configure the auth handlers.
+     */
+    private void addAuthFilter(Context ctx) {
+        FilterDef reqCtxFilterDef = new FilterDef();
+        FilterMap reqCtxFilterMap = new FilterMap();
+        FilterDef authFilterDef = new FilterDef();
+        FilterMap authFilterMap = new FilterMap();
+
+        reqCtxFilterDef.setFilterName("rangerPdpRequestContextFilter");
+        reqCtxFilterDef.setFilter(new RangerPdpRequestContextFilter());
+
+        reqCtxFilterMap.setFilterName("rangerPdpRequestContextFilter");
+        reqCtxFilterMap.addURLPattern("/*");
+
+        authFilterDef.setFilterName("rangerPdpAuthFilter");
+        authFilterDef.setFilter(new RangerPdpAuthNFilter());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_TYPES, config.getAuthnTypes());
+
+        // HTTP Header auth
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, Boolean.toString(config.isHeaderAuthnEnabled()));
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, config.getHeaderAuthnUsername());
+
+        // JWT bearer token auth
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, Boolean.toString(config.isJwtAuthnEnabled()));
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, config.getJwtProviderUrl());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, config.getJwtPublicKey());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, config.getJwtCookieName());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, config.getJwtAudiences());
+
+        // Kerberos / SPNEGO
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, Boolean.toString(config.isKerberosAuthnEnabled()));
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL,   config.getSpnegoPrincipal());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB,      config.getSpnegoKeytab());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES,         config.getKerberosNameRules());
+        authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, String.valueOf(config.getKerberosTokenValiditySeconds()));
+
+        authFilterMap.setFilterName("rangerPdpAuthFilter");
+        authFilterMap.addURLPattern("/authz/*");
+
+        ctx.addFilterDef(reqCtxFilterDef);
+        ctx.addFilterMap(reqCtxFilterMap);
+        ctx.addFilterDef(authFilterDef);
+        ctx.addFilterMap(authFilterMap);
+    }
+
+    /**
+     * Registers the Jersey {@link ServletContainer} backed by {@link RangerPdpApplication}.
+     * The Jersey application binds the {@link RangerAuthorizer} singleton via HK2 so
+     * that it can be injected into {@link org.apache.ranger.pdp.rest.RangerPdpREST}.
+     */
+    private void addJerseyServlet(Context ctx) {
+        RangerPdpApplication jerseyApp     = new RangerPdpApplication(authorizer, config);
+        Servlet              jerseyServlet = new ServletContainer(jerseyApp);
+
+        Tomcat.addServlet(ctx, "jerseyServlet", jerseyServlet).setLoadOnStartup(1);
+
+        ctx.addServletMappingDecoded("/authz/*", "jerseyServlet");
+    }
+
+    private void addStatusEndpoints(Context ctx) {
+        HttpServlet liveServlet    = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.LIVE);
+        HttpServlet readyServlet   = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.READY);
+        HttpServlet metricsServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.METRICS);
+
+        Tomcat.addServlet(ctx, "pdpLiveServlet", liveServlet).setLoadOnStartup(1);
+        Tomcat.addServlet(ctx, "pdpReadyServlet", readyServlet).setLoadOnStartup(1);
+        Tomcat.addServlet(ctx, "pdpMetricsServlet", metricsServlet).setLoadOnStartup(1);
+
+        ctx.addServletMappingDecoded("/health/live", "pdpLiveServlet");
+        ctx.addServletMappingDecoded("/health/ready", "pdpReadyServlet");
+        ctx.addServletMappingDecoded("/metrics", "pdpMetricsServlet");
+    }
+
+    private void addAccessLogValve() {
+        String logDir = config.getLogDir();
+
+        new File(logDir).mkdirs();
+
+        AccessLogValve valve = new AccessLogValve();
+
+        valve.setDirectory(logDir);
+        valve.setPrefix("ranger-pdp-access");
+        valve.setSuffix(".log");
+        valve.setPattern("%h %l %u %t \"%r\" %s %b %D");
+        valve.setRotatable(true);
+
+        tomcat.getHost().getPipeline().addValve(valve);
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java
new file mode 100644
index 000000000..c11c42d8e
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java
@@ -0,0 +1,113 @@
+/*
+ * 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.pdp;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class RangerPdpStats {
+    private final AtomicBoolean serverStarted        = new AtomicBoolean(false);
+    private final AtomicBoolean authorizerInitialized = new AtomicBoolean(false);
+    private final AtomicBoolean acceptingRequests     = new AtomicBoolean(false);
+
+    private final AtomicLong totalRequests        = new AtomicLong(0);
+    private final AtomicLong totalAuthzSuccess    = new AtomicLong(0);
+    private final AtomicLong totalAuthzBadRequest = new AtomicLong(0);
+    private final AtomicLong totalAuthzErrors     = new AtomicLong(0);
+    private final AtomicLong totalAuthFailures    = new AtomicLong(0);
+    private final AtomicLong totalLatencyNanos    = new AtomicLong(0);
+
+    public boolean isServerStarted() {
+        return serverStarted.get();
+    }
+
+    public void setServerStarted(boolean value) {
+        serverStarted.set(value);
+    }
+
+    public boolean isAuthorizerInitialized() {
+        return authorizerInitialized.get();
+    }
+
+    public void setAuthorizerInitialized(boolean value) {
+        authorizerInitialized.set(value);
+    }
+
+    public boolean isAcceptingRequests() {
+        return acceptingRequests.get();
+    }
+
+    public void setAcceptingRequests(boolean value) {
+        acceptingRequests.set(value);
+    }
+
+    public void recordRequestSuccess(long elapsedNanos) {
+        totalRequests.incrementAndGet();
+        totalAuthzSuccess.incrementAndGet();
+        totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+    }
+
+    public void recordRequestBadRequest(long elapsedNanos) {
+        totalRequests.incrementAndGet();
+        totalAuthzBadRequest.incrementAndGet();
+        totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+    }
+
+    public void recordRequestError(long elapsedNanos) {
+        totalRequests.incrementAndGet();
+        totalAuthzErrors.incrementAndGet();
+        totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+    }
+
+    public void recordAuthFailure(long elapsedNanos) {
+        totalRequests.incrementAndGet();
+        totalAuthFailures.incrementAndGet();
+        totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+    }
+
+    public long getTotalRequests() {
+        return totalRequests.get();
+    }
+
+    public long getTotalAuthzSuccess() {
+        return totalAuthzSuccess.get();
+    }
+
+    public long getTotalAuthzBadRequest() {
+        return totalAuthzBadRequest.get();
+    }
+
+    public long getTotalAuthzErrors() {
+        return totalAuthzErrors.get();
+    }
+
+    public long getTotalAuthFailures() {
+        return totalAuthFailures.get();
+    }
+
+    public long getTotalLatencyNanos() {
+        return totalLatencyNanos.get();
+    }
+
+    public long getAverageLatencyMs() {
+        long requests = totalRequests.get();
+        return requests > 0 ? (totalLatencyNanos.get() / requests) / 1_000_000L : 0L;
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java
new file mode 100644
index 000000000..9725909c8
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java
@@ -0,0 +1,129 @@
+/*
+ * 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.pdp;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class RangerPdpStatusServlet extends HttpServlet {
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final RangerPdpStats  runtimeState;
+    private final Mode            mode;
+
+    public enum Mode { LIVE, READY, METRICS }
+
+    public RangerPdpStatusServlet(RangerPdpStats runtimeState, Mode mode) {
+        this.runtimeState = runtimeState;
+        this.mode         = mode;
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        switch (mode) {
+            case LIVE:
+                writeLive(resp);
+                break;
+            case READY:
+                writeReady(req, resp);
+                break;
+            case METRICS:
+                writeMetrics(req, resp);
+                break;
+            default:
+                resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    private void writeLive(HttpServletResponse resp) throws IOException {
+        Map<String, Object> payload = new LinkedHashMap<>();
+
+        payload.put("status", runtimeState.isServerStarted() ? "UP" : "DOWN");
+        payload.put("service", "ranger-pdp");
+        payload.put("live", runtimeState.isServerStarted());
+
+        resp.setStatus(runtimeState.isServerStarted() ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+        resp.setContentType(MediaType.APPLICATION_JSON);
+
+        MAPPER.writeValue(resp.getOutputStream(), payload);
+    }
+
+    private void writeReady(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        boolean ready = runtimeState.isServerStarted() && runtimeState.isAuthorizerInitialized() && runtimeState.isAcceptingRequests();
+
+        Map<String, Object> payload = new LinkedHashMap<>();
+
+        payload.put("status", ready ? "READY" : "NOT_READY");
+        payload.put("service", "ranger-pdp");
+        payload.put("ready", ready);
+        payload.put("authorizerInitialized", runtimeState.isAuthorizerInitialized());
+        payload.put("acceptingRequests", runtimeState.isAcceptingRequests());
+        payload.put("loadedServicesCount", getLoadedServicesCount(req));
+
+        resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+        resp.setContentType(MediaType.APPLICATION_JSON);
+
+        MAPPER.writeValue(resp.getOutputStream(), payload);
+    }
+
+    private void writeMetrics(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        StringBuilder sb = new StringBuilder(512);
+
+        sb.append("# TYPE ranger_pdp_requests_total counter\n");
+        sb.append("ranger_pdp_requests_total ").append(runtimeState.getTotalRequests()).append('\n');
+        sb.append("# TYPE ranger_pdp_requests_success_total counter\n");
+        sb.append("ranger_pdp_requests_success_total ").append(runtimeState.getTotalAuthzSuccess()).append('\n');
+        sb.append("# TYPE ranger_pdp_requests_bad_request_total counter\n");
+        sb.append("ranger_pdp_requests_bad_request_total ").append(runtimeState.getTotalAuthzBadRequest()).append('\n');
+        sb.append("# TYPE ranger_pdp_requests_error_total counter\n");
+        sb.append("ranger_pdp_requests_error_total ").append(runtimeState.getTotalAuthzErrors()).append('\n');
+        sb.append("# TYPE ranger_pdp_auth_failures_total counter\n");
+        sb.append("ranger_pdp_auth_failures_total ").append(runtimeState.getTotalAuthFailures()).append('\n');
+        sb.append("# TYPE ranger_pdp_request_latency_avg_ms gauge\n");
+        sb.append("ranger_pdp_request_latency_avg_ms ").append(runtimeState.getAverageLatencyMs()).append('\n');
+        sb.append("# TYPE ranger_pdp_loaded_services_count gauge\n");
+        sb.append("ranger_pdp_loaded_services_count ").append(getLoadedServicesCount(req)).append('\n');
+
+        resp.setStatus(HttpServletResponse.SC_OK);
+        resp.setContentType("text/plain; version=0.0.4");
+
+        resp.getWriter().write(sb.toString());
+    }
+
+    private int getLoadedServicesCount(HttpServletRequest req) {
+        ServletContext context    = req.getServletContext();
+        Object         authorizer = context != null ? context.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER) : null;
+        Set<String>    services   = authorizer instanceof RangerEmbeddedAuthorizer ? ((RangerEmbeddedAuthorizer) authorizer).getLoadedServices() : null;
+
+        return services != null ? services.size() : 0;
+    }
+}
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
new file mode 100644
index 000000000..11aea39ea
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
@@ -0,0 +1,281 @@
+/*
+ * 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.pdp.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.plugin.util.XMLUtils;
+import org.ietf.jgss.GSSCredential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Properties;
+
+/**
+ * Reads Ranger PDP configuration from {@code ranger-pdp-default.xml} (classpath)
+ * overridden by {@code ranger-pdp-site.xml} (classpath or filesystem).
+ *
+ * <p>Both files use the Hadoop {@code <configuration>} XML format, consistent
+ * with other Ranger server modules (tagsync, kms, etc.).
+ * The format is parsed directly using the JDK DOM API to avoid an early
+ * class-load dependency on Hadoop's {@code Configuration} class.
+ *
+ * <p>Authentication property names:
+ * <ul>
+ *   <li>Kerberos/SPNEGO:    {@code ranger.pdp.authn.kerberos.*}
+ *   <li>JWT bearer token:   {@code ranger.pdp.authn.jwt.*}
+ *   <li>HTTP header:        {@code ranger.pdp.authn.header.*}
+ * </ul>
+ */
+public class RangerPdpConfig {
+    private static final Logger LOG = LoggerFactory.getLogger(RangerPdpConfig.class);
+
+    private static final String DEFAULT_CONFIG_FILE = "ranger-pdp-default.xml";
+    private static final String SITE_CONFIG_FILE    = "ranger-pdp-site.xml";
+
+    private final Properties props = new Properties();
+
+    public RangerPdpConfig() {
+        loadFromClasspath(DEFAULT_CONFIG_FILE);
+        loadFromClasspath(SITE_CONFIG_FILE);
+
+        String confDir = System.getProperty(RangerPdpConstants.PROP_CONF_DIR, "");
+
+        if (StringUtils.isNotBlank(confDir)) {
+            loadFromFile(new File(confDir, SITE_CONFIG_FILE));
+        }
+
+        applySystemPropertyOverrides();
+
+        LOG.info("RangerPdpConfig initialized (conf.dir={})", confDir);
+    }
+
+    public int getPort() {
+        return getInt(RangerPdpConstants.PROP_PORT, 6500);
+    }
+
+    public String getLogDir() {
+        return get(RangerPdpConstants.PROP_LOG_DIR, "/var/log/ranger/pdp");
+    }
+
+    public boolean isSslEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_SSL_ENABLED, false);
+    }
+
+    public String getKeystoreFile() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_FILE, "");
+    }
+
+    public String getKeystorePassword() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_PASSWORD, "");
+    }
+
+    public String getKeystoreType() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_TYPE, "JKS");
+    }
+
+    public boolean isTruststoreEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_SSL_TRUSTSTORE_ENABLED, false);
+    }
+
+    public String getTruststoreFile() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_FILE, "");
+    }
+
+    public String getTruststorePassword() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_PASSWORD, "");
+    }
+
+    public String getTruststoreType() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_TYPE, "JKS");
+    }
+
+    public boolean isHttp2Enabled() {
+        return getBoolean(RangerPdpConstants.PROP_HTTP2_ENABLED, true);
+    }
+
+    public int getHttpConnectorMaxThreads() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_THREADS, 200);
+    }
+
+    public int getHttpConnectorMinSpareThreads() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS, 20);
+    }
+
+    public int getHttpConnectorAcceptCount() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_ACCEPT_COUNT, 100);
+    }
+
+    public int getHttpConnectorMaxConnections() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_CONNECTIONS, 10000);
+    }
+
+    public String getAuthnTypes() {
+        return get(RangerPdpConstants.PROP_AUTHN_TYPES, "header,jwt,kerberos");
+    }
+
+    // --- HTTP Header auth ---
+    public boolean isHeaderAuthnEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, false);
+    }
+
+    public String getHeaderAuthnUsername() {
+        return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User");
+    }
+
+    // --- JWT bearer token auth ---
+    public boolean isJwtAuthnEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, false);
+    }
+
+    public String getJwtProviderUrl() {
+        return get(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, "");
+    }
+
+    public String getJwtPublicKey() {
+        return get(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, "");
+    }
+
+    public String getJwtCookieName() {
+        return get(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, "hadoop-jwt");
+    }
+
+    public String getJwtAudiences() {
+        return get(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, "");
+    }
+
+    // --- Kerberos / SPNEGO ---
+    public boolean isKerberosAuthnEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, false);
+    }
+
+    public String getSpnegoPrincipal() {
+        return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, "");
+    }
+
+    public String getSpnegoKeytab() {
+        return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, "");
+    }
+
+    public int getKerberosTokenValiditySeconds() {
+        return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, GSSCredential.INDEFINITE_LIFETIME);
+    }
+
+    public String getKerberosNameRules() {
+        return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT");
+    }
+
+    /**
+     * Returns all properties for forwarding to {@code RangerEmbeddedAuthorizer}.
+     */
+    public Properties getAuthzProperties() {
+        return new Properties(props);
+    }
+
+    public String get(String key, String defaultValue) {
+        String val = props.getProperty(key);
+
+        return StringUtils.isNotBlank(val) ? val.trim() : defaultValue;
+    }
+
+    public int getInt(String key, int defaultValue) {
+        String val = props.getProperty(key);
+
+        if (StringUtils.isNotBlank(val)) {
+            try {
+                return Integer.parseInt(val.trim());
+            } catch (NumberFormatException e) {
+                LOG.warn("Invalid integer for {}: '{}'; using default {}", key, val, defaultValue);
+            }
+        }
+
+        return defaultValue;
+    }
+
+    public boolean getBoolean(String key, boolean defaultValue) {
+        String val = props.getProperty(key);
+
+        return StringUtils.isNotBlank(val) ? Boolean.parseBoolean(val.trim()) : defaultValue;
+    }
+
+    private void loadFromClasspath(String resourceName) {
+        try (InputStream in = getClass().getClassLoader().getResourceAsStream(resourceName)) {
+            if (in != null) {
+                parseHadoopXml(in, resourceName);
+            } else {
+                LOG.debug("Config resource not found on classpath: {}", resourceName);
+            }
+        } catch (IOException e) {
+            LOG.warn("Failed to close stream for classpath resource: {}", resourceName, e);
+        }
+    }
+
+    private void loadFromFile(File file) {
+        if (!file.exists() || !file.isFile()) {
+            LOG.debug("Config file not found: {}", file);
+            return;
+        }
+
+        try (InputStream in = Files.newInputStream(file.toPath())) {
+            parseHadoopXml(in, file.getAbsolutePath());
+        } catch (IOException e) {
+            LOG.warn("Failed to read config file: {}", file, e);
+        }
+    }
+
+    /**
+     * Parses a Hadoop-style {@code <configuration>} XML document and merges all
+     * {@code <property>} entries into {@link #props}.  Later entries override earlier
+     * ones, matching Hadoop's own override semantics.
+     *
+     * <pre>
+     * {@code
+     * <configuration>
+     *   <property>
+     *     <name>some.key</name>
+     *     <value>some-value</value>
+     *   </property>
+     * </configuration>
+     * }
+     * </pre>
+     */
+    private void parseHadoopXml(InputStream in, String source) {
+        LOG.info("Loading from {}. Properties count {}", source, props.size());
+
+        XMLUtils.loadConfig(in, props);
+
+        LOG.info("Loaded from {}. Properties count {}", source, props.size());
+    }
+
+    /**
+     * Apply JVM system-property overrides for operationally sensitive keys so Kubernetes
+     * (or any orchestrator) can drive runtime config with JAVA_OPTS/-D flags.
+     */
+    private void applySystemPropertyOverrides() {
+        for (String key : System.getProperties().stringPropertyNames()) {
+            if (key.startsWith(RangerPdpConstants.PROP_PDP_PREFIX) || key.startsWith(RangerPdpConstants.PROP_AUTHZ_PREFIX)) {
+                props.setProperty(key, System.getProperty(key));
+            }
+        }
+    }
+}
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
new file mode 100644
index 000000000..c1a431351
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
@@ -0,0 +1,94 @@
+/*
+ * 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.pdp.config;
+
+public final class RangerPdpConstants {
+    private RangerPdpConstants() {
+        // no instances
+    }
+
+    // Servlet context attributes
+    public static final String SERVLET_CTX_ATTR_CONFIG        = "ranger.pdp.config";
+    public static final String SERVLET_CTX_ATTR_AUTHORIZER    = "ranger.pdp.authorizer";
+    public static final String SERVLET_CTX_ATTR_RUNTIME_STATE = "ranger.pdp.runtime.state";
+
+    // Request attributes set by auth filter
+    public static final String ATTR_AUTHENTICATED_USER = "ranger.pdp.authenticated.user";
+    public static final String ATTR_AUTHN_TYPE         = "ranger.pdp.authn.type";
+
+    // Server
+    public static final String PROP_CONF_DIR = "ranger.pdp.conf.dir";
+    public static final String PROP_PORT    = "ranger.pdp.port";
+    public static final String PROP_LOG_DIR = "ranger.pdp.log.dir";
+
+    // SSL/TLS
+    public static final String PROP_SSL_ENABLED             = "ranger.pdp.ssl.enabled";
+    public static final String PROP_SSL_KEYSTORE_FILE       = "ranger.pdp.ssl.keystore.file";
+    public static final String PROP_SSL_KEYSTORE_PASSWORD   = "ranger.pdp.ssl.keystore.password";
+    public static final String PROP_SSL_KEYSTORE_TYPE       = "ranger.pdp.ssl.keystore.type";
+    public static final String PROP_SSL_TRUSTSTORE_ENABLED  = "ranger.pdp.ssl.truststore.enabled";
+    public static final String PROP_SSL_TRUSTSTORE_FILE     = "ranger.pdp.ssl.truststore.file";
+    public static final String PROP_SSL_TRUSTSTORE_PASSWORD = "ranger.pdp.ssl.truststore.password";
+    public static final String PROP_SSL_TRUSTSTORE_TYPE     = "ranger.pdp.ssl.truststore.type";
+
+    // HTTP/2
+    public static final String PROP_HTTP2_ENABLED = "ranger.pdp.http2.enabled";
+
+    // HTTP connector limits
+    public static final String PROP_HTTP_CONNECTOR_MAX_THREADS       = "ranger.pdp.http.connector.maxThreads";
+    public static final String PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS = "ranger.pdp.http.connector.minSpareThreads";
+    public static final String PROP_HTTP_CONNECTOR_ACCEPT_COUNT      = "ranger.pdp.http.connector.acceptCount";
+    public static final String PROP_HTTP_CONNECTOR_MAX_CONNECTIONS   = "ranger.pdp.http.connector.maxConnections";
+
+    // Authentication types
+    public static final String PROP_AUTHN_PREFIX = "ranger.pdp.authn.";
+    public static final String PROP_AUTHN_TYPES  = PROP_AUTHN_PREFIX + "types";
+
+    // HTTP header auth
+    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";
+
+    // JWT auth
+    public static final String PROP_AUTHN_JWT_PREFIX       = PROP_AUTHN_PREFIX + "jwt.";
+    public static final String PROP_AUTHN_JWT_ENABLED      = PROP_AUTHN_JWT_PREFIX + "enabled";
+    public static final String PROP_AUTHN_JWT_PROVIDER_URL = PROP_AUTHN_JWT_PREFIX + "provider.url";
+    public static final String PROP_AUTHN_JWT_PUBLIC_KEY   = PROP_AUTHN_JWT_PREFIX + "public.key";
+    public static final String PROP_AUTHN_JWT_COOKIE_NAME  = PROP_AUTHN_JWT_PREFIX + "cookie.name";
+    public static final String PROP_AUTHN_JWT_AUDIENCES    = PROP_AUTHN_JWT_PREFIX + "audiences";
+
+    // Kerberos/SPNEGO auth
+    public static final String PROP_AUTHN_KERBEROS_PREFIX             = PROP_AUTHN_PREFIX + "kerberos.";
+    public static final String PROP_AUTHN_KERBEROS_ENABLED            = PROP_AUTHN_KERBEROS_PREFIX + "enabled";
+    public static final String PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL   = PROP_AUTHN_KERBEROS_PREFIX + "spnego.principal";
+    public static final String PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB      = PROP_AUTHN_KERBEROS_PREFIX + "spnego.keytab";
+    public static final String PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY = PROP_AUTHN_KERBEROS_PREFIX + "token.valid.seconds";
+    public static final String PROP_AUTHN_KERBEROS_NAME_RULES         = PROP_AUTHN_KERBEROS_PREFIX + "name.rules";
+
+    // Authorizer/audit properties referenced by PDP code
+    public static final String PROP_AUTHZ_POLICY_CACHE_DIR = "ranger.authz.default.policy.cache.dir";
+    public static final String PROP_AUTHZ_PREFIX           = "ranger.authz.";
+    public static final String PROP_PDP_PREFIX             = "ranger.pdp.";
+    public static final String PROP_PDP_SERVICE_PREFIX     = PROP_PDP_PREFIX + "service.";
+
+    // delegation users
+    public static final String PROP_SUFFIX_DELEGATION_USERS = ".delegation.users";
+    public static final String WILDCARD_SERVICE_NAME        = "*";
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java b/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java
new file mode 100644
index 000000000..d06c1e670
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java
@@ -0,0 +1,45 @@
+/*
+ * 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.pdp.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import javax.ws.rs.core.Response;
+
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ErrorResponse {
+    private final String code;
+    private final String message;
+
+    public ErrorResponse(Response.Status status, String message) {
+        this.code    = status.name();
+        this.message = message;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java
new file mode 100644
index 000000000..42371b452
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java
@@ -0,0 +1,62 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+
+/**
+ * Jersey 2.x {@link ResourceConfig} for the Ranger PDP application.
+ *
+ * <p>Registers:
+ * <ul>
+ *   <li>{@link JacksonFeature} – Jackson 2.x JSON provider (honours all {@code @Json*}
+ *       annotations on the {@code authz-api} model classes)
+ *   <li>{@link AuthorizerBinder} – HK2 binder that makes the {@link RangerAuthorizer}
+ *       instance injectable into {@link RangerPdpREST} via {@code @Inject}
+ *   <li>{@link RangerPdpREST} – the JAX-RS resource class
+ * </ul>
+ */
+public class RangerPdpApplication extends ResourceConfig {
+    public RangerPdpApplication(RangerAuthorizer authorizer, RangerPdpConfig config) {
+        register(JacksonFeature.class);
+        register(new AuthorizerBinder(authorizer, config));
+        register(RangerPdpREST.class);
+    }
+
+    private static class AuthorizerBinder extends AbstractBinder {
+        private final RangerAuthorizer authorizer;
+        private final RangerPdpConfig  config;
+
+        AuthorizerBinder(RangerAuthorizer authorizer, RangerPdpConfig config) {
+            this.authorizer = authorizer;
+            this.config     = config;
+        }
+
+        @Override
+        protected void configure() {
+            bind(authorizer).to(RangerAuthorizer.class);
+            bind(config).to(RangerPdpConfig.class);
+        }
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java
new file mode 100644
index 000000000..1f447f706
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java
@@ -0,0 +1,491 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.model.RangerAccessInfo;
+import org.apache.ranger.authz.model.RangerAuthzRequest;
+import org.apache.ranger.authz.model.RangerAuthzResult;
+import org.apache.ranger.authz.model.RangerMultiAuthzRequest;
+import org.apache.ranger.authz.model.RangerMultiAuthzResult;
+import org.apache.ranger.authz.model.RangerResourceInfo;
+import org.apache.ranger.authz.model.RangerResourcePermissions;
+import org.apache.ranger.authz.model.RangerResourcePermissionsRequest;
+import org.apache.ranger.authz.model.RangerUserInfo;
+import org.apache.ranger.pdp.RangerPdpStats;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.pdp.model.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+import static javax.ws.rs.core.Response.Status.OK;
+import static javax.ws.rs.core.Response.Status.UNAUTHORIZED;
+import static org.apache.ranger.authz.api.RangerAuthzApiErrorCode.INVALID_REQUEST_USER_INFO_MISSING;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.WILDCARD_SERVICE_NAME;
+
+/**
+ * REST resource that exposes the three core {@link RangerAuthorizer} methods over HTTP.
+ *
+ * <p>All endpoints are under {@code /authz/v1} and produce/consume {@code application/json}.
+ * Authentication is enforced upstream by {@link org.apache.ranger.pdp.security.RangerPdpAuthNFilter}; the authenticated
+ * caller's identity is read from the {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER}
+ * request attribute.
+ *
+ * <table border="1">
+ *   <tr><th>Method</th><th>Path</th><th>Request body</th><th>Response body</th></tr>
+ *   <tr><td>POST</td><td>/authz/v1/authorize</td>
+ *       <td>{@link RangerAuthzRequest}</td><td>{@link RangerAuthzResult}</td></tr>
+ *   <tr><td>POST</td><td>/authz/v1/authorizeMulti</td>
+ *       <td>{@link RangerMultiAuthzRequest}</td><td>{@link RangerMultiAuthzResult}</td></tr>
+ *   <tr><td>POST</td><td>/authz/v1/permissions</td>
+ *       <td>{@link RangerResourcePermissionsRequest}</td>
+ *       <td>{@link RangerResourcePermissions}</td></tr>
+ * </table>
+ */
+@Path("/v1")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Singleton
+public class RangerPdpREST {
+    private static final Logger LOG = LoggerFactory.getLogger(RangerPdpREST.class);
+
+    private static final Response RESPONSE_OK = Response.ok().build();
+
+    private final Map<String, Set<String>> delegationUsersByService = new HashMap<>();
+
+    @Inject
+    private RangerAuthorizer authorizer;
+
+    @Inject
+    private RangerPdpConfig config;
+
+    @Context
+    private ServletContext servletContext;
+
+    @PostConstruct
+    public void initialize() {
+        initializeDelegationUsers();
+    }
+
+    /**
+     * Evaluates a single access request.
+     *
+     * @param request the authorization request
+     * @return {@code 200 OK} with {@link RangerAuthzResult}, or {@code 400} / {@code 500} on error
+     */
+    @POST
+    @Path("/authorize")
+    public Response authorize(RangerAuthzRequest request, @Context HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String           requestId   = request != null ? request.getRequestId() : null;
+            String           caller      = getAuthenticatedUser(httpRequest);
+            String           serviceName = getServiceName(request);
+            RangerUserInfo   user        = request != null ? request.getUser() : null;
+            RangerAccessInfo access      = request != null ? request.getAccess() : null;
+
+            LOG.debug("==> authorize(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName);
+
+            ret = validateCaller(caller, user, access, serviceName);
+
+            if (isStatusOk(ret)) {
+                try {
+                    RangerAuthzResult result = authorizer.authorize(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("authorize(requestId={}): authorization error; caller={}", requestId, caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("authorize(requestId={}): internal error; caller={}", requestId, caller, e);
+
+                    ret = serverError();
+                }
+            }
+
+            LOG.debug("<== authorize(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Evaluates multiple access requests in a single call.
+     *
+     * @param request the multi-access authorization request
+     * @return {@code 200 OK} with {@link RangerMultiAuthzResult}, or {@code 400} / {@code 500} on error
+     */
+    @POST
+    @Path("/authorizeMulti")
+    public Response authorizeMulti(RangerMultiAuthzRequest request, @Context HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String                 requestId   = request != null ? request.getRequestId() : null;
+            String                 caller      = getAuthenticatedUser(httpRequest);
+            String                 serviceName = getServiceName(request);
+            RangerUserInfo         user        = request != null ? request.getUser() : null;
+            List<RangerAccessInfo> accesses    = request != null ? request.getAccesses() : null;
+
+            LOG.debug("==> authorizeMulti(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName);
+
+            ret = validateCaller(caller, user, accesses, serviceName);
+
+            if (isStatusOk(ret)) {
+                try {
+                    RangerMultiAuthzResult result = authorizer.authorize(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("authorizeMulti(requestId={}): authorization error; caller={}", requestId, caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("authorizeMulti(requestId={}): internal error; caller={}", requestId, caller, e);
+
+                    ret = serverError();
+                }
+            }
+
+            LOG.debug("<== authorizeMulti(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Returns the effective permissions for a resource, broken down by user/group/role.
+     *
+     * @param request wrapper containing the resource info and access context
+     * @return {@code 200 OK} with {@link RangerResourcePermissions}, or {@code 400} / {@code 500} on error
+     */
+    @POST
+    @Path("/permissions")
+    public Response getResourcePermissions(RangerResourcePermissionsRequest request, @Context HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String caller      = getAuthenticatedUser(httpRequest);
+            String serviceName = getServiceName(request);
+
+            LOG.debug("==> getResourcePermissions(caller={}, serviceName={})", caller, serviceName);
+
+            ret = validateCaller(caller, serviceName);
+
+            if (isStatusOk(ret)) {
+                try {
+                    RangerResourcePermissions result = authorizer.getResourcePermissions(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("getResourcePermissions(): validation error; caller={}", caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("getResourcePermissions(): unexpected error; caller={}", caller, e);
+
+                    ret = serverError();
+                }
+            }
+
+            LOG.debug("<== getResourcePermissions(caller={}, serviceName={}): ret={}", caller, serviceName, ret != null ? ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    private String getAuthenticatedUser(HttpServletRequest httpRequest) {
+        Object user = httpRequest.getAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER);
+
+        return user != null ? user.toString() : null;
+    }
+
+    private static String getServiceName(RangerAuthzRequest request) {
+        return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+    }
+
+    private static String getServiceName(RangerMultiAuthzRequest request) {
+        return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+    }
+
+    private static String getServiceName(RangerResourcePermissionsRequest request) {
+        return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+    }
+
+    private Response validateCaller(String caller, RangerUserInfo user, RangerAccessInfo access, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(UNAUTHORIZED)
+                    .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+                    .build();
+        } else if (user == null || StringUtils.isBlank(user.getName())) {
+            ret = Response.status(BAD_REQUEST)
+                    .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage()))
+                    .build();
+        } else {
+            boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(access);
+
+            if (needsDelegation) {
+                if (!isDelegationUserForService(serviceName, caller)) {
+                    LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+                    ret = Response.status(FORBIDDEN)
+                            .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+                            .build();
+                } else {
+                    ret = RESPONSE_OK;
+                }
+            } else {
+                ret = RESPONSE_OK;
+            }
+        }
+
+        return ret;
+    }
+
+    private Response validateCaller(String caller, RangerUserInfo user, List<RangerAccessInfo> accesses, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(UNAUTHORIZED)
+                    .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+                    .build();
+        } else if (user == null || StringUtils.isBlank(user.getName())) {
+            ret = Response.status(BAD_REQUEST)
+                    .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage()))
+                    .build();
+        } else {
+            boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(accesses);
+
+            if (needsDelegation) {
+                if (!isDelegationUserForService(serviceName, caller)) {
+                    LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+                    ret = Response.status(FORBIDDEN)
+                            .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+                            .build();
+                } else {
+                    ret = RESPONSE_OK;
+                }
+            } else {
+                ret = RESPONSE_OK;
+            }
+        }
+
+        return ret;
+    }
+
+    private Response validateCaller(String caller, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(UNAUTHORIZED)
+                    .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+                    .build();
+        } else if (!isDelegationUserForService(serviceName, caller)) {
+            LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+            ret = Response.status(FORBIDDEN)
+                    .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+                    .build();
+        } else {
+            ret = RESPONSE_OK;
+        }
+
+        return ret;
+    }
+
+    private boolean isDelegationNeeded(String caller, RangerUserInfo user) {
+        String  userName        = user.getName();
+        boolean needsDelegation = !caller.equals(userName);
+
+        if (!needsDelegation) {
+            // don't trust user-attributes/groups/roles if caller doesn't have delegation permission
+            needsDelegation = MapUtils.isNotEmpty(user.getAttributes()) || CollectionUtils.isNotEmpty(user.getGroups()) || CollectionUtils.isNotEmpty(user.getRoles());
+        }
+
+        return needsDelegation;
+    }
+
+    private boolean isDelegationNeeded(RangerAccessInfo access) {
+        RangerResourceInfo resource = access != null ? access.getResource() : null;
+
+        // delegation permission is needed when resource attributes are specified
+        return (resource != null && MapUtils.isNotEmpty(resource.getAttributes()));
+    }
+
+    private boolean isDelegationNeeded(List<RangerAccessInfo> accesses) {
+        if (accesses != null) {
+            for (RangerAccessInfo access : accesses) {
+                if (isDelegationNeeded(access)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private RangerPdpStats getRuntimeState(HttpServletRequest httpRequest) {
+        Object state = httpRequest.getServletContext().getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE);
+
+        if (state instanceof RangerPdpStats) {
+            return (RangerPdpStats) state;
+        } else {
+            Object fallback = servletContext != null ? servletContext.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE) : null;
+
+            return (fallback instanceof RangerPdpStats) ? (RangerPdpStats) fallback : new RangerPdpStats();
+        }
+    }
+
+    private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequest httpRequest) {
+        RangerPdpStats state   = getRuntimeState(httpRequest);
+        int            status  = ret != null ? ret.getStatus() : 500;
+        long           elapsed = System.nanoTime() - startNanos;
+
+        if (status >= 200 && status < 300) {
+            state.recordRequestSuccess(elapsed);
+        } else if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN
+            state.recordAuthFailure(elapsed);
+        } else if (status == 400) {
+            state.recordRequestBadRequest(elapsed);
+        } else {
+            state.recordRequestError(elapsed);
+        }
+    }
+
+    private boolean isDelegationUserForService(String serviceName, String userName) {
+        final boolean            ret;
+        Map<String, Set<String>> delegationUsersByService = this.delegationUsersByService;
+
+        if (delegationUsersByService.isEmpty()) {
+            ret = false;
+        } else if (StringUtils.isBlank(serviceName) || StringUtils.isBlank(userName)) {
+            ret = false;
+        } else {
+            Set<String> delegationUsers = delegationUsersByService.get(serviceName);
+
+            if (delegationUsers == null) {
+                delegationUsers = delegationUsersByService.get(RangerPdpConstants.WILDCARD_SERVICE_NAME);
+            }
+
+            ret = delegationUsers != null && delegationUsers.contains(userName);
+        }
+
+        LOG.debug("isDelegationUserForService(serviceName={}, userName={}): ret={}", serviceName, userName, ret);
+
+        return ret;
+    }
+
+    private void initializeDelegationUsers() {
+        Properties properties = config != null ? config.getAuthzProperties() : new Properties();
+
+        for (String key : properties.stringPropertyNames()) {
+            if (key.startsWith(PROP_PDP_SERVICE_PREFIX) && key.endsWith(PROP_SUFFIX_DELEGATION_USERS)) {
+                String serviceName = key.substring(PROP_PDP_SERVICE_PREFIX.length(), key.length() - PROP_SUFFIX_DELEGATION_USERS.length());
+
+                if (StringUtils.isBlank(serviceName)) {
+                    continue;
+                }
+
+                Set<String> delegationUsers = Arrays.stream(StringUtils.defaultString(properties.getProperty(key)).split(","))
+                                                 .map(String::trim)
+                                                 .filter(StringUtils::isNotBlank)
+                                                 .collect(Collectors.toSet());
+
+                if (!delegationUsers.isEmpty()) {
+                    delegationUsersByService.put(serviceName, delegationUsers);
+
+                    LOG.info("Delegation users for service '{}': {}", serviceName, delegationUsers);
+                }
+            }
+        }
+
+        if (delegationUsersByService.isEmpty()) {
+            LOG.warn("No delegation users configured");
+        } else {
+            Set<String> wildcardDelegationUsers = delegationUsersByService.get(WILDCARD_SERVICE_NAME);
+
+            // delegation users for WILDCARD_SERVICE_NAME are delegates for all services
+            if (wildcardDelegationUsers != null && !wildcardDelegationUsers.isEmpty()) {
+                delegationUsersByService.forEach((k, v) -> v.addAll(wildcardDelegationUsers));
+            }
+        }
+    }
+
+    private Response badRequest(RangerAuthzException e) {
+        return Response.status(BAD_REQUEST)
+                       .entity(new ErrorResponse(BAD_REQUEST, e.getMessage()))
+                       .build();
+    }
+
+    private Response serverError() {
+        return Response.serverError()
+                       .entity(new ErrorResponse(INTERNAL_SERVER_ERROR, "Internal Server Error"))
+                       .build();
+    }
+
+    private boolean isStatusOk(Response resp) {
+        return resp != null && resp.getStatus() == OK.getStatusCode();
+    }
+}
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
new file mode 100644
index 000000000..60df86daf
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
@@ -0,0 +1,71 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Properties;
+
+/**
+ * Authenticates requests by extracting the username from a trusted HTTP request header.
+ *
+ * <p>This handler is intended for deployments where a trusted reverse proxy / API gateway
+ * has already authenticated the caller and propagates the identity via a header
+ * (e.g., {@code X-Remote-User}).  The header value is accepted as-is; no further
+ * validation is performed.
+ *
+ * <p><strong>Security note:</strong> Only enable this handler when the Ranger PDP server
+ * is reachable exclusively through the trusted proxy.  Direct client access would allow
+ * unauthenticated identity spoofing.
+ */
+public class HttpHeaderAuthNHandler implements PdpAuthNHandler {
+    private static final Logger LOG = LoggerFactory.getLogger(HttpHeaderAuthNHandler.class);
+
+    public static final String AUTH_TYPE = "HEADER";
+
+    private String usernameHeader;
+
+    @Override
+    public void init(Properties config) {
+        usernameHeader = config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User");
+
+        LOG.info("HttpHeaderAuthHandler initialized; username header={}", usernameHeader);
+    }
+
+    @Override
+    public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+        String userName = request.getHeader(usernameHeader);
+
+        LOG.debug("authenticate(): user={} (from header {})", userName, usernameHeader);
+
+        return StringUtils.isBlank(userName) ? Result.skip() : Result.authenticated(userName.trim(), AUTH_TYPE);
+    }
+
+    @Override
+    public String getChallengeHeader() {
+        return null;
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java
new file mode 100644
index 000000000..4e45c0031
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java
@@ -0,0 +1,105 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.ranger.authz.handler.RangerAuth;
+import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Properties;
+
+/**
+ * Authenticates requests using a JWT bearer token.
+ *
+ * <p>Checks for the token in the {@code Authorization: Bearer <token>} header first,
+ * then in the configured JWT cookie.  Delegates signature verification and expiry/audience
+ * checks to {@link RangerDefaultJwtAuthHandler} from the {@code ranger-authn} module.
+ *
+ * <p>Configuration keys (all prefixed with {@code ranger.pdp.authn.jwt.}):
+ * <ul>
+ *   <li>{@code provider.url}  – JWKS endpoint URL (optional if public key is set)
+ *   <li>{@code public.key}    – PEM-encoded public key (optional if provider URL is set)
+ *   <li>{@code cookie.name}   – JWT cookie name (default: {@code hadoop-jwt})
+ *   <li>{@code audiences}     – comma-separated list of accepted audiences (optional)
+ * </ul>
+ */
+public class JwtAuthNHandler implements PdpAuthNHandler {
+    private static final Logger LOG = LoggerFactory.getLogger(JwtAuthNHandler.class);
+
+    public static final String AUTH_TYPE = "JWT";
+
+    private RangerDefaultJwtAuthHandler delegate;
+
+    @Override
+    public void init(Properties config) throws Exception {
+        Properties jwtConfig = new Properties();
+
+        copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, jwtConfig, RangerDefaultJwtAuthHandler.KEY_PROVIDER_URL);
+        copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_PUBLIC_KEY);
+        copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_COOKIE_NAME);
+        copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_AUDIENCES);
+
+        delegate = new RangerDefaultJwtAuthHandler();
+
+        delegate.initialize(jwtConfig);
+
+        LOG.info("JwtAuthHandler initialized");
+    }
+
+    @Override
+    public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+        if (!RangerDefaultJwtAuthHandler.canAuthenticateRequest(request)) {
+            return Result.skip();
+        }
+
+        RangerAuth rangerAuth = delegate.authenticate(request);
+
+        if (rangerAuth != null && rangerAuth.isAuthenticated()) {
+            LOG.debug("authenticate(): user={}", rangerAuth.getUserName());
+
+            return Result.authenticated(rangerAuth.getUserName(), AUTH_TYPE);
+        }
+
+        LOG.warn("authenticate(): JWT validation failed");
+
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.addHeader("WWW-Authenticate", getChallengeHeader() + ", error=\"invalid_token\"");
+
+        return Result.challenge();
+    }
+
+    @Override
+    public String getChallengeHeader() {
+        return "Bearer realm=\"Ranger PDP\"";
+    }
+
+    private void copyIfPresent(Properties src, String srcKey, Properties dst, String dstKey) {
+        String val = src.getProperty(srcKey);
+
+        if (val != null && !val.isEmpty()) {
+            dst.setProperty(dstKey, val);
+        }
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java
new file mode 100644
index 000000000..e46e3e5d3
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java
@@ -0,0 +1,275 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Authenticates requests using Kerberos SPNEGO (HTTP Negotiate).
+ *
+ * <p>Uses the JDK's built-in GSSAPI/JGSS support – no external Kerberos library is required.
+ * The service principal and keytab must be configured via:
+ * <ul>
+ *   <li>{@code ranger.pdp.authn.kerberos.spnego.principal} – e.g. {@code HTTP/host.example.com@REALM}
+ *   <li>{@code ranger.pdp.authn.kerberos.spnego.keytab}    – absolute path to the keytab file
+ *   <li>{@code ranger.pdp.authn.kerberos.name.rules}       – Hadoop-style name rules (default: {@code DEFAULT})
+ * </ul>
+ *
+ * <p>Authentication flow:
+ * <ol>
+ *   <li>If no {@code Authorization: Negotiate} header is present the handler returns {@code SKIP}.
+ *   <li>The SPNEGO token is extracted, validated via GSSAPI, and – if a response token is
+ *       produced (mutual authentication) – written to {@code WWW-Authenticate: Negotiate <token>}.
+ *   <li>On success the short-form principal name (strip {@literal @REALM} and host components)
+ *       is returned as the authenticated user.
+ *   <li>On failure a {@code 401 Negotiate} challenge is sent and {@code CHALLENGE} is returned.
+ * </ol>
+ */
+public class KerberosAuthNHandler implements PdpAuthNHandler {
+    private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthNHandler.class);
+
+    public static final String AUTH_TYPE = "KERBEROS";
+
+    private static final String NEGOTIATE_PREFIX    = "Negotiate ";
+    private static final String WWW_AUTHENTICATE    = "WWW-Authenticate";
+    private static final String AUTHORIZATION       = "Authorization";
+    private static final Oid    SPNEGO_OID;
+    private static final Oid    KRB5_OID;
+
+    static {
+        try {
+            SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
+            KRB5_OID   = new Oid("1.2.840.113554.1.2.2");
+        } catch (GSSException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    private Subject       serviceSubject;
+    private GSSManager    gssManager;
+    private GSSCredential serverCred;
+
+    @Override
+    public void init(Properties config) throws Exception {
+        String principal     = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL);
+        String keytab        = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB);
+        String nameRules     = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT");
+        String tokenValidity = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY);
+
+        int tokenLifetime = StringUtils.isBlank(tokenValidity) ? GSSCredential.INDEFINITE_LIFETIME : Integer.parseInt(tokenValidity);
+
+        if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) {
+            throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB);
+        }
+
+        serviceSubject = loginWithKeytab(principal, keytab);
+
+        initializeKerberosNameRules(nameRules);
+
+        gssManager = GSSManager.getInstance();
+
+        GSSName serverName = gssManager.createName(principal, GSSName.NT_USER_NAME);
+
+        // Create acceptor credentials in the logged-in Subject to avoid fallback to JVM default keytab.
+        serverCred = Subject.doAs(serviceSubject, (PrivilegedExceptionAction<GSSCredential>) () ->
+                gssManager.createCredential(serverName, tokenLifetime, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY));
+
+        LOG.info("KerberosAuthNHandler initialized; principal={} (bound acceptor credential to configured principal)", principal);
+    }
+
+    @Override
+    public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+        String authHeader = request.getHeader(AUTHORIZATION);
+
+        if (authHeader == null || !authHeader.startsWith(NEGOTIATE_PREFIX)) {
+            return Result.skip();
+        }
+
+        byte[] inputToken = Base64.decodeBase64(authHeader.substring(NEGOTIATE_PREFIX.length()).trim());
+
+        try {
+            return Subject.doAs(serviceSubject, (PrivilegedExceptionAction<Result>) () -> validateSpnegoToken(inputToken, response));
+        } catch (Exception e) {
+            LOG.warn("authenticate(): SPNEGO validation error", e);
+
+            sendUnauthorized(response, null);
+
+            return Result.challenge();
+        }
+    }
+
+    @Override
+    public String getChallengeHeader() {
+        return NEGOTIATE_PREFIX.trim();
+    }
+
+    private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse response) throws GSSException {
+        GSSContext gssCtx = gssManager.createContext(serverCred);
+
+        try {
+            byte[] outputToken = gssCtx.acceptSecContext(inputToken, 0, inputToken.length);
+
+            if (outputToken != null && outputToken.length > 0) {
+                response.addHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken));
+            }
+
+            if (!gssCtx.isEstablished()) {
+                sendUnauthorized(response, outputToken);
+
+                return Result.challenge();
+            }
+
+            String principal = gssCtx.getSrcName().toString();
+            String userName  = applyNameRules(principal);
+
+            LOG.debug("authenticate(): SPNEGO success, principal={}, user={}", principal, userName);
+
+            return Result.authenticated(userName, AUTH_TYPE);
+        } finally {
+            try {
+                gssCtx.dispose();
+            } catch (GSSException excp) {
+                LOG.debug("GSSContext.dispose() failed. Ignored", excp);
+            }
+        }
+    }
+
+    private void sendUnauthorized(HttpServletResponse response, byte[] outputToken) {
+        if (!response.isCommitted()) {
+            if (outputToken != null && outputToken.length > 0) {
+                response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken));
+            } else {
+                response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX.trim());
+            }
+
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        }
+    }
+
+    /**
+     * Applies configured auth_to_local rules.
+     *
+     * <p>Uses Hadoop {@link KerberosName} with configured auth_to_local rules.
+     * Falls back to DEFAULT short-name mapping only if transformation fails.
+     */
+    private String applyNameRules(String principal) {
+        try {
+            String shortName = new KerberosName(principal).getShortName();
+
+            if (StringUtils.isNotBlank(shortName)) {
+                return shortName;
+            }
+        } catch (Exception e) {
+            LOG.warn("Failed KerberosName transformation for principal '{}'; using DEFAULT short-name mapping", principal, e);
+        }
+
+        return defaultShortName(principal);
+    }
+
+    private void initializeKerberosNameRules(String configuredRules) {
+        String effectiveRules = StringUtils.defaultIfBlank(configuredRules, "DEFAULT").trim();
+
+        KerberosName.setRules(effectiveRules);
+
+        LOG.info("Initialized Kerberos name rules: {}='{}'", RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, effectiveRules);
+    }
+
+    private String defaultShortName(String principal) {
+        String name   = principal;
+        int    atSign = name.indexOf('@');
+
+        if (atSign >= 0) {
+            name = name.substring(0, atSign);
+        }
+
+        int slash = name.indexOf('/');
+
+        if (slash >= 0) {
+            name = name.substring(0, slash);
+        }
+
+        return name;
+    }
+
+    private Subject loginWithKeytab(String principal, String keytab) throws LoginException {
+        Configuration jaasConfig = new Configuration() {
+            @Override
+            public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+                Map<String, String> opts = new HashMap<>();
+
+                opts.put("useKeyTab", "true");
+                opts.put("storeKey", "true");
+                opts.put("doNotPrompt", "true");
+                opts.put("useTicketCache", "false");
+                opts.put("keyTab", keytab);
+                opts.put("principal", principal);
+                opts.put("refreshKrb5Config", "true");
+                opts.put("isInitiator", "false");
+
+                return new AppConfigurationEntry[] {
+                    new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts)
+                };
+            }
+        };
+
+        LoginContext lc = new LoginContext("Ranger-PDP-Kerberos", null, noOpCallbackHandler(), jaasConfig);
+
+        lc.login();
+
+        return lc.getSubject();
+    }
+
+    private static CallbackHandler noOpCallbackHandler() {
+        return (Callback[] callbacks) -> {
+            for (Callback cb : callbacks) {
+                LOG.warn("Unexpected JAAS callback: {}", cb.getClass().getName());
+
+                throw new UnsupportedCallbackException(cb);
+            }
+        };
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java
new file mode 100644
index 000000000..4bb11aea5
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java
@@ -0,0 +1,111 @@
+/*
+ * 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.pdp.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Contract for PDP authentication handlers.
+ *
+ * <p>Each handler is responsible for a single credential type (Kerberos, JWT, HTTP Header).
+ * The {@link RangerPdpAuthNFilter} tries handlers in configured order and uses the first one
+ * that returns {@link Result.Status#AUTHENTICATED}.
+ */
+public interface PdpAuthNHandler {
+    /**
+     * Initializes the handler with filter init parameters.
+     *
+     * @param config filter init parameters
+     * @throws Exception on initialization failure
+     */
+    void init(Properties config) throws Exception;
+
+    /**
+     * Attempts to authenticate the incoming request.
+     *
+     * <p>Handlers must follow this contract:
+     * <ul>
+     *   <li>{@link Result.Status#AUTHENTICATED} - credentials were present and valid.
+     *       The handler may write {@code WWW-Authenticate} response headers (e.g., for SPNEGO
+     *       mutual authentication) but must NOT commit the response.
+     *   <li>{@link Result.Status#CHALLENGE} - credentials were present but invalid, or a
+     *       multi-round negotiation step was sent. The handler has already written a
+     *       {@code 401} response with an appropriate challenge header; the filter must not
+     *       continue processing.
+     *   <li>{@link Result.Status#SKIP} - this handler cannot process the request (the expected
+     *       credential type is absent). The filter should try the next handler.
+     * </ul>
+     *
+     * @param request  the HTTP request
+     * @param response the HTTP response (may be written to for challenges)
+     * @return authentication result
+     * @throws IOException on I/O error
+     */
+    Result authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException;
+
+    /**
+     * Returns the {@code WWW-Authenticate} header value to include in a terminal 401
+     * response when no handler can process the request.
+     * Returns {@code null} if this handler does not contribute a challenge header.
+     */
+    String getChallengeHeader();
+
+    class Result {
+        public enum Status { AUTHENTICATED, CHALLENGE, SKIP }
+
+        private final Status status;
+        private final String userName;
+        private final String authType;
+
+        private Result(Status status, String userName, String authType) {
+            this.status   = status;
+            this.userName = userName;
+            this.authType = authType;
+        }
+
+        public static Result authenticated(String userName, String authType) {
+            return new Result(Status.AUTHENTICATED, userName, authType);
+        }
+
+        public static Result challenge() {
+            return new Result(Status.CHALLENGE, null, null);
+        }
+
+        public static Result skip() {
+            return new Result(Status.SKIP, null, null);
+        }
+
+        public Status getStatus() {
+            return status;
+        }
+
+        public String getUserName() {
+            return userName;
+        }
+
+        public String getAuthType() {
+            return authType;
+        }
+    }
+}
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
new file mode 100644
index 000000000..32d04ef0a
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
@@ -0,0 +1,189 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_AUTHN_TYPES;
+
+/**
+ * Servlet filter that enforces authentication for all PDP REST endpoints.
+ *
+ * <p>Handlers are configured via the {@code ranger.pdp.authn.types} filter init parameter
+ * (comma-separated list of {@code header}, {@code jwt}, {@code kerberos}).  Handlers are
+ * tried in the listed order; the first successful match wins.
+ *
+ * <p>On success the authenticated username is stored in the request attribute
+ * {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} so that REST resources can read it.
+ *
+ * <p>If all handlers return {@code SKIP} (no recognisable credentials found), the filter
+ * sends a {@code 401} response with {@code WWW-Authenticate} headers for every
+ * configured handler that provides a challenge.
+ */
+public class RangerPdpAuthNFilter implements Filter {
+    private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthNFilter.class);
+
+    private final List<PdpAuthNHandler> handlers = new ArrayList<>();
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        Properties config     = toProperties(filterConfig);
+        String     authnTypes = filterConfig.getInitParameter(PROP_AUTHN_TYPES);
+
+        if (StringUtils.isNotBlank(authnTypes)) {
+            for (String authnType : authnTypes.split(",")) {
+                PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig);
+
+                if (handler == null) {
+                    continue;
+                }
+
+                try {
+                    handler.init(config);
+
+                    handlers.add(handler);
+
+                    LOG.info("{}: successfully registered authentication handler", authnType);
+                } catch (Exception excp) {
+                    LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp);
+                }
+            }
+        }
+
+        if (handlers.isEmpty()) {
+            throw new ServletException("No valid authentication handlers configured");
+        }
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest  httpReq  = (HttpServletRequest) request;
+        HttpServletResponse httpResp = (HttpServletResponse) response;
+
+        for (PdpAuthNHandler handler : handlers) {
+            PdpAuthNHandler.Result result = handler.authenticate(httpReq, httpResp);
+
+            switch (result.getStatus()) {
+                case AUTHENTICATED:
+                    httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER, result.getUserName());
+                    httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHN_TYPE, result.getAuthType());
+
+                    LOG.debug("doFilter(): authenticated user={}, type={}", result.getUserName(), result.getAuthType());
+
+                    chain.doFilter(request, response);
+                    return;
+
+                case CHALLENGE:
+                    // handler has already written the 401; stop processing
+                    return;
+
+                case SKIP:
+                default:
+                    // try the next handler
+                    break;
+            }
+        }
+
+        // No handler could authenticate the request; send a 401 with all challenge headers
+        LOG.debug("doFilter(): no handler authenticated request from {}", httpReq.getRemoteAddr());
+
+        sendUnauthenticated(httpResp);
+    }
+
+    @Override
+    public void destroy() {
+        handlers.clear();
+    }
+
+    private void sendUnauthenticated(HttpServletResponse response) throws IOException {
+        for (PdpAuthNHandler handler : handlers) {
+            String challenge = handler.getChallengeHeader();
+
+            if (StringUtils.isNotBlank(challenge)) {
+                response.addHeader("WWW-Authenticate", challenge);
+            }
+        }
+
+        response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
+        response.setContentType(MediaType.APPLICATION_JSON);
+        response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"Authentication required\"}");
+    }
+
+    private PdpAuthNHandler createHandler(String type, FilterConfig filterConfig) {
+        final PdpAuthNHandler ret;
+
+        switch (type) {
+            case "header":
+                ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED) ? new HttpHeaderAuthNHandler() : null;
+                break;
+            case "jwt":
+                ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_JWT_ENABLED) ? new JwtAuthNHandler() : null;
+                break;
+            case "kerberos":
+                ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED) ? new KerberosAuthNHandler() : null;
+                break;
+            default:
+                ret = null;
+                break;
+        }
+
+        if (ret == null) {
+            LOG.warn("{}: authentication type unknown or disabled. Ignored", type);
+        }
+
+        return ret;
+    }
+
+    private Properties toProperties(FilterConfig filterConfig) {
+        Properties                    props = new Properties();
+        java.util.Enumeration<String> names = filterConfig.getInitParameterNames();
+
+        while (names.hasMoreElements()) {
+            String name = names.nextElement();
+
+            props.setProperty(name, filterConfig.getInitParameter(name));
+        }
+
+        return props;
+    }
+
+    private boolean getBoolean(FilterConfig config, String name) {
+        return Boolean.parseBoolean(config.getInitParameter(name));
+    }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java
new file mode 100644
index 000000000..8d9ccc16f
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java
@@ -0,0 +1,73 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.MDC;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.UUID;
+
+public class RangerPdpRequestContextFilter implements Filter {
+    public static final String REQ_HEADER_REQUEST_ID = "X-Request-Id";
+    public static final String RES_HEADER_REQUEST_ID = "X-Request-Id";
+    public static final String MDC_REQUEST_ID        = "requestId";
+
+    @Override
+    public void init(FilterConfig filterConfig) {
+        // no-op
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest  req = (HttpServletRequest) request;
+        HttpServletResponse res = (HttpServletResponse) response;
+
+        String requestId = req.getHeader(REQ_HEADER_REQUEST_ID);
+
+        if (StringUtils.isBlank(requestId)) {
+            requestId = UUID.randomUUID().toString();
+        }
+
+        MDC.put(MDC_REQUEST_ID, requestId);
+        req.setAttribute(MDC_REQUEST_ID, requestId);
+        res.setHeader(RES_HEADER_REQUEST_ID, requestId);
+
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            MDC.remove(MDC_REQUEST_ID);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        // no-op
+    }
+}
diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml
new file mode 100644
index 000000000..ddf2ff934
--- /dev/null
+++ b/pdp/src/main/resources/ranger-pdp-default.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  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.
+-->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+    <description>Port the PDP server listens on.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+    <description>Directory for PDP server log files.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value/>
+    <description>Comma-separated users, allowed to call on behalf of other users in all services</description>
+  </property>
+
+  <!-- SSL/TLS -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+    <description>Set to true to enable HTTPS.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.file</name>
+    <value/>
+    <description>Path to the keystore file (required when SSL is enabled).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.password</name>
+    <value/>
+    <description>Keystore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.type</name>
+    <value>JKS</value>
+    <description>Keystore type (JKS, PKCS12).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.enabled</name>
+    <value>false</value>
+    <description>Set to true to require client certificate authentication.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.file</name>
+    <value/>
+    <description>Path to the truststore file.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.password</name>
+    <value/>
+    <description>Truststore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.type</name>
+    <value>JKS</value>
+    <description>Truststore type (JKS, PKCS12).</description>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+    <description>
+      Enable HTTP/2 via upgrade on the connector.
+      Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1.
+    </description>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+    <description>Maximum number of worker threads handling simultaneous requests.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+    <description>Minimum number of spare worker threads kept ready.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+    <description>Queued connection backlog when all worker threads are busy.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+    <description>Maximum concurrent TCP connections accepted by the connector.</description>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.types</name>
+    <value>header,jwt,kerberos</value>
+    <description>
+      Comma-separated list of authentication methods for incoming REST requests,
+      tried in listed order. Supported values: header, jwt, kerberos.
+    </description>
+  </property>
+
+  <!-- HTTP Header authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.header.enabled</name>
+    <value>false</value>
+    <description>
+      Enable trusted HTTP header authentication. Use only behind a trusted proxy.
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.header.username</name>
+    <value>X-Forwarded-User</value>
+    <description>HTTP header name from which the authenticated username is read.</description>
+  </property>
+
+  <!-- JWT bearer token authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.jwt.enabled</name>
+    <value>false</value>
+    <description>Enable JWT authentication.</description>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.jwt.provider.url</name>
+    <value/>
+    <description>URL of the JWT provider (used to fetch the public key).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.public.key</name>
+    <value/>
+    <description>PEM-encoded public key for verifying JWT signatures.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.cookie.name</name>
+    <value>hadoop-jwt</value>
+    <description>Cookie name from which a JWT bearer token may be read.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.jwt.audiences</name>
+    <value/>
+    <description>Comma-separated list of accepted JWT audiences. Empty means any audience is accepted.</description>
+  </property>
+
+  <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.kerberos.enabled</name>
+    <value>false</value>
+    <description>Enable Kerberos authentication.</description>
+  </property>
+
+    <property>
+    <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+    <value/>
+    <description>Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+    <value/>
+    <description>Path to the keytab file for the SPNEGO service principal.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.token.valid.seconds</name>
+    <value>3600</value>
+    <description>Validity period (seconds) for Kerberos authentication tokens.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.kerberos.name.rules</name>
+    <value>DEFAULT</value>
+    <description>Rules for mapping Kerberos principal names to short usernames.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.app.type</name>
+    <value>ranger-pdp</value>
+    <description>Application type reported in audit records.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.init.services</name>
+    <value/>
+    <description>
+      Comma-separated list of Ranger service names to eagerly initialize at startup.
+      Leave empty for lazy initialization on first access.
+      Example: dev_hive,dev_hdfs
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.source.impl</name>
+    <value>org.apache.ranger.admin.client.RangerAdminRESTClient</value>
+    <description>Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.url</name>
+    <value>http://localhost:6080</value>
+    <description>
+      URL of the Ranger Admin server. Comma-separated for HA.
+      Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.ssl.config.file</name>
+    <value/>
+    <description>
+      Path to the XML file containing SSL keystore/truststore settings for the
+      connection to Ranger Admin (required when the Admin URL uses https://).
+      The file uses the standard Ranger policymgr SSL property names:
+        xasecure.policymgr.clientssl.truststore
+        xasecure.policymgr.clientssl.truststore.credential.file
+        xasecure.policymgr.clientssl.keystore             (for mutual TLS)
+        xasecure.policymgr.clientssl.keystore.credential.file
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.connection.timeoutMs</name>
+    <value>120000</value>
+    <description>HTTP connection timeout (ms) for calls to Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.read.timeoutMs</name>
+    <value>30000</value>
+    <description>HTTP read timeout (ms) for calls to Ranger Admin.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.pollIntervalMs</name>
+    <value>30000</value>
+    <description>How often (ms) to poll Ranger Admin for updated policies and roles.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.cache.dir</name>
+    <value>/var/ranger/cache/pdp</value>
+    <description>
+      Directory for local JSON cache files (policies, tags, roles, userstore, GDS).
+      The PDP can serve authorization requests from cache while Ranger Admin is unavailable.
+    </description>
+  </property>
+
+  <!-- Basic Auth authentication for connecting to Ranger Admin -->
+  <property>
+    <name>ranger.authz.default.policy.rest.client.username</name>
+    <value>admin</value>
+    <description>Username for connecting to Ranger Admin (basic auth).</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.policy.rest.client.password</name>
+    <value>admin</value>
+    <description>Password for connecting to Ranger Admin (basic auth).</description>
+  </property>
+
+  <!-- Kerberos authentication for connecting to Ranger Admin-->
+  <property>
+    <name>ranger.authz.default.ugi.initialize</name>
+    <value>false</value>
+    <description>Set to true to enable Kerberos login for the Ranger Admin connection.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.login.type</name>
+    <value/>
+    <description>Kerberos login type: keytab or jaas.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.keytab.principal</name>
+    <value/>
+    <description>Kerberos principal for keytab-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.keytab.file</name>
+    <value/>
+    <description>Path to the keytab file for keytab-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.ugi.jaas.appconfig</name>
+    <value/>
+    <description>JAAS application configuration name for jaas-based login.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.default.use.rangerGroups</name>
+    <value>false</value>
+    <description>
+      When true, group membership is resolved from Ranger's internal userstore
+      (downloaded from Ranger Admin) rather than the local OS.
+    </description>
+  </property>
+
+  <!-- Audit -->
+  <property>
+    <name>ranger.authz.audit.is.enabled</name>
+    <value>true</value>
+    <description>Master switch for audit logging.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr</name>
+    <value>false</value>
+    <description>Enable Solr as an audit destination.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.solr.urls</name>
+    <value/>
+    <description>Solr URL for audit logging.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.hdfs</name>
+    <value>false</value>
+    <description>Enable HDFS as an audit destination.</description>
+  </property>
+
+  <property>
+    <name>ranger.authz.audit.destination.hdfs.dir</name>
+    <value/>
+    <description>HDFS directory path for audit log files.</description>
+  </property>
+</configuration>
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java
new file mode 100644
index 000000000..5ed3cadd3
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.pdp;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpStatsTest {
+    @Test
+    public void testRequestCountersAndAverageLatency() {
+        RangerPdpStats stats = new RangerPdpStats();
+
+        stats.recordRequestSuccess(5_000_000L);    // 5 ms
+        stats.recordRequestBadRequest(15_000_000L); // 15 ms
+        stats.recordRequestError(10_000_000L);      // 10 ms
+        stats.recordAuthFailure(10_000_000L);
+
+        assertEquals(4L, stats.getTotalRequests());
+        assertEquals(1L, stats.getTotalAuthzSuccess());
+        assertEquals(1L, stats.getTotalAuthzBadRequest());
+        assertEquals(1L, stats.getTotalAuthzErrors());
+        assertEquals(1L, stats.getTotalAuthFailures());
+        assertEquals(40_000_000L, stats.getTotalLatencyNanos());
+        assertEquals(10L, stats.getAverageLatencyMs());
+    }
+
+    @Test
+    public void testNegativeLatencyIsClampedToZero() {
+        RangerPdpStats stats = new RangerPdpStats();
+
+        stats.recordRequestSuccess(-100L);
+
+        assertEquals(0L, stats.getTotalLatencyNanos());
+        assertEquals(0L, stats.getAverageLatencyMs());
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java
new file mode 100644
index 000000000..3b8f16c74
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.pdp;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerPdpStatusServletTest {
+    @AfterEach
+    public void clearOverrides() {
+        System.clearProperty(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR);
+    }
+
+    @Test
+    public void testMetricsEndpointRendersCounters() throws Exception {
+        RangerPdpStats stats = new RangerPdpStats();
+
+        stats.recordRequestSuccess(5_000_000L);
+        stats.recordRequestError(5_000_000L);
+        stats.recordAuthFailure(5_000_000L);
+
+        RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(stats, RangerPdpStatusServlet.Mode.METRICS);
+        HttpServletRequest     req     = proxy(HttpServletRequest.class, (proxy, method, args) -> null);
+        ResponseCapture        capture = new ResponseCapture();
+        HttpServletResponse    resp    = capture.responseProxy();
+
+        servlet.doGet(req, resp);
+
+        assertEquals(HttpServletResponse.SC_OK, capture.status);
+        assertTrue(capture.body.toString().contains("ranger_pdp_requests_total 3"));
+        assertTrue(capture.body.toString().contains("ranger_pdp_auth_failures_total 1"));
+    }
+
+    @Test
+    public void testLoadedServicesCount() throws Exception {
+        RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(new RangerPdpStats(), RangerPdpStatusServlet.Mode.READY);
+        HttpServletRequest     req     = proxy(HttpServletRequest.class, (proxy, method, args) -> null);
+        Method                 method  = RangerPdpStatusServlet.class.getDeclaredMethod("getLoadedServicesCount", HttpServletRequest.class);
+
+        method.setAccessible(true);
+
+        Integer servicesCount = (Integer) method.invoke(servlet, req);
+
+        assertTrue(servicesCount >= 0L);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> T proxy(Class<T> iface, InvocationHandler handler) {
+        return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[] {iface}, handler);
+    }
+
+    private static final class ResponseCapture {
+        private       int                 status;
+        private final StringWriter        body    = new StringWriter();
+        private final Map<String, String> headers = new HashMap<>();
+
+        private HttpServletResponse responseProxy() {
+            return proxy(HttpServletResponse.class, (proxy, method, args) -> {
+                switch (method.getName()) {
+                    case "setStatus":
+                        status = (Integer) args[0];
+                        return null;
+                    case "setContentType":
+                        headers.put("Content-Type", (String) args[0]);
+                        return null;
+                    case "getWriter":
+                        return new PrintWriter(body, true);
+                    case "setHeader":
+                        headers.put((String) args[0], (String) args[1]);
+                        return null;
+                    default:
+                        return null;
+                }
+            });
+        }
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java
new file mode 100644
index 000000000..79119171b
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.pdp.config;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpConfigTest {
+    @AfterEach
+    public void clearSystemOverrides() {
+        System.clearProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME);
+        System.clearProperty(RangerPdpConstants.PROP_PORT);
+    }
+
+    @Test
+    public void testHeaderUserNameCanBeOverriddenBySystemProperty() {
+        System.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Test-User");
+
+        RangerPdpConfig config = new RangerPdpConfig();
+
+        assertEquals("X-Test-User", config.getHeaderAuthnUsername());
+    }
+
+    @Test
+    public void testInvalidPortFallsBackToDefault() {
+        System.setProperty(RangerPdpConstants.PROP_PORT, "not-a-number");
+
+        RangerPdpConfig config = new RangerPdpConfig();
+
+        assertEquals(6500, config.getPort());
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java
new file mode 100644
index 000000000..1221af3bb
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java
@@ -0,0 +1,360 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzApiErrorCode;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.model.RangerAccessContext;
+import org.apache.ranger.authz.model.RangerAuthzRequest;
+import org.apache.ranger.authz.model.RangerAuthzResult;
+import org.apache.ranger.authz.model.RangerMultiAuthzRequest;
+import org.apache.ranger.authz.model.RangerMultiAuthzResult;
+import org.apache.ranger.authz.model.RangerResourcePermissions;
+import org.apache.ranger.authz.model.RangerResourcePermissionsRequest;
+import org.apache.ranger.authz.model.RangerUserInfo;
+import org.apache.ranger.pdp.RangerPdpStats;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpRESTTest {
+    private static final String              CALLER_DELEGATION_ALLOWED     = "alice";
+    private static final String              CALLER_DELEGATION_NOT_ALLOWED = "bob";
+    private static final RangerUserInfo      USER_USER1                    = new RangerUserInfo("user1");
+    private static final RangerAccessContext CONTEXT_SERVICE_SVC1          = new RangerAccessContext("hive", "svc1");
+
+    @Test
+    public void testAuthorizeReturnsUnauthorizedWhenCallerMissing() throws Exception {
+        RangerPdpStats     stats      = new RangerPdpStats();
+        TestAuthorizer     authorizer = new TestAuthorizer();
+        RangerPdpREST      rest       = createRest(authorizer, stats);
+        RangerAuthzRequest request    = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response           response   = rest.authorize(request, httpRequest(null, stats));
+
+        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testAuthorizeReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+        RangerPdpStats     stats      = new RangerPdpStats();
+        TestAuthorizer     authorizer = new TestAuthorizer();
+        RangerPdpREST      rest       = createRest(authorizer, stats);
+        RangerAuthzRequest request    = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response           response   = rest.authorize(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+        assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testAuthorizeReturnsOkAndRecordsSuccess() throws Exception {
+        RangerPdpStats     stats      = new RangerPdpStats();
+        TestAuthorizer     authorizer = new TestAuthorizer(new RangerAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW));
+        RangerPdpREST      rest       = createRest(authorizer, stats);
+        RangerAuthzRequest request    = new RangerAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response           response   = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalRequests());
+        assertEquals(1L, stats.getTotalAuthzSuccess());
+    }
+
+    @Test
+    public void testAuthorizeReturnsBadRequestOnAuthzException() throws Exception {
+        RangerPdpStats     stats      = new RangerPdpStats();
+        TestAuthorizer     authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+        RangerPdpREST      rest       = createRest(authorizer, stats);
+        RangerAuthzRequest request    = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response           response   = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthzBadRequest());
+    }
+
+    @Test
+    public void testAuthorizeMultiReturnsUnauthorizedWhenCallerMissing() throws Exception {
+        RangerPdpStats          stats      = new RangerPdpStats();
+        TestAuthorizer          authorizer = new TestAuthorizer();
+        RangerPdpREST           rest       = createRest(authorizer, stats);
+        RangerMultiAuthzRequest request    = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response                response   = rest.authorizeMulti(request, httpRequest(null, stats));
+
+        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testAuthorizeMultiReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+        RangerPdpStats          stats      = new RangerPdpStats();
+        TestAuthorizer          authorizer = new TestAuthorizer();
+        RangerPdpREST           rest       = createRest(authorizer, stats);
+        RangerMultiAuthzRequest request    = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response                response   = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+        assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testAuthorizeMultiReturnsOkAndRecordsSuccess() throws Exception {
+        RangerPdpStats          stats      = new RangerPdpStats();
+        TestAuthorizer          authorizer = new TestAuthorizer(new RangerMultiAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW));
+        RangerPdpREST           rest       = createRest(authorizer, stats);
+        RangerMultiAuthzRequest request    = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response                response   = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalRequests());
+        assertEquals(1L, stats.getTotalAuthzSuccess());
+    }
+
+    @Test
+    public void testAuthorizeMultiReturnsBadRequestOnAuthzException() throws Exception {
+        RangerPdpStats          stats      = new RangerPdpStats();
+        TestAuthorizer          authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+        RangerPdpREST           rest       = createRest(authorizer, stats);
+        RangerMultiAuthzRequest request    = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+        Response                response   = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthzBadRequest());
+    }
+
+    @Test
+    public void testGetResourcePermissionsReturnsUnauthorizedWhenCallerMissing() throws Exception {
+        RangerPdpStats                   stats      = new RangerPdpStats();
+        TestAuthorizer                   authorizer = new TestAuthorizer();
+        RangerPdpREST                    rest       = createRest(authorizer, stats);
+        RangerResourcePermissionsRequest request    = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+        Response                         response   = rest.getResourcePermissions(request, httpRequest(null, stats));
+
+        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testGetResourcePermissionsReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+        RangerPdpStats                   stats      = new RangerPdpStats();
+        TestAuthorizer                   authorizer = new TestAuthorizer();
+        RangerPdpREST                    rest       = createRest(authorizer, stats);
+        RangerResourcePermissionsRequest request    = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+        Response                         response   = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+        assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthFailures());
+    }
+
+    @Test
+    public void testGetResourcePermissionsReturnsOkAndRecordsSuccess() throws Exception {
+        RangerPdpStats                   stats      = new RangerPdpStats();
+        TestAuthorizer                   authorizer = new TestAuthorizer(new RangerResourcePermissions());
+        RangerPdpREST                    rest       = createRest(authorizer, stats);
+        RangerResourcePermissionsRequest request    = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+        Response                         response   = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalRequests());
+        assertEquals(1L, stats.getTotalAuthzSuccess());
+    }
+
+    @Test
+    public void testGetResourcePermissionsReturnsBadRequestOnAuthzException() throws Exception {
+        RangerPdpStats                   stats      = new RangerPdpStats();
+        TestAuthorizer                   authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+        RangerPdpREST                    rest       = createRest(authorizer, stats);
+        RangerResourcePermissionsRequest request    = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+        Response                         response   = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(1L, stats.getTotalAuthzBadRequest());
+    }
+
+    private static RangerPdpREST createRest(TestAuthorizer authorizer, RangerPdpStats stats) throws Exception {
+        RangerPdpREST rest = new RangerPdpREST();
+
+        setField(rest, "authorizer", authorizer);
+        setField(rest, "config", new TestConfig(defaultConfig()));
+        setField(rest, "servletContext", servletContextWithStats(stats));
+        rest.initialize();
+
+        return rest;
+    }
+
+    private static HttpServletRequest httpRequest(String user, RangerPdpStats stats) {
+        Map<String, Object> attrs = new HashMap<>();
+
+        if (user != null) {
+            attrs.put(RangerPdpConstants.ATTR_AUTHENTICATED_USER, user);
+        }
+
+        ServletContext servletContext = servletContextWithStats(stats);
+
+        return (HttpServletRequest) Proxy.newProxyInstance(
+                HttpServletRequest.class.getClassLoader(),
+                new Class<?>[] {HttpServletRequest.class},
+                (proxy, method, args) -> {
+                    switch (method.getName()) {
+                        case "getAttribute":
+                            return attrs.get(args[0]);
+                        case "setAttribute":
+                            attrs.put((String) args[0], args[1]);
+                            return null;
+                        case "getServletContext":
+                            return servletContext;
+                        default:
+                            return null;
+                    }
+                });
+    }
+
+    private static ServletContext servletContextWithStats(RangerPdpStats stats) {
+        Map<String, Object> attrs = new HashMap<>();
+
+        attrs.put(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE, stats);
+
+        return (ServletContext) Proxy.newProxyInstance(
+                ServletContext.class.getClassLoader(),
+                new Class<?>[] {ServletContext.class},
+                (proxy, method, args) -> {
+                    if ("getAttribute".equals(method.getName())) {
+                        return attrs.get(args[0]);
+                    } else if ("setAttribute".equals(method.getName())) {
+                        attrs.put((String) args[0], args[1]);
+                        return null;
+                    }
+                    return null;
+                });
+    }
+
+    private static void setField(Object target, String fieldName, Object value) throws Exception {
+        Field field = target.getClass().getDeclaredField(fieldName);
+
+        field.setAccessible(true);
+        field.set(target, value);
+    }
+
+    private static Properties defaultConfig() {
+        Properties ret = new Properties();
+
+        ret.setProperty(PROP_PDP_SERVICE_PREFIX +  "svc1" + PROP_SUFFIX_DELEGATION_USERS, CALLER_DELEGATION_ALLOWED);
+
+        return ret;
+    }
+
+    private static class TestConfig extends RangerPdpConfig {
+        private final Properties props;
+
+        TestConfig(Properties props) {
+            this.props = props;
+        }
+
+        @Override
+        public Properties getAuthzProperties() {
+            return new Properties(props);
+        }
+    }
+
+    private static class TestAuthorizer extends RangerAuthorizer {
+        private final RangerAuthzResult         authzResult;
+        private final RangerMultiAuthzResult    multiAuthzResult;
+        private final RangerResourcePermissions permissions;
+        private final RangerAuthzException      authzException;
+
+        TestAuthorizer() {
+            this(null, null, null, null);
+        }
+
+        TestAuthorizer(RangerAuthzResult result) {
+            this(result, null, null, null);
+        }
+
+        TestAuthorizer(RangerMultiAuthzResult result) {
+            this(null, result, null, null);
+        }
+
+        TestAuthorizer(RangerResourcePermissions permissions) {
+            this(null, null, permissions, null);
+        }
+
+        TestAuthorizer(RangerAuthzException excp) {
+            this(null, null, null, excp);
+        }
+
+        TestAuthorizer(RangerAuthzResult result, RangerMultiAuthzResult multiResult, RangerResourcePermissions permissions, RangerAuthzException excp) {
+            super(new Properties());
+
+            this.authzResult      = result;
+            this.multiAuthzResult = multiResult;
+            this.permissions      = permissions;
+            this.authzException   = excp;
+        }
+
+        @Override
+        public void init() {
+        }
+
+        @Override
+        public void close() {
+        }
+
+        @Override
+        public RangerAuthzResult authorize(RangerAuthzRequest request) throws RangerAuthzException {
+            if (authzException != null) {
+                throw authzException;
+            }
+
+            return authzResult != null ? authzResult : new RangerAuthzResult();
+        }
+
+        @Override
+        public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request) throws RangerAuthzException {
+            if (authzException != null) {
+                throw authzException;
+            }
+
+            return multiAuthzResult != null ? multiAuthzResult : new RangerMultiAuthzResult();
+        }
+
+        @Override
+        public RangerResourcePermissions getResourcePermissions(RangerResourcePermissionsRequest request) throws RangerAuthzException {
+            if (authzException != null) {
+                throw authzException;
+            }
+
+            return permissions != null ? permissions : new RangerResourcePermissions();
+        }
+    }
+}
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
new file mode 100644
index 000000000..c497be908
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HttpHeaderAuthNHandlerTest {
+    @Test
+    public void testAuthenticate_usesDefaultHeaderName() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        handler.init(config);
+
+        HttpServletRequest     request = requestWithHeader("X-Forwarded-User", "alice");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
+        assertEquals("alice", result.getUserName());
+        assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType());
+    }
+
+    @Test
+    public void testAuthenticate_usesConfiguredHeaderName() {
+        HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+        Properties             config  = new Properties();
+
+        config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Authenticated-User");
+
+        handler.init(config);
+
+        HttpServletRequest     request = requestWithHeader("X-Authenticated-User", "bob");
+        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
+        assertEquals("bob", result.getUserName());
+    }
+
+    private static HttpServletRequest requestWithHeader(String expectedHeader, String headerValue) {
+        InvocationHandler invocationHandler = (proxy, method, args) -> {
+            String methodName = method.getName();
+
+            if ("getHeader".equals(methodName)) {
+                return expectedHeader.equals(args[0]) ? headerValue : null;
+            } else if ("getHeaders".equals(methodName) || "getHeaderNames".equals(methodName)) {
+                return java.util.Collections.emptyEnumeration();
+            } else if ("getMethod".equals(methodName)) {
+                return "POST";
+            } else if ("toString".equals(methodName)) {
+                return "HttpServletRequest(test)";
+            }
+
+            return null;
+        };
+
+        return (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), new Class<?>[] {HttpServletRequest.class}, invocationHandler);
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java
new file mode 100644
index 000000000..891cd2bb8
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class KerberosAuthHandlerTest {
+    @AfterEach
+    public void restoreDefaultRules() {
+        KerberosName.setRules("DEFAULT");
+    }
+
+    @Test
+    public void testApplyNameRules_usesConfiguredRules() throws Exception {
+        KerberosAuthNHandler handler = new KerberosAuthNHandler();
+
+        invokePrivateVoid(handler, "initializeKerberosNameRules", new Class<?>[] {String.class}, "DEFAULT");
+
+        String shortName = (String) invokePrivate(handler, "applyNameRules", new Class<?>[] {String.class}, "alice@EXAMPLE.COM");
+
+        assertEquals("alice", shortName);
+    }
+
+    @Test
+    public void testApplyNameRules_fallsBackForInvalidPrincipal() throws Exception {
+        KerberosAuthNHandler handler = new KerberosAuthNHandler();
+
+        invokePrivateVoid(handler, "initializeKerberosNameRules", new Class<?>[] {String.class}, "DEFAULT");
+
+        String shortName = (String) invokePrivate(handler, "applyNameRules", new Class<?>[] {String.class}, "svc/host");
+
+        assertEquals("svc", shortName);
+    }
+
+    private static void invokePrivateVoid(Object target, String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
+        invokePrivate(target, methodName, paramTypes, args);
+    }
+
+    private static Object invokePrivate(Object target, String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
+        Method method = target.getClass().getDeclaredMethod(methodName, paramTypes);
+
+        method.setAccessible(true);
+
+        return method.invoke(target, args);
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java
new file mode 100644
index 000000000..0d0e183c5
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class RangerPdpAuthNFilterTest {
+    @Test
+    public void testInit_skipsHeaderHandlerWhenDisabled() {
+        RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter();
+        Map<String, String>  params = new HashMap<>();
+
+        params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header");
+        params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "false");
+
+        assertThrows(ServletException.class, () -> filter.init(new TestFilterConfig(params)));
+    }
+
+    @Test
+    public void testInit_registersHeaderHandlerWhenEnabled() throws Exception {
+        RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter();
+        Map<String, String>  params = new HashMap<>();
+
+        params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header");
+        params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "true");
+
+        filter.init(new TestFilterConfig(params));
+
+        Field handlersField = RangerPdpAuthNFilter.class.getDeclaredField("handlers");
+
+        handlersField.setAccessible(true);
+
+        @SuppressWarnings("unchecked")
+        List<PdpAuthNHandler> handlers = (List<PdpAuthNHandler>) handlersField.get(filter);
+
+        assertEquals(1, handlers.size());
+        assertEquals(HttpHeaderAuthNHandler.class, handlers.get(0).getClass());
+    }
+
+    private static final class TestFilterConfig implements FilterConfig {
+        private final Map<String, String> initParams;
+
+        private TestFilterConfig(Map<String, String> initParams) {
+            this.initParams = initParams;
+        }
+
+        @Override
+        public String getFilterName() {
+            return "testFilter";
+        }
+
+        @Override
+        public ServletContext getServletContext() {
+            return null;
+        }
+
+        @Override
+        public String getInitParameter(String name) {
+            return initParams.get(name);
+        }
+
+        @Override
+        public Enumeration<String> getInitParameterNames() {
+            return Collections.enumeration(initParams.keySet());
+        }
+    }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java
new file mode 100644
index 000000000..b23023ce4
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.pdp.security;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.MDC;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerPdpRequestContextFilterTest {
+    @Test
+    public void testUsesIncomingRequestIdAndClearsMdc() throws Exception {
+        RangerPdpRequestContextFilter filter               = new RangerPdpRequestContextFilter();
+        Map<String, Object>           requestAttrs         = new HashMap<>();
+        Map<String, String>           responseHeaders      = new HashMap<>();
+        final String[]                requestIdSeenInChain = new String[1];
+        String                        requestId            = "abc-123";
+
+        HttpServletRequest  req  = requestProxy(requestId, requestAttrs);
+        HttpServletResponse resp  = responseProxy(responseHeaders);
+        FilterChain         chain = chainProxy((request, response) -> requestIdSeenInChain[0] = MDC.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+
+        filter.doFilter(req, resp, chain);
+
+        assertEquals(requestId, requestAttrs.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+        assertEquals(requestId, responseHeaders.get(RangerPdpRequestContextFilter.RES_HEADER_REQUEST_ID));
+        assertEquals(requestId, requestIdSeenInChain[0]);
+        assertNull(MDC.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+    }
+
+    @Test
+    public void testGeneratesRequestIdWhenMissing() throws Exception {
+        RangerPdpRequestContextFilter filter          = new RangerPdpRequestContextFilter();
+        Map<String, Object>           requestAttrs    = new HashMap<>();
+        Map<String, String>           responseHeaders = new HashMap<>();
+
+        HttpServletRequest  req   = requestProxy(null, requestAttrs);
+        HttpServletResponse resp  = responseProxy(responseHeaders);
+        FilterChain         chain = chainProxy((request, response) -> {});
+
+        filter.doFilter(req, resp, chain);
+
+        Object requestId = requestAttrs.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID);
+
+        assertNotNull(requestId);
+        assertTrue(requestId.toString().length() > 10);
+        assertEquals(requestId.toString(), responseHeaders.get(RangerPdpRequestContextFilter.RES_HEADER_REQUEST_ID));
+    }
+
+    private static HttpServletRequest requestProxy(String incomingRequestId, Map<String, Object> attrs) {
+        InvocationHandler handler = (proxy, method, args) -> {
+            switch (method.getName()) {
+                case "getHeader":
+                    return RangerPdpRequestContextFilter.REQ_HEADER_REQUEST_ID.equals(args[0]) ? incomingRequestId : null;
+                case "setAttribute":
+                    attrs.put((String) args[0], args[1]);
+                    return null;
+                case "getAttribute":
+                    return attrs.get((String) args[0]);
+                default:
+                    return null;
+            }
+        };
+
+        return (HttpServletRequest) Proxy.newProxyInstance(
+                HttpServletRequest.class.getClassLoader(),
+                new Class<?>[] {HttpServletRequest.class},
+                handler);
+    }
+
+    private static HttpServletResponse responseProxy(Map<String, String> headers) {
+        InvocationHandler handler = (proxy, method, args) -> {
+            if ("setHeader".equals(method.getName())) {
+                headers.put((String) args[0], (String) args[1]);
+            }
+            return null;
+        };
+
+        return (HttpServletResponse) Proxy.newProxyInstance(
+                HttpServletResponse.class.getClassLoader(),
+                new Class<?>[] {HttpServletResponse.class},
+                handler);
+    }
+
+    private static FilterChain chainProxy(ChainAction action) {
+        InvocationHandler handler = (proxy, method, args) -> {
+            if ("doFilter".equals(method.getName())) {
+                action.apply(args[0], args[1]);
+            }
+            return null;
+        };
+
+        return (FilterChain) Proxy.newProxyInstance(
+                FilterChain.class.getClassLoader(),
+                new Class<?>[] {FilterChain.class},
+                handler);
+    }
+
+    @FunctionalInterface
+    private interface ChainAction {
+        void apply(Object request, Object response) throws Exception;
+    }
+}
diff --git a/pom.xml b/pom.xml
index 2c7ad5dc6..ef1bffb40 100755
--- a/pom.xml
+++ b/pom.xml
@@ -813,6 +813,7 @@
                 <module>jisql</module>
                 <module>kms</module>
                 <module>knox-agent</module>
+                <module>pdp</module>
                 <module>plugin-atlas</module>
                 <module>plugin-elasticsearch</module>
                 <module>plugin-kafka</module>
@@ -1162,6 +1163,7 @@
                 <module>jisql</module>
                 <module>kms</module>
                 <module>knox-agent</module>
+                <module>pdp</module>
                 <module>plugin-atlas</module>
                 <module>plugin-elasticsearch</module>
                 <module>plugin-kafka</module>
@@ -1250,6 +1252,7 @@
                 <module>jisql</module>
                 <module>kms</module>
                 <module>knox-agent</module>
+                <module>pdp</module>
                 <module>plugin-atlas</module>
                 <module>plugin-elasticsearch</module>
                 <module>plugin-kafka</module>
diff --git a/ranger-examples/sample-client/src/main/python/sample_kms_client.py b/ranger-examples/sample-client/src/main/python/sample_kms_client.py
new file mode 100644
index 000000000..c7abce92b
--- /dev/null
+++ b/ranger-examples/sample-client/src/main/python/sample_kms_client.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client     import HadoopSimpleAuth
+from apache_ranger.model.ranger_kms         import RangerKey
+import time
+
+
+##
+## Step 1: create a client to connect to Ranger KMS
+##
+kms_url  = "http://localhost:9292"
+kms_auth = HadoopSimpleAuth("keyadmin")
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# kms_auth = HTTPKerberosAuth()
+#
+# For HTTP Basic authentication
+#
+# kms_auth = ("keyadmin", "rangerR0cks!")
+
+print(f"\nUsing Ranger KMS at {kms_url}")
+
+kms_client = RangerKMSClient(kms_url, kms_auth)
+
+##
+## Step 2: call KMS APIs
+##
+kms_status = kms_client.kms_status()
+print("kms_status():", kms_status)
+print()
+
+key_name = "test_" + str(int(time.time() * 1000))
+
+key = kms_client.create_key(RangerKey({"name": key_name}))
+print("create_key(" + key_name + "):", key)
+print()
+
+rollover_key = kms_client.rollover_key(key_name, key.material)
+print("rollover_key(" + key_name + "):", rollover_key)
+print()
+
+kms_client.invalidate_cache_for_key(key_name)
+print("invalidate_cache_for_key(" + key_name + ")")
+print()
+
+key_metadata = kms_client.get_key_metadata(key_name)
+print("get_key_metadata(" + key_name + "):", key_metadata)
+print()
+
+current_key = kms_client.get_current_key(key_name)
+print("get_current_key(" + key_name + "):", current_key)
+print()
+
+encrypted_keys = kms_client.generate_encrypted_key(key_name, 2)
+print("generate_encrypted_key(" + key_name + ", 2):")
+for i in range(len(encrypted_keys)):
+    encrypted_key   = encrypted_keys[i]
+    decrypted_key   = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+    reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+    print("  encrypted_keys[" + str(i) + "]: ", encrypted_key)
+    print("  decrypted_key[" + str(i) + "]:  ", decrypted_key)
+    print("  reencrypted_key[" + str(i) + "]:", reencrypted_key)
+print()
+
+kms_client.delete_key(key_name)
+print("delete_key(" + key_name + ")")
diff --git a/ranger-examples/sample-client/src/main/python/sample_pdp_client.py b/ranger-examples/sample-client/src/main/python/sample_pdp_client.py
new file mode 100644
index 000000000..705deb9ca
--- /dev/null
+++ b/ranger-examples/sample-client/src/main/python/sample_pdp_client.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz       import RangerAccessContext, RangerAccessInfo
+from apache_ranger.model.ranger_authz       import RangerAuthzRequest, RangerMultiAuthzRequest
+from apache_ranger.model.ranger_authz       import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo
+
+
+##
+## Step 1: create a client to connect to Ranger PDP
+##
+pdp_url = "http://localhost:6500"
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth())
+
+# For trusted-header authN with PDP (example only):
+#
+pdp = RangerPDPClient(pdp_url, auth=None, headers={"X-Forwarded-User": "hive"})
+
+print(f"\nUsing Ranger PDP at {pdp_url}")
+
+##
+## Step 2: call PDP authorization APIs
+##
+req = RangerAuthzRequest({
+    "requestId": "req-1",
+    "user":      RangerUserInfo({"name": "alice"}),
+    "access":    RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1"}), "permissions": ["create"]}),
+    "context":   RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize(req)
+
+print("authorize():")
+print(f"    {req}")
+print(f"    {res}")
+print()
+
+req = RangerAuthzRequest({
+    "requestId": "req-2",
+    "user":      RangerUserInfo({"name": "alice"}),
+    "access":    RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]}), "permissions": ["select"]}),
+    "context":   RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize(req)
+
+print("authorize():")
+print(f"    {req}")
+print(f"    {res}")
+print()
+
+req = RangerMultiAuthzRequest({
+    "requestId": "req-3",
+    "user":      RangerUserInfo({"name": "alice"}),
+    "accesses": [
+        RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"], "attributes": {"OWNER": "alice"}}), "permissions": ["select"]}),
+        RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_vw1"}), "permissions": ["create"]})
+    ],
+    "context": RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize_multi(req)
+
+print("authorize_multi():")
+print(f"    {req}")
+print(f"    {res}")
+print()
+
+req = RangerResourcePermissionsRequest({
+    "requestId": "req-4",
+    "resource":  RangerResourceInfo({"name": "table:default/test_tbl1"}),
+    "context":   RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.get_resource_permissions(req)
+
+print("get_resource_permissions():")
+print(f"    {req}")
+print(f"    {res}")
+print()