2026-11-06 01:15:03 +01:00
|
|
|
"""NK-WP-0058-T05: the agent status grant stays read-only and narrow, and what it can read holds no secret.
|
2026-11-06 00:52:35 +01:00
|
|
|
|
2026-11-06 01:15:03 +01:00
|
|
|
The grant is railiance-platform's add-on argocd/platform-addons/nk-agent-status/ (decision 4af1ec01, option A). Its
|
|
|
|
|
own guard is railiance-platform tests/test_nk_agent_status_rbac.py; this is net-kingdom's consumer-side check of the
|
|
|
|
|
same files, plus the sso workloads it can read: `get` on a pod or Deployment returns its env, so a literal secret
|
|
|
|
|
value there would become readable.
|
2026-11-06 00:52:35 +01:00
|
|
|
"""
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-11-06 01:15:03 +01:00
|
|
|
import pytest
|
2026-11-06 00:52:35 +01:00
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[3]
|
2026-11-06 01:15:03 +01:00
|
|
|
ADDON = REPO.parent / 'railiance-platform/argocd/platform-addons/nk-agent-status'
|
2026-11-06 00:52:35 +01:00
|
|
|
READ = {'get', 'list', 'watch'}
|
|
|
|
|
NEVER = {'secrets', 'pods/log', 'pods/exec', 'pods/attach', 'pods/portforward', 'serviceaccounts/token'}
|
|
|
|
|
ALLOWED = {('sso', 'batch', 'jobs'), ('sso', '', 'pods'), ('sso', 'apps', 'deployments'),
|
|
|
|
|
('sso', 'external-secrets.io', 'externalsecrets'), ('informed-decision', '', 'configmaps'),
|
|
|
|
|
('argocd', 'argoproj.io', 'applications')}
|
|
|
|
|
SECRET_WORDS = {'PASS', 'PASSWORD', 'SECRET', 'TOKEN', 'KEY', 'SEED', 'CREDENTIAL', 'CREDENTIALS'}
|
|
|
|
|
NOT_A_VALUE = {'URL', 'URLS', 'ORIGINS', 'SECONDS', 'RETURNS', 'FILE', 'PATH', 'ENABLED'} # what the value is
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def secret_named(name):
|
|
|
|
|
parts = name.upper().split('_')
|
|
|
|
|
return bool(SECRET_WORDS & set(parts)) and parts[-1] not in NOT_A_VALUE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def docs():
|
2026-11-06 01:15:03 +01:00
|
|
|
if not (ADDON / 'kustomization.yaml').exists():
|
|
|
|
|
pytest.skip('railiance-platform checkout with the nk-agent-status add-on required')
|
|
|
|
|
resources = yaml.safe_load((ADDON / 'kustomization.yaml').read_text())['resources']
|
|
|
|
|
return [d for r in resources for d in yaml.safe_load_all((ADDON / r).read_text()) if d]
|
2026-11-06 00:52:35 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_only_namespaced_read_rules_on_the_listed_resources():
|
|
|
|
|
kinds = {d['kind'] for d in docs()}
|
|
|
|
|
assert kinds == {'ServiceAccount', 'Role', 'RoleBinding'} # nothing cluster-wide
|
|
|
|
|
for role in (d for d in docs() if d['kind'] == 'Role'):
|
|
|
|
|
ns = role['metadata']['namespace']
|
|
|
|
|
for rule in role['rules']:
|
|
|
|
|
assert set(rule['verbs']) <= READ, rule
|
|
|
|
|
for group in rule['apiGroups']:
|
|
|
|
|
for resource in rule['resources']:
|
|
|
|
|
assert resource not in NEVER and '*' not in (group, resource)
|
|
|
|
|
assert (ns, group, resource) in ALLOWED, (ns, group, resource)
|
|
|
|
|
if ns != 'sso':
|
|
|
|
|
assert rule.get('resourceNames'), f'{ns}: outside sso the grant names its objects'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bound_only_to_the_dedicated_identity():
|
|
|
|
|
sa = next(d for d in docs() if d['kind'] == 'ServiceAccount')
|
|
|
|
|
assert sa['metadata'] == {**sa['metadata'], 'name': 'nk-agent-status', 'namespace': 'sso'}
|
|
|
|
|
assert sa['automountServiceAccountToken'] is False
|
|
|
|
|
for binding in (d for d in docs() if d['kind'] == 'RoleBinding'):
|
|
|
|
|
assert binding['subjects'] == [{'kind': 'ServiceAccount', 'name': 'nk-agent-status', 'namespace': 'sso'}]
|
|
|
|
|
assert binding['roleRef']['kind'] == 'Role'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sso_workloads_carry_no_literal_secret_values():
|
|
|
|
|
"""Values of secret-named env vars in sso workloads are references or file paths, never the secret itself."""
|
|
|
|
|
found = []
|
|
|
|
|
for path in sorted((REPO / 'sso-mfa/k8s').glob('*/*.yaml')):
|
|
|
|
|
for doc in yaml.safe_load_all(path.read_text()):
|
|
|
|
|
if not isinstance(doc, dict) or doc.get('metadata', {}).get('namespace') != 'sso':
|
|
|
|
|
continue
|
|
|
|
|
spec = doc.get('spec') or {}
|
|
|
|
|
pod = (spec.get('template') or {}).get('spec') or {}
|
|
|
|
|
for container in (pod.get('containers') or []) + (pod.get('initContainers') or []):
|
|
|
|
|
for env in container.get('env') or []:
|
|
|
|
|
value = env.get('value')
|
|
|
|
|
if value is not None and secret_named(env['name']) and not str(value).startswith('/'):
|
|
|
|
|
found.append(f"{path.relative_to(REPO)}:{env['name']}")
|
|
|
|
|
assert found == [], found
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_secret_named():
|
|
|
|
|
assert secret_named('USER_ENGINE_TENANT_MANAGEMENT_TOKEN') and secret_named('SMTP_PASSWORD')
|
|
|
|
|
assert not secret_named('KEYCAPE_ACCOUNT_PORTAL_URL') and not secret_named('PASSWORD_SETUP_TTL_SECONDS')
|
|
|
|
|
assert not secret_named('AUTHELIA_JWT_SECRET_FILE')
|