Skip to content

Compliance Remediation Plans

The evaluation of a compliance pack tells a tenant which items fail and which keys they name. A remediation plan turns those findings into work: for each failing item, the operation that fixes it, who owns it, by when, and whether it is done. The plan executes only operations the platform already has, only after a person previews and confirms them, and under that person's own permissions.

There is one plan per tenant and entitled pack. It is derived live from the same evaluation the evaluation screen and the evidence package show, so a finding is never stored twice and a plan is never out of date with the evaluation.

What the plan never does

  • It never destroys a key. No step marks a key or a material version for destruction, destroys it, archives it, deletes it or suspends it. Destruction stays a separate key-lifecycle operation, outside the plan, because destroying a version can make customer data unreadable.
  • It never re-encrypts or re-signs your data. ANKASecure does not hold customer data. After a rotation, data protected under the previous version stays readable; your applications move it to the new version with reencryptData and resignData, when and if they choose to.
  • It never runs on its own. Nothing executes without a preview and an explicit confirmation, and there are no automatic runs, reminders or notifications.
  • It never changes the score by itself. A risk acceptance or an attestation is shown beside the score and never inside it. An action is resolved only when the evaluation stops failing the item.

The plan

The plan is computed from one live evaluation of the pack, with the same entitlement, key population and reference day as getCompliancePackEvaluation. It lists:

  • Actions: one per item whose result is FAIL, or WARNING with reason BEFORE_EFFECTIVE_DAY, with the keys that item names. The ALGORITHM_CALENDAR summary control has no action, because each of its calendar entries is an action of its own. A PASS item has no entry.
  • Attestable items: one per item that is NOT_EVALUATED with reason CUSTOMER_ATTESTED or UNVERIFIED. The platform cannot measure these; the tenant records attestations for them.
  • Resolved actions: an action that has stored workflow state and whose item no longer fails stays listed as RESOLVED.

Each action has a stable action key: the item's controlId, or, for a calendar entry, calendar: followed by the lowercase hexadecimal SHA-256 of the entry's canonical content. A calendar action keeps its key, and so its owner, target date and history, when only the pack version changes.

The plan also serves:

Member Meaning
scoreToday The evaluation's score, unchanged
projectedScore The score the evaluation would give if every proposal of every AUTOMATIC and GUIDED action were applied. DECISION actions and actions under a risk acceptance are left as they are, and a NEUTRAL pack stays NEUTRAL
scoreDelta Per action, what its proposals add to the score
steps and paths Per action, every step offered, and the ways a key is moved: each path is a sequence of steps taken in order, and the paths are alternatives (a key takes one). A key-population action serves [ROTATE_KEY] and [CREATE_SUCCESSOR_KEY, REVOKE_SUPERSEDED_KEY]; a cryptoperiod action [SET_KEY_EXPIRY] and [ROTATE_KEY]. APPLY_ALGORITHM_TEMPLATE changes the tenant's policy, not a key, and is in no path. Which path applies to a key is answered by previewing the path's first step
deadline Per calendar action, the entry's effective day
nextDeadline The earliest effective day on or after the reference day among the WARNING and FAIL calendar actions, or null
Progress Resolved actions over listed actions

Every response repeats the evaluation's statement: a satisfied AUTOMATED item does not mean the regime is satisfied. A plan derived this way carries licensing: LICENSED.

The read-only plan of a pack that left the licence

A pack the licence no longer entitles (a locked pack) keeps its plan, read-only, with licensing: NOT_LICENSED. Nothing is evaluated and no key population is read, so the plan holds only what was recorded:

  • each action that has stored workflow state, in pack order, with its statement, kind and closing rule from the pack definition, and its stored owner, targetDate, riskAcceptance, migrationDeclaration and version;
  • its status: the stored one, or RISK_ACCEPTED while an acceptance is in force, and never RESOLVED, which only an evaluation decides.

