feat(privacyidea): add guarded resolver reconciliation helper
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
parent
edee5c28ab
commit
f2e578cf3e
3 changed files with 137 additions and 0 deletions
|
|
@ -126,6 +126,12 @@ material.
|
|||
- A named operator window, driver, abort operator, provider authority, and
|
||||
protected verification method are still required before any live action.
|
||||
|
||||
NetKingdom added
|
||||
`sso-mfa/k8s/privacyidea/update-lldap-resolver-live.sh` as the bounded
|
||||
resolver-only operation. It requires explicit `--apply` plus an interactive
|
||||
terminal, passes password files by pathname to a child process, updates only
|
||||
`lldap-coulomb`, and emits only a boolean result. It has not been run.
|
||||
|
||||
## Owner-reported cutover outcome
|
||||
|
||||
KeyCape reported on 2026-08-23 that its governed recovery path completed the
|
||||
|
|
|
|||
125
sso-mfa/k8s/privacyidea/update-lldap-resolver-live.sh
Executable file
125
sso-mfa/k8s/privacyidea/update-lldap-resolver-live.sh
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env bash
|
||||
# update-lldap-resolver-live.sh — attended, resolver-only privacyIDEA update.
|
||||
#
|
||||
# This does not repair realms or policies. It updates only the persisted
|
||||
# lldap-coulomb resolver after the LLDAP bind credential has changed.
|
||||
#
|
||||
# Usage:
|
||||
# ./update-lldap-resolver-live.sh --apply
|
||||
#
|
||||
# The operator supplies both passwords interactively. Values are kept in a
|
||||
# mode-0700 temporary directory and mode-0600 files, passed to a child process
|
||||
# by pathname, and never printed or placed in command arguments.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${1:-}" != "--apply" || "${2:-}" != "" ]]; then
|
||||
echo "Usage: $0 --apply" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "ERROR: --apply requires an interactive terminal." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
PI_URL="${PI_URL:-https://pink.coulomb.social}"
|
||||
RESOLVER_NAME="${RESOLVER_NAME:-lldap-coulomb}"
|
||||
LLDAP_URL="${LLDAP_URL:-ldap://lldap.sso.svc.cluster.local:3890}"
|
||||
LLDAP_BASE_DN="${LLDAP_BASE_DN:-dc=netkingdom,dc=local}"
|
||||
LLDAP_BIND_DN="${LLDAP_BIND_DN:-uid=admin,ou=people,dc=netkingdom,dc=local}"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
chmod 700 "$tmp"
|
||||
cleanup() {
|
||||
if [[ -f "$tmp/pi-admin" ]]; then shred -u "$tmp/pi-admin" 2>/dev/null || rm -f "$tmp/pi-admin"; fi
|
||||
if [[ -f "$tmp/lldap-bind" ]]; then shred -u "$tmp/lldap-bind" 2>/dev/null || rm -f "$tmp/lldap-bind"; fi
|
||||
rmdir "$tmp" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
printf 'privacyIDEA pi-admin password: ' >&2
|
||||
IFS= read -r -s pi_admin_password
|
||||
printf '\n' >&2
|
||||
printf 'LLDAP bind/admin password: ' >&2
|
||||
IFS= read -r -s lldap_bind_password
|
||||
printf '\n' >&2
|
||||
if [[ -z "$pi_admin_password" || -z "$lldap_bind_password" ]]; then
|
||||
echo "ERROR: passwords must not be empty." >&2
|
||||
exit 2
|
||||
fi
|
||||
printf '%s' "$pi_admin_password" > "$tmp/pi-admin"
|
||||
printf '%s' "$lldap_bind_password" > "$tmp/lldap-bind"
|
||||
unset pi_admin_password lldap_bind_password
|
||||
chmod 600 "$tmp/pi-admin" "$tmp/lldap-bind"
|
||||
|
||||
python3 - "$tmp/pi-admin" "$tmp/lldap-bind" "$PI_URL" "$RESOLVER_NAME" \
|
||||
"$LLDAP_URL" "$LLDAP_BASE_DN" "$LLDAP_BIND_DN" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
pi_path, ldap_path, base_url, resolver, ldap_url, base_dn, bind_dn = sys.argv[1:]
|
||||
|
||||
def read_secret(path: str) -> str:
|
||||
value = Path(path).read_text(encoding="utf-8")
|
||||
if not value:
|
||||
raise RuntimeError("empty protected input")
|
||||
return value
|
||||
|
||||
def post(path: str, payload: dict, token: str | None = None) -> dict:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token is not None:
|
||||
headers["Authorization"] = token
|
||||
request = urllib.request.Request(
|
||||
base_url.rstrip("/") + path,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=20) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise RuntimeError(f"HTTP status {exc.code}") from None
|
||||
except (urllib.error.URLError, TimeoutError):
|
||||
raise RuntimeError("request failed") from None
|
||||
|
||||
try:
|
||||
auth = post("/auth", {"username": "pi-admin", "password": read_secret(pi_path)})
|
||||
token = str(auth.get("result", {}).get("value", {}).get("token", ""))
|
||||
if not token:
|
||||
raise RuntimeError("privacyIDEA authentication failed")
|
||||
|
||||
resolver_body = {
|
||||
"type": "ldapresolver",
|
||||
"LDAPURI": ldap_url,
|
||||
"BINDDN": bind_dn,
|
||||
"BINDPW": read_secret(ldap_path),
|
||||
"LDAPBASE": base_dn,
|
||||
"LOGINNAMEATTRIBUTE": "uid",
|
||||
"LDAPSEARCHFILTER": "(objectClass=inetOrgPerson)",
|
||||
"LDAPFILTER": "(&(objectClass=inetOrgPerson)(uid=%s))",
|
||||
"USERINFO": json.dumps({
|
||||
"username": "uid",
|
||||
"phone": "telephoneNumber",
|
||||
"mobile": "mobile",
|
||||
"email": "mail",
|
||||
"surname": "sn",
|
||||
"givenname": "givenName",
|
||||
}),
|
||||
"UIDTYPE": "uid",
|
||||
"NOREFERRALS": True,
|
||||
"NOSCHEMAS": True,
|
||||
}
|
||||
result = post(f"/resolver/{resolver}", resolver_body, token)
|
||||
status = result.get("result", {}).get("status")
|
||||
if status not in (True, "true", "True"):
|
||||
raise RuntimeError("privacyIDEA resolver update was rejected")
|
||||
except RuntimeError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
print("privacyIDEA resolver update: PASS")
|
||||
PY
|
||||
|
|
@ -117,6 +117,12 @@ clients are a list, so the verifier now uses Authelia's supported template
|
|||
filter over the Secret-mounted file. The revision was applied live and
|
||||
Authelia returned Ready with startup complete.
|
||||
|
||||
NetKingdom also added the unattended-safe shape of the remaining provider
|
||||
operation in `sso-mfa/k8s/privacyidea/update-lldap-resolver-live.sh`. It is
|
||||
explicitly gated by `--apply`, requires an interactive terminal, uses protected
|
||||
0600 files, updates only `lldap-coulomb`, and emits no credential values. It
|
||||
has not been run; attended provider-admin reconciliation remains pending.
|
||||
|
||||
Do not use `sso-mfa/bootstrap/creds-rotate.sh` through an agent as currently
|
||||
written: it prints generated replacement values and its signing-key path
|
||||
causes immediate invalidation. Do not use a helper that reads the live Secret
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue