Prepare independent outage recovery and Storage Share documentation

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
tegwick 2026-10-16 17:59:18 +02:00
parent 2a3800e7f1
commit 1764f40b43
12 changed files with 1050 additions and 22 deletions

View file

@ -7,6 +7,12 @@ continuity and current/legacy caller acceptance remain open for NK-WP-0049.
Approved execution is recorded in decisions40bfae40 and5edd5203; this document
records their scope and evidence.
**Outage quick start:** [Storage Share disaster recovery guide](disaster-recovery-storage-share.md).
The independently prompted provider download and offline tools ZIP are prepared;
actual outage-path retrieval and guide publication are separately tracked in
T23/T26. The [isolated service restore packet](identity-service-restore-packet.md)
contains the draft native-service sequence and remaining T15 admission inputs.
## What has been established
On 2026-10-15 the operator identified Scaleway and Storage Share as the off-host

View file

@ -0,0 +1,136 @@
# NetKingdom disaster recovery — start here
Updated 2026-10-16. Owner: NetKingdom with Railiance Platform and the individual
identity service owners. This guide covers the identity backup, not whole-host,
OpenBao, Forgejo, User Engine or application recovery.
## If the identity services are down
1. Use a trusted recovery workstation. Obtain Storage Share access independently
from your password safe/offline custody, and the independently held
`railiance-backup` age private key. Never paste either into chat or a ticket.
The private key must match this **public** recipient:
`age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz`.
The provider password and encryption key are separate requirements.
2. Open <https://nx4069.your-storageshare.de> using the independent provider
account, not NetKingdom SSO. In the Backup account, navigate to
`railiance-backups/net-kingdom-identity/`. Recovery documentation and a small
versioned recovery-tools ZIP are placed beside the encrypted archives.
Keep a copy of this guide and ZIP outside Storage Share as well.
3. Choose an intact recovery generation. The full identity filename starts
`net-kingdom-identity-` and ends `.tar.age`. A `privacyidea_db-*.dump.age`
database-only backup cannot replace the matching service keys and configs.
Each generation has a `.receipt.json` index. Preserve the original objects.
4. Check timestamp, size and SHA256 against a separately trusted receipt. The
index is useful metadata, not a signed independent trust anchor. For the
first actually recovered, protected generation the trusted values are:
- Artifact: `net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age`
- Bytes: `109738344`
- SHA256: `7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`
- Strong ETag at publication: `"28ec1f7014e492de4c40e1ae75502f08"`
This is a known good fallback, not a claim that it is the latest backup.
For a newer generation, reconcile its index with the independently retained
publisher receipt before constructing a new download input. Never edit a
receipt just to make a failed integrity check pass. ETag changes require
owner reconciliation even if the filename is unchanged.
5. On Linux with Python 3, unpack the recovery-tools ZIP into a private working
directory and enter it. Verify its `SHA256SUMS` against a trusted repository
copy or independently held kit digest before executing code. A checksum
stored only alongside the download detects damage, not a malicious provider.
Alternatively use an independently available trusted net-kingdom checkout.
```bash
python3 tools/bootstrap-recovery/retrieve-identity-offline.py \
--source-receipt docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json \
--auth account --receipt /tmp/nk49-outage-transfer.json
```
In your own terminal, enter the independently held **Backup** account app
password at the hidden prompt. The fixed account name is `Backup`.
If your independently held credential is a read-capable public share token
for this exact identity folder, use `--auth folder-share` instead. An
upload-only token, another account's password or a share for the backup root
is insufficient for this command. A 401/403/404 stops; do not widen permissions
or substitute another token silently. Download performs only HEAD/GET against
the fixed provider, rejects redirects/proxies, and verifies ETag/size/hash.
Failure removes its incomplete local download. Success leaves ciphertext
in a private `/tmp/nk49-identity-fetched-*` directory named in the receipt.
6. Verify and decrypt through the existing hidden key helper. Requirements:
`age`, `age-keygen`, `openssl`, Python 3 and Docker; the following PostgreSQL
image must already be present on the recovery host:
`postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70`.
Stage and verify this image before a drill. The tools ZIP contains no images.
Allow enough private disk and RAM for a roughly 110 MB ciphertext, a bounded
512 MiB archive and the disposable PostgreSQL container (2 GiB memory limit).
```bash
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
--prompt -- \
python3 tools/bootstrap-recovery/verify-identity.py \
--transfer-receipt /tmp/nk49-outage-transfer.json \
--receipt /tmp/nk49-outage-archive-recovery.json
```
Use new receipt names on every run. The helper removes its temporary private
key on exit. The verifier checks the encrypted manifest and all file hashes,
original key pairs, SQLite integrity, wrong-key refusal and an isolated
PostgreSQL restore. It removes decrypted staging and the disposable database.
It deliberately does not install restored secrets or alter production.
7. Preserve the sanitized receipts and original ciphertext. Remove your local
downloaded ciphertext directory when no longer needed. If the verifier fails,
stop and check the bounded receipt with the service owner; do not regenerate
encryption/signing keys, reset user passwords or delete the provider backup.
## What the backup contains
| Component | Recovery material |
| --- | --- |
| LLDAP | Native SQLite snapshot, remaining durable data, deployment and referenced secrets |
| Authelia | Native SQLite snapshot, encryption/session/OIDC material, current configuration and referenced secrets |
| KeyCape | Original signing/configuration material, client configuration, authentication policies and receipts |
| privacyIDEA | Fresh PostgreSQL dump, original encryption key, matching original audit RSA key pair, current configuration and required durable files |
The encrypted manifest records exact files and capture intervals. This is not
a global atomic snapshot of all services. Historical backups and selected
notification/log files are excluded as documented by the capture profile.
The backup contains sensitive service credentials; only ciphertext belongs on
ordinary persistent recovery media. The age private key is never in the bundle.
## Restoring working services
**Archive recovery passed on 2026-10-16. Full restored-service acceptance remains
open.** The archive verifier deletes its plaintext and is not a service importer.
Follow `docs/identity-service-restore-packet.md` with service owners before an
isolated service drill. Its pending target and image fields must be resolved.
Restore the exact matching data/config/original keys into an isolated recovery
environment first. Keep production ingress, public DNS, notifications, external
provider writes and real consumer traffic disconnected. Establish directory and
database readiness; prove LLDAP existing-user authentication; then privacyIDEA
retained-token MFA/audit continuity, Authelia storage/OIDC, and KeyCape/current
consumer continuity according to their actual dependencies. Existing user/MFA
inputs stay in operator custody. Public issuer identity must be preserved while
test traffic is routed privately; do not change production DNS for a drill.
Only after owner acceptance of native positive/negative tests should the owners
approve a production cutover, including target identity, rollback, DNS/ingress,
current consumers, credential dispositions and monitoring. This guide does not
authorize a production restore or automatic credential rotation.
## Schedule, protection and limits
Daily capture is scheduled at 03:45 UTC with up to five minutes jitter. Hourly
host-local checks flag a backup older than 25 hours. Retention protects the
independently recovered pinned generation, newest generation, seven distinct
daily and two weekly points; unknown/unindexed objects are not auto-deleted.
At this revision the full identity backup has one provider copy on Storage Share.
Existing Scaleway database backups do not establish a second full identity copy.
Host-local health cannot report a dead host. Full service login/MFA/audit proofs,
an independently executed outage-path retrieval drill, a second provider copy
and external host-loss monitoring must not be assumed from an upload or this
guide. Track remaining acceptance in NK-WP-0049; current task state lives in the
repository and may be newer than this offline guide.

