Review reset authority transition and same-process role refresh
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m14s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
tegwick 2026-10-31 14:26:17 +01:00
parent 776dfc20a0
commit d5adc4b998
7 changed files with 431 additions and 0 deletions

View file

@ -220,3 +220,112 @@ including the full non-admin reach established by the native matrix. No grant,
reset, app authorization or productive acceptance is contained in this receipt.
Do not rerun the completed observer command. Evidence: reset-role-observation-
2026-10-31.json and reset-role-return-verification-2026-10-31.json.
## Reviewed role-transition proposal — 2026-10-31
**Prepared, not admitted or executed.** Owner preparation is NK-WP-0054-T14;
founder admission/execution is T15; declaration alignment and productive reset plus
fresh identity restore acceptance are T16. T10 and CSOC-WP-0015-T06 remain open.
Proposal: `sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. Exact controller and plan are
`/home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py` and
`reset-role-plan.json`. The digest binds the controller, existing transport and
observer sources, current role observation, native rights matrix, pinned runtime
identities/configuration and final validation. No new resources or recurring
capacity are requested; no credential rotation, service rename or restart is
included. Both pinned workloads were Ready when observed.
### Decision and scope
The proposed change replaces **only svc-authelia's strict-readonly membership**
with **lldap_password_manager**. This restores the native password-write capability
needed by the accepted tegwick recovery flow. The role can change passwords for
**all non-LLDAP-admin directory identities**, including other service accounts and
application administrators outside lldap_admin. It is not limited to Coulomb or to
human accounts. LLDAP administrators are excluded. Seven tested unrelated writes
remain refused; this is not an exhaustive rights audit. Both roles already permit
own-profile/password operations in the pinned [LLDAP access-control source](https://github.com/lldap/lldap/blob/v0.6.2/crates/auth/src/access_control.rs).
The same dedicated SERVICE account and single Authelia consumer remain. The plan
contains the proposed declaration: directory read USE plus narrowly described
password ADMINISTER scope, without the read-only bind convention. SI-3 permits a
service-owned write account with its narrowest working scope. The canonical
read-only declaration has not been changed to imply approval. Following a verified
grant, T16 must align it and existing bind verification/repair expectations. Do
not rerun the historical read-only bind migration as reset verification.
**Rollback limit:** membership rollback restores fresh Authelia reset denial, but
it does not revoke previously issued HTTP tokens or undo changed user passwords.
A native test proves a token obtained during the grant can still change an eligible
password after membership rollback; a freshly issued token cannot. The pinned
[LLDAP token implementation](https://github.com/lldap/lldap/blob/v0.6.2/server/src/auth_service.rs)
embeds groups in a token with a one-day expiry. Treat retained token authority as
lasting up to 24 hours unless separately revoked. This packet includes no token
revocation or incident-containment operation. Admission includes this limitation.
### Execution and recovery
Run in the founder's workstation terminal only after accepting the scope above:
```bash
python3 /home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py \
--execute --expect-digest sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f
```
The hidden prompt requires the human admin password. Never paste it into chat.
After reviewing the printed scope, enter exactly:
```text
GRANT sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f
```
The controller checks the exact live cluster/namespace/deployment identities,
images and Authelia configuration before private authentication and again after
confirmation. It rechecks the human admin role, tegwick's non-admin classification
and svc-authelia's exact baseline before writing. The old role is removed before
the new one is added; a brief directory lookup failure is possible. Every effect
has a durable intent record. All user IDs and group memberships are compared after
each step; unknown concurrent changes fail verification. Profile fields/passwords
are not copied into the receipt or claimed as independently compared.
Success is `password_manager_role_verified`. Paste only the returned JSON; Codex
will verify the durable mode0600 receipt in
`~/.local/state/net-kingdom/reset-authority/753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f/journal.json`.
An existing attempt refuses replay. Do not delete its state or repeat the grant.
If the operation is interrupted or reports uncertain status, restore read-only
membership with this separate attended recovery:
```bash
python3 /home/worsch/net-kingdom/sso-mfa/k8s/lldap/reset-role-transition.py \
--recover --expect-digest sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f
```
Confirm `RECOVER sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. For an already completed successful
grant, use `--rollback` instead of `--recover` and confirm
`ROLLBACK sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f`. Both restore only the role and retain
previous journal versions. They preserve unrelated subsequent memberships and
user password changes. Recovery tolerates workload unready status but still
requires pinned identities, images and configuration; runtime drift requires
review, not a bypass. If the directory itself is unreachable, recovery cannot
claim completion until the owner restores access. These are role restoration
commands with the token limitation above, not full credential revocation.
### Evidence and remaining acceptance
25 unit checks and four native controller checks pass with no skips/failures.
The native checks include real process termination immediately after removal and
addition, successful recovery without replay, and the retained-token boundary.
A separate confined native rehearsal uses **one running Authelia process** through
read-only → password-manager → read-only: reset refuses, succeeds, then refuses;
old/new passwords and consumed challenge denial are checked each time. No restart
is needed in this tested configuration. Synthetic fixtures and generated passwords
are removed; no provider SMTP or productive password reset was used.
Evidence: `reset-transition-validation-2026-10-31.json`,
`reset-role-refresh-2026-10-31.json` and its source manifest. A successful role
receipt still leaves T16: source alignment, productive tegwick reset/app/MFA and
fresh identity backup/restore evidence. It does not complete the four goal workplans.
The final consumer `make check` gate passes546 tests (zero failures/skips;69
existing warnings). Receipt: reset-transition-consumer-gate-2026-10-31.json.

