the-custodian/docs/changes/CUST-WP-0073/credential-boundary-r1/verify_agent_denials.py
codex 02e69269c8
Some checks failed
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 11s
Python Tests / pytest (push) Failing after 28s
Adopt staged generation and prepare weekly sizing and credential isolation
2026-10-19 12:42:11 +02:00

110 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Read-only actual-account probes. Never prints command output or credentials."""
import argparse
import grp
import hashlib
import re
import json
import os
import pwd
import subprocess
IDENTITY = 'system:serviceaccount:custodian-agents:custodian-codex'
DENIALS = [
('get', 'secrets'), ('list', 'secrets'), ('watch', 'secrets'),
('get', 'configmaps'), ('get', 'pods'), ('get', 'pods/log'),
('create', 'pods/exec'), ('create', 'pods/attach'), ('create', 'pods'),
('patch', 'deployments.apps'), ('create', 'serviceaccounts/token'),
('patch', 'applications.argoproj.io'),
]
CLUSTER_DENIALS = [('impersonate', 'users'),
('bind', 'clusterroles.rbac.authorization.k8s.io'),
('escalate', 'clusterroles.rbac.authorization.k8s.io')]
def inventory_valid(inventory):
if not isinstance(inventory, dict):
return False
namespaces = inventory.get('namespaces')
return (inventory.get('cluster') == 'railiance01'
and inventory.get('complete') is True
and isinstance(inventory.get('supervisor_receipt_ref'), str)
and bool(inventory['supervisor_receipt_ref'].strip())
and isinstance(namespaces, list) and bool(namespaces)
and all(isinstance(n, str) and len(n) <= 63
and re.fullmatch(r'[a-z0-9](?:[a-z0-9-]*[a-z0-9])?', n)
for n in namespaces)
and len(set(namespaces)) == len(namespaces))
def run(argv):
try:
p = subprocess.run(argv, capture_output=True, timeout=20, check=False)
return p.returncode, p.stdout
except (OSError, subprocess.TimeoutExpired):
return None, b''
def probe(kubectl, context, account, inventory):
if not inventory_valid(inventory):
return {"status": "refused_namespace_inventory", "passed": False}
# Abort before even auth-only Kubernetes requests under the old account.
if os.geteuid() == 0 or pwd.getpwuid(os.geteuid()).pw_name != account:
return {'status': 'refused_wrong_account', 'passed': False}
base = [kubectl, '--context', context, '--request-timeout=10s', 'auth']
code, output = run(base + ['whoami', '-o', 'json'])
try:
info = json.loads(output)['status']['userInfo']
identity = code == 0 and info['username'] == IDENTITY and 'system:masters' not in info.get('groups', [])
except (KeyError, TypeError, ValueError):
identity = False
if not identity:
return {'status': 'refused_wrong_identity', 'passed': False}
checks = {}
for verb, resource in DENIALS:
code, output = run(base + ['can-i', verb, resource, '--all-namespaces'])
checks[f'all-namespaces:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
# An all-namespaces denial can coexist with a namespaced RoleBinding grant.
# Probe each namespace supplied in the separately supervised complete inventory.
for namespace in inventory['namespaces']:
for verb, resource in DENIALS:
code, output = run(base + ['can-i', verb, resource, '--namespace', namespace])
checks[f'{namespace}:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
for verb, resource in CLUSTER_DENIALS:
code, output = run(base + ['can-i', verb, resource])
checks[f'cluster:{verb}:{resource}'] = code == 1 and output.strip() == b'no'
dangerous = {'sudo', 'wheel', 'docker', 'lxd', 'incus-admin', 'disk', 'libvirt'}
groups = {grp.getgrgid(gid).gr_name for gid in set(os.getgroups()) | {os.getegid()}}
checks['no_privileged_groups'] = not (groups & dangerous)
checks['host_admin_kubeconfig_unreadable'] = not os.access('/etc/rancher/k3s/k3s.yaml', os.R_OK)
checks['legacy_admin_home_unsearchable'] = not os.access('/home/tegwick', os.X_OK)
checks['root_home_unsearchable'] = not os.access('/root', os.X_OK)
for label, path in [('docker', '/var/run/docker.sock'), ('containerd', '/run/k3s/containerd/containerd.sock')]:
checks[f'{label}_socket_not_writable'] = not os.access(path, os.W_OK)
# This is a partial negative proof, never assignment admission on its own.
return {'status': 'negative_probes_passed' if all(checks.values()) else 'negative_probes_failed',
'passed': all(checks.values()), 'checks': checks, 'assignment_admitted': False,
'namespace_inventory_sha256': hashlib.sha256(json.dumps(inventory, sort_keys=True, separators=(',', ':')).encode()).hexdigest(),
'namespace_count': len(inventory['namespaces']),
'supervisor_receipt_ref': inventory['supervisor_receipt_ref'],
'remaining': ['supervisor_verified_complete_current_namespace_inventory', 'supervisor_verified_sudoers_and_ACLs', 'all_external_tool_and_credential_paths',
'old_admin_SSH_denial', 'exact_approved_action_and_changed_action_refusal',
'actual_interactive_runtime_admission']}
if __name__ == '__main__':
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--kubectl', default='/usr/local/bin/kubectl')
p.add_argument('--context', required=True)
p.add_argument('--namespace-inventory', required=True, help='Supervisor-supplied complete current namespace inventory JSON')
p.add_argument('--account', default='custodian-agt', choices=['custodian-agt'])
a = p.parse_args()
try:
with open(a.namespace_inventory, encoding='utf-8') as stream:
inventory = json.load(stream)
except (OSError, ValueError):
inventory = None
receipt = probe(a.kubectl, a.context, a.account, inventory)
print(json.dumps(receipt, sort_keys=True))
raise SystemExit(0 if receipt['passed'] else 1)