jenkins: qualify nested test namespaces inside mapped pods

This commit is contained in:
jenkins 2026-11-08 16:52:44 -06:00
parent e58dbb774d
commit cdb67252bc

View File

@ -1,9 +1,19 @@
# services/jenkins/lesavka-userns-probe.yaml
# Qualification only: never relax the compiler pod to bypass a failed gate.
apiVersion: v1
kind: ConfigMap
metadata:
name: lesavka-userns-probe-mappings
namespace: jenkins
data:
# The subordinate range must fit inside Kubernetes's 65536-ID pod mapping.
subuid: "jenkins:10000:20000\n"
subgid: "jenkins:10000:20000\n"
---
apiVersion: batch/v1
kind: Job
metadata:
name: lesavka-userns-probe-1
name: lesavka-userns-probe-2
namespace: jenkins
spec:
backoffLimit: 0
@ -26,23 +36,33 @@ spec:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
# Nested private mounts need syscalls denied by RuntimeDefault. This
# exception is confined to a hostUsers:false pod with no host mounts.
seccompProfile:
type: RuntimeDefault
type: Unconfined
containers:
- name: probe
image: registry.bstein.dev/bstein/lesavka-ci@sha256:7d3a2548ef0df8da053a2e2939675f5a3cf23e26517828e5fa9a3185911a8e75
securityContext:
allowPrivilegeEscalation: false
# Permit only the image's setuid UID/GID mapping helpers. Their root
# is mapped to an unprivileged host ID, checked before the probe.
allowPrivilegeEscalation: true
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
add: ["SETUID", "SETGID"]
resources:
requests: {cpu: 10m, memory: 32Mi}
limits: {cpu: 100m, memory: 64Mi, ephemeral-storage: 32Mi}
limits: {cpu: 100m, memory: 128Mi, ephemeral-storage: 32Mi}
volumeMounts:
- {name: mappings, mountPath: /etc/subuid, subPath: subuid, readOnly: true}
- {name: mappings, mountPath: /etc/subgid, subPath: subgid, readOnly: true}
- {name: scratch, mountPath: /tmp}
command: ["python3", "-I", "-c"]
args:
- |
import json, os
import json, os, subprocess
from pathlib import Path
maps = {name: [[int(v) for v in line.split()] for line in
Path('/proc/self/' + name).read_text().splitlines()]
@ -52,5 +72,21 @@ spec:
assert len(entries) == 1
inside, outside, count = entries[0]
assert inside == 0 and outside >= 65536 and count >= 65536
subprocess.run(['bwrap', '--unshare-user', '--uid', '0', '--gid', '0',
'--ro-bind', '/', '/', '--', '/bin/true'], check=True, timeout=10)
subprocess.run(['unshare', '--user', '--mount', '--map-auto',
'--map-root-user', '--pid', '--fork', '--kill-child=KILL',
'--propagation', 'private', 'python3', '-I', '-c',
'import os; os.setgroups([]); os.setgid(1001); os.setuid(1001); '
'assert os.geteuid() == 1001 and os.getegid() == 1001'],
check=True, timeout=10)
print(json.dumps({'pod_user_namespace': 'pass', 'maps': maps,
'scope': 'outer mapping only; nested test namespaces unqualified'}))
'nested_mapping_private_mount': 'pass',
'scope': 'namespace prerequisites only; full suite unqualified'}))
volumes:
- name: mappings
configMap:
name: lesavka-userns-probe-mappings
- name: scratch
emptyDir:
sizeLimit: 16Mi