View file

@ -0,0 +1,76 @@
{
"schema": "coulomb.native-reset-role-refresh.v1",
"observed_at": "2026-10-31T13:23:16.597535+00:00",
"data": "synthetic",
"status": "native_role_refresh_verified",
"images": {
"lldap": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d",
"authelia": "sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54",
"client": "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd"
},
"production_access": false,
"provider_smtp_contacted": false,
"owner_runtime_or_permission_change_performed": false,
"no_ports_host_mounts_or_external_routes": true,
"backend_container_flags_verified_before_fixture_import": true,
"source_manifest_sha256": "f3c6d7b5a286d15d3049bba7e4c543052388f6a95a5e3254414f3b51da1285d0",
"findings": {
"scenarios": [
{
"bind_role": "lldap_strict_readonly",
"same_authelia_process": true,
"status": "reset_refused",
"only_loopback_interface": true,
"external_routes_absent": true,
"old_password_initially_accepted": true,
"new_password_initially_refused": true,
"native_first_factor": true,
"interactive_service_login_refused": true,
"reset_http_status": 200,
"reset_response_status": "KO",
"native_identity_finish": true,
"reset_without_verified_challenge_refused": true,
"consumed_identity_token_replay_refused": true,
"old_password_accepted_after": true,
"new_password_accepted_after": false
},
{
"bind_role": "lldap_password_manager",
"same_authelia_process": true,
"status": "reset_completed",
"only_loopback_interface": true,
"external_routes_absent": true,
"old_password_initially_accepted": true,
"new_password_initially_refused": true,
"native_first_factor": true,
"interactive_service_login_refused": true,
"reset_http_status": 200,
"reset_response_status": null,
"native_identity_finish": true,
"reset_without_verified_challenge_refused": true,
"consumed_identity_token_replay_refused": true,
"old_password_accepted_after": false,
"new_password_accepted_after": true
},
{
"bind_role": "lldap_strict_readonly",
"same_authelia_process": true,
"status": "reset_refused",
"only_loopback_interface": true,
"external_routes_absent": true,
"old_password_initially_accepted": true,
"new_password_initially_refused": true,
"native_first_factor": true,
"interactive_service_login_refused": true,
"reset_http_status": 200,
"reset_response_status": "KO",
"native_identity_finish": true,
"reset_without_verified_challenge_refused": true,
"consumed_identity_token_replay_refused": true,
"old_password_accepted_after": true,
"new_password_accepted_after": false
}
],
"cleanup_verified": true
}
}

