NK-WP-0054-T05 phase 3b: restore launchers follow the archived identities; startup proof
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:
parent
e6f4e28164
commit
05a9aefaed
4 changed files with 81 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue