openapi: 3.1.0 info: title: ZETA Guard — OOB Admin API (Account Deletion / Recovery Escape Hatch) version: 0.1.0-draft description: | Privileged, operator-facing interface for the **out-of-band (OOB) deletion** escape hatch: the last resort when a user has lost ALL online factors (instance key, email) and therefore cannot self-recover via the SDK API. This is a separate interface from the SDK HTTP API on purpose: * Audience: human operators / an operator console — never the client SDK. * Transport: privileged/internal, mutual-TLS protected. * Authentication: IDP-INDEPENDENT operator credentials (the SEK-IDP is assumed potentially compromised; the whole point of OOB is to not depend on it). The client-facing counterpart lives in `zeta-guard-client-management.yaml`. It exposes exactly one OOB operation — the **veto** (`POST /zeta/deletions/{deletion_id}/veto`), by which a surviving enrolled client (F2) cancels a scheduled deletion. No operator action in this spec is reachable from the SDK surface, and the veto is never performed by an operator. ## Guarantees this interface must preserve * It can ONLY tombstone an identity — it never grants access and never re-pins a factor. * Deletion is scheduled with a veto window, not executed immediately; all surviving pinned channels are notified, and a surviving enrolled client (F2) can veto (via the SDK side). * Separation of duties: the second approval (dual control) and the re-registration confirmation MUST be performed by a DIFFERENT operator than the initiator/executor. * Re-acquiring access after deletion requires a fresh first-use ceremony plus a SECOND independent OOB confirmation during the tombstone cooling-off period. * Every action is audited (operator identity, time, justification, evidence reference). ## Deletion state machine ``` (initiate) ──► awaiting_approval ──(approve, different op)──► scheduled │ │ │ abort │ ▼ │ (veto window elapses, no veto) aborted │ │ vetoed ◄──(veto: user, surviving factor)─────────────────┤ ▼ executed ──► identity TOMBSTONED (cooling_off_until) │ (reregister-confirm, different op) │ ▼ re-registration unblocked ``` servers: - url: https://{adminHost}/realms/{realm}/zeta/admin description: Privileged admin channel for one guard (one realm = one Fachdienst) variables: adminHost: default: admin.guard-epa.internal realm: default: zeta-epa security: - mutualTLS: [] operatorAuth: [zeta.oob.read] tags: - name: deletions description: Lifecycle of OOB deletion requests - name: identities description: Identity status lookup (to judge whether OOB deletion is warranted) - name: audit description: Audit trail of OOB actions paths: /identities: get: tags: [identities] summary: Resolve a KVNR / Telematik-ID to an identity reference and status description: | Used by the operator after completing the OOB identity proofing, to locate the record and to CHECK whether any factor still survives — if one does, the user should self-recover via the SDK rather than be OOB-deleted. security: - mutualTLS: [] operatorAuth: [zeta.oob.read] parameters: - name: kvnr in: query required: true schema: { type: string } description: Provided over the privileged channel only. responses: '200': description: Identity status content: application/json: schema: { $ref: '#/components/schemas/IdentityStatus' } '404': description: No identity record at this guard for the given selector /identities/{identity_ref}: parameters: - $ref: '#/components/parameters/IdentityRef' get: tags: [identities] summary: Get identity status by reference description: | Returns the current lifecycle state of a single identity (active, pending deletion, or tombstoned), together with which factors still survive and any cooling-off window. Use it to track an identity across the OOB deletion lifecycle once its `identity_ref` is known. security: - mutualTLS: [] operatorAuth: [zeta.oob.read] responses: '200': description: Identity status content: application/json: schema: { $ref: '#/components/schemas/IdentityStatus' } '404': description: Unknown identity reference /identities/{identity_ref}/reregister-confirm: parameters: - $ref: '#/components/parameters/IdentityRef' post: tags: [identities] summary: Record the SECOND independent OOB confirmation to unblock re-registration description: | During the cooling-off period after a tombstone, first use at this identity is blocked until a second, independent OOB confirmation is recorded. This MUST be a different operator than the one who executed the deletion (separation of duties); otherwise the request is rejected with `sameOperatorForbidden`. security: - mutualTLS: [] operatorAuth: [zeta.oob.reregister] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [oob_proof, justification] properties: oob_proof: { $ref: '#/components/schemas/OobProof' } justification: type: string description: Operator's reason for confirming re-registration; recorded in the audit trail. responses: '200': description: Re-registration unblocked for the cooling-off identity content: application/json: schema: { $ref: '#/components/schemas/IdentityStatus' } '403': description: sameOperatorForbidden — must differ from the deletion executor content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } '409': description: Identity is not in a tombstoned/cooling-off state content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } /deletions: get: tags: [deletions] summary: List OOB deletion requests description: | Lists OOB deletion requests known to this guard, e.g. for an operator-console overview. Results can be narrowed by lifecycle `status` and/or a specific `identity_ref`. security: - mutualTLS: [] operatorAuth: [zeta.oob.read] parameters: - name: status in: query required: false description: Restrict the result to deletion requests in this lifecycle state. schema: { $ref: '#/components/schemas/DeletionStatus' } - name: identity_ref in: query required: false description: Restrict the result to deletion requests for this identity. schema: { type: string } responses: '200': description: Matching deletion requests content: application/json: schema: type: array items: { $ref: '#/components/schemas/Deletion' } post: tags: [deletions] summary: Initiate an OOB deletion request description: | Requires a high-assurance, IDP-independent identity proof (`oob_proof`, assurance level MUST be high) — A_29941. The guard SHOULD first check that no factor survives and otherwise refer the user to self-recovery (A_29947). The request lands in `awaiting_approval` and must be approved by a different operator (dual control) before the veto window starts (A_29946). The scheduled deletion runs only after a veto window with channel notification (A_29942); execution merely tombstones the identity — it never grants access or re-pins a factor (A_29940, A_29944). security: - mutualTLS: [] operatorAuth: [zeta.oob.initiate] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DeletionRequest' } responses: '201': description: Deletion request created headers: Location: description: URL of the newly created deletion request (`/deletions/{deletion_id}`). schema: { type: string } content: application/json: schema: { $ref: '#/components/schemas/Deletion' } '400': description: Missing/invalid evidence, or assurance level not 'high' content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } '409': description: | A surviving factor was detected (use self-recovery instead), or an active deletion already exists for this identity. content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } /deletions/{deletion_id}: parameters: - $ref: '#/components/parameters/DeletionId' get: tags: [deletions] summary: Get a deletion request (with audit detail) description: | Returns a single deletion request as the extended detail view: the identity proof, justification, any veto or abort record, and the chronological audit trail of every action taken on the request. security: - mutualTLS: [] operatorAuth: [zeta.oob.read] responses: '200': description: Deletion detail content: application/json: schema: { $ref: '#/components/schemas/DeletionDetail' } '404': description: Unknown deletion id /deletions/{deletion_id}/approve: parameters: - $ref: '#/components/parameters/DeletionId' post: tags: [deletions] summary: Second-operator approval (dual control) — arms the veto window description: | Maker-checker step. The approving operator MUST differ from the initiator; otherwise `sameOperatorForbidden`. On approval the request moves to `scheduled` and `effective_after` is set. security: - mutualTLS: [] operatorAuth: [zeta.oob.approve] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: type: object properties: note: type: string description: Optional free-text note from the approving operator; recorded in the audit trail. responses: '200': description: Approved and scheduled content: application/json: schema: { $ref: '#/components/schemas/Deletion' } '403': description: sameOperatorForbidden content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } '409': description: Not in awaiting_approval state content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } /deletions/{deletion_id}/abort: parameters: - $ref: '#/components/parameters/DeletionId' post: tags: [deletions] summary: Operator-side abort of a pending deletion description: | Distinct from a user veto. Cancels a deletion that is awaiting_approval or scheduled (before execution). Requires a reason and is audited. security: - mutualTLS: [] operatorAuth: [zeta.oob.initiate] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: type: string description: Operator's reason for aborting the deletion; recorded in the audit trail. responses: '200': description: Aborted content: application/json: schema: { $ref: '#/components/schemas/Deletion' } '409': description: Already executed/vetoed/aborted (terminal) content: application/problem+json: schema: { $ref: '#/components/schemas/Error' } /audit/events: get: tags: [audit] summary: Query the OOB audit trail description: | Returns the immutable audit events recorded for OOB actions (initiation, approval, scheduling, veto, abort, execution, and re-registration confirmation), per the tamper-evident lifecycle-logging requirement A_29948 (timestamp, operator identity and justification for operator-driven actions). Filter by `identity_ref`, `deletion_id`, and/or a `since` timestamp. security: - mutualTLS: [] operatorAuth: [zeta.oob.read] parameters: - name: identity_ref in: query required: false description: Return only events for this identity. schema: { type: string } - name: deletion_id in: query required: false description: Return only events for this deletion request. schema: { type: string } - name: since in: query required: false description: Return only events at or after this time (Unix epoch seconds). schema: { type: integer, format: int64 } responses: '200': description: Audit events content: application/json: schema: type: array items: { $ref: '#/components/schemas/AuditEvent' } components: securitySchemes: mutualTLS: type: mutualTLS description: | Client-certificate mutual TLS on the privileged admin channel (RFC 8705). Mandated by A_29949. operatorAuth: type: oauth2 description: | Operator identity and role, from an IDP-INDEPENDENT operator identity provider (deliberately not the SEK-IDP; A_29949). Scopes map to roles enforcing separation of duties (A_29945, A_29946). flows: clientCredentials: tokenUrl: https://operators.internal/oauth2/token scopes: zeta.oob.read: Read deletions, identities, audit zeta.oob.initiate: Initiate or abort a deletion zeta.oob.approve: Second-operator approval (dual control) zeta.oob.reregister: Confirm re-registration of a tombstoned identity parameters: DeletionId: name: deletion_id in: path required: true description: Server-assigned identifier of the OOB deletion request. schema: { type: string } IdentityRef: name: identity_ref in: path required: true description: Opaque reference to an identity record at this guard (from GET /identities). schema: { type: string } IdempotencyKey: name: Idempotency-Key in: header required: false schema: { type: string } description: Safe-retry key; replays of the same key return the original result. schemas: DeletionStatus: type: string description: | Lifecycle state of an OOB deletion request: * `awaiting_approval` - created; waiting for the second operator's dual-control approval. * `scheduled` - approved; the veto window is running and channels have been notified. * `vetoed` - cancelled during the veto window by a surviving factor (terminal). * `aborted` - cancelled by an operator before execution (terminal). * `executed` - the veto window elapsed with no veto; the identity is tombstoned (terminal). enum: [awaiting_approval, scheduled, vetoed, aborted, executed] OobProof: type: object description: Reference to the out-of-band identity proofing performed by the operator. required: [method, evidence_ref, assurance_level, proofed_at] properties: method: type: string description: | How the identity was proven out of band: * `postal_ident` - postal identity verification (e.g. POSTIDENT). * `in_person_kyc` - in-person know-your-customer check. * `video_ident` - attended video identification. * `other_attested` - another attested procedure documented in `evidence_ref`. enum: [postal_ident, in_person_kyc, video_ident, other_attested] evidence_ref: type: string description: Case/ticket reference; the underlying evidence is stored out of band. assurance_level: type: string enum: [high, substantial] description: Assurance level of the proof. Must be 'high' for deletion; 'substantial' is rejected. proofed_at: type: integer format: int64 description: When the proofing was completed (Unix epoch seconds). OperatorRef: type: object description: Identifies an operator acting on the privileged channel. properties: operator_id: type: string description: Stable identifier of the operator from the IDP-independent operator IDP. display_name: type: string description: Human-readable operator name, for display in an operator console. DeletionRequest: type: object description: Operator's request to initiate an out-of-band deletion for one identity. required: [identity_ref, justification, oob_proof] properties: identity_ref: type: string description: Obtained from GET /identities (avoids raw KVNR in the deletion record). justification: type: string description: Operator's reason for the deletion; recorded in the audit trail. oob_proof: $ref: '#/components/schemas/OobProof' confirm_no_surviving_factor: type: boolean description: Operator asserts they verified the user has no usable surviving factor. Deletion: type: object description: State of an OOB deletion request as tracked by the guard. required: [deletion_id, identity_ref, status, scope, requested_by, requested_at] properties: deletion_id: type: string description: Server-assigned identifier of the deletion request. identity_ref: type: string description: Reference to the identity being deleted. status: { $ref: '#/components/schemas/DeletionStatus' } scope: type: string const: identity description: Deletion scope. Always `identity` - OOB deletion only ever tombstones a whole identity. requested_by: { $ref: '#/components/schemas/OperatorRef' } requested_at: type: integer format: int64 description: When the request was initiated (Unix epoch seconds). approved_by: { $ref: '#/components/schemas/OperatorRef' } approved_at: type: integer format: int64 description: When the second operator approved the request; absent until approved (Unix epoch seconds). effective_after: type: integer format: int64 description: End of the veto window, after which execution may proceed; set once scheduled (Unix epoch seconds). executed_at: type: integer format: int64 description: When the deletion was executed and the identity tombstoned; absent until executed (Unix epoch seconds). cooling_off_until: type: integer format: int64 description: End of the tombstone cooling-off period; set once executed (Unix epoch seconds). notified_channels: type: array description: Pinned channels notified when the deletion was scheduled (e.g. the bound email). items: { type: string } DeletionDetail: description: | Extended view of a deletion request: the base `Deletion` plus the sensitive justification and proof, any veto or abort record, and the full audit trail. allOf: - $ref: '#/components/schemas/Deletion' - type: object properties: justification: type: string description: Operator's reason given when the deletion was initiated. oob_proof: { $ref: '#/components/schemas/OobProof' } veto: type: object description: Present when the deletion was vetoed by a surviving factor. properties: vetoed_at: type: integer format: int64 description: When the veto was received (Unix epoch seconds). via: { type: string, description: Which surviving factor vetoed. } abort: type: object description: Present when the deletion was aborted by an operator. properties: aborted_by: { $ref: '#/components/schemas/OperatorRef' } aborted_at: type: integer format: int64 description: When the abort was performed (Unix epoch seconds). reason: type: string description: Operator's reason for the abort. events: type: array description: Chronological audit trail of actions on this deletion request. items: { $ref: '#/components/schemas/AuditEvent' } IdentityStatus: type: object description: Current lifecycle state of an identity record at this guard. required: [identity_ref, state] properties: identity_ref: type: string description: Opaque reference to the identity record. state: type: string description: | Lifecycle state of the identity: * `active` - usable; normal operation. * `pending_deletion` - an OOB deletion is scheduled within its veto window. * `tombstoned` - deleted; re-registration is blocked until a second OOB confirmation. enum: [active, pending_deletion, tombstoned] client_count: type: integer description: Number of client registrations currently bound to the identity. surviving_factors: type: object description: Whether each factor is still usable (informs whether OOB delete is warranted). properties: pinned_email: type: boolean description: The bound email (F1) is still usable. instance_key: type: boolean description: A registered client's instance key (F2) is still usable. cooling_off_until: type: integer format: int64 description: End of the tombstone cooling-off period; present when tombstoned (Unix epoch seconds). reregister_confirmed: type: boolean description: Whether the second, independent OOB confirmation has been recorded. AuditEvent: type: object description: A single immutable entry in the OOB audit trail. required: [event_id, type, at] properties: event_id: type: string description: Server-assigned identifier of the audit event. deletion_id: type: string description: Deletion request the event relates to, if any. identity_ref: type: string description: Identity the event relates to, if any. type: type: string description: | Kind of action recorded: * `requested` - a deletion was initiated. * `approved` - a second operator approved the deletion. * `scheduled` - the veto window started and channels were notified. * `vetoed` - a surviving factor cancelled the deletion. * `aborted` - an operator cancelled the deletion. * `executed` - the identity was tombstoned. * `reregister_confirmed` - the second OOB confirmation unblocked re-registration. enum: [requested, approved, scheduled, vetoed, aborted, executed, reregister_confirmed] operator: { $ref: '#/components/schemas/OperatorRef' } at: type: integer format: int64 description: When the event occurred (Unix epoch seconds). detail: type: string description: Optional free-text detail about the event. Error: type: object description: >- Problem Details object per RFC 9457 (`application/problem+json`); `code` carries the machine-readable failure reason. required: [code] properties: code: type: string description: | Machine-readable failure reason: * `assuranceTooLow` - the identity proof's assurance level is below 'high'. * `evidenceMissing` - required proof/evidence was missing or invalid. * `survivingFactorPresent` - a usable factor still exists; use self-recovery instead. * `duplicateActiveDeletion` - an active deletion already exists for this identity. * `sameOperatorForbidden` - the acting operator must differ from the earlier one (separation of duties). * `invalidState` - the request is not in a state that allows this action. * `notTombstoned` - the identity is not in a tombstoned/cooling-off state. enum: - assuranceTooLow - evidenceMissing - survivingFactorPresent - duplicateActiveDeletion - sameOperatorForbidden - invalidState - notTombstoned title: type: string description: Short, human-readable summary of the problem type. detail: type: string description: Human-readable explanation specific to this occurrence.