NK-WP-0054-T05 phase 3b: restore launchers follow the archived identities; startup proof
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 308701@bnt-lap001
Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
This commit is contained in:
tegwick 2026-10-29 22:11:19 +01:00
parent e6f4e28164
commit 05a9aefaed
4 changed files with 81 additions and 1 deletions

View file

@ -0,0 +1,15 @@
{
"data": "synthetic",
"findings": {
"admin_still_signs_in": true,
"mismatched_configured_password_ignored": true,
"missing_configured_admin_created": false
},
"image": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d",
"observed_at": "2026-10-29T21:10:51.513396+00:00",
"production_contacted": false,
"schema": "netkingdom.lldap-bind-split-rehearsal.v1",
"stage": "startup",
"task": "NK-WP-0054-T05",
"values_recorded": false
}

View file

@ -14,6 +14,8 @@ It answers the questions the live migration depends on and writes a value-free J
- does a `lldap_strict_readonly` bind read users and groups but fail to write;
- what is the narrowest group with which the provisioner can create users and set membership;
- can a `svc-*` account sign in interactively to LLDAP (SI-3);
- `--stage startup`: once `lldap_admin` has a member, does LLDAP ignore the configured admin password
and refuse to create a missing configured admin (so a backup from any point of the switch restores);
- `--stage authelia`: does the repository's Authelia configuration, bound as `svc-authelia`, admit a
human and refuse `svc-*` and `ld-admin` (and would the previous filter have admitted them).
"""
@ -377,6 +379,31 @@ def rehearse(tag):
return result
def rehearse_startup(tag):
admin, ld_admin = secrets.token_hex(16), secrets.token_hex(16)
directory = Directory(tag)
result = {}
try:
directory.start("admin", admin)
directory.stop()
directory.start("ld-admin", secrets.token_hex(16))
result["missing_configured_admin_created"] = (
directory.login("ld-admin", ld_admin) is not None
)
result["admin_still_signs_in"] = directory.login("admin", admin) is not None
token = directory.login("admin", admin)
directory.create_user(token, "ld-admin", ["lldap_admin"])
directory.set_password(token, "ld-admin", ld_admin)
directory.stop()
directory.start("ld-admin", secrets.token_hex(16))
result["mismatched_configured_password_ignored"] = (
directory.login("ld-admin", ld_admin) is not None
)
finally:
directory.remove()
return result
def authelia_config(users_filter, password):
config = yaml.safe_load(
yaml.safe_load(CONFIGMAP.read_text())["data"]["configuration.yml"]
@ -509,7 +536,7 @@ def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--receipt", type=Path, required=True)
parser.add_argument(
"--stage", choices=["directory", "authelia"], default="directory"
"--stage", choices=["directory", "startup", "authelia"], default="directory"
)
args = parser.parse_args(argv)
if args.receipt.exists():
@ -521,6 +548,8 @@ def main(argv=None):
with tempfile.TemporaryDirectory(prefix="nk54-authelia-") as workdir:
findings = rehearse_authelia(tag, Path(workdir))
elif args.stage == "startup":
findings = rehearse_startup(tag)
else:
findings = rehearse(tag)
receipt = {

View file

@ -56,6 +56,26 @@ def test_configuration_preserves_original_seed_and_disables_reset_sending(tmp_pa
assert PASSWORD.encode() not in value and jwt not in value
@pytest.mark.parametrize('configured', [None, 'ld-admin'])
def test_configured_admin_follows_the_archived_deployment(tmp_path, configured):
# NK-WP-0054-T05: before and after the bind split the restore takes the configured
# super-admin from the archived deployment, never a hard-coded name.
resources(tmp_path)
if configured:
path = tmp_path / 'resources/sso/deployment/lldap.json'
deployment = json.loads(path.read_text())
deployment['spec']['template']['spec']['containers'][0]['env'].append(
{'name': 'LLDAP_LDAP_USER_DN', 'value': configured})
path.write_text(json.dumps(deployment))
data = tmp_path / 'data/lldap'
data.mkdir(parents=True)
(data / 'lldap_config.toml').write_text('key_seed = "synthetic-original-seed"\n')
import tomllib
value, _, password, _ = m.configuration(tmp_path)
assert tomllib.loads(value.decode())['ldap_user_dn'] == (configured or 'admin')
assert password == PASSWORD.encode()
def test_tar_transport_refuses_path_escape():
with pytest.raises(m.bundle.Refused):
m.archive_files({'../escape': b'synthetic'})

View file

@ -326,6 +326,22 @@ rotations `lldap LLDAP_LDAP_USER_PASS` and `authelia LLDAP_SVC_AUTHELIA_PASS ->
existing Secrets are switched only by explicit, conditional rotation (97 tests). Left: 3b (backup
profile and the three restore launchers), then phase 4.
**2026-10-29 phase 3b (restore path):** no launcher change is needed, which corrects this task's
earlier assumption. `isolated-lldap.py` takes the configured super-admin name from the archived
deployment's `LLDAP_LDAP_USER_DN` and its password from the archived `lldap-secrets`, and prompts for
the password of the user under test; `isolated-authelia.py` binds with the archived configmap user and
`authelia-secrets`; `isolated-privacyidea-mfa.py` rewrites only the resolver `LDAPURI` and keeps the
archived bind DN and password. After the split all three therefore follow `ld-admin`, `svc-authelia`
and `svc-privacyidea` from the backup itself. Regression test: the configured admin follows the archived
deployment (`tests/test_isolated_lldap_recovery.py`). `bind-split-rehearsal.py --stage startup`
([receipt](../docs/evidence/2026-10-28-nk54-bind-split-rehearsal/synthetic-startup.json)) proves on
v0.6.2 that once `lldap_admin` has a member, LLDAP ignores the configured admin password and does not
create a missing configured admin: a backup taken at any point of the switch restores, and
**`bind-split.py accounts` must run before the LLDAP deployment switch**. The backup profile is
unchanged: its components carry their own Secrets with the new values, and identity-provisioner is not
an identity-restore component (its password is re-derivable from custody). Phase 3 is complete;
phase 4 (runbook, founder rehearsal on a restored backup, live run) remains.
## Disposition of per-service admin accounts
```task