Files
oldipmi/test.py
T
feedc0de 815c5d752e
Validate, publish and promote Helm chart / Validate chart (push) Successful in 7s
Validate, publish and promote Helm chart / Publish and promote chart (push) Successful in 2s
Contain legacy VNC behind a loopback inetd listener
2026-10-31 12:28:52 +01:00

105 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check rendered security relationships; optionally smoke-test that exact runtime."""
import base64, json, os, pathlib, socket, subprocess, sys, tempfile, time, urllib.request
import yaml
ROOT = pathlib.Path(__file__).resolve().parent
os.chdir(ROOT)
def run(*args, **kw):
return subprocess.check_output(args, text=True, **kw).strip()
subprocess.run(['helm', 'lint', 'chart'], check=True)
docs = list(yaml.safe_load_all(run('helm', 'template', 'smoke', 'chart')))
objects = {x['kind']: x for x in docs}
pod = objects['Deployment']['spec']['template']['spec']
app = pod['containers'][0]
assert not pod['automountServiceAccountToken'] and not pod['enableServiceLinks']
assert pod['securityContext']['runAsNonRoot']
assert pod['securityContext']['seccompProfile']['type'] == 'RuntimeDefault'
assert pod['dnsPolicy'] == 'None' and not pod['dnsConfig'].get('searches')
assert all(not x.get(k) for k in ['hostNetwork', 'hostPID', 'hostIPC'] for x in [pod])
assert not any('hostPath' in x or 'persistentVolumeClaim' in x for x in pod['volumes'])
for container in pod['initContainers'] + pod['containers']:
s = container['securityContext']
assert s['readOnlyRootFilesystem'] and not s['allowPrivilegeEscalation']
assert s['capabilities']['drop'] == ['ALL']
assert app['image'] == pod['initContainers'][0]['image']
assert objects['NetworkPolicy']['spec']['ingress'] == []
assert objects['NetworkPolicy']['spec']['egress'] == []
assert objects['Service']['spec']['type'] == 'ClusterIP'
assert objects['Service']['spec']['selector'] == objects['Deployment']['spec']['selector']['matchLabels']
assert set(['startupProbe', 'readinessProbe', 'livenessProbe']) <= app.keys()
assert not any(x['kind'] == 'Ingress' for x in docs)
print('Rendered security and service relationships passed', flush=True)
if '--runtime' not in sys.argv:
sys.exit(0)
with tempfile.TemporaryDirectory(prefix='oldipmi-test-') as tmp:
os.chmod(tmp, 0o755)
dirs = {}
for volume in pod['volumes']:
path = pathlib.Path(tmp) / volume['name']
path.mkdir(mode=0o777)
path.chmod(0o777)
dirs[volume['name']] = path
if 'configMap' in volume:
for name, data in objects['ConfigMap']['data'].items():
(path / name).write_text(data)
def container_args(container):
args = ['podman', 'run', '--read-only', '--read-only-tmpfs=false', '--cap-drop=ALL',
'--security-opt=no-new-privileges', '--userns=keep-id',
'--user', str(container['securityContext'].get('runAsUser', pod['securityContext']['runAsUser']))]
for mount in container['volumeMounts']:
args += ['-v', str(dirs[mount['name']]) + ':' + mount['mountPath'] + (':ro' if mount.get('readOnly') else '')]
return args
init = pod['initContainers'][0]
subprocess.run(container_args(init) + ['--rm', '--network=none', init['image']] + init['command'] + init['args'], check=True)
args = container_args(app) + ['-d', '--workdir', app['workingDir'], '-p', '127.0.0.1::' + str(app['ports'][0]['containerPort'])]
for env in app['env']:
args += ['-e', env['name'] + '=' + env['value']]
cid = run(*args, app['image'], *app['command'])
try:
port = int(run('podman', 'port', cid).split(':')[-1])
for attempt in range(30):
try:
with urllib.request.urlopen(f'http://127.0.0.1:{port}' + app['readinessProbe']['httpGet']['path'], timeout=2) as response:
assert response.status == 200
break
except Exception:
time.sleep(1)
else:
raise AssertionError('noVNC did not start')
# A real WebSocket upgrade must reach the local X11 VNC server.
for attempt in range(15):
try:
with socket.create_connection(('127.0.0.1', port), timeout=10) as ws:
key = base64.b64encode(os.urandom(16)).decode()
ws.sendall((f'GET /websockify HTTP/1.1\r\nHost: localhost:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Protocol: binary\r\n\r\n').encode())
received = b''
while b'RFB ' not in received:
data = ws.recv(4096)
assert data, 'VNC handshake closed early'
received += data
assert b'101 Switching Protocols' in received
break
except (AssertionError, OSError):
if attempt == 14:
raise
time.sleep(1)
listeners = run('podman', 'exec', cid, 'netstat', '-lntp')
assert not any('x11vnc' in line for line in listeners.splitlines()), listeners
http_port = str(app['ports'][0]['containerPort'])
for line in listeners.splitlines()[2:]:
address = line.split()[3]
assert address == '0.0.0.0:' + http_port or address.rsplit(':', 1)[0] == '127.0.0.1', listeners
status = run('podman', 'exec', cid, 'cat', '/proc/1/status')
assert 'NoNewPrivs:\t1' in status and 'CapEff:\t0000000000000000' in status
assert run('podman', 'exec', cid, 'id', '-u') != '0'
processes = run('podman', 'exec', cid, 'ps', '-eo', 'args')
assert '/usr/lib/chromium-browser/chromium-browser' in processes
assert '/usr/bin/Xvfb' in processes
print('Non-root read-only runtime, browser, HTTP, WebSocket/RFB, and zero capabilities passed')
except Exception:
print(run('podman', 'exec', cid, 'sh', '-c', 'tail -n 25 /tmp/x11vnc-stderr* /tmp/novnc-stderr*'))
raise
finally:
subprocess.run(['podman', 'rm', '-f', cid], check=True, stdout=subprocess.DEVNULL)
subprocess.run(container_args(init) + ['--rm', '--network=none', init['image'], '/bin/sh', '-ec', 'rm -rf /opt/novnc/site'], check=True)