diff --git a/docs/playwright-registration.md b/docs/playwright-registration.md index fd42e57..49d8b1f 100644 --- a/docs/playwright-registration.md +++ b/docs/playwright-registration.md @@ -107,6 +107,11 @@ reviewed source revision. Its result contains the private original registry and must remain inside that execution path. Only `summary` is a public receipt. Source revision and digest bindings do not constitute an approval or custody grant. +Inventory checks use the issuer's effective audience, including its client-ID +fallback when `audience` is absent/empty. A second issuer environment variable +pointing to the same proposed agent Secret/key is refused even if its name differs. +These checks do not replace Platform's wider custody/consumer isolation review. + The adapter admits one replica, `Recreate`, the pinned immutable KeyCape image, the expected registry mount and explicit environment bindings. It preserves unrelated settings and refuses differing bindings. `check_registry` pins the diff --git a/sso-mfa/k8s/keycape/playwright-activation.py b/sso-mfa/k8s/keycape/playwright-activation.py index ebda16d..211e7fb 100644 --- a/sso-mfa/k8s/keycape/playwright-activation.py +++ b/sso-mfa/k8s/keycape/playwright-activation.py @@ -83,6 +83,12 @@ def build(secret, deployment, identities, source_revision): records = registration.clients(identities) for record in records[1:]: binding = registration.registry.env_binding(record) + target_ref = binding['valueFrom']['secretKeyRef'] + for entry in proposed_env: + reference = entry.get('valueFrom', {}).get('secretKeyRef', {}) + require(entry['name'] == binding['name'] or + (reference.get('name'), reference.get('key')) != (target_ref['name'], target_ref['key']), + 'delivery_secret_already_bound') existing = [e for e in proposed_env if e['name'] == binding['name']] require(not existing or existing == [binding], 'existing_binding_requires_reconciliation') if not existing: diff --git a/sso-mfa/k8s/keycape/playwright-registration.py b/sso-mfa/k8s/keycape/playwright-registration.py index f6c8386..0f78edd 100644 --- a/sso-mfa/k8s/keycape/playwright-registration.py +++ b/sso-mfa/k8s/keycape/playwright-registration.py @@ -100,14 +100,14 @@ def prepare_registry(secret, identities, operation='add'): require(isinstance(config.get('clients'), list), 'client_sequence_required') for existing in config['clients']: require(isinstance(existing, dict), 'client_shape_unexpected') - if existing.get('audience') == AUDIENCE: + if (existing.get('audience') or existing.get('clientId')) == AUDIENCE: require(existing in wanted, 'unreviewed_playwright_audience_registration') # Preflight every record against the same original, including collisions. for client in wanted: _, current, _ = registry._inspect(secret, client) if 'serviceSubject' in client: for other in current['clients']: - if other['clientId'] != client['clientId'] and other.get('audience') == AUDIENCE: + if other['clientId'] != client['clientId'] and (other.get('audience') or other.get('clientId')) == AUDIENCE: require(other.get('serviceSubject') != client['serviceSubject'], 'playwright_subject_already_registered') matching = [c for c in current['clients'] if c['clientId'] == client['clientId']] diff --git a/sso-mfa/k8s/keycape/test_playwright_activation.py b/sso-mfa/k8s/keycape/test_playwright_activation.py index 33b7dfd..056223a 100644 --- a/sso-mfa/k8s/keycape/test_playwright_activation.py +++ b/sso-mfa/k8s/keycape/test_playwright_activation.py @@ -217,5 +217,12 @@ class ActivationTests(unittest.TestCase): with self.assertRaisesRegex(a.Refused, '^native_verifier_digest_mismatch$'): a.check_registry(self.proposal, binary, 'wrong') + def test_other_env_name_cannot_alias_a_proposed_agent_secret(self): + binding = a.registration.registry.env_binding(a.registration.agent_client(IDENTITIES[0])) + binding['name'] = 'OTHER_CLIENT_SECRET' + a.container(self.deployment['spec'])['env'].append(binding) + with self.assertRaisesRegex(a.Refused, 'delivery_secret_already_bound'): + a.build(self.secret, self.deployment, IDENTITIES, REVISION) + if __name__ == '__main__': unittest.main() diff --git a/sso-mfa/k8s/keycape/test_playwright_registration.py b/sso-mfa/k8s/keycape/test_playwright_registration.py index 8aba4c3..2ac03fc 100644 --- a/sso-mfa/k8s/keycape/test_playwright_registration.py +++ b/sso-mfa/k8s/keycape/test_playwright_registration.py @@ -119,6 +119,14 @@ class RegistrationTests(unittest.TestCase): encoded, _ = p.prepare_registry(secret, [IDENTITY]) self.assertIn(existing, yaml.safe_load(decode(encoded))['clients']) + def test_default_audience_client_id_cannot_bypass_dedicated_inventory(self): + base, _ = fixture() + default_audience = dict(p.public_client(), clientId=p.AUDIENCE) + del default_audience['audience'] + secret = altered(base, lambda c: c['clients'].append(default_audience)) + with self.assertRaises(p.Refused): + p.prepare_registry(secret, [IDENTITY]) + def test_cli_outputs_only_proposal_and_no_false_admission(self): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / 'agents.json'; path.write_text(json.dumps([IDENTITY])) diff --git a/workplans/NK-WP-0050-playwright-consumer-registration.md b/workplans/NK-WP-0050-playwright-consumer-registration.md index 2808f87..00d4833 100644 --- a/workplans/NK-WP-0050-playwright-consumer-registration.md +++ b/workplans/NK-WP-0050-playwright-consumer-registration.md @@ -81,6 +81,11 @@ check-config accepts both synthetic configurations. Real identity/custody owner returns, executor/recovery-state integration, exact current image verification and the supervised maintenance action remain open. +Follow-up review reproduced two collision gaps: implicit client-ID audience and +different env names referencing the same proposed agent Secret/key. Both are now +refused; two new regressions failed before repair and pass afterward. This does +not establish isolation outside the observed registry/issuer environment. + ## Verify issuer and real consumer acceptance ```task