View file

@ -0,0 +1,3 @@
scripts/rehearse-reset-role-refresh.py: c84751c3ed2893ef25460401ab760f109ac8502b308aae120990d402f85ed42f
scripts/rehearse-native-password-reset.py: 133c8230c524fbbaf7f66e4872e14b826ed2c5a4f4de3bc47fa86cfef7631d5d
scripts/native_recovery/probe.py: eab3bae55626abd4920eb6aa2e329c83559007c54ac74c3aaa1cf41e84eb6556

View file

@ -0,0 +1,12 @@
{
"schema": "coulomb.reset-transition-consumer-gate.v1",
"command": "make check",
"passed": 546,
"failures": 0,
"skipped": 0,
"warnings": 69,
"elapsed_seconds": 55.15,
"junit_sha256": "717412e60cbe12d52c4a14f124fbe60a59789bc52467c0b938645c2b28c24232",
"proposal_digest": "sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f",
"production_grant_performed": false
}

View file

@ -0,0 +1,74 @@
{
"schema": "netkingdom.reset-transition-validation.v1",
"status": "tested_proposal_not_granted",
"unit_checks": 25,
"native_controller_checks": 4,
"failures": 0,
"errors": 0,
"skipped": 0,
"junit_sha256": "26911e26b410c242f3dafb1915301875a4bfd3f5d88770be3b7bec5ce4c5c3d5",
"ruff": "passed",
"native_findings": [
"grant and rollback preserve all other user IDs and group memberships",
"real process death after removal or addition recovers read-only without replaying grant",
"membership rollback leaves prior HTTP token rights active; freshly issued token loses those rights"
],
"authelia_findings": "same running process refuses reset before grant, allows after grant, refuses after rollback; three fresh consumed challenges, old/new password checks; all confined containers removed",
"runtime_observation": {
"schema": "netkingdom.reset-runtime.v1",
"kube-system_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
"sso_uid": "a4a148d8-92ab-41e6-b975-4e67b73edfc0",
"authelia": {
"uid": "dc40a925-2306-4a3b-89c0-e714187503c3",
"images": [
"docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54"
],
"ready": true
},
"lldap": {
"uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8",
"images": [
"docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d"
],
"ready": true
},
"authelia_configuration": {
"uid": "7af1fa0d-1bd4-47e1-8873-0f620788ef3f",
"sha256": "0a8bca50641a4c6f27830f476c2c9f413fc3795711becccb8344ee4d6c376e80",
"bind_is_svc_authelia": true
}
},
"production_membership_changed": false,
"grant_approved": false,
"productive_acceptance": false,
"sources": [
{
"path": "sso-mfa/k8s/lldap/reset-role-transition.py",
"sha256": "cc405861bb5cc0adf419a1cf98a90872d8395eecbfc7d4d38947cb4008bee217"
},
{
"path": "sso-mfa/k8s/lldap/reset-role-plan.json",
"sha256": "703e281ee6a17e1ef0a1dffbad35b8025ab3480d51d3fc2c36323f2bc6227106"
},
{
"path": "tests/test_reset_role_transition.py",
"sha256": "1437761ac60a7aeba16db70ccf07d48a9f5332617a00e7a325e678951985a08e"
},
{
"path": "sso-mfa/k8s/lldap/test_reset_role_transition_native.py",
"sha256": "13c53e8a757ed0e954ffd28df14283d51dd9d7d3278c1d7d7d5759b419a2a89a"
},
{
"path": "sso-mfa/k8s/lldap/test_bind_split.py",
"sha256": "ae69051a3df4596fe220306d88a28b2c12e42fbfe3587e02135e3684eb5f354a"
},
{
"path": "sso-mfa/k8s/lldap/bind-split-rehearsal.py",
"sha256": "9ab873d1e941807fb19a48fb18c3af246a98bf0788660d8d79a14fb0c0e0a067"
},
{
"path": "docs/evidence/2026-10-31-reset-authority/role-refresh.json",
"sha256": "325f446960134e9510591a2a27f5833bc80fa858d77e6a88082af41307993b65"
}
]
}