scoreStatus, scoreToday, projectedScoreStatus, projectedScore, progress and nextDeadline are absent; an action carries no result, reason, kids, deadline or scoreDelta, and its steps is empty; attestableItems is empty. The executions and the attestations stay listed by their own reads, which carry the same licensing member. Updating an action, previewing or executing a step and recording an attestation are refused 403 Compliance Pack Not Licensed, with nothing written, audited or sent to the Core API; an execution already recorded under the same Idempotency-Key is still answered from storage (Retrying safely). The plan's licensing is LICENSED or NOT_LICENSED, never UNRESOLVED. If the licence cannot be read, the plan answers 502, because its shape depends on the verdict; the executions and attestations still answer, with licensing: UNRESOLVED.

The three kinds of action

Kind Meaning
AUTOMATIC The platform knows the fix and proposes it in full; a person confirms it
GUIDED The platform proposes a target and the person may choose another from the admissible set
DECISION Only the person can decide; the platform offers a step, or guidance only

The catalogue

Each control type maps to one kind, a fixed set of steps and a closing rule. Every step is an existing Admin API operation, called with the caller's own identity:

Step Existing operation
SET_KEY_EXPIRY patchKeyMetadata (expiresAt, and softLimitExpiration when it must move)
ROTATE_KEY rotateKey, always with a target algorithm and an explicit expiresAt
CREATE_SUCCESSOR_KEY createKey
REVOKE_SUPERSEDED_KEY revokeKey, always with cause ALGORITHM_DEPRECATED
APPLY_ALGORITHM_TEMPLATE the service operation setAlgorithmPolicy calls, with templateName
Control type Kind Steps Closes when
CRYPTOPERIOD AUTOMATIC SET_KEY_EXPIRY, or ROTATE_KEY with the same algorithm when the key is past its cryptoperiod (below) Every live key is within its maximum
GOVERNANCE (CSWP39.POLICY-PROFILE) AUTOMATIC APPLY_ALGORITHM_TEMPLATE, with any of the platform's explicit algorithm policy templates (the control names none) The tenant declares a template
ALGORITHM_ADMISSIBILITY, MIN_SECURITY_LEVEL, HYBRID_USE, a calendar entry GUIDED ROTATE_KEY (same kid); where core-api refuses that rotation, CREATE_SUCCESSOR_KEY then REVOKE_SUPERSEDED_KEY (new kid). Optionally first, APPLY_ALGORITHM_TEMPLATE when the control names a template the tenant has not declared The evaluation stops naming the key: at the rotation (same kid) or at the revocation (new kid)
KEY_CUSTODY DECISION The person chooses ROTATE_KEY to an admissible stateless signature algorithm, or follows the guidance to replace the key with a public-only import The evaluation stops naming the key
EVIDENCE_INTEGRITY (CSWP39.EVIDENCE-INTEGRITY.PLATFORM) DECISION None: guidance to open a security incident Never by the plan
KEK_CRYPTOPERIOD (NIST.CRYPTOPERIOD.KEK) DECISION None: guidance naming who acts (below) The tenant key-encryption key is within its cryptoperiod

APPLY_ALGORITHM_TEMPLATE passes the same tenant-administrator edit gate as setAlgorithmPolicy: on a tenant whose allowsTenantOverride is false, the preview refuses it with TENANT_OVERRIDE_NOT_ALLOWED, naming the platform administrator as the one who can change it, and an execution records the key REFUSED with nothing written. Moving a tenant to a hardware backend is not a step: a tenant's backend changes only through a key custody move, which its operator schedules, and an HSM would not satisfy the stateful hash-based custody requirement anyway.

Rotation or a new kid

A crypto-agile migration rotates the same kid. ROTATE_KEY adds a new material version under the same Stable KID with the target algorithm. The previous version becomes RETIRED: it no longer encrypts or signs, and it still decrypts and verifies what it protected. Applications keep the same kid, their capability grants are unchanged, and new operations use the new version. See Key Rotation*.

