SenseCrypt Docs
Guides

Rotating signing keys

Operator how-to for rotating a tenant's OIDC and SAML signing keys — one-shot OIDC rotate vs make-before-break SAML staging, the overlap (grace) windows, staged certs by kid, KMS vs software custody, and what your relying parties and service providers must do during the overlap.

Each SenseCrypt tenant is its own OIDC issuer and SAML IdP, and signs with its own keys: an ES256 key published at /.well-known/jwks.json, and an RSA key plus X.509 certificate embedded in the SAML IdP metadata. This guide is for operators rotating those keys through the admin signing-keys surface, and for the RP/SP owners who need to keep verifying tokens across a rotation.

This guide is about using the rotation feature and coordinating downstream. For how key material is sealed at rest, see Security.

The core idea: overlap, not swap

A rotation is never an instant cutover for verifiers. A fresh key becomes the live signer immediately, but the outgoing key stays published for an overlap window — the grace window — so that:

  • tokens and assertions already signed with the old key keep verifying until they expire, and
  • relying parties and service providers have time to pick up the new key.

Rotations are gated per capability: viewing keys needs signing_keys:read; rotating needs signing_keys:rotate. As with every tenant-scoped admin route, the tenant is the one resolved from the request host (or X-Tenant-Id), never a path parameter — so you always rotate the keys of the tenant you are operating as.

You can see the current state, and the public URLs where each key is published, from the overview:

GET /v1/admin/signing-keys

Each key reports a lifecycle status: current (the live signer), grace (superseded but still published during an overlap), retired (no longer published), and — for SAML — pending (staged but not yet activated).

OIDC rotation: one-shot

OIDC RPs re-fetch the JWKS endpoint on demand, so OIDC uses a single-step rotate. One call mints a new ES256 key, makes it the live signer, and grace-retires the previous one:

POST /v1/admin/signing-keys/oidc/rotate
Content-Type: application/json

{ "grace_period": "24h" }
  • grace_period is how long the previous key stays published in the JWKS. Options: immediate, 24h, 72h, 7d. A body-less POST defaults to immediate (the old key is retired at once).
  • The response returns the new_kid, the previous_kid, and previous_expires_at (when the old key stops being published; null for an immediate rotation).

Both keys appear in the JWKS during the overlap, each under its own kid. Pick a grace_period at least as long as your longest access-token / ID-token lifetime so nothing signed by the old key outlives its publication.

SAML rotation: make-before-break (stage → activate)

SAML service providers commonly cache or pin the IdP certificate and re-fetch metadata rarely, so you cannot safely swap a SAML cert in one shot. SAML rotation is therefore make-before-break: you publish the new certificate first, give SPs time to trust it, and only then switch signing onto it.

1. Stage a new key

POST /v1/admin/signing-keys/saml/stage

This creates a new SAML key in pending status. It is published (so SPs can fetch it) but is not yet signing — the current cert still signs every assertion. Staging changes nothing about live traffic.

2. Pre-configure your SPs with the staged cert

Hand the staged certificate to each SP before activating. Fetch it by its kid:

GET /v1/idp/saml/certificate?kid=<staged_kid>

Without kid, the certificate endpoint returns the current signing cert; with kid, it returns that specific published cert — including the staged one. Upload this PEM to any SP that verifies assertions by a pinned/uploaded certificate (for example, Google Workspace's manual SSO setup). SPs that ingest IdP metadata will also see both certs published in /v1/idp/saml/metadata.

A staged cert has a guardrail: if it is never activated within the deployment's staging-age limit, a background sweeper auto-discards it, and the certificate endpoint stops handing it out (it's about to disappear). The overview surfaces this limit so you can activate in time.

3. Activate

When your SPs trust the staged cert, activate it. This promotes the staged cert to the live signer and grace-retires the outgoing one:

POST /v1/admin/signing-keys/saml/activate
Content-Type: application/json

{ "grace_period": "7d" }
  • SAML offers a longer overlap menu than OIDC because SPs re-fetch rarely: immediate, 24h, 72h, 7d, 30d.
  • Activation defaults to 7d (not immediate) precisely so slow-to-refresh SPs have time to learn the new cert before the old one drops. During the overlap both certs are published in metadata.

Discard a staged key

If you staged a key but decide not to proceed, discard it before activation:

POST /v1/admin/signing-keys/saml/discard-staged