View file

@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Verify role changes with one running Authelia process; synthetic peers only."""
import argparse
import hashlib
import json
import re
import runpy
import secrets
import urllib.parse
import uuid
from datetime import UTC, datetime
from pathlib import Path
HERE = Path(__file__).resolve().parent
BASE = runpy.run_path(str(HERE / 'rehearse-native-password-reset.py'))
docker, container, archive, probe, configuration = (
BASE[n] for n in ('docker', 'container', 'archive', 'probe', 'configuration'))
def set_password(anchor, token, target, password):
assert target in {'alice', 'bob', 'operator', 'appoperator', 'svc-other', 'svc-auth'}
return docker('exec', '-i', anchor, '/bin/sh', '-c',
'read -r token; read -r LLDAP_USER_PASSWORD; export LLDAP_USER_PASSWORD; '
'exec /app/lldap_set_password --base-url http://127.0.0.1:17170 '
'--token "$token" --username ' + target,
data=(token + '\n' + password + '\n').encode(), check=False).returncode
def rehearse():
if not __debug__:
raise RuntimeError('acceptance_assertions_must_be_enabled')
context = json.loads(docker('context', 'inspect', 'default').stdout)[0]
assert context['Endpoints']['docker']['Host'] == 'unix:///var/run/docker.sock'
for image in (BASE['LLDAP'], BASE['AUTHELIA'], BASE['CLIENT']):
assert json.loads(docker('image', 'inspect', image).stdout)[0]['Id'] == image
name = 'csoc-reset-transition-' + uuid.uuid4().hex[:12]
auth = name + '-auth'
values = {key: secrets.token_urlsafe(24) for key in (
'admin_password', 'bind_password', 'old_password', 'new_password')}
result = {'scenarios': []}
try:
container(name, BASE['LLDAP'], 'none', {
'LLDAP_JWT_SECRET_FILE': '/run/fixture/jwt',
'LLDAP_LDAP_USER_PASS_FILE': '/run/fixture/admin-password',
'LLDAP_LDAP_BASE_DN': 'dc=example,dc=test',
'LLDAP_LDAP_HOST': '127.0.0.1', 'LLDAP_HTTP_HOST': '127.0.0.1'})
docker('exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({
'jwt': secrets.token_hex(32).encode(),
'admin-password': values['admin_password'].encode(),
'lldap.toml': ('database_url = "sqlite:///data/users.db?mode=rwc"\nkey_seed = '
+ json.dumps(secrets.token_hex(32)) + '\n').encode()}))
docker('exec', '-d', name, '/bin/sh', '-c',
'exec /app/lldap run --config-file /run/fixture/lldap.toml '
'> /run/fixture/server.log 2>&1')
prepared = probe(name, {**values, 'mode': 'bootstrap', 'authority_matrix': True})
assert set_password(name, prepared['admin_token'], 'svc-auth',
values['bind_password']) == 0
container(auth, BASE['AUTHELIA'], 'container:' + name, {})
docker('exec', '-i', auth, 'tar', '-xf', '-', '-C', '/run/fixture', data=archive({
'configuration.yml': json.dumps(configuration(values['bind_password'])).encode()}))
# One Authelia process for all three phases, including rollback.
probe(name, {**values, 'mode': 'bind_role', 'role': 'lldap_strict_readonly'})
docker('exec', '-d', auth, '/bin/sh', '-c',
'exec /app/authelia --config /run/fixture/configuration.yml '
'> /run/fixture/server.log 2>&1')
process_marker = None
tokens = set()
for index, role in enumerate(['lldap_strict_readonly', 'lldap_password_manager',
'lldap_strict_readonly']):
if index:
probe(name, {**values, 'mode': 'bind_role', 'role': role})
assert set_password(name, prepared['admin_token'], 'alice',
values['old_password']) == 0
docker('exec', auth, 'rm', '-f', '/data/notifications.txt')
requested = probe(name, {**values, 'mode': 'reset_request', 'target': 'alice'})
marker = docker('exec', auth, '/bin/sh', '-c',
'for p in /proc/[0-9]*; do '
'if [ "$(cat "$p/comm" 2>/dev/null)" = authelia ]; '
'then cat "$p/stat"; fi; done').stdout
# /proc stat changes CPU counters; compare PID and start ticks only.
fields = marker.decode().split()
current_marker = (fields[0], fields[21])
if process_marker is None:
process_marker = current_marker
assert current_marker == process_marker, 'authelia_process_restarted'
notification = docker('exec', auth, 'cat', '/data/notifications.txt').stdout.decode()
links = re.findall(r'https://auth\.recovery\.test/reset-password/step2\?[^\s<>]+',
notification)
assert len(links) == 1, ('one_new_native_challenge_required_phase_'
+ str(index) + '_count_' + str(len(links)))
token = urllib.parse.parse_qs(urllib.parse.urlsplit(links[-1]).query)['token'][0]
assert token not in tokens, 'fresh_challenge_required'
tokens.add(token)
settled = probe(name, {**values, 'mode': 'reset_finish', 'target': 'alice',
'reset_token': token})
expected = role == 'lldap_password_manager'
assert settled['new_password_accepted_after'] is expected
assert settled['old_password_accepted_after'] is not expected
assert (settled['status'] == 'reset_completed') is expected
result['scenarios'].append({'bind_role': role, 'same_authelia_process': True,
**requested, **settled})
finally:
for current in [name + '-probe', auth, name]:
docker('rm', '-f', '-v', current, check=False)
result['cleanup_verified'] = all(not docker(
'ps', '-aq', '--filter', 'name=^/' + n + '$').stdout.strip()
for n in [name + '-probe', auth, name])
if not result['cleanup_verified']:
raise RuntimeError('fixture_cleanup_requires_review')
assert not any(value in json.dumps(result) for value in values.values())
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
manifest_path = args.output.with_suffix('.sources.txt')
if args.output.exists() or manifest_path.exists():
raise SystemExit('Fresh receipt and source manifest required')
findings = rehearse()
paths = [Path(__file__), HERE / 'rehearse-native-password-reset.py',
HERE / 'native_recovery/probe.py']
manifest = ''.join(str(p.relative_to(HERE.parent)) + ': '
+ hashlib.sha256(p.read_bytes()).hexdigest() + '\n' for p in paths).encode()
receipt = {'schema': 'coulomb.native-reset-role-refresh.v1',
'observed_at': datetime.now(UTC).isoformat(), 'data': 'synthetic',
'status': 'native_role_refresh_verified',
'images': {n: BASE[n.upper()] for n in ('lldap', 'authelia', 'client')},
'production_access': False, 'provider_smtp_contacted': False,
'owner_runtime_or_permission_change_performed': False,
'no_ports_host_mounts_or_external_routes': True,
'backend_container_flags_verified_before_fixture_import': True,
'source_manifest_sha256': hashlib.sha256(manifest).hexdigest(),
'findings': findings}
with manifest_path.open('xb') as stream:
stream.write(manifest)
with args.output.open('x') as stream:
json.dump(receipt, stream, indent=2)
stream.write('\n')
print(json.dumps({'status': receipt['status'], 'roles': len(findings['scenarios']),
'cleanup_verified': findings['cleanup_verified']}))
if __name__ == '__main__':
main()

View file

@ -300,3 +300,13 @@ reset-role-observation-2026-10-31.json and reset-role-return-verification-2026-1
## Completion
Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records.
2026-10-31 reset transition preparation: owner T14 prepares the source-bound,
crash-recoverable role controller; T15 explicitly holds founder admission and
execution; T16 holds source alignment, productive reset/app/MFA and identity
backup/restore acceptance. The same running Authelia process picks up grant and
rollback in the confined native fixture. Membership rollback does not revoke
HTTP tokens issued during a grant; the review includes their up-to-24-hour scope.
No production role/password changed. T06 remains progress and consumes owner T10,
whose acceptance is preserved rather than closed by fixture proof.