diff --git a/docs/audit-core-intake-identity-providers.md b/docs/audit-core-intake-identity-providers.md new file mode 100644 index 0000000..70c0321 --- /dev/null +++ b/docs/audit-core-intake-identity-providers.md @@ -0,0 +1,450 @@ +--- +title: "audit-core intake packages: Authelia, LLDAP, privacyIDEA" +status: draft +owner: net-kingdom +work: NK-WP-0055-T11 +answers: audit-core AUDIT-WP-0017-T04 (three of the four identity providers; KeyCape is separate) +contract: canon/standards/audit-correlation_v0.1.md (accepted) +created: "2026-10-27" +--- + +# audit-core intake packages: Authelia, LLDAP, privacyIDEA (draft) + +This document gives audit-core the intake package for each of the three identity providers that NetKingdom deploys: + +- Authelia (`sso-mfa/k8s/authelia`); +- LLDAP (`sso-mfa/k8s/lldap`); +- privacyIDEA (`sso-mfa/k8s/privacyidea`). + +It follows the common package in audit-core `AUDIT-WP-0017` ("Common intake package for every new sender") and +the wire contract in audit-core `docs/event-envelope.md`. KeyCape's package belongs to key-cape and is not covered here. + +**Status.** Draft for audit-core review. It plans work only: + +- it activates no sender; +- it mints no credential; +- it makes no live change. + +**How each claim is marked:** + +- **[repo]**: read from this repository's manifests. +- **[upstream]**: from upstream product behaviour as we know it. Not yet verified against the pinned image. +- **[estimate]**: a volume figure that must be measured before audit-core sets thresholds. +- **[unknown]**: an open question. + +## 1. Choices that apply to all three providers + +**Separate sources.** Each provider gets its own `source`, as AUDIT-WP-0017-T04 asks, so that silence in one of them +can be detected: + +- `netkingdom.authelia` +- `netkingdom.lldap` +- `netkingdom.privacyidea` + +**Owner.** The owning repo for all three sources is `net-kingdom`. ADR-0015 moves runtime packaging to `rapp-*` +repositories. If that move happens, the source name stays the same and only the owning repo changes. + +**Tenant policy.** All three providers are platform-scoped: there is a single realm and directory, `dc=netkingdom,dc=local`. +- The envelope requires `tenant` to be a non-empty value. +- Proposal: use the fixed value `platform` for every event, and give the sender credential only `platform` as a permitted tenant. +- [unknown] Is `platform` the spelling audit-core uses for platform-scoped sources? If not, use audit-core's spelling. + +**Shipper.** One small collector per provider: the *intake shipper*. +- It runs in the provider's namespace, either as a sidecar or as a CronJob. +- Each shipper has its own sender credential. +- It normalises native records into the envelope and posts them to `POST /v1/events`. +- On retry it reuses the same `id`, so delivery is idempotent. + +**Envelope mapping:** + +| Envelope field | Mapping rule | +|---|---| +| `id` and `Idempotency-Key` | A deterministic hash of the provider name and a stable native key (defined per provider below). Retries and re-reads after a restart produce the same id. | +| `type` | One value from the provider's action list below. The receiver stores it as `action`. | +| `subject` | The affected object as a **pseudonymous** reference, e.g. `user:`, `group:` or `token:`. Never a raw username or email address. | +| `occurred_at` | The provider's native event time as RFC 3339 with an explicit `Z`. All three providers run in UTC. | +| `correlation_id` | The 32-hex trace id (AC-1) if a valid `traceparent` was seen. Otherwise a newly generated trace id, with `data.correlation_origin: generated` (AC-4). | +| `data` | Holds the outcome, the actor (pseudonymised), the native record id, `correlation_origin`, and the fields listed per provider. | + +**Placement of `actor` and `outcome`.** The receiver fixes `actor=null` and `outcome=recorded` at ingest. The real +actor and outcome therefore go inside `data`: + +- `data.outcome` is one of `success`, `failure` or `refused`. +- `data.actor` is the acting principal. + +**Pseudonymisation of personal data.** The rules are the same for all three providers. + +- **Usernames, emails and display names:** + - Replace them with a keyed pseudonym before sending: HMAC-SHA256 truncated to 32 hex characters. + - The key is a NetKingdom-held value in OpenBao and is never sent to audit-core. + - This keeps records from the three providers joinable on the same user without exposing the name. + - Raw names must never appear in `subject` or `id`. + - [unknown] Whether audit-core prefers its own per-subject encrypted personal payload (`docs/erasure-and-audit.md`) + instead. If so, the raw name may travel in that payload, and our pseudonym becomes only the join key. +- **Synthetic, test and break-glass accounts:** Service and break-glass account names (`admin`, the KeyCape bind + user, `identity-provisioner`) are operational identifiers, not personal data. They may be sent in clear. This is + what lets break-glass use stand out. +- **Client IP addresses:** Personal data. Send them as a truncated prefix (IPv4 /24, IPv6 /48) plus a keyed + pseudonym of the full address. Ban and regulation events need the full-address pseudonym for correlation. + [unknown] Whether security operations need the clear IP for 30 days. That would need an owner decision under a + separate retention period. +- **Never sent:** + - passwords and password attempts; + - OTP values; + - session cookies; + - JWTs; + - OIDC codes and tokens; + - client secrets; + - enrollment seeds and QR payloads; + - LDAP bind credentials. + + The receiver's `secret_policy` is the backstop for these, not the control. + +**Credential.** One sender credential per source: + +- it is minted through the attended OpenBao path that `warden route find "audit-core sender credential"` points to; +- it is delivered to the shipper as a mounted file; +- audit-core never holds or prints it; +- `may_read` is false. + +**Retention.** The default applies: a 30-day query window plus archive. privacyIDEA records are the exception +(section 4.5). + +**Heartbeat.** Each shipper emits `type: source.heartbeat` every 15 minutes, even when there are no events. The +heartbeat's `data` carries: +- the shipper's cursor (last native id or log offset); +- the count of records read and sent since the last heartbeat. + +A heartbeat with a stalled cursor while the provider is serving traffic is the completeness signal (audit-core +`docs/stream-completeness.md`). + +## 2. Authelia: `netkingdom.authelia` + +### 2.1 Registration + +| Item | Value | +|---|---| +| Deployment | Namespace `sso`, Deployment `authelia`, selector `app.kubernetes.io/name=authelia`, 1 replica [repo] | +| Image | `authelia/authelia` 4.39.28, pinned by digest [repo] | +| Emitting identity | A shipper ServiceAccount `audit-shipper-authelia` in `sso`. Proposed; it does not exist yet. | +| Role | First-factor login (Authelia checks the password against LLDAP), forward-auth for `activity`/`temporal`, and the OIDC upstream for client `keycape` [repo] | +| Exposure | Behind the Traefik Ingress at `auth.coulomb.social` and `login.coulomb.social` [repo] | + +### 2.2 Where the events are + +**1. Structured logs.** +- `log.level: info`. No `log.format` is set, so output is text [repo]. +- Proposal: set `log.format: json`. The schema is unchanged, and this is a small config change. +- At info level, Authelia logs these events [upstream]: + - unsuccessful first-factor and second-factor attempts, with the username and remote IP; + - regulation bans; + - authorization denials; + - OIDC authorize and token errors. +- Successful logins are not reliably logged at info [upstream]. + +**2. The `authentication_logs` table in Authelia storage.** +- Storage here is SQLite at `/var/authelia/data/db.sqlite3` on the `authelia` PVC [repo]. +- Each row records [upstream]: + - one first-factor or second-factor attempt, success or failure; + - username, `auth_type`, `remote_ip`, `request_uri` and `request_method`; + - a monotonic `id`. +- Since 4.38, the `banned_user` and `banned_ip` tables also exist [upstream]. +- This is the **authoritative** source for login outcomes. Logs supplement it for OIDC and authorization events. + +### 2.3 Collection + +Recommended: a **sidecar** in the `authelia` pod with two read-only inputs. + +**(a) The SQLite file, read-only:** +- The sidecar polls `authentication_logs` and the ban tables by `id`, with a cursor kept on a small shipper PVC. +- SQLite's WAL mode allows a concurrent read-only reader [upstream]. +- The sidecar opens the database with `mode=ro` and never writes to it. + +**(b) The container stdout, with `log.format: json`:** +- The sidecar reads it through a shared `emptyDir` log file. This needs a `--log.file_path` setting [upstream]. +- Alternative: a node-level collector, if railiance-cluster provides one. [unknown] + +**Native key for `id`:** +- Table rows: `authelia|authentication_logs|`. +- Log lines: `authelia|log|`. + +**Constraint.** Storage is a single-writer SQLite file on an RWO PVC, so the sidecar must run in the same pod. +[unknown] Whether a planned move of Authelia storage to PostgreSQL would replace (a) with a DB reader. + +### 2.4 Correlation + +- Browser traffic arrives through Traefik. Today Authelia neither reads nor logs a `traceparent`, and it does not + propagate one [upstream]. +- When KeyCape redirects the user to Authelia, that is a browser navigation, so any trace context is lost. +- **Default:** `correlation_origin: generated`. The shipper creates one trace id per record. +- **Improvement, needs work by railiance-cluster:** + - Enable the Traefik access log in JSON with the `traceparent` request header kept. Header names only; never `Cookie` or `Authorization`. + - Join on time, router and client IP to recover propagated ids for OIDC back-channel calls from KeyCape: `/api/oidc/token` and `/api/oidc/userinfo`. + - Only server-to-server calls can carry an inbound `traceparent`. + - [unknown] Whether Traefik tracing or header logging is enabled on railiance01. +- **Same login, two records:** The OIDC `authorize` → `token` exchange is the hop KeyCape shares with Authelia. + - The KeyCape issuance record holds the propagated id. + - The Authelia record for the same login can carry it only if KeyCape sends `traceparent` on the token call **and** Traefik logs it. + +### 2.5 Action list (scope rows) + +| `type` | From | `data` beyond the common fields | +|---|---|---| +| `auth.first_factor.success` / `.failure` | `authentication_logs` (1FA) | `auth_method: password`, `request_host`, `ip_prefix`, `ip_pseudonym` | +| `auth.second_factor.success` / `.failure` | `authentication_logs` (2FA rows; TOTP/WebAuthn/Duo if ever enabled) | `auth_type` | +| `regulation.user_banned` | Ban table or log | `ban_until`, `failed_count` | +| `regulation.ip_banned` | Ban table (4.38+) | `ban_until` | +| `regulation.ban_lifted` | Ban table `revoked` / expiry | `by` (admin or expiry) | +| `access.denied` | Log, authz deny | `request_host` (e.g. `activity.coulomb.social`), `rule` | +| `oidc.authorize.success` / `.failure` | Log | `client_id` (`keycape`), `scopes`, `error` | +| `oidc.token.issued` / `.failure` | Log | `client_id`, `grant_type`. **No token values.** | +| `oidc.consent.granted` | Log (implicit consent today) | `client_id` | +| `identity.reset_password.requested` / `.completed` | Log, notifier | (Password reset is via LLDAP/Authelia. [unknown] Whether it is enabled.) | +| `source.heartbeat` | Shipper | cursor, counts | + +### 2.6 Volume, cadence and retention + +- **Volume** [estimate]: a few hundred authentication rows per day across operators, demo users and probes. Peak + under 5 events/s, except during a brute-force attempt. +- **Brute force:** Regulation caps each username at 5 failures per 2 minutes. IP-spread attacks are not capped. + Proposed shipper rate limit: 50 events/s, with overflow counted in the heartbeat. Overflow is never silently dropped. +- **Heartbeat:** every 15 minutes. +- **Allowed silence:** 30 minutes of shipper silence. Gaps between authentication events are normal at night. +- **Retention:** default. [unknown] Whether 90 days is wanted for ban events. + +## 3. LLDAP: `netkingdom.lldap` + +### 3.1 Registration + +| Item | Value | +|---|---| +| Deployment | Namespace `sso`, Deployment `lldap`, 1 replica. Ports: LDAP 3890, HTTP/GraphQL 17170 [repo] | +| Image | `lldap/lldap:stable`. **Not pinned** [repo]. Pinning it is a prerequisite for a stable log format. | +| Storage | Default SQLite on the `lldap` PVC. No `LLDAP_DATABASE_URL` is set [repo]. | +| Emitting identity | Shipper ServiceAccount `audit-shipper-lldap` in `sso` (proposed) | +| LDAP clients | Authelia and KeyCape bind as `uid=admin` [repo, Authelia config]. Admin automation: `lldap_admin.py`, `identity-provisioner-reconcile.py`, `break-glass.sh` [repo] | + +### 3.2 Where the events are + +- **Logs.** LLDAP has no audit table. Its only audit surface is the `tracing` log on stdout [upstream]. +- **At default verbosity, LLDAP logs [upstream]:** + - LDAP bind attempts with the bind DN and success or failure; + - GraphQL mutations, i.e. admin changes: + - create, update and delete user; + - create and delete group; + - add and remove a member; + - password change through the OPAQUE flow. +- **Field coverage at default verbosity** [unknown]: + - Which fields appear without `verbose=true` must be verified against the pinned image. + - `verbose=true` logs too much, including search filters that contain usernames. +- **Logins to the web UI** (`/auth/opaque/login/*`, `/auth/simple/login`) are logged with the username [upstream]. + +### 3.3 Collection + +- Recommended: a log **sidecar**, or a node log collector, reading the `lldap` container's stdout. +- **Parsing:** + - LLDAP logs are text spans, not JSON [upstream]. + - The shipper keeps a strict per-version parser and an allowlist of message patterns. + - A line it does not recognise is counted in the heartbeat and never guessed. +- **Native key for `id`:** `lldap|||`. This stays stable + when the shipper re-reads after its own restart. [unknown] Whether the collector exposes a stable offset. If not, + use a sha256 of pod uid, timestamp and raw line. +- **Cross-check through admin tooling:** + - The repo's admin tools (`lldap_admin.py`, `manage-group-members.sh`, `create-user.sh`) could emit their own + events, as the actor's view of the change. + - Proposal: not in this first package. Rely on the server log so that changes made outside the tools are captured too. + +### 3.4 Correlation + +- **LDAP (3890):** There are no headers, so `correlation_origin: generated` always (AC-2). The calling component + (Authelia or KeyCape) records its own inbound id. LLDAP records only its own. +- **HTTP/GraphQL (17170)** is behind Traefik (`ingress.yaml`). LLDAP does not log `traceparent` [upstream]. The same + Traefik access-log join as for Authelia could recover it for admin API calls. [unknown] +- **Joining LDAP records:** A bind from Authelia happens within milliseconds of an Authelia `auth.first_factor.*` + record for the same user pseudonym. audit-core may record a `parent_event_id` link only if the shipper can make it + deterministically. Proposal: do not; a time-and-pseudonym join is analysis, not correlation. + +### 3.5 Action list + +| `type` | Notes | +|---|---| +| `ldap.bind.success` / `.failure` | `bind_dn` (service DN in clear; a user DN is pseudonymised), `client_ip_prefix`. Failures are the main signal. | +| `ldap.bind.service` | Optional. A successful bind by the `uid=admin` service account, which is expected high-volume noise. Proposal: count these in the heartbeat instead of sending one event per bind. | +| `user.created` / `user.updated` / `user.deleted` | `actor` (admin principal), changed attribute **names** only | +| `user.password_changed` | `actor`, `self` true/false. No password material. | +| `group.created` / `group.deleted` | `group` cn in clear (group names are operational) | +| `group.member_added` / `group.member_removed` | `group`, member pseudonym, `actor` | +| `admin.login.success` / `.failure` | Web UI or API login | +| `source.heartbeat` | Includes `service_bind_count` and `unparsed_line_count` | + +### 3.6 Volume, cadence and retention + +- **Volume** [estimate]: + - Service binds: from hundreds up to a few thousand per day. They follow Authelia logins and KeyCape lookups, and + appear only as counts. + - Events actually sent: under 200 per day. + - Admin changes: a handful per week, with bursts during reconciliation runs (`identity-provisioner-reconcile.py`). +- **Peak:** under 5 events/s. +- **Heartbeat:** every 15 minutes. +- **Allowed silence:** 30 minutes. +- **Retention:** default. Group-membership changes are authorization-relevant. [unknown] Whether audit-core should + keep them for longer. + +## 4. privacyIDEA: `netkingdom.privacyidea` + +### 4.1 Registration + +| Item | Value | +|---|---| +| Deployment | Namespace `mfa`, Deployment `privacyidea`, 1 replica [repo] | +| Image | `ghcr.io/gpappsoft/privacyidea-docker:3.12.2` [repo] | +| Database | PostgreSQL cluster `net-kingdom-pg`, namespace `databases`. Audit uses the default SQL audit module [repo, README; `PI_AUDIT_MODULE` not set, so the default `sqlaudit` applies, upstream] | +| Audit signing | `PI_AUDIT_KEY_PRIVATE` / `PI_AUDIT_KEY_PUBLIC` at `/etc/privacyidea/{private,public}.pem`, from Secret `privacyidea-auditkeys` [repo] | +| Callers | KeyCape (MFA challenge on `/authorize/callback`), NetKingdom admin scripts (`pi_api.py`, `check-user-mfa-state.sh`, onboarding guard) [repo] | +| Emitting identity | Shipper ServiceAccount `audit-shipper-privacyidea`, with a **read-only** PostgreSQL role on the audit table only (proposed) | + +### 4.2 Where the events are + +**The table.** The signed audit table `pidea_audit` in PostgreSQL [upstream schema, 3.x]. Each request writes one +row with these columns: + +- `id`, `date`, `startdate`, `duration` +- `action` (e.g. `POST /validate/check`), `success`, `action_detail`, `info` +- `serial`, `token_type` +- `user`, `realm`, `resolver`, `administrator` +- `client` (IP), `privacyidea_server` +- `policies`, `container_serial` +- `signature` + +**The signature.** Each row is signed with the audit private key. + +**Proof of the signed history (NK-WP-0049-T20, 2026-10-16):** +- All 279,209 stored rows verified against the retained original audit key pair. None were unsigned, invalid or legacy. +- After restore, a new entry was signed with the preserved identity. +- Tampered rows and a wrong key were refused. + +**Consequence.** The audit key pair is a retained identity: +- It must never be regenerated to repair anything (`KEY-RECOVERY.md`). +- The public key is the verification anchor that audit-core needs. + +**The application log** at `/var/log/privacyidea/privacyidea.log` (`PI_LOGLEVEL=20`) [repo] duplicates part of this +and is not signed. Out of scope. + +### 4.3 Collection: database export with the signature preserved + +**Export method.** An exporter (CronJob or sidecar) reads the table: + +- `SELECT ... FROM pidea_audit WHERE id > :cursor ORDER BY id LIMIT 500` +- It runs under a dedicated read-only role. +- The cursor is kept on the shipper side and echoed in the heartbeat. + +**What each row becomes:** +- `id` = sha256 of `privacyidea|pidea_audit||`. + - The signature is included so that a restored database with re-used ids cannot collide silently with earlier events. + - [unknown] Whether a restore can roll ids back. T20 restored into an isolated target, not production. +- `data.native` holds **every signed column, unchanged**, plus `signature`. +- `data.native_signature_alg` holds the privacyIDEA version and audit module. +- `data.verification_key_fingerprint` holds the SHA-256 of the public key. + +**Personal data in the signed row.** The `user`, `client`, `administrator` and `info` columns can contain usernames and IPs. + +- Pseudonymising them in `data.native` would break the signature. +- **Proposed resolution:** send them in audit-core's per-subject encrypted personal payload, if one exists (`docs/erasure-and-audit.md`), so the row stays verifiable. +- Use pseudonymous copies in the plain `data`, `subject` and `actor` fields. +- **Owner decision needed:** if audit-core cannot hold an encrypted personal payload, we must choose one of: + - (a) keep raw columns in the signed payload and accept them under audit-core's PII handling; or + - (b) send only pseudonymised fields plus the signature and the row hash. Verification would then be possible only + against the source database while it is retained. + +**Verification by audit-core** (proposal): +- Store the public key once as source metadata. +- The shipper verifies each row before sending. A failed row is sent as `type: audit.signature_invalid` with the row + id and never dropped. +- audit-core can re-verify a sample. +- [unknown] Whether privacyIDEA's signed-string format is stable across 3.12.x. Pin the exporter to the image version. + +**Not used: privacyIDEA's logger or container audit module** (`PI_AUDIT_MODULE` set to a logger-based module) [upstream]. +- It would stream audit records to a log handler. +- Its records are **not signed** and would bypass the T20 continuity property. +- Rejected as the primary path; possibly useful later as a low-latency mirror. + +### 4.4 Correlation + +- privacyIDEA 3.12 neither records nor propagates `traceparent` [upstream]. `pidea_audit` has no column for it. +- Default: `correlation_origin: generated`. +- **Improvement:** KeyCape's MFA call (`/validate/check`) is server-to-server. If KeyCape sends `traceparent` and the + privacyIDEA Ingress/Traefik access log records it, the shipper can join on these fields: + - `startdate`; + - `client` (KeyCape pod IP); + - `serial` / `user`. + + The join recovers the propagated id. This is the same dependency on railiance-cluster as for Authelia. [unknown] +- **Cluster-internal calls:** If KeyCape calls privacyIDEA by cluster-internal Service address, the call bypasses + Traefik. Then only a privacyIDEA-side WSGI middleware could log the header. That is an upstream or plugin change. + [unknown] Which address KeyCape uses in production. + +### 4.5 Action list + +The `type` is derived from `action`, the HTTP method plus endpoint: + +| `type` | Native `action` | Notes | +|---|---|---| +| `mfa.challenge.success` / `.failure` | `POST /validate/check`, `/validate/samlcheck` | `serial`, `token_type`, user pseudonym, `realm` | +| `mfa.challenge.triggered` | `POST /validate/triggerchallenge` | | +| `token.enrolled` / `token.enrollment_confirmed` | `POST /token/init` (2-step) | No seed or QR material; `info` is checked for secret-shaped values | +| `token.deleted` / `.disabled` / `.enabled` / `.revoked` / `.reset` / `.unassigned` | `DELETE /token/`, `POST /token/disable` etc. | `administrator` in `actor` | +| `token.resync` / `.setpin` | `POST /token/resync`, `/token/setpin` | | +| `policy.changed` / `policy.deleted` | `POST/DELETE /policy/*` | Admin | +| `config.changed` | `POST /system/*`, `/resolver/*`, `/realm/*`, `/caconnector/*` | Admin | +| `admin.auth.success` / `.failure` | `POST /auth` | Includes the bootstrap-admin and script logins | +| `audit.read` | `GET /audit/` | Who read the audit log | +| `audit.signature_invalid` | Exporter | Never dropped | +| `privacyidea.other` | Any other `action` | Counted and sent, so the action list can be refined | +| `source.heartbeat` | Exporter | Cursor, `max(id)`, `rows_behind` | + +### 4.6 Volume, cadence and retention + +- **Volume** [estimate]: + - Average: about 1,300 rows per day. That is 279,209 rows over about 210 days, from deployment on 2026-03-19 to the T20 count on 2026-10-16. + - The rows are probably dominated by automation: MFA-state checks, the guard, probes. + - [unknown] The split between automated calls and human logins. Measure it per `action` before setting thresholds. +- **Peak:** under 10 rows/s. +- **Backfill:** Exporting the historical rows is a one-off backfill of about 280k rows. + - Proposal: run it in batches under 50 events/s. + - The cursor starts at `id=0`. + - [unknown] Whether audit-core wants the history or only the rows from activation onwards. +- **Heartbeat:** every 15 minutes, with `rows_behind`. +- **Allowed silence:** 30 minutes. The automated checks make zero rows over several hours anomalous. +- **Retention.** A longer retention is requested. + - MFA token lifecycle and admin policy changes are the evidence behind privileged-access claims (SI-X4, aal2). + - Proposal: the default 30-day query window plus an archive that keeps signed rows for at least 1 year. + - The source database stays authoritative. [unknown] Its retention and rotation are not configured in the repo today. + +## 5. Prerequisites and owner asks + +| # | Item | Owner | +|---|---|---| +| P1 | Pin the `lldap/lldap` image to a digest | net-kingdom (or the `rapp-*` owner after ADR-0015) | +| P2 | Set Authelia `log.format: json` and a file path the sidecar can read | net-kingdom | +| P3 | Read-only PostgreSQL role for `pidea_audit` | rapp-postgres / railiance-platform with net-kingdom | +| P4 | Pseudonymisation key in OpenBao, and a sender credential for each source | net-kingdom via the attended OpenBao path (`warden route`) | +| P5 | Traefik JSON access log with `traceparent` kept (no cookies or auth headers) | railiance-cluster | +| P6 | `tenant` value for platform-scoped sources; `correlation_origin` field placement (AUDIT-WP-0017-T05); per-subject encrypted personal payload for signed privacyIDEA rows; whether to backfill privacyIDEA history; longer retention for privacyIDEA | audit-core | +| P7 | KeyCape sends `traceparent` on its back-channel calls to Authelia and privacyIDEA | key-cape | + +## 6. Unknowns to verify before overlays are prepared + +1. **Log content.** Exact Authelia 4.39.28 and LLDAP log messages and fields at info level. Capture them on synthetic + accounts in a non-production instance. +2. **Concurrent SQLite reads.** Whether Authelia's SQLite storage tolerates a concurrent read-only reader on the RWO PVC + without lock contention. The alternative is to move Authelia storage to PostgreSQL. +3. **Signed-string format.** The privacyIDEA signed-string format and its stability across patch releases. + Exporter-side verification depends on it. +4. **Ingress path and logging.** Whether KeyCape reaches Authelia and privacyIDEA through Traefik or through cluster + Services, and whether Traefik logs request headers on railiance01. +5. **Traffic split and volume.** The real distribution per action and the human-versus-automation split. Every volume + figure above is an estimate. +6. **Password reset.** Whether Authelia password reset is enabled. The notifier is configured, but the reset policy is + not set explicitly in `configmap.yaml`. +7. **Deployed config.** Whether the deployed configuration still matches the repo manifests. ADR-0015 migration and + live drift are not checked in this draft.