View file

@ -0,0 +1,97 @@
{
"schema": "netkingdom.identity-recovery-image-observation.v1",
"observed_at": "2026-10-16T15:56:10.384927+00:00",
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
"read_only": true,
"secret_values_read": false,
"archive_generation_match_proven": false,
"independent_image_availability_proven": false,
"pods": [
{
"namespace": "sso",
"pod": "authelia-7f57c4c4fc-mk5hb",
"pod_uid": "7678e756-0af9-4eb9-bc25-c8b09ca490fc",
"owners": [
{
"kind": "ReplicaSet",
"name": "authelia-7f57c4c4fc",
"uid": "de04027d-ebff-45bc-8aff-73583907b57e"
}
],
"containers": [
{
"name": "authelia",
"image_id": "docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54",
"ready": true
}
]
},
{
"namespace": "sso",
"pod": "keycape-5fc5dd9b7f-zv5xm",
"pod_uid": "bf0d4674-cbe4-4e44-b105-b799eb4c124e",
"owners": [
{
"kind": "ReplicaSet",
"name": "keycape-5fc5dd9b7f",
"uid": "8d862e2d-532c-4f1f-b510-cbe489ec579b"
}
],
"containers": [
{
"name": "keycape",
"image_id": "forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010",
"ready": true
}
]
},
{
"namespace": "sso",
"pod": "lldap-f6df76764-jjp97",
"pod_uid": "b05329a6-8b9d-45d6-a573-c03c04e667be",
"owners": [
{
"kind": "ReplicaSet",
"name": "lldap-f6df76764",
"uid": "306a97b4-a5f7-421f-971d-36a3dd5fbc47"
}
],
"containers": [
{
"name": "lldap",
"image_id": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d",
"ready": true
}
]
},
{
"namespace": "mfa",
"pod": "privacyidea-86b695c88c-9kh7g",
"pod_uid": "2a44aa01-84e0-4778-a7c3-d0597b192259",
"owners": [
{
"kind": "ReplicaSet",
"name": "privacyidea-86b695c88c",
"uid": "7db5316a-0b89-431e-a678-a5dad372fc9c"
}
],
"containers": [
{
"name": "factor-recovery",
"image_id": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473",
"ready": true
},
{
"name": "privacyidea",
"image_id": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473",
"ready": true
}
]
}
],
"knowledge": {
"version": "0.2.6",
"package_digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02",
"source_status": "withheld; current SECURITY-COMPANION and owner route sources reviewed directly; orientation only"
}
}