A new kid only where core-api refuses the rotation. Rotation cannot turn a composite key into a simple one (reason code structure-composite-to-simple). For such a key the plan proposes CREATE_SUCCESSOR_KEY, named <oldKid>-<algorithm token> by default (editable, within the kid length bound), then REVOKE_SUPERSEDED_KEY on the old kid with cause ALGORITHM_DEPRECATED. That cause keeps historical decrypt and verify on the old kid while it refuses new encryption and signing. A CREATE_SUCCESSOR_KEY preview refuses a key core-api admits rotating under the same kid with the code ROTATION_ADMISSIBLE, and an execution naming one is refused 422 Compliance Remediation Rotation Admissible, with nothing reserved. The revocation is admitted only for a kid whose successor an execution of the plan created (a DONE CREATE_SUCCESSOR_KEY outcome; otherwise the preview refuses it NO_SUCCESSOR_CREATED), and only while, once revoked for ALGORITHM_DEPRECATED, every non-destroyed material version of the kid still admits DECRYPT and VERIFY (and the re-encryption and re-signature sources): the tenant's effective lifecycle policy AND-composed with the platform's lifecycle cause rule, per version, exactly as core-api admits a read. Otherwise the preview refuses the kid REVOKED_MATERIAL_UNREADABLE. A policy that denies both on a revoked key is lifted by the tenant administrator; a version already revoked for SECURITY_COMPROMISE keeps that cause through the revocation and stays unreadable under any policy, so a kid holding one is never revoked by the plan. The old material therefore always stays readable. No capability grant is copied to the successor: authorizing an application on the new kid stays an explicit, separate act, and the preview says so.

Targets

A key's candidate targets are the algorithms both rules accept: for the same kid, the rotation admissibility check admits it without a consent; for a new kid, the tenant's admission rule admits it for the key's purpose. In both cases, evaluating the pack with the key on that algorithm must stop the item failing. The proposal is the candidate with the highest security level, catalogue order breaking ties, and the caller may choose any other candidate. An empty candidate set is a refusal shown in the preview, NO_ADMISSIBLE_TARGET.

The plan asks core-api nothing: it projects each key on the target the tenant's admission rule proposes for the key's purpose, so reading a plan costs no service call per failing key. Whether core-api admits the move under the same kid is stated by the preview, for the keys it lists.

Preview and confirmation

