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

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git

commit 5041af985d35497c03444c41d5184c1414931822
Author: Sandor Molnar <[email protected]>
AuthorDate: Sun Jun 21 10:04:13 2026 +0200

    KNOX:3353: Add Docker-based integration tests for k8s 
ServiceAccountValidator (#1271)
    
    (cherry picked from commit 24dccd701813c370b766f9fd3a9ed1ebef26bbd4)
---
 .github/workflows/build/Dockerfile                 |  1 +
 .../workflows/build/conf/topologies/k8sauth.xml    | 52 ++++++++++++
 .github/workflows/compose/docker-compose.yml       | 71 +++++++++++++++-
 .../workflows/compose/k8s/manifests/test-sa.yaml   | 38 +++++++++
 .../tests/test_k8s_serviceaccount_validation.py    | 94 ++++++++++++++++++++++
 5 files changed, 255 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build/Dockerfile 
b/.github/workflows/build/Dockerfile
index c06db8cf4..5c0407661 100644
--- a/.github/workflows/build/Dockerfile
+++ b/.github/workflows/build/Dockerfile
@@ -43,6 +43,7 @@ ADD .github/workflows/build/conf/topologies/knoxtoken.xml 
/knox-runtime/conf/top
 ADD .github/workflows/build/conf/topologies/health.xml 
/knox-runtime/conf/topologies/health.xml
 ADD .github/workflows/build/conf/topologies/knoxldap.xml 
/knox-runtime/conf/topologies/knoxldap.xml
 ADD .github/workflows/build/conf/topologies/remoteauth.xml 
/knox-runtime/conf/topologies/remoteauth.xml
+ADD .github/workflows/build/conf/topologies/k8sauth.xml 
/knox-runtime/conf/topologies/k8sauth.xml
 ADD .github/workflows/build/conf/topologies/knoxidf-ldap.xml 
/knox-runtime/conf/topologies/knoxidf-ldap.xml
 ADD .github/workflows/build/conf/topologies/knoxidf-token.xml 
/knox-runtime/conf/topologies/knoxidf-token.xml
 
diff --git a/.github/workflows/build/conf/topologies/k8sauth.xml 
b/.github/workflows/build/conf/topologies/k8sauth.xml
new file mode 100644
index 000000000..1d7a1ed00
--- /dev/null
+++ b/.github/workflows/build/conf/topologies/k8sauth.xml
@@ -0,0 +1,52 @@
+<!--
+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.
+-->
+<topology>
+    <gateway>
+        <provider>
+            <role>federation</role>
+            <name>HeaderPreAuth</name>
+            <enabled>true</enabled>
+            <param>
+                <name>preauth.validation.method</name>
+                <value>preauth.k8s.service.account.validation</value>
+            </param>
+            <!--
+              HeaderPreAuth extracts the asserted principal from this header.
+              The k8s ServiceAccountValidator independently reads the asserted
+              user from its own 'preauth.k8s.sa.custom.header' param, which
+              defaults to 'x-knoxidf-obo.username' - the same header set here,
+              so the principal and the validated user line up.
+            -->
+            <param>
+                <name>preauth.custom.header</name>
+                <value>x-knoxidf-obo.username</value>
+            </param>
+            <param>
+                <name>preauth.k8s.sa.spiffe.header</name>
+                <value>x-spiffe-id</value>
+            </param>
+            <param>
+                <name>preauth.k8s.sa.user.annotation</name>
+                <value>knox.apache.org/owner-username</value>
+            </param>
+        </provider>
+    </gateway>
+    <service>
+        <role>HEALTH</role>
+    </service>
+</topology>
diff --git a/.github/workflows/compose/docker-compose.yml 
b/.github/workflows/compose/docker-compose.yml
index 3364c7f08..35036921d 100644
--- a/.github/workflows/compose/docker-compose.yml
+++ b/.github/workflows/compose/docker-compose.yml
@@ -26,15 +26,81 @@ services:
     depends_on:
       - knox-dev
 
+  # Lightweight single-node Kubernetes (k3s) used to back the k8s
+  # ServiceAccountValidator. It exposes a real API server that the fabric8
+  # client inside Knox talks to. --tls-san=k3s makes the API server cert valid
+  # for the "k3s" compose hostname so the rewritten kubeconfig can target it.
+  k3s:
+    image: rancher/k3s:v1.31.5-k3s1
+    command:
+      - server
+      - --disable=traefik
+      - --disable=metrics-server
+      - --disable=servicelb
+      - --disable=local-storage
+      - --tls-san=k3s
+      - --write-kubeconfig-mode=666
+    privileged: true
+    environment:
+      - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml
+    tmpfs:
+      - /run
+      - /var/run
+    volumes:
+      - k3s-output:/output
+
+  # One-shot bootstrap: waits for the k3s API, rewrites the generated 
kubeconfig
+  # so its server points at the "k3s" hostname (instead of 127.0.0.1), and
+  # applies the test ServiceAccount fixtures. Knox waits for this to finish.
+  k8s-bootstrap:
+    image: rancher/k3s:v1.31.5-k3s1
+    entrypoint:
+      - /bin/sh
+      - -c
+    command:
+      - |
+        set -e
+        echo 'Waiting for k3s kubeconfig...'
+        until [ -f /output/kubeconfig.yaml ]; do sleep 2; done
+        export KUBECONFIG=/output/knox-kubeconfig.yaml
+        echo 'Waiting for k3s API to be ready...'
+        # k3s regenerates its CA and rewrites kubeconfig.yaml on every 
(re)start,
+        # and a persisted volume may still hold a stale copy from a prior run.
+        # Re-derive the knox kubeconfig on each attempt so we converge on the 
live
+        # CA: the loop only exits once kubectl actually connects with it.
+        until
+          sed 's#https://127.0.0.1:6443#https://k3s:6443#g' \
+            /output/kubeconfig.yaml > /output/knox-kubeconfig.yaml
+          kubectl get --raw='/readyz' >/dev/null 2>&1
+        do
+          sleep 3
+        done
+        echo 'Applying test fixtures...'
+        kubectl apply -f /manifests/
+        kubectl get serviceaccount test-sa -n test
+        echo 'k8s bootstrap complete'
+    volumes:
+      - k3s-output:/output
+      - ./k8s/manifests:/manifests:ro
+    depends_on:
+      - k3s
+
   knox:
     image: apache/knox-dev:${IMAGE_TAG:-master}
     command: /gateway.sh
+    environment:
+      # Point the fabric8 client used by the k8s ServiceAccountValidator at 
k3s.
+      - KUBECONFIG=/k3s/knox-kubeconfig.yaml
     volumes:
 #      - ./topologies:/knox-runtime/conf/topologies
       - ./logs:/knox-runtime/logs
 #      - ./knoxshell:/knoxshell
+      - k3s-output:/k3s:ro
     depends_on:
-      - ldap
+      ldap:
+        condition: service_started
+      k8s-bootstrap:
+        condition: service_completed_successfully
 
   tests:
     image: python:3.10-slim
@@ -51,3 +117,6 @@ services:
       && pytest --junitxml=test-results.xml"
     depends_on:
       - knox
+
+volumes:
+  k3s-output:
diff --git a/.github/workflows/compose/k8s/manifests/test-sa.yaml 
b/.github/workflows/compose/k8s/manifests/test-sa.yaml
new file mode 100644
index 000000000..bf88f5476
--- /dev/null
+++ b/.github/workflows/compose/k8s/manifests/test-sa.yaml
@@ -0,0 +1,38 @@
+# 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.
+#
+# Fixtures applied to the throwaway k3s cluster by the k8s-bootstrap service.
+# The Knox container connects to k3s as cluster admin (via the generated
+# kubeconfig), so the Role/RoleBinding a real deployment would use to grant the
+# Knox ServiceAccount read access are not needed here - only the annotated
+# ServiceAccount the validator looks up matters.
+#
+# SPIFFE id under test: spiffe://cluster.local/ns/test/sa/test-sa
+#   -> namespace "test", service account "test-sa"
+# The validator reads the 'knox.apache.org/owner-username' annotation ("bob")
+# and compares it to the asserted user from the x-knoxidf-obo.username header.
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: test
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: test-sa
+  namespace: test
+  annotations:
+    knox.apache.org/owner-username: "bob"
diff --git a/.github/workflows/tests/test_k8s_serviceaccount_validation.py 
b/.github/workflows/tests/test_k8s_serviceaccount_validation.py
new file mode 100644
index 000000000..c304240ad
--- /dev/null
+++ b/.github/workflows/tests/test_k8s_serviceaccount_validation.py
@@ -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.
+
+"""Integration tests for the k8s pre-auth ServiceAccountValidator.
+
+These exercise the 'k8sauth' topology (HeaderPreAuth federation backed by the
+k8s ServiceAccountValidator) against a throwaway k3s cluster. The cluster has a
+ServiceAccount 'test-sa' in namespace 'test' annotated with
+'knox.apache.org/owner-username: bob'. The validator parses the SPIFFE id, 
looks
+up that ServiceAccount, and only lets the request through when the annotation
+value equals the asserted user from the x-knoxidf-obo.username header.
+"""
+
+import unittest
+
+from common_utils import gateway_base_url, knox_get
+
+# A request is authenticated only when both headers are present, the SPIFFE id
+# parses to namespace 'test' / service account 'test-sa', and the asserted user
+# matches the ServiceAccount's owner-username annotation ("bob").
+SPIFFE_HEADER = "x-spiffe-id"
+USER_HEADER = "x-knoxidf-obo.username"
+VALID_SPIFFE_ID = "spiffe://cluster.local/ns/test/sa/test-sa"
+
+
+class TestK8sServiceAccountValidation(unittest.TestCase):
+    """SPIFFE id to ServiceAccount annotation matching via the k8sauth 
topology."""
+
+    def setUp(self):
+        self.base_url = gateway_base_url()
+        # 'ping' always returns 200/OK once auth passes, so it isolates the
+        # auth outcome from gateway readiness (unlike 'gateway-status').
+        self.ping_url = self.base_url + "gateway/k8sauth/v1/ping"
+
+    def test_matching_user_is_authorized(self):
+        """Asserted user matching the SA annotation passes pre-auth (200, 
OK)."""
+        response = knox_get(
+            self.ping_url,
+            headers={SPIFFE_HEADER: VALID_SPIFFE_ID, USER_HEADER: "bob"},
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.text.strip(), "OK")
+
+    def test_mismatching_user_is_forbidden(self):
+        """Asserted user not matching the SA annotation is rejected (403)."""
+        response = knox_get(
+            self.ping_url,
+            headers={SPIFFE_HEADER: VALID_SPIFFE_ID, USER_HEADER: "sam"},
+        )
+        self.assertEqual(response.status_code, 403)
+
+    def test_missing_spiffe_header_is_forbidden(self):
+        """Without the SPIFFE header there is nothing to validate (403)."""
+        response = knox_get(self.ping_url, headers={USER_HEADER: "bob"})
+        self.assertEqual(response.status_code, 403)
+
+    def test_missing_user_header_is_forbidden(self):
+        """Without the asserted-user header the principal is missing (403)."""
+        response = knox_get(self.ping_url, headers={SPIFFE_HEADER: 
VALID_SPIFFE_ID})
+        self.assertEqual(response.status_code, 403)
+
+    def test_unparseable_spiffe_id_is_forbidden(self):
+        """A SPIFFE id that is not in ns/sa form cannot be resolved (403)."""
+        response = knox_get(
+            self.ping_url,
+            headers={SPIFFE_HEADER: "spiffe://cluster.local/not/a/valid/path",
+                     USER_HEADER: "bob"},
+        )
+        self.assertEqual(response.status_code, 403)
+
+    def test_unknown_service_account_is_forbidden(self):
+        """A SPIFFE id pointing at a non-existent ServiceAccount is rejected 
(403)."""
+        response = knox_get(
+            self.ping_url,
+            headers={SPIFFE_HEADER: 
"spiffe://cluster.local/ns/test/sa/does-not-exist",
+                     USER_HEADER: "bob"},
+        )
+        self.assertEqual(response.status_code, 403)
+
+
+if __name__ == "__main__":
+    unittest.main()

Reply via email to