View file

@ -0,0 +1,66 @@
{
"schema": "netkingdom.identity-outage-preparation.v1",
"date": "2026-10-16",
"status": "source_fixture_verified_owner_execution_pending",
"tests": {
"command": "NK49_RUN_DISPOSABLE_POSTGRES_TEST=1 python3 -m pytest -q tests/test_identity_offline_recovery.py tests/test_identity_offsite.py tests/test_identity_bundle.py tests/test_privacyidea_offhost_recovery.py",
"passed": 65,
"failed": 0,
"skipped": 0
},
"kit_preview": {
"schema": "netkingdom.recovery-kit.v1",
"status": "built",
"secret_values_exported": false,
"new_grants_created": false,
"files": [
{
"name": "net-kingdom-recovery-62b9464609b8e0fd-START-HERE.md",
"bytes": 8381,
"sha256": "1531675f2e7d3713a3a7fc28a5143fb24f201b19cd05d14c59ee485a0bc11e61"
},
{
"name": "net-kingdom-recovery-62b9464609b8e0fd.zip",
"bytes": 34527,
"sha256": "62b9464609b8e0fde2c05527fc79eff742ee3f626262d847df8fb579548a9470"
},
{
"name": "net-kingdom-recovery-62b9464609b8e0fd.zip.sha256",
"bytes": 108,
"sha256": "55e375291b27c2977049b89a5deddaed5e39c7ee9179013ce73d2ad43f2fb308"
}
],
"finished_at": "2026-10-16T15:56:50.813341+00:00"
},
"sources": [
{
"path": "tools/bootstrap-recovery/retrieve-identity-offline.py",
"sha256": "80e9f755b2304c222822b49ab563600832b9452c0c387a0523f4323e591e6270"
},
{
"path": "tools/bootstrap-recovery/recovery-kit.py",
"sha256": "55df6283898fadae316b89304bacf485dec88459b35ea2b9599c5d387c44ca8c"
},
{
"path": "tools/bootstrap-recovery/run-offline-recovery.sh",
"sha256": "0e3cfe8cc66d63b91a5c3f90912ce700c648c55b685ac11151631d22d87b40fd"
},
{
"path": "docs/disaster-recovery-storage-share.md",
"sha256": "1531675f2e7d3713a3a7fc28a5143fb24f201b19cd05d14c59ee485a0bc11e61"
},
{
"path": "docs/identity-service-restore-packet.md",
"sha256": "34f90e0ee1425083b26a763777b0150241e1cd5fb7dbdf8917f7a7a99482376d"
},
{
"path": "tests/test_identity_offline_recovery.py",
"sha256": "d88a752fd862983e3c01ebe6aea4baffa4168802d8ec616db161585a80c65f62"
}
],
"actual_outage_retrieval_verified": false,
"actual_storage_share_guide_publication_verified": false,
"service_restore_executed": false,
"new_grants_created": false,
"secret_values_exported": false
}

View file