Preview (previewComplianceRemediationAction) writes nothing: no database row, no HSM call, no Redis, Kafka or audit write. It lists the action's keys one page at a time, in the evaluation's order: page is zero-based and size is 1 to 100 (default 25), 100 being the most keys one execution names; the response states keyCount and totalPages. Only the listed keys are asked about, so a ROTATE_KEY preview makes one core-api read per listed key. For one step of one action it returns, per listed key:

  • the current algorithm and material version, and the outcome the step would produce;
  • the proposed target, the candidate set and the proposed expiry;
  • a refusal and its reason, if any: the rotation reasonCode, TENANT_OVERRIDE_NOT_ALLOWED, NO_ADMISSIBLE_TARGET, NO_CRYPTOPERIOD_MAXIMUM, EXPIRY_BELOW_FLOOR (a SET_KEY_EXPIRY preview of a key past its cryptoperiod, or within 90 days of its end, whose step is ROTATE_KEY instead), NO_SUCCESSOR_CREATED or REVOKED_MATERIAL_UNREADABLE (a REVOKE_SUPERSEDED_KEY preview, see above), ROTATION_ADMISSIBLE (a CREATE_SUCCESSOR_KEY preview of a key the rotation path moves);
  • the conflicts the listed keys' targets would cause, and the projected score change of those targets;
  • requiredScopes (the remediation write scope and the step's own) and callerMayExecute: whether this caller holds every required scope and passes the tenant-owned boundary (false for a platform administrator addressing the tenant directly, who executes through an impersonation session).

For APPLY_ALGORITHM_TEMPLATE the preview lists no key: it shows the tenant's declared template, the proposed one, the candidate templates and any refusal.

It also returns an opaque basis: a SHA-256 over the reference day, the pack version, the action key, the step, and the kid, primary material version, algorithm and expiry of every key of the action, not only the listed page. An execution of any page echoes it while the state is unchanged.

Confirmation (executeComplianceRemediationAction) carries the step, the preview's basis, the conflicts the person acknowledged, and 1 to 100 keys. Each key may name a target (one of the preview's candidates for that kid; the proposal when omitted), a successorKid (any kid other than the superseded one; the proposal when omitted) and an expiresAt (the preview's proposedExpiresAt for that kid when omitted; when given, exactly that value). A proposed expiry does not depend on the instant of the request: it is anchored to 00:00 UTC of the reference day (see Maximum cryptoperiod), so it is the same value all day and an execution can echo it. The template step names no key and may carry a templateName from the preview's candidate templates. The basis is checked before the values: if anything the basis covers has changed since the preview, including the reference day (a preview served on another day), the request is refused with 409 Compliance Remediation Basis Changed, and the person previews again. Under a matching basis, a value outside the preview's is refused 400, and so is a REVOKE_SUPERSEDED_KEY key the preview refuses. In the console, the confirm button is enabled only after a preview, and each conflict needs its own checkbox.

Retrying safely

The confirmation requires an Idempotency-Key header (a UUID), as the IETF Idempotency-Key HTTP header draft (draft 07, §2.7) describes. The key is looked up before the basis is checked and before the licence is read, so a retry is answered from what was recorded, even after the pack left the licence (a replay writes, emits and calls nothing):

The same Idempotency-Key… Answer
…with the same request, after the run completed 200 with the stored execution; nothing is called again
…with the same request, while the first run is still in progress 409 Idempotency Key In Progress; nothing is called
…with the same request, after a run that stopped before finishing 200 with the outcomes recorded so far and state: INTERRUPTED
…with a different request 422 Idempotency Key Reused
absent 400

Before the first step the platform records an immutable execution header with a deadline (the number of keys plus one, times the per-step deadline). A run whose header has no completion record after that deadline is INTERRUPTED, so a crashed run is never repeated by a retry.

One run per action at a time. Whatever the Idempotency-Key, an action has at most one execution running. Its follow-up row is the serialization point: the first execution of an action stores that row (OPEN) when the action has none, and every execution locks it before recording its header. While another execution of the same action has no completion record and is before its deadline, a confirmation under a different Idempotency-Key is refused 409 Compliance Remediation Execution In Progress, with nothing reserved and nothing called, so no key is stepped by two runs of one action at once. The guarantee is per action, not per key: runs of different actions that name the same key, in the same pack or in another, are not serialized against each other, and a key whose state another run moved before its turn is SKIPPED_ALREADY_DONE. The wait for the follow-up row's lock is bounded to 5 seconds; a wait that expires is refused 503 Serialised Operation Timed Out with Retry-After: 5, nothing reserved and nothing called. It is never a 409: whether another execution was running was not evaluated. A new Idempotency-Key with the old request after a successful run does not run again: the run changed what the confirmation was built on, so it is refused 409 basis-changed, or 400 or 404 when the key or the action has left the plan.

How a run proceeds

Keys are processed one at a time, and one key's failure does not stop the others. Each key's operation commits with its own audit row, then its outcome is recorded:

Outcome Meaning
DONE The operation succeeded
SKIPPED_ALREADY_DONE The key already satisfied the step when its turn came, for example rotated by another session; nothing was changed
REFUSED The operation was refused; the outcome carries the problem type
FAILED The operation failed; the outcome carries the problem type and names the failing service by role, never its text
UNKNOWN A rotation was sent to the key service and no answer arrived, so it may have been applied; the outcome carries the problem type rotation-outcome-unknown and names the key service by role. Only a rotation step records it

A rotation step rotates through the same path as a manual rotation, recorded with cause MANUAL and the confirming user as initiator, and names the material version the plan read as the version it advances from. The cause stays MANUAL (a person confirmed the plan; the cause is a signed, persisted value every reader of the previous release must recognise), and the admin rotation row of a remediation rotation carries remediation: {executionId, packId, actionKey}, naming the execution it belongs to. Nothing new is sent to the key service. The key service applies the rotation only if the key is still at that version, so a key another session rotated between the step's own check and the rotation is refused there and recorded SKIPPED_ALREADY_DONE too. An UNKNOWN key is never retried inside the run: a later execution of the action re-evaluates it, and a key the unknown rotation did advance is then SKIPPED_ALREADY_DONE.

A completion record closes the run as COMPLETED, or as INTERRUPTED when its deadline stopped it. The execution history is append-only and listed newest first by listComplianceRemediationExecutions.

Each DONE outcome of ROTATE_KEY, SET_KEY_EXPIRY or CREATE_SUCCESSOR_KEY states the expiry the step wrote (expiresAt); no other outcome carries one. The execution names its requester as the audit trail does: requestedByDisplayName (a user's login, an application's name) and requestedByPlane (TENANT, or PLATFORM for a platform operator acting in a support session, who is named by plane only and never by name or email). requestedBy keeps the caller's audit username as recorded; a support-session token carries no login, so for such a run it holds the subject of the tenant administrator the session mirrors and does not name the person. Executions recorded before 3.2.0 carry neither member nor expiresAt.

Who may do what

Act Scope Roles holding it
Read the plan, a preview, the executions and the attestations admin.tenant.analytics.read Every tenant role that reads the evaluation, including TENANT_AUDITOR
Change an action's owner, target date or status; execute a step; record an attestation admin.tenant.compliance.remediation.write TENANT_ADMINISTRATOR, TENANT_KEY_MANAGER
Set or withdraw a risk acceptance admin.tenant.compliance.risk.accept, in addition TENANT_ADMINISTRATOR only

The plan never confers more than the caller holds. Executing a step also requires that step's own scope: admin.keys.patch for SET_KEY_EXPIRY, admin.keys.rotate for ROTATE_KEY, admin.keys.orchestrate for CREATE_SUCCESSOR_KEY, admin.keys.revoke for REVOKE_SUPERSEDED_KEY and admin.policy.tenant.algorithm.write for APPLY_ALGORITHM_TEMPLATE. Accepting a risk is a governance act, which is why an operator who may execute steps may not accept a risk.

Every write is tenant-owned: a platform administrator signed in directly to the platform tenant is refused 403, and reaches a tenant's plan only through an impersonation session the tenant consented to.

Tracking an action

An action's workflow state is stored per tenant, pack and action key:

  • owner: free text, 1 to 128 characters, not blank; clearOwner: true clears it.
  • targetDate: not before the reference day; clearTargetDate: true clears it.
  • status: the person may set OPEN or IN_PROGRESS.
  • riskAcceptance: a reason (1 to 1,000 characters) and an until date after the reference day and at most 365 days after it; withdrawRiskAcceptance: true withdraws it. Setting and withdrawing both require admin.tenant.compliance.risk.accept, even when there is nothing to withdraw.
  • migrationDeclaredComplete: true only; see the migration tracker.

A member left out, or sent as null, is unchanged: nothing is cleared by a null, because a client that drops nulls could never send one. Clearing is the explicit boolean member, and a value sent together with its clearing member is refused 400. The free-text members (owner, the risk reason) refuse any control character: C0 (U+0000 to U+001F, tab, CR and LF included), DEL (U+007F) and C1 (U+0080 to U+009F, NEL U+0085 and the one-character CSI U+009B included), and the line and paragraph separators U+2028 and U+2029. These values are rendered into the evidence package's PDF report, the console and the tenant's SIEM, where each of those characters forges a line or starts a terminal sequence, so they are refused, never stripped. A value outside these bounds is refused 400 Validation. Each write carries the action's version; a stale value is refused 409 Concurrent Modification.

The status shown is derived on every read, and a read writes nothing:

  1. RESOLVED when the live evaluation no longer fails the item;
  2. otherwise RISK_ACCEPTED while an acceptance is in force (its until is after the reference day);
  3. otherwise the stored state, IN_PROGRESS or OPEN (the default).

An item that fails again after being resolved reopens to its stored state. The first execution of an action that has no follow-up row stores one, OPEN. An execution with a DONE outcome stores IN_PROGRESS on an action whose stored state is still OPEN; a state a person set is kept. A follow-up row is never deleted: the service's database role holds no DELETE or TRUNCATE on it. A risk acceptance changes neither the evaluation's result nor its score.

The migration tracker

After a rotation, the previous version still decrypts and verifies; after a new-kid migration, the old kid does. Whether applications still use it is something only they can stop. For each DONE outcome the action shows what was superseded, the (kid, material version) or the old kid → successor kid, and the console shows its use over the last 30 days, read from the Audit API's getKeyMaterialUsage*.

The tracker is informational. It changes no status and no score, and it never proposes retiring or destroying anything. Re-encrypting or re-signing the data is the application's call, through reencryptData and resignData. When the tenant is satisfied that the migration is complete, it may record migrationDeclaredComplete: true, which stores who declared it and when; the declaration is refused 422 Unprocessable Entity unless an execution of the action recorded a DONE ROTATE_KEY or REVOKE_SUPERSEDED_KEY outcome. A declaration is never withdrawn: false is refused 400.

What counts as a use of a version. The Audit API counts, per day, each successful operation once per distinct (kid, material version) among its keys whose role is a use: DECRYPTING_RECIPIENT, ENCRYPTING_RECIPIENT, SIGNER or VERIFIED_SIGNER. A recipient a re-encryption kept, a signer a re-signing kept, and the key a rotation acted on are not a use of that version and count nothing. A successful key-use operation (ENCRYPT, DECRYPT, SIGN, VERIFY, REENCRYPT, RESIGN, SIGN_ENCRYPT, DECRYPT_VERIFY or MIGRATION_CONVERT_PKCS7) that names the kid but records no key list has no known version: it is served apart as unattributedCount and never folded into a version. Key management operations that name the kid (creation, metadata reads, rotation, revocation) use no material and are never counted. Refused and failed operations count nothing.

The tenant key-encryption key

The NIST.CRYPTOPERIOD.KEK action is opened by a FAIL, and by a WARNING with reason KEK_ROTATION_DUE. It offers no step: the key-encryption key is not one of the tenant's keys, and no operation of the tenant's plan changes it. Executing or previewing a step for it is refused with 422 compliance-remediation-step-not-applicable, and nothing is written. Who acts depends on who rotates the key:

  • KEK_CRYPTOPERIOD_EXCEEDED or KEK_ROTATION_DUE on a key-encryption key the platform rotates (on a PKCS#11 token): the platform operator rotates the tenant key-encryption key now, or shortens the carrier's rotation period.
  • KEK_PROVIDER_ROTATION_OFF (an ANKA-managed Cloud KMS tier): the platform operator enables the provider's rotation, for example by giving the tier's credential the rotation permission; the platform alert kek.rotation.missing is raised meanwhile.
  • KEK_CUSTOMER_ROTATION_OFF, or KEK_CRYPTOPERIOD_EXCEEDED on a tenant's own key: the customer enables rotation at its provider, with a period of 730 days or less.

The action closes when the item passes.

Maximum cryptoperiod

The two CRYPTOPERIOD controls (NIST.CRYPTOPERIOD in NIST_PQC_TRANSITION, at pack version 2026-10-19, and CSWP39.ALGORITHM-TRANSITIONS in NIST_CSWP_39, at 2026-10-07) measure each live key against a maximum originator-usage period per key class, served as maximumCryptoperiods on the control. The figures are the upper end of the ranges in NIST SP 800-57 Part 1 Rev. 5 (May 2020), §5.3.6, Table 1 "Suggested cryptoperiods for key types", printed pages 45-46:

Key class Maximum Table 1
SYMMETRIC 24 months Symmetric authentication, data-encryption and key-wrapping keys (rows 3, 6, 7): originator-usage period < 2 years
ASYMMETRIC_SIGNING 36 months Private signature key (row 1): 1 to 3 years
ASYMMETRIC_KEY_ESTABLISHMENT 24 months Private key-transport key (row 10): < 2 years; public key-transport and static key-agreement keys (rows 11, 13, 14): 1 to 2 years
PUBLIC_VERIFICATION none Public signature-verification key (row 2): "several years (depends on key size)", no fixed figure

The class is decided on the key's primary material: SYMMETRIC when its kty is oct; otherwise ASYMMETRIC_SIGNING when its keyOps contain sign; PUBLIC_VERIFICATION when they contain only verify; otherwise ASYMMETRIC_KEY_ESTABLISHMENT.

The rule. The control fails naming every live key whose expiry date is absent, or is later than the creation of its primary material plus its class maximum (calendar months, UTC). Otherwise it passes. A key created on 2026-01-15 for ML-DSA-87 signing passes with an expiry of 2029-01-15 and fails with 2029-02-15. A key created or imported without an expiry receives the creation of its primary material plus its class maximum (a PUBLIC_VERIFICATION key: the ASYMMETRIC_SIGNING maximum of its pair), so it passes; an expiry stated on create or import is accepted as stated, and fails when it is later. An imported key that carries a certificate never receives an expiry after the certificate's notAfter; a trust anchor keeps its notAfter.

The proposal. For each failing key the plan proposes the expiry E = creation of the primary material + maximum:

  • When E is at least the reference day + 90 days, the step is SET_KEY_EXPIRY to E. When the key's softLimitExpiration is at or after E, it moves too, keeping the key's current warning lead before E and never earlier than the reference day; the preview shows both values. This step never targets a key whose expiry is already reached, which the key-metadata patch refuses 409 key-protection-limit-reached (a reached expiry is final): the control names a key only when its expiry is absent or later than E, and E is at least 90 days ahead, so the key's expiry is absent or still ahead.
  • Otherwise the key is past its cryptoperiod, and the step is ROTATE_KEY with the same algorithm and expiresAt = 00:00 UTC of the reference day + maximum. A key whose expiry arrives stops encrypting and signing, so the plan gives a fresh cryptoperiod rather than leaving applications less than 90 days. A SET_KEY_EXPIRY preview of such a key refuses it with EXPIRY_BELOW_FLOOR.
  • A PUBLIC_VERIFICATION key without an expiry still fails, but has no maximum to propose from: the preview refuses it with NO_CRYPTOPERIOD_MAXIMUM, and the person sets an expiry with patchKeyMetadata.

Every rotation the plan requests states expiresAt, because a rotation that omits it inherits the current validity period, which for a key whose expiry exceeds its maximum is the same excessive period. A rotation or a successor for another control takes 00:00 UTC of the reference day + the maximum of the target's key class; when that class has no maximum it keeps the key's current expiry, and a key with neither is refused NO_CRYPTOPERIOD_MAXIMUM.

Conflicts between packs

A target that satisfies one pack can fail another. For each chosen target, the platform re-evaluates every entitled pack with that key substituted, and once more with every chosen target substituted together, so an item that fails only in combination is found too. Each item that would newly fail is a conflict {packId, itemKey, statement, kids}, naming the keys it is attributed to.

For example, with NSA_CNSA_2_0 and BSI_TR_02102_1 both entitled, moving the composite vault-kem-hybrid to stand-alone ML-KEM-1024 satisfies CNSA 2.0 and newly fails BSI.HYBRID.KEM. The platform never picks a side, because each pack is an obligation of the customer's, not the platform's. An execution whose conflicts are not all acknowledged is refused 409 Compliance Remediation Conflict Unacknowledged, listing them.

Attestations

For the items the platform cannot measure, the tenant records attestations (createComplianceAttestation):

Member Rule
itemKey An attestable item of the pack; any other item is refused 422 Compliance Attestation Not Applicable
statement 1 to 2,000 characters, no control character or line separator (as for owner)
attestedBy 1 to 128 characters, no control character or line separator
assessedOn Not after the reference day
expiresOn After assessedOn
evidence 1 to 10 entries {uri, sha256}: an https URI of at most 2,048 characters with no control character or line separator, and the SHA-256 of the evidence as 64 lowercase hexadecimal characters

The platform never fetches an evidence URI; it stores the link and its digest so that an auditor can retrieve the document and check it. Attestations are append-only: the newest one per item is in force until its expiresOn, and earlier ones are listed as superseded. An attestation is never scored: it changes no evaluation result and no score, and is shown beside them.

In the evidence package

The evidence package report carries a Remediation plan section: each action with its kind, status, owner, target date, risk acceptance (reason and until), its last execution with its outcome counts, and any migration declaration; and every attestation in force with its item, statement, attester, expiry, and each evidence uri and sha256. The section is part of report.pdf, so the signed report hash covers it, and the package still holds its six files.

Audit

Each underlying operation writes its own audit row as it always does. In addition, signed admin audit rows record the plan's own acts:

Act Event type Entity
Each execution admin.complianceremediation.create ComplianceRemediation, the execution id; payload {packId, packVersion, actionKey, step, basis, acknowledgedConflicts, outcomes[{kid, outcome, fromVersion, toVersion, successorKid, expiresAt, problemType}]} (an outcome also carries relayedProblemType when the upstream's problem type exceeds 255 characters; its problemType then names upstream-rejected-request), with the same correlation id as the operations of the run
Each workflow change admin.complianceremediation.update ComplianceRemediation, with the changed members
Each attestation admin.complianceattestation.create ComplianceAttestation; payload {packId, itemKey, expiresOn, evidenceCount, contentSha256}, where contentSha256 is the SHA-256 of the statement, the attester, the assessment and expiry days and every evidence uri and sha256, so the signature covers the attested content and not only its count

A refused request writes none of these rows. The admin rotation row each ROTATE_KEY step writes keeps cause MANUAL and carries remediation: {executionId, packId, actionKey} in its changes; the row of the other steps is joined to the execution by the shared correlation id. On a run confirmed in a support session every row names the operator in its impersonator fields and carries no actor_display_name, by design: the session's token names no principal of its own.

Operations

Method Endpoint (/api/v3/admin/tenants/{tenantId}/compliance-remediation/{packId} …) operationId
GET the base path itself (the plan) getComplianceRemediationPlan
PATCH /actions/{actionKey} updateComplianceRemediationAction
GET /actions/{actionKey}/preview?step=&page=&size= previewComplianceRemediationAction
POST /actions/{actionKey}/executions executeComplianceRemediationAction
GET /executions listComplianceRemediationExecutions
POST /attestations createComplianceAttestation
GET /attestations listComplianceAttestations

Status codes, scopes and refusals: Admin API endpoints*.

  • Compliance Packs — the packs and their evaluation
  • Compliance Evidence — the signed evidence and the package
  • Key Rotation & Revocation* — versions, RETIRED and revocation causes
* restricted content — see higher-tier documentation