Skip to content

Key Protection Backend Already Bound

URI: https://docs.ankatech.co/errors/backend-already-bound
HTTP Status: 409 Conflict

When you see this

A deployment-plane operation tried to change the platform's declared key-protection backend after that backend was already bound. Backend declaration is changeable only until the bind: once every bootstrap tenant holds an active key-encryption key, the declared backend is immutable, and the request is rejected before any mutation and before any audit event is emitted.

The window is defined by coverage, not by a count. The deployment is bound once both bootstrap tenants hold an active key-encryption key; while at least one of them does not, the deployment is unbound and the pre-bind route below is open. One key present is therefore still an open window, not a closed one.

The bind is not necessarily a host action, and it is not uniformly a console one either. POST /api/v3/admin/platform/setup/backend/activate binds every declarable family — the four PKCS#11 HSM tokens as well as the five Cloud KMS backends — and boundness for the HSM family travels on the published deployment descriptor rather than on an .env flag. Which caller reaches it depends on the route: the console offers the activation control for softhsm and for the five cloud tokens, and withholds it for luna, nshield and cloudhsm, whose bind is issued from the host with init-hsm.sh <env> --activate.

DELETE /api/v3/admin/platform/setup/backend/declare — the withdrawal — reads the same gate and answers the same 409 here.

Common Causes

  • Re-running POST /api/v3/admin/platform/setup/backend/declare on an environment whose backend is already bound.
  • Replacing the deployment backend configuration through PUT /api/v3/admin/platform/setup/backend/configuration after key material has been wrapped under the existing backend.
  • Calling POST /api/v3/admin/platform/setup/backend/activate on a platform already bound to a different backend — the conflict is evaluated first, so no credential is unsealed and no connection is opened.
  • Withdrawing the declaration with DELETE /api/v3/admin/platform/setup/backend/declare after the bind. The withdrawal exists for the pre-bind window only and reuses this refusal outside it.

    The instance member does not tell a refused declaration apart from a refused withdrawal: POST and DELETE share the path /api/v3/admin/platform/setup/backend/declare, and they share this type and this cause on purpose. The HTTP method in the access log is what distinguishes them.

Response Example

{
  "type": "https://docs.ankatech.co/errors/backend-already-bound",
  "title": "Key protection backend already bound",
  "status": 409,
  "detail": "The key-protection backend is already bound and can no longer be changed. Changing it requires destroying and recreating the environment.",
  "instance": "/api/v3/admin/platform/setup/backend/declare",
  "timestamp": 1730000000,
  "extensions": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000"
  }
}

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

How to Resolve

  1. Read the current declaration with GET /api/v3/admin/platform/setup/backend/configuration to confirm which backend is in force.
  2. If the bound backend is the intended one, no action is required — the refusal protects the key material already wrapped under it.
  3. If a different backend is genuinely required and the deployment is bound, destroy and recreate the environment; re-pointing a bound backend orphans the existing key material permanently and is not recoverable through this API.

Before the bind, there is a route out — and this page is not it

Getting this 409 means the window has already closed. It is worth knowing what was available before it did, because a failed first activation is recoverable and the remedy is not "recreate the environment".

While the deployment is not bound — while at least one bootstrap tenant still lacks an active key-encryption key — an operator may withdraw the declaration with DELETE /api/v3/admin/platform/setup/backend/declare and start again on a different backend. The withdrawal clears both declaration rows, disposes the sealed credential together with its retained predecessor and any transient candidate, clears the bootstrap key-encryption-key rows it can prove belong to the withdrawn backend, and names on its audit row every key reference it stopped tracking on the operator's cloud account.

Two things about that route matter before you need it:

  • It is the only route between two declared tokens. Re-configuring coordinates onto a different token is refused 422 Declared Backend Cannot Be Changed In Place. Withdraw first, then declare.
  • It is not free. The sealed credential goes with the declaration, so moving A → B and back to A costs a re-seal of A's credential each way, and there is no restore point across the change.

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