diff --git a/services/jenkins/lesavka-userns-probe.yaml b/services/jenkins/lesavka-userns-probe.yaml index 38438c80..4239fbc9 100644 --- a/services/jenkins/lesavka-userns-probe.yaml +++ b/services/jenkins/lesavka-userns-probe.yaml @@ -1,19 +1,9 @@ # 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-4 + name: lesavka-userns-probe-5 namespace: jenkins spec: backoffLimit: 0 @@ -36,37 +26,23 @@ 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: Unconfined - # The CRI default denies userns_create even inside an isolated pod. - appArmorProfile: - type: Unconfined + type: RuntimeDefault containers: - name: probe image: registry.bstein.dev/bstein/lesavka-ci@sha256:7d3a2548ef0df8da053a2e2939675f5a3cf23e26517828e5fa9a3185911a8e75 securityContext: - # 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 + allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] - # newuidmap opens a uid_map owned by the unprivileged child. - add: ["SETUID", "SETGID", "DAC_OVERRIDE"] resources: requests: {cpu: 10m, memory: 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} + limits: {cpu: 100m, memory: 64Mi, ephemeral-storage: 32Mi} command: ["python3", "-I", "-c"] args: - | - import json, os, subprocess + import json, os, stat from pathlib import Path maps = {name: [[int(v) for v in line.split()] for line in Path('/proc/self/' + name).read_text().splitlines()] @@ -76,21 +52,9 @@ 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, - '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 + helpers = {name: {'mode': oct(stat.S_IMODE(Path('/usr/bin/' + name).stat().st_mode)), + 'uid': Path('/usr/bin/' + name).stat().st_uid, + 'nosuid_mount': bool(os.statvfs('/usr/bin/' + name).f_flag & os.ST_NOSUID)} + for name in ('newuidmap', 'newgidmap')} + print(json.dumps({'pod_user_namespace': 'pass', 'maps': maps, 'helpers': helpers, + 'scope': 'outer mapping only; nested test namespaces unqualified'}))