This removes the pending cert from publication (and disables its KMS key, if it had one). It never touches the live signer.

Roll back an activation

If an activation causes trouble, you can bring a previously retired cert back into service as the live signer and grace-retire whatever is currently signing:

POST /v1/admin/signing-keys/saml/reactivate
Content-Type: application/json

{ "kid": "<retired_kid>", "grace_period": "7d" }

This re-enables the old cert (un-deleting its KMS key if needed) so SPs that never moved off it keep working while you sort things out.

Key custody: software vs KMS

When you rotate or stage, you choose the custody of the new key with the optional backend field:

  • software — the private key is generated by the service and sealed at rest in the database under an encryption envelope.
  • kms — the private key is generated inside AWS KMS and never leaves it; the service signs through KMS but can never export the key.

Omitting backend preserves the current key's custody (a plain rotate keeps whatever you had). You can also use a rotation to migrate custody — for example rotate onto kms — and the old (software) key keeps verifying through its grace window.

KMS custody is only offered when the deployment has it enabled. Rotating onto kms on a deployment without KMS configured is rejected (400). The overview's kms_available flag tells you whether the option exists. Custody is purely an at-rest concern — it does not change how anyone verifies tokens: verifiers always use the public JWKS or certificate.

What relying parties and service providers must do during overlap

The whole point of the grace window is to make rotation safe for downstream verifiers — but only if they follow the rules.

OIDC relying parties:

  • Select the verification key by kid. Every ID/access token's JWT header carries the signing key's kid; match it to the JWKS entry with the same kid. Do not assume a single key.
  • Re-fetch the JWKS on an unknown kid. If a token arrives with a kid you don't have cached, re-fetch /.well-known/jwks.json before rejecting it. This is what lets a new key roll out without downtime.
  • Cache the JWKS sensibly — long enough to avoid hammering the endpoint, short enough (or kid-triggered) that a rotation is picked up within the grace window.

SAML service providers:

  • Trust both certificates during the overlap. While a rotation is in grace, metadata publishes the old and new certs; your SP must accept assertions signed by either. Most SAML libraries do this automatically when you load all <KeyDescriptor use="signing"> certs from metadata.
  • Re-fetch metadata (or upload the new cert) before the old one's grace expires. If your SP pins a single cert, use the staged-cert step above to pre-load the new one ahead of activation.

If a verifier ignores the new kid/cert past the grace window, it will start rejecting valid tokens — so align the grace_period you choose with how quickly your downstream partners actually refresh.

Using the Management API

Everything above is also drivable over the Management API, so you can script a rotation instead of clicking through the console. These are the same tenant-scoped /v1/admin/signing-keys routes the console itself calls.

Authenticate with an M2M token. These admin calls need a machine-to-machine access token — see Machine-to-machine for how to mint one with the client-credentials grant. The token's bound roles must carry the relevant capability: signing_keys:read to view keys, signing_keys:rotate for every rotate / stage / activate / discard / reactivate call. Pass the token as Authorization: Bearer {mgmtToken}. The base path is your tenant's admin API on the tenant host ({tenant-host}); as with every admin route, the tenant is resolved from that host, never a path parameter — so these routes carry no {tenant_id}, and a token can only rotate the keys of the one tenant it was minted for.

View the current keys

curl https://{tenant-host}/v1/admin/signing-keys \
  -H "Authorization: Bearer {mgmtToken}"

The response is the same overview the console renders — both key families with their derived status, custody backend, the public publication URLs, and the kms_available / staging_max_age_days guardrails:

{
  "oidc": [
    {
      "kid": "oidc-2f9c1a…",
      "alg": "ES256",
      "status": "current",
      "backend": "software",
      "kms_key_arn": null,
      "created_at": "2026-07-16T10:12:00Z",
      "retired_at": null,
      "cert_fingerprint_sha256": null
    }
  ],
  "saml": [
    {
      "kid": "saml-8b41d7…",
      "alg": "RS256",
      "status": "current",
      "backend": "software",
      "kms_key_arn": null,
      "created_at": "2026-07-16T10:12:00Z",
      "retired_at": null,
      "cert_fingerprint_sha256": "9F:86:D0:81:88:5C:…"
    }
  ],
  "jwks_uri": "https://{tenant-host}/.well-known/jwks.json",
  "saml_metadata_url": "https://{tenant-host}/v1/idp/saml/metadata",
  "saml_certificate_url": "https://{tenant-host}/v1/idp/saml/certificate",
  "kms_available": false,
  "staging_max_age_days": 30
}

