Skip to content

Lifecycle Policy Protection Invariant

URI: https://docs.ankatech.co/errors/lifecycle-policy-protection-invariant
HTTP Status: 400 Bad Request

When you see this

The lifecycle policy you submitted would let a key that is not in use apply protection. Nothing was stored: the policy in force is unchanged.

Only an ACTIVE alias over a PRIMARY material applies protection. Every lifecycle policy, tenant and deployment alike, must therefore set DENY for the four operations that apply protection — ENCRYPT, SIGN, REENCRYPT and RESIGN — in every other status:

Dimension Statuses where the four protect operations must be DENY
alias SUSPENDED, RETIRED, DELETED_LOGICAL
material PENDING_PRIMARY, RETIRED, REVOKED, PENDING_DESTRUCTION, DESTROYED

This follows NIST SP 800-57 Part 1 §5.3: a key past its originator-usage period may still process information it already protected, but never applies new protection.

The other rows are not judged. DECRYPT and VERIFY stay yours to allow on a suspended or retired key, so data can still be read, and re-encrypted or re-signed away from such a key (the source of a re-encryption is judged as DECRYPT, of a re-signing as VERIFY). ROTATE and the lifecycle-management operations are not judged either.

The check runs on both lifecycle policy writes, before anything is read or stored. On a tenant write it runs before the deployment ceiling check, so a tenant policy that breaks both is answered with this 400, not with Lifecycle Policy Subset Violation.

The rule does not depend on the policy alone: the key service refuses ENCRYPT, SIGN and the target of a re-encryption or re-signing with a non-ACTIVE alias or a non-PRIMARY material whatever a stored policy says, with its generic 403.

Common Causes

  • Allowing ENCRYPT or SIGN on a SUSPENDED alias so that applications keep working during an investigation.
  • Allowing REENCRYPT or RESIGN on a RETIRED alias or material, intending to migrate data off the key: migration off a key is governed by its DECRYPT and VERIFY rows.
  • Copying a whole row from ACTIVE / PRIMARY into the other statuses.

Response Example

{
  "type": "https://docs.ankatech.co/errors/lifecycle-policy-protection-invariant",
  "title": "Lifecycle Policy Protection Invariant",
  "status": 400,
  "detail": "The lifecycle policy allows ENCRYPT, SIGN, REENCRYPT or RESIGN on a key that is not in use, in 2 cells. Protection is applied only by an ACTIVE alias over a PRIMARY material: set these cells to DENY.",
  "instance": "/api/v3/admin/tenants/550e8400-e29b-41d4-a716-446655440000/policies/lifecycle",
  "timestamp": 1790000000,
  "extensions": {
    "violations": [
      { "dimension": "alias", "operation": "ENCRYPT", "status": "SUSPENDED" },
      { "dimension": "material", "operation": "SIGN", "status": "PENDING_PRIMARY" }
    ],
    "requestId": "3f7c2a91-6d4e-4b8a-9c15-0e2d8b7a4f63"
  }
}

Content-Type: application/problem+json — the response follows RFC 9457 Problem Details.

Extension Meaning
violations Each offending cell as {dimension, operation, status}: dimension is alias or material, operation one of ENCRYPT, SIGN, REENCRYPT, RESIGN, and status the status where the policy set ALLOW. Listed alias first, then material, in operation and status order
requestId The request's correlation identifier

detail is fixed text apart from the cell count; read the cells from extensions.violations, never from detail.

How to Resolve

  1. Set every cell listed in extensions.violations to DENY.
  2. To keep data readable on a suspended or retired key, allow DECRYPT or VERIFY in that status instead; re-encryption and re-signing away from the key are judged on those rows.
  3. Re-submit the whole policy.

Operations that emit it

Method Endpoint
PUT /api/v3/admin/tenants/{tenantId}/policies/lifecycle
PUT /api/v3/admin/platform/deployment/policies/lifecycle

For full schema definitions, examples, and interactive testing, see the Developer Hub Reference.