jenkins: qualify mapped UID helpers with narrow file capabilities
This commit is contained in:
parent
58ae383c38
commit
a9fabc8b2c
@ -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-5
|
||||
name: lesavka-userns-probe-6
|
||||
namespace: jenkins
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
@ -26,23 +36,72 @@ 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
|
||||
# The CRI default denies userns_create even inside an isolated pod.
|
||||
appArmorProfile:
|
||||
type: Unconfined
|
||||
initContainers:
|
||||
- name: mapping-tools
|
||||
image: registry.bstein.dev/bstein/lesavka-ci@sha256:7d3a2548ef0df8da053a2e2939675f5a3cf23e26517828e5fa9a3185911a8e75
|
||||
securityContext:
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
appArmorProfile:
|
||||
type: RuntimeDefault
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
add: ["SETFCAP"]
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {cpu: 100m, memory: 64Mi, ephemeral-storage: 8Mi}
|
||||
volumeMounts:
|
||||
- {name: mapping-tools, mountPath: /mapping-tools}
|
||||
command: ["sh", "-ec"]
|
||||
args:
|
||||
- |
|
||||
# Fail before preparing helpers unless pod root is an unprivileged host ID.
|
||||
python3 -I -c 'from pathlib import Path; rows=[list(map(int,Path("/proc/self/"+name).read_text().split())) for name in ("uid_map","gid_map")]; assert all(len(r)==3 and r[0]==0 and r[1]>=65536 and r[2]>=65536 for r in rows)'
|
||||
cp /usr/bin/newuidmap /usr/bin/newgidmap /mapping-tools/
|
||||
chmod 0755 /mapping-tools/newuidmap /mapping-tools/newgidmap
|
||||
setcap cap_setuid=ep /mapping-tools/newuidmap
|
||||
setcap cap_setgid=ep /mapping-tools/newgidmap
|
||||
getcap /mapping-tools/newuidmap /mapping-tools/newgidmap
|
||||
sha256sum /usr/bin/newuidmap /mapping-tools/newuidmap /usr/bin/newgidmap /mapping-tools/newgidmap
|
||||
containers:
|
||||
- name: probe
|
||||
image: registry.bstein.dev/bstein/lesavka-ci@sha256:7d3a2548ef0df8da053a2e2939675f5a3cf23e26517828e5fa9a3185911a8e75
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
# File capabilities keep the helper's effective UID equal to the
|
||||
# target namespace owner. No SYS_ADMIN or host identity is granted.
|
||||
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}
|
||||
env:
|
||||
- name: PATH
|
||||
value: /mapping-tools:/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumeMounts:
|
||||
- {name: mapping-tools, mountPath: /mapping-tools, readOnly: true}
|
||||
- {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, stat
|
||||
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,9 +111,24 @@ spec:
|
||||
assert len(entries) == 1
|
||||
inside, outside, count = entries[0]
|
||||
assert inside == 0 and outside >= 65536 and count >= 65536
|
||||
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'}))
|
||||
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: mapping-tools
|
||||
emptyDir:
|
||||
sizeLimit: 2Mi
|
||||
- name: mappings
|
||||
configMap:
|
||||
name: lesavka-userns-probe-mappings
|
||||
- name: scratch
|
||||
emptyDir:
|
||||
sizeLimit: 16Mi
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user