Rotate OIDC keys (one-shot)

curl -X POST https://{tenant-host}/v1/admin/signing-keys/oidc/rotate \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{ "grace_period": "24h" }'

The body is optional. grace_period accepts immediate (the default for a body-less POST), 24h, 72h, or 7d. Add an optional backend of software or kms to also migrate custody on this rotate; omit it to preserve the current key's custody. The response reports the new live signer, the superseded key, and when it stops being published (null for an immediate rotation):

{
  "kind": "oidc",
  "new_kid": "oidc-7d1a3e…",
  "backend": "software",
  "kms_key_arn": null,
  "previous_kid": "oidc-2f9c1a…",
  "previous_expires_at": "2026-07-17T10:20:00Z"
}

Stage a SAML key

curl -X POST https://{tenant-host}/v1/admin/signing-keys/saml/stage \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{ "backend": "software" }'

The body is optional — send { "backend": "kms" } to stage a KMS-resident key, or omit the body entirely to preserve the current key's custody. The new key is created in pending status (published but not yet signing) and its kid comes back so you can hand out its certificate:

{
  "kind": "saml",
  "staged_kid": "saml-c30e5f…",
  "backend": "software",
  "kms_key_arn": null
}

Fetch the staged certificate

This is the public IdP certificate endpoint (no bearer token) — the same one referenced in the console steps above. Pass the staged_kid from the previous call to download that specific pending cert as a PEM file, and pre-load it into any SP that pins a certificate:

curl "https://{tenant-host}/v1/idp/saml/certificate?kid={staged_kid}"
-----BEGIN CERTIFICATE-----
MIID…snip…AB
-----END CERTIFICATE-----

Without kid the endpoint returns the current signing cert instead. SPs that ingest metadata will also find both certs in https://{tenant-host}/v1/idp/saml/metadata.

Activate the staged SAML key

curl -X POST https://{tenant-host}/v1/admin/signing-keys/saml/activate \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{ "grace_period": "7d" }'

The body is optional and defaults to a real 7d overlap (not immediate). SAML offers the longer menu: immediate, 24h, 72h, 7d, or 30d. This promotes the staged cert to the live signer and grace-retires the outgoing one, echoing the same rotated shape as the OIDC call:

{
  "kind": "saml",
  "new_kid": "saml-c30e5f…",
  "backend": "software",
  "kms_key_arn": null,
  "previous_kid": "saml-8b41d7…",
  "previous_expires_at": "2026-07-23T10:30:00Z"
}

Discard a staged SAML key

If you staged a key but decide not to proceed, discard it before activation. There is no request body, and a success returns 204 No Content:

curl -X POST https://{tenant-host}/v1/admin/signing-keys/saml/discard-staged \
  -H "Authorization: Bearer {mgmtToken}"

If there is no staged key to discard, the call returns 400.

Roll back a SAML activation

To bring a previously retired cert back into service as the live signer, POST its kid. Here kid is required; grace_period is optional and defaults to 7d (same SAML menu as activate):

curl -X POST https://{tenant-host}/v1/admin/signing-keys/saml/reactivate \
  -H "Authorization: Bearer {mgmtToken}" \
  -H "Content-Type: application/json" \
  -d '{ "kid": "saml-8b41d7…", "grace_period": "7d" }'

The response is the rotated shape again, where new_kid is the cert you brought back and previous_kid is whatever had been signing (now grace-retired):

{
  "kind": "saml",
  "new_kid": "saml-8b41d7…",
  "backend": "software",
  "kms_key_arn": null,
  "previous_kid": "saml-c30e5f…",
  "previous_expires_at": "2026-07-23T10:45:00Z"
}

Note that a rotate/stage onto kms when the deployment has no KMS custody configured is rejected with 400 (the overview's kms_available flag tells you whether the option exists). There is no Management API call to change how verifiers check tokens — custody is an at-rest concern only, and downstream RPs and SPs always verify against the public JWKS or certificate exactly as described above.

  • Multi-tenancy — per-tenant issuers and per-tenant keys.
  • Tokens & sessions — token signing and the kid header.
  • Security — key custody (software envelope vs KMS) internals.
  • SAML — IdP metadata, certificates, and how SPs consume them.

On this page