NK-WP-0055-T11: audit-core intake packages for Authelia, LLDAP, privacyIDEA (draft)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4515@bnt-lap001
Assistant-Session: 0bf96127-6e04-41be-bfda-d11717c3f354
This commit is contained in:
tegwick 2026-10-27 23:30:14 +01:00
parent a60091775f
commit ccd0b15ee0

View file

@ -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:<pseudonym>`, `group:<cn>` or `token:<serial>`. 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|<id>`.
- Log lines: `authelia|log|<sha256 of the raw JSON line>`.
**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|<pod uid>|<container restart count>|<line sequence number>`. 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|<id>|<signature>`.
- 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/<serial>`, `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.