@ -0,0 +1,97 @@
# Isolated identity service restore packet
NK-WP-0049-T15. Prepared 2026-10-16; **draft, not admitted or executed**.
Accountable preparer: codex. Execution/acceptance: Railiance Platform and the
LLDAP, Authelia, KeyCape and privacyIDEA service owners.
## Fixed starting point
Use the actual recovered artifact and receipt in
`docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json`, with matching
original keys and manifest. The completed archive recovery verified 36 file
hashes, SQLite integrity, RSA pairs and native PostgreSQL restore. It did not
prove service authentication or audit-signature continuity. A newer generation
needs equivalent provenance and archive checks before replacing this input.
## Targets and image admission still required
No execution target has been admitted. Select a disposable host/context and
record its immutable identity, operator, private endpoints, maximum lifetime,
resource limits and exact owned cleanup inventory. Never select the production
context or reuse live PVCs. Use fresh private storage; no production host mounts,
service-account tokens, ingress controller or external DNS update credentials.
The October 16 read-only pod observation is recorded in
`docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json`.
These are candidate inputs, not confirmation of the images in the selected archive:
| Component | Candidate reference | Admission gap |
| --- | --- | --- |
| PostgreSQL | `postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70` | Passed archive DB restore; stage independently of failed registry |
| LLDAP | `docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d` | Actual running digest resolved; compare selected archive/runtime lineage and stage it |
| Authelia | `docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54` | Compare selected archive deployment and stage image |
| KeyCape | `forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010` | Compare selected archive; require independent image availability |
| privacyIDEA | `ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473` | Actual main and sidecar digest observed separately; compare archive/runtime lineage and stage it |
Record image archive digests and offline load/inspect results. Resolve mutable
references from trusted runtime/image metadata rather than pulling today's tag.
The encrypted archive deployment records must be compared privately; do not
export literal environment values, Secret payloads or arbitrary configs.
## Restore order and containment
1. Freeze the selected artifact/manifest/image/target references. The owner
supplies existing-user credentials and an existing retained MFA token through
private inputs. Agree time synchronization and test identities without
exposing passwords, seed material, client secrets or user records.
2. Establish an isolated network with default-deny external egress, no published
ports, no host networking and no production DNS/ingress. Permit only named
internal service links. The test browser, if needed, must use a separately
reviewed local route and hosts mapping. Preserve issuer/client identity while
keeping all test traffic private. Verify denied external connection attempts.
3. Import manifest-checked data and original keys into fresh private storage.
Do not apply archived Kubernetes objects wholesale: metadata UIDs, mounts,
service accounts, endpoints and sidecars require explicit translation. Keep
notification/provider credentials sealed and unused; disable sending paths.
4. Restore the linked privacyIDEA database with native PostgreSQL first; restore
LLDAP data/config and verify directory readiness. Run directory authentication
before dependent provider checks. Restore privacyIDEA with its original enckey
and audit pair against the isolated database/directory. Restore Authelia with
original storage/OIDC keys and private directory routes. Restore KeyCape with
original signing/config/policies and private MFA/directory dependencies.
5. Run the individual acceptance checks below; do not start provisioning,
enrollment, rotation, scheduled jobs or production consumers automatically.
An unsupported dependency is a named gap, never an implicit production route.
6. Remove only the named disposable containers/network/storage and temporary
plaintext/key files. Confirm absence and production resources unchanged;
retain sanitized receipts, original ciphertext and independently held keys.
## Acceptance matrix
| Task / owner lane | Positive check | Negative check | Safe evidence |
| --- | --- | --- | --- |
| T16 / LLDAP | Preserved user/group/membership identities; existing-user bind/login | Wrong password rejected | Image/target IDs, aggregate comparisons and booleans |
| T17 / Authelia | Native storage decryption; same signing identity/client configs; isolated authentication | Wrong credentials or unregistered client refused | Public key thumbprint, test outcome, notification suppression |
| T19 / privacyIDEA MFA | Existing retained token decrypts internally and actual existing-token MFA succeeds | Wrong enckey and invalid code refused in disposable copies | Token material never exported; bounded outcomes only |
| T20 / privacyIDEA audit | Verify historical signatures; create and verify one isolated entry with original key | Mismatched public key/tampered disposable record rejected | Signature counts, continuity result, cleanup |
| T18 / KeyCape | Same issuer/JWKS, registry/policy/receipt continuity; named current consumer authenticates | Wrong secret and denied scope rejected | Public fingerprints, owner-selected test IDs and outcomes |
Do not silently reset credentials or re-enroll tokens to pass. Historical audit
verification failures must be reported, not hidden with replacement keys.
## Unresolved inputs before execution
- Named immutable disposable target, approved private browser route if required,
resource limits, cleanup inventory and operator window.
- Confirm archive/runtime lineage for the observed image digests and stage all
images independently of production and the failed registry.
- A retained user and MFA token available in private owner custody; exact native
verification commands reviewed against the chosen service versions.
- T02 current-caller ledger and T21 per-caller owner admission, including legacy
token, diagnostic, resolver and LDAP-group boundaries.
- Service-owner acceptance of network/config translations and all required
grants. New grants or production effects require their own owner decisions.
Until resolved, T15 stays in progress and T16–T20 remain waiting. Production
cutover, full-host/OpenBao restoration and application database recovery are
separate owner procedures.