---
title: Rotation
path: core-concepts/rotation
status: published
---

# Rotation

Static credentials age into liabilities. ScaiVault has first-class rotation: define a policy once, attach it to a set of secrets, and ScaiVault drives the schedule, keeps an old version readable during cutover, and emits events so your systems can respond.

## The model

A **rotation policy** is a schedule plus behavior:

- **Interval.** How often (`90d`, `7d`, `24h`).
- **Grace period.** How long the previous version stays readable after rotation (`48h` by default).
- **Warn-before.** When to emit `rotation.due` events (`["7d", "1d"]`).
- **Auto-generate.** Whether ScaiVault generates the new value itself (random string) or just fires an event and waits for you to `PUT` a new value.
- **Webhook IDs.** Where to route rotation events.

Attach a rotation policy to one or more secrets. The schedule runs independently per secret, based on its last-rotated timestamp.

```json
{
  "id": "rot_quarterly",
  "name": "quarterly",
  "description": "90-day rotation with 48h grace",
  "interval": "90d",
  "interval_seconds": 7776000,
  "grace_period": "48h",
  "warn_before": "7d,1d",
  "auto_generate": false,
  "webhook_ids": ["wh_rotation_alerts"],
  "secrets_count": 42,
  "is_active": true
}
```

## Lifecycle of a rotation

Take a secret with a 90-day policy, currently at version 3, last rotated on day 0.

```mermaid
sequenceDiagram
    participant SV as ScaiVault
    participant Auto as Your Automation
    participant Src as Source System
    participant Consumers

    Note over SV: Day 0 — v3 written
    Note over SV: Day 83 (T-7d)
    SV->>Auto: rotation.due (threshold=7d)
    Note over SV: Day 89 (T-1d)
    SV->>Auto: rotation.due (threshold=1d)
    Note over SV: Day 90 — rotation fires
    SV->>Auto: rotation.due (due_now=true)
    Auto->>Src: mint new credential
    Src-->>Auto: new value
    Auto->>SV: PUT new value (v4)
    SV->>Consumers: secret.rotated (old=3, new=4)
    Note over SV: v3 still readable (grace)
    Note over SV: Day 92 — grace ends
    Note over SV: v4 is the only current version
```

- **Day 83** (7 days before rotation). Webhook fires with `rotation.due`, `warn_before: "7d"`. Your automation can do anything — prepare the new value, tell a human, open a ticket.
- **Day 89** (1 day before). Second `rotation.due` webhook, `warn_before: "1d"`.
- **Day 90.** Rotation fires:
  - If `auto_generate: true`, ScaiVault writes a new version (v4) with a random value.
  - If `auto_generate: false`, ScaiVault emits `rotation.due` with `due_now: true` and waits. Your automation must `PUT /v1/secrets/{path}` with the new value. If it doesn't within the grace period, `rotation.overdue` fires.
- **Day 90.5.** `secret.rotated` webhook fires with `old_version: 3, new_version: 4`. Consumers can pick up the new value.
- **Day 92** (48h grace period ends). Version 3 is no longer reachable; v4 is the only readable version. Rotation complete.

## Auto-generate vs event-driven

**`auto_generate: true`** works for secrets whose value is just a random string — database passwords where you also control the database, internal service tokens. ScaiVault generates a cryptographically random value (configurable length and alphabet via a linked *secret policy*) and writes it as the new version. You then need to propagate the value to the downstream system.

**`auto_generate: false`** works when the value comes from somewhere else — an external OAuth provider, a third-party API key you request through a portal, a human picking a value. ScaiVault fires the event; you do the rest. This is the common case for third-party credentials.

The pattern is:

1. ScaiVault emits `rotation.due`.
2. Your automation generates (or obtains) the new credential from the source system.
3. Your automation writes the new version: `PUT /v1/secrets/{path}`.
4. ScaiVault records the rotation and emits `secret.rotated`.

