NK-WP-0049-T42: rebuild the MFA stage around the restored directory; check by user and realm
The founder's first real run stopped at ResolverError on all checks: real tokens belong to an LDAP user and the archived resolver names the in-cluster directory. Restored LLDAP now joins the database's network-free namespace, archived resolver addresses are repointed to loopback in the disposable copy only (any other address is refused), and the checks call POST /validate/check by user and realm as KeyCape does. Fifteen native fixture tests pass (HOTP and TOTP+PIN, negatives, bundle). Also covers the user-to-token resolver and the encrypted bind password. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 328925@bnt-lap001 Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed
This commit is contained in:
parent
9a3a84c99c
commit
5008e34f1a
6 changed files with 365 additions and 200 deletions
|
|
@ -265,41 +265,53 @@ procedure) and your authenticator ready:
|
|||
bash tools/bootstrap-recovery/run-isolated-privacyidea-mfa.sh /path/to/verified/transfer.json
|
||||
```
|
||||
|
||||
Prompts, in order: the `railiance-backup` age key (hidden); a token serial chosen
|
||||
from the list printed on your terminal (serial, type, usable flag; the list never
|
||||
enters the receipt); then that token's PIN plus current code exactly as typed at
|
||||
login (hidden). Pick a token you hold and whose PIN you know. A TOTP code is valid
|
||||
Prompts, in order: the `railiance-backup` age key (hidden); your login name (and
|
||||
the realm, if more than one exists); then a token PIN plus current code exactly as
|
||||
typed at login (hidden). Any one of your usable tokens works. A TOTP code is valid
|
||||
for tens of seconds, so enter it only when asked and keep the authenticator open.
|
||||
|
||||
The restore is the privacyIDEA stage's: restored database and original keys in
|
||||
`--network none` containers, read-only, capabilities dropped, user 1000, tmpfs
|
||||
only, isolation inspected before private import. Three checks then run through
|
||||
privacyIDEA's own `check_serial_pass`, each in a separate process:
|
||||
**Why by user.** A first version checked a token by serial. On the real backup every
|
||||
check stopped with `ResolverError`: real tokens belong to an LDAP user, and the
|
||||
archived resolver `lldap-coulomb` points at the in-cluster address
|
||||
`ldap://lldap.sso.svc.cluster.local:3890`, which does not exist in isolation. Fixture
|
||||
tokens had no owner, so tests had not shown it, and the run correctly claimed no proof.
|
||||
|
||||
1. **Wrong key.** The application is pointed at a random key file; the run asserts
|
||||
the configuration really names it. The typed code must be refused (native
|
||||
decryption raises). This is the discriminating step: the same code is accepted
|
||||
with the original key in step 3.
|
||||
2. **Wrong code.** The final digit of what you typed is changed; it must be refused.
|
||||
3. **Your code.** Accepted by the original key and retained token material.
|
||||
**Topology.** The restored database container is the `--network none` anchor.
|
||||
Restored LLDAP and the privacyIDEA worker join its namespace (`container:<anchor>`)
|
||||
and talk over loopback only. All three are read-only, capabilities dropped, user
|
||||
non-root, tmpfs-only, with isolation inspected before private import.
|
||||
|
||||
The order is deliberate. Only the last step can spend a counter or replay-protected
|
||||
code. A first fixture run showed why: when the wrong-key step silently used the
|
||||
original key, it accepted the code and the real check was then refused. The
|
||||
configuration assertion now prevents that vacuous result, and the receipt only
|
||||
claims proof when all three outcomes line up.
|
||||
**Resolver rewrite, disposable copy only.** After `pg_restore`, every `ldapresolver`
|
||||
`LDAPURI` must equal the archived in-cluster address exactly, or the run is refused
|
||||
as an owner decision. They are then repointed to `ldap://127.0.0.1:3890`. The stored
|
||||
bind password is not touched, so the original encryption key must decrypt it and the
|
||||
restored LLDAP must accept it before any user can be looked up.
|
||||
|
||||
The receipt carries booleans, bounded refusal categories, token type, a SHA-256 of
|
||||
the serial, counts and source hashes only. No serial, PIN or code is recorded, and
|
||||
the disposable copy is destroyed. Fixture tests cover HOTP and TOTP-with-PIN, a
|
||||
wrong code alone, an unknown serial, a malformed original key and an encrypted
|
||||
bundle through `main()`.
|
||||
**Checks.** The call is `POST /validate/check` with user, realm and pass, through
|
||||
privacyIDEA's own application, so stored policies and the resolver apply as for KeyCape:
|
||||
|
||||
**Not proven here:** the user-to-token path. KeyCape calls `/validate/check` with
|
||||
user, realm and pass, so the restored LDAP resolver, its encrypted bind password
|
||||
and the LLDAP lookup are exercised only by a chained run (T18 phase B). A policy
|
||||
such as `otppin=userstore` would not apply to a serial-based check. The receipt
|
||||
therefore records `user_to_token_resolver_proven: false`.
|
||||
1. The user is found through the restored resolver and directory, and has a usable token.
|
||||
2. **Wrong key.** The application is pointed at a random key file (the run asserts the
|
||||
configuration really names it). The typed code must be refused; privacyIDEA returns
|
||||
its own error 907.
|
||||
3. **Wrong code.** The last digit of what you typed is changed; refused as a wrong value.
|
||||
4. **Your code.** Accepted with the original key and the retained token material.
|
||||
|
||||
The order is deliberate: only the last step can spend a counter or replay-protected
|
||||
code. An earlier fixture run in which the wrong-key step silently used the original key
|
||||
showed why; the configuration assertion now prevents that vacuous result, and the
|
||||
receipt claims proof only when all three outcomes line up.
|
||||
|
||||
The receipt carries booleans, bounded refusal categories (for example `error_907`,
|
||||
`wrong_value`), token types, counts, a SHA-256 of the login and source hashes. No login,
|
||||
PIN or code is recorded, and the disposable copy is destroyed. Fixture tests cover HOTP
|
||||
and TOTP-with-PIN owned by an LDAP user, a wrong code alone, an unknown user, a malformed
|
||||
original key, the rewrite refusing an unknown address, and an encrypted bundle through
|
||||
`main()`.
|
||||
|
||||
**Limits.** Policies that call outside the namespace (for example event handlers that
|
||||
send notifications) cannot succeed and are not exercised. Token ownership is checked for
|
||||
the one user you name, not for every user.
|
||||
|
||||
## Unresolved inputs before execution
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue