# Rotation Procedure Schema — Layer 3 Reusable Entity Type # KNO Schema Version: 0.1.0 # # A Rotation Procedure entity describes HOW to rotate a class of secrets: # which provider implementation to invoke, what the parent credential must # be able to do, what kind of credential to GENERATE, how to VERIFY the new # value works, and how to REVOKE the old value. Procedures are FACTORED # OUT of secret entities so that secrets sharing a rotation pattern (e.g., # every Cloudflare scoped token) reference one procedure. # # DESIGN DECISIONS: # - Separate from secret-schema because the same procedure applies to # many secrets (one Cloudflare scoped-token procedure → 6 secrets). # Embedding it would duplicate ~30 lines per secret. # - The procedure declares a `provider` string (e.g., `cloudflare`, # `azure-sp`, `bao-approle`, `keycloak-client`) which the rotator # uses to dispatch to a TypeScript adapter implementing the # RotationProvider interface. # - Each of generate/verify/revoke is an object with a `kind` # discriminator. The rotator validates that the (provider, kind) # pair is implemented at startup; unknown pairs fail loudly. # - The schema does NOT contain any credentials or any code — it # is pure declarative configuration. # # THREE-GATE TEST: # Gate 1 (Distinctness): YES — a rotation procedure is fundamentally # different from a connector (which describes a provider integration # for runtime API calls) and from a workflow (which is an end-user # orchestration). It is the rotation-specific configuration of a # provider adapter. # Gate 2 (Reusability): YES — at Phase 1A introduction we expect # ~6 procedures (cloudflare-scoped-token, sendgrid-api-key, # azure-sp-secret, bao-approle-secret, keycloak-client-secret, # ssh-keypair) shared across ~12+ secrets. Sharing IS the value # proposition. # Gate 3 (Clarity): YES — separation makes the rotator's "what runs # for this secret?" question a single XRI lookup. # # RELATED: # - secret-schema.kno — references this schema via rotation.procedure_xri # - docs/architecture/secret-rotation.md § Provider interface # ============================================================================= # SCHEMA DECLARATION # ============================================================================= $schema: kno@0.0.9 # ============================================================================= # IDENTITY # ============================================================================= id: 01KQYR9D7BX2KGTJ6SVPMA8FRC slug: rotation-procedure-schema type: spec version: 0.1.0 # ============================================================================= # STANDARD TIER # ============================================================================= title: "Rotation Procedure Schema" purpose: | Define the schema for Rotation Procedure entities — declarative descriptions of HOW to rotate a class of secrets. **What is a Rotation Procedure entity?** A `.kno` file in `content/rotation-procedures/` declaring: - the `provider` adapter to use (a TypeScript implementation of `RotationProvider`) - what permissions the parent credential must have (`check`) - what kind of new credential to mint (`generate`) - how to prove the new credential works (`verify`) - how to revoke the old credential (`revoke`) **Why factored out of secret-schema?** Many secrets share the same rotation mechanism. Six Cloudflare scoped tokens all rotate the same way; embedding the procedure in each would duplicate it six times and create six places where a procedure bug must be fixed. Extracting it makes the procedure the canonical place to change rotation behavior for an entire class of secrets. **What this schema is NOT:** - Not a connector — connectors describe runtime provider integrations (e.g., "the Cloudflare adapter for managing DNS records"). A rotation procedure describes the rotation-specific configuration on top. - Not a workflow — workflows orchestrate user-facing flows. Procedures are pure rotator-internal configuration. - Not the rotator's code — the schema is declarative; the actual rotation logic is in `services/pspace-rotator/src/providers/.ts`. # ============================================================================= # RICH TIER — Provenance & Taxonomy # ============================================================================= provenance: origin: id: 01KQYR9D7BX2KGTJ6SVPMA8FRC timestamp: "2026-04-27T00:00:00Z" tool: manual taxonomy: topics: - secrets - rotation - procedures - security - operations keywords: - rotation-procedure - rotation - provider - generate - verify - revoke - pspace-rotator # ============================================================================= # RICH TIER — Relationships # ============================================================================= relationships: depends_on: - xri: "kno://specs/kno-spec" reason: "Conforms to KNO format specification v0.0.9" composes: - xri: "kno://specs/identity-schema" reason: "Layer 1: id, slug, provenance" - xri: "kno://specs/history-schema" reason: "Layer 1: _history, changelog" - xri: "kno://specs/quality-schema" reason: "Layer 1: quality, validation" related_to: - xri: "kno://specs/secret-schema" reason: "Secret entities reference rotation procedures via rotation.procedure_xri" - xri: "kno://specs/connector-schema" reason: "Procedures and connectors both describe provider integrations; procedures are the rotation-specific subset" enables: - xri: "kno://content/rotation-procedures/*" reason: "Rotation procedure instance files consumed by pspace-rotator" # ============================================================================= # RICH TIER — Quality # ============================================================================= quality: completeness: 0.85 last_reviewed: "2026-04-27" review_status: draft reviewed_by: "claude" # ============================================================================= # HISTORY # ============================================================================= _history: retention: full format: changelog changelog: - version: "0.1.0" date: "2026-04-27" author: "claude" summary: "Initial rotation procedure schema" changes: - "Created for pspace-rotator Phase 1A (docs/architecture/secret-rotation.md)" - "Fields: provider, parent_credential.permissions[], generate{}, verify{}, revoke{}" - "Generate / verify / revoke each carry a kind discriminator dispatched by the rotator at runtime" # ============================================================================= # REQUIRED FIELDS # ============================================================================= fields: # --------------------------------------------------------------------------- # IDENTITY # --------------------------------------------------------------------------- id: type: string format: ulid required: true description: "ULID of this Rotation Procedure entity" slug: type: string required: true pattern: "^[a-z0-9][a-z0-9-]*$" description: | Human-readable identifier matching the file basename. Examples: `cloudflare-scoped-token`, `azure-sp-secret`, `bao-approle-secret`. type: type: string enum: [ rotation-procedure ] required: true version: type: string format: semver required: true name: type: string required: true description: "Short human-readable name (1 line)." description: type: string required: true description: | What this procedure does, and which class of secrets it applies to. Included verbatim in the rotator's audit log entries. # --------------------------------------------------------------------------- # PROVIDER — which adapter implements this procedure # --------------------------------------------------------------------------- provider: type: string required: true pattern: "^[a-z][a-z0-9-]*$" description: | Identifier of the TypeScript provider adapter that implements this procedure. Maps 1:1 to a file in `services/pspace-rotator/src/providers/.ts`. The rotator validates at startup that every referenced provider has a loaded adapter; unknown providers fail loudly, never silently. examples: - "cloudflare" - "sendgrid" - "azure-sp" - "bao-approle" - "keycloak-client" - "ssh-keypair" # --------------------------------------------------------------------------- # PARENT CREDENTIAL — what the rotator needs to be able to do # --------------------------------------------------------------------------- parent_credential: type: object required: true description: | Constraints on the parent credential that secrets using this procedure must reference. The rotator's `check()` step runs at the start of every rotation to confirm the parent credential still has the required capabilities. properties: kind: type: string required: true enum: - api-token - oauth-client-credentials - service-principal - approle - keypair description: "What KIND of credential the parent must be." permissions: type: array required: true minItems: 0 items: type: string description: | Provider-specific permission strings the parent credential must hold. Example for `cloudflare`: `["zone.dns:edit"]`. Example for `azure-sp`: `["Microsoft.KeyVault/vaults/secrets/write"]`. The provider adapter's `check()` interprets these. ttl_warning_days: type: integer required: false minimum: 1 description: | When the parent credential itself is within this many days of its OWN rotation_due_at, the rotator emits a warning before using it. Default 14. # --------------------------------------------------------------------------- # GENERATE — how to mint a new credential # --------------------------------------------------------------------------- generate: type: object required: true description: | How the provider adapter mints the new credential. The (provider, kind) pair is validated at rotator startup. properties: kind: type: string required: true description: | Provider-specific generate strategy. Examples: - cloudflare: `scoped-token` - sendgrid: `api-key` - azure-sp: `client-secret`, `certificate` - bao-approle: `secret-id` - keycloak-client: `client-secret` - ssh-keypair: `ed25519`, `rsa-4096` ttl_days: type: integer required: false minimum: 1 description: | Lifetime to set on the new credential at the provider, when the provider supports it. Convention: set to `cadence_days + 10` so the credential outlives one rotation cycle (allows graceful swap + audit window). permissions: type: array required: false items: type: string description: | Provider-specific permissions to grant the new credential. For most secrets this is a SUBSET of `parent_credential.permissions` — the parent can mint scoped children but not unscope itself. labels: type: object required: false description: | Provider-specific labels/tags to attach to the new credential (e.g., Cloudflare token name, Azure app secret displayName). Convention: include `pspace:rotation_audit_id` so the credential can be traced back to the rotation that minted it. # --------------------------------------------------------------------------- # VERIFY — how to prove the new credential works # --------------------------------------------------------------------------- verify: type: object required: true description: | How the rotator proves the new credential is functional BEFORE writing it to OpenBao. A failed verify aborts the rotation and revokes the just-minted credential. properties: kind: type: string required: true description: | Verification strategy. Examples: - `api-call`: hit a provider endpoint - `bao-write-read`: write a probe value to BAO and read it back - `ssh-connect`: open an SSH session to a known host - `sign-and-verify`: sign a probe and verify the signature endpoint: type: string required: false description: "For `api-call`: relative endpoint path (`/zones`, `/v3/scopes`)." method: type: string required: false enum: [ GET, POST, PUT, DELETE, HEAD ] description: "For `api-call`: HTTP method. Default GET." expect_status: type: integer required: false description: "For `api-call`: HTTP status code that proves the credential works. Default 200." timeout_seconds: type: integer required: false minimum: 1 description: "Verify deadline. Default 30." # --------------------------------------------------------------------------- # REVOKE — how to invalidate the old credential # --------------------------------------------------------------------------- revoke: type: object required: true description: | How the rotator invalidates the OLD credential after consumers have switched to the new one. Must be IDEMPOTENT — revoking an already-revoked credential MUST succeed, not fail. properties: kind: type: string required: true description: | Revocation strategy. Examples: - `delete-token-by-id`: delete via provider API by token ID - `disable-credential`: set the credential to disabled state - `rotate-without-delete`: provider replaces atomically (no separate revoke step needed; the revoke call is a no-op confirmation) - `manual`: rotator pauses and asks the operator to confirm revocation (use only when the provider has no API) grace_period_seconds: type: integer required: false minimum: 0 description: | Wait this many seconds after the LAST consumer healthcheck passes before revoking the old credential. Provides a safety window in case a slow consumer is still using the old value. Default 30. retain_for_audit_days: type: integer required: false minimum: 0 description: | For providers that support disabled-but-not-deleted state, keep the revoked credential in disabled form for this many days before hard-deletion. Allows post-incident forensics. Default 0 (immediate hard-delete) for short-TTL credentials, 30 for platform-critical credentials by convention. # ============================================================================= # VALIDATION RULES # ============================================================================= validation: rules: - id: provider-implemented level: error description: | `provider` must match a loaded adapter in `services/pspace-rotator/src/providers/`. Validated at rotator startup; unknown providers fail the load with an explicit message. - id: kind-pair-supported level: error description: | Each of (provider, generate.kind), (provider, verify.kind), (provider, revoke.kind) must be a pair the adapter supports. Adapters expose their supported pairs via a static `supports()` method that the rotator queries on load. - id: revoke-idempotent level: error description: | Provider adapters MUST implement `revoke()` idempotently. This is a code-level invariant the unit tests enforce; this rule documents that the schema relies on the invariant.