## Grace periods

The grace period keeps the old version readable after rotation. Without grace, a consumer that just loaded v3 and is mid-request might fail with "this credential is invalid" because the underlying resource has already moved to v4.

```mermaid
gantt
    title Grace window during rotation
    dateFormat X
    axisFormat %s
    section v3
    Active (default current)        :0, 90
    Readable explicitly (grace)     :90, 92
    section v4
    Active (default current)        :90, 130
```

During grace, a read with no explicit version returns v4. Reads with `?version=3` return v3. After grace, `?version=3` returns the value but consumers treating it as current should use the latest — the audit log will flag reads of stale versions.

Set grace long enough to cover your longest-running job. `24h` is usually safe for typical web services; batch pipelines may want `72h` or more.

## Secrets due for rotation

Dashboards and automation can ask "what's due soon?":

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/rotation/due?within_hours=168"
```

```json
{
  "secrets": [
    {
      "path": "environments/production/salesforce/oauth",
      "policy_id": "rot_quarterly",
      "policy_name": "quarterly",
      "next_rotation_at": "2026-04-30T00:00:00Z",
      "is_overdue": false
    },
    {
      "path": "environments/staging/db-password",
      "policy_id": "rot_monthly",
      "policy_name": "monthly",
      "next_rotation_at": "2026-04-20T00:00:00Z",
      "is_overdue": true
    }
  ]
}
```

Use this in CI or a scheduled job to surface overdue rotations.

## Triggering an immediate rotation

Sometimes you need to rotate *now* — a credential leaked, a new hire left, a third party asked.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/secrets/app/db/password/rotate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Suspected compromise",
    "new_value": {"password": "new-random-value"},
    "grace_period": "1h"
  }'
```

`grace_period: "1h"` overrides the default for this one rotation — you probably don't want 48 hours of grace if you're responding to a compromise. For a policy-triggered emergency rotation across many secrets, `POST /v1/rotation/policies/{id}/rotate` with optional `secret_paths` filters.

## Rotation history

Every rotation (successful or failed) is recorded:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/history
```

```json
{
  "data": [
    {
      "id": "rh_abc",
      "policy_id": "rot_quarterly",
      "secret_path": "environments/production/salesforce/oauth",
      "status": "success",
      "old_version": 3,
      "new_version": 4,
      "rotated_at": "2026-04-23T00:00:00Z",
      "rotated_by": "system:rotation-scheduler"
    },
    {
      "id": "rh_def",
      "policy_id": "rot_quarterly",
      "secret_path": "environments/staging/api-token",
      "status": "failed",
      "error_message": "auto_generate disabled, no new value provided within grace period",
      "rotated_at": "2026-04-23T00:00:00Z"
    }
  ],
  "has_more": false
}
```

Failed rotations are retried on a backoff schedule. After the configured retry limit, they stop and require manual intervention.

## Enabling and disabling

`POST /v1/rotation/policies/{id}/disable` pauses the policy. Attached secrets stop rotating but are not detached. Re-enable with `POST /v1/rotation/policies/{id}/enable`. Useful during incidents or infrastructure migrations.

## Secret policies (value generation)

A *secret policy* (different from an *access policy*) controls how ScaiVault generates random values when `auto_generate: true`. Specify length, character classes, and constraints:

```json
{
  "id": "sp_db_passwords",
  "name": "db-passwords",
  "length": 32,
  "charset": "alphanumeric+symbols",
  "require_upper": true,
  "require_lower": true,
  "require_digit": true,
  "require_symbol": true
}
```

Attach a secret policy to a rotation policy or directly to a secret. `POST /v1/secret-policies` to create.

## What's next

- [Rotation Policies Guide](../api-guides/rotation) — end-to-end setup.
- [Events and Webhooks](./events-and-webhooks) — consuming rotation events.
- [Rotation Reference](../reference/rotation) — endpoints.
