Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions docs/api-key-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# API Key Terraform State Design

## Decision

The masked `kernel_api_key` data source is safe to implement after a tagged
Kernel Go SDK exposes the API's exact-name list filter.

The `kernel_api_key` resource is deferred. Its Terraform state shape is
accepted below, but Create and Rotate must not ship until the API provides
replayable idempotency and lets the provider identify or reject rotation of the
credential authenticating the current request. The API must also expose that
credential's effective project scope so provider-default fallback can settle
predictably into Terraform state.

## First Principles

An API key is durable infrastructure, but its plaintext is not durable API
metadata. Kernel returns plaintext exactly once after Create or Rotate. Later
reads return a masked key and stable metadata.

Terraform can manage that lifecycle only when:

- retrying an uncertain Create or Rotate returns the same key and plaintext;
- refresh never replaces plaintext state with a masked value;
- import does not pretend it can recover plaintext;
- rotation is an explicit configuration change;
- the provider cannot rotate away the credential it is using unknowingly; and
- project scope follows the provider's documented precedence and settles into
durable state.

## Current Kernel Contract

Kernel Go SDK v0.76.0, the latest tagged release at the time of this decision,
exposes Create, Get, Update, List, Delete, and Rotate. The durable API contract
has these properties:

- Create returns a new canonical ID, plaintext key, masked key, name, creator,
timestamps, and optional project metadata.
- Get and List return masked metadata only.
- Update changes only the name.
- Delete soft-deletes a key and rejects deletion of the current key with the
coded `cannot_delete_current_key` error.
- Rotate creates a new canonical key, returns its plaintext once, and schedules
the old key to expire at the earlier of its existing expiration and the grace
deadline. Rotation never extends an old key's life.
- Rotation does not soft-delete the old key. Its expired record remains in
ordinary active listings because that status currently means non-deleted.
- Names are not unique.
- Project-scoped callers can manage keys only in their own project and cannot
create or access organization-wide keys.
- A deleted key is hidden from ordinary Get and a repeated Delete returns
`not_found`.

The API supports an exact-name List filter. It is generated in the pending Go
SDK v0.77.0 release, so the provider must wait for that tag rather than adding
direct HTTP or a second client.

## Future Resource State

The resource should use this state model once the API blockers are resolved:

| Attribute | Terraform behavior |
| --- | --- |
| `id` | Computed canonical API-key ID. Rotate replaces it with the new key's ID. |
| `name` | Required durable name. Updated in place. Names are labels, not identity. |
| `project_id` | Optional scope override. It defaults to the provider-level `project_id`, then the authenticated key's project binding. Null after resolution creates an organization-wide key. Changing resolved scope replaces the key. |
| `days_to_expire` | Optional lifetime from 1 through 3650 days on Create. When omitted on a new key, the key does not expire. A later finite change is valid only with a simultaneous `rotation_keeper` change; because v1 uses the seven-day default grace, the changed lifetime must be 8 through 3650 days. Removing a known finite value is rejected because Rotate cannot produce a never-expiring replacement. After metadata-only import, omission means the original relative lifetime is unmanaged. |
| `rotation_keeper` | Optional opaque string. Only a change after Create triggers Rotate. Its value is provider state, not remote metadata. |
Comment thread
cursor[bot] marked this conversation as resolved.
| `key` | Computed, sensitive plaintext returned by Create or Rotate. Read preserves the prior value; import leaves it null. |
| `masked_key` | Computed durable masked value from Get. It never substitutes for `key`. |
| `created_at` | Computed creation timestamp. |
| `expires_at` | Computed nullable expiration timestamp. |
| `created_by` | Computed creator metadata. |
| `project_name` | Computed nullable project metadata. |

`key` being sensitive controls display, not storage encryption. The plaintext
remains in Terraform state after Create or Rotate so downstream configuration
can use it. Users must use an encrypted remote backend with restricted state
access. Removing the value on the next Read would make dependent configuration
unstable and would not erase it from state history.

`days_to_expire` records create-time intent; Kernel reads expose the resulting
absolute `expires_at`, not the original relative input. Import therefore cannot
reconstruct that input and must not guess it from timestamp subtraction.

Project resolution is resource override, provider default, authenticated-key
binding, then organization-wide when no project applies. State stores the
resolved project ID. The current API requires a project-scoped caller to send
`project_id` but does not expose that caller's canonical scope to this provider.
Until current-key metadata closes that gap, callers must configure either the
resource or provider project explicitly.

## Lifecycle

Create sends explicit `name`, resolved `project_id`, and `days_to_expire`
values. It disables automatic mutation retries. On success it stores the
canonical ID and plaintext before any later operation can fail.

Read uses masked Get metadata. It refreshes readable fields while preserving
the prior plaintext, create-time lifetime, and rotation keeper. A coded
`not_found` response removes the resource from state. Expiration alone does not
remove the durable key record.

Update patches a changed name before Rotate so the replacement key copies the
new name. A changed `rotation_keeper` then calls Rotate and stores the returned
new ID and plaintext immediately. If `days_to_expire` changes in the same plan,
Rotate uses that new lifetime; changing the lifetime without changing the
keeper is a plan error. The old key is no longer owned by the Terraform
resource. Kernel expires it by the grace deadline, sooner when its existing
expiration is earlier, and immediately when it is already expired. Kernel
retains the non-deleted historical record; deleting the resource later deletes
only the replacement key. Rotation uses the API's default grace and otherwise
preserves the old key's original lifetime for the replacement. Custom grace
controls are deferred until a concrete Terraform workflow requires them.

## Null, Empty, And Defaults

- `name` must contain 1 through 255 UTF-8 bytes and must equal its trimmed value.
Empty, whitespace-only, and leading/trailing-whitespace values are invalid so
API normalization cannot create a perpetual diff.
- `project_id` may be omitted for scope resolution, but an explicit empty string
is invalid. A resolved null value means organization-wide.
- `days_to_expire` omitted or null means no expiry for a newly created key. On
Rotate, omission asks Kernel to preserve the old key's original lifetime. A
changed rotation lifetime must be at least eight days so the replacement
outlives the seven-day default grace window; this does not restrict a new
key's initial 1-3650-day lifetime. Removing a known finite value is a plan
error because the API treats rotation null as preserve, not never-expiring.
Moving to no expiry requires creating a separate key and migrating consumers
before removing the old resource.
- `rotation_keeper` may be omitted. When configured it must be non-empty. Create
records its initial value without an extra Rotate because Create already
issued fresh plaintext. Import leaves it null; setting it after import is an
explicit first rotation, and every later value change rotates again.
- The API's omitted rotation grace defaults to seven days. The v1 resource does
not expose a second grace control.
- Nullable read fields remain Terraform null. Zero times and empty strings must
not be used as substitutes for API null values.

Delete treats `not_found` as success. The coded self-delete response is a
diagnostic and retains the resource ID so a different provider credential can
retry. Terraform must not hide that failure or remove state.

Import accepts only the canonical API-key ID. Read then imports masked metadata.
The plaintext `key`, original `days_to_expire`, and prior rotation keeper remain
unknown or unset. Import documentation must call this metadata-only import.

## Self-Use Rule

A provider instance must be authenticated by a separate administrative key
from every `kernel_api_key` resource it manages. Provider configuration cannot
depend safely on a key created by that same provider instance.

The API already prevents self-delete, but Rotate currently allows the current
key to rotate and schedules it to expire. The provider cannot derive the
authenticated key's canonical ID from its plaintext configuration or masked
metadata. Documentation alone is not a sufficient guard for an operation that
can make the next Terraform run unable to authenticate.

Before the resource ships, Kernel must expose request-context metadata that
identifies the current key and its canonical project scope, or reject
self-rotation with a stable coded error and separately expose the effective
project binding. The provider will use that signal as an internal safety check;
current-key identity does not belong in durable Terraform state.

## Idempotency Gate

Create and Rotate have no idempotency key. A connection failure after the API
commits but before Terraform receives the response loses the only plaintext
response. Create can leave an untracked credential. Rotate can additionally
schedule the old credential to expire, and a retry can create another key.

Before the resource ships, both operations must accept an idempotency key and
replay the same canonical ID and plaintext response for the same request. It is
not enough to deduplicate by name because names are intentionally non-unique.
The implementation design must prove that its request token survives a failed
apply and process restart; API support without a stable Terraform-side token is
not sufficient. Generic SDK mutation retries remain disabled outside that
replay contract.

## Masked Data Source

The data source is independent of plaintext lifecycle and may ship first. It
should accept exactly one of canonical `id` or exact `name`, scan all pages for
name lookup, deduplicate by ID, and diagnose zero or multiple non-deleted
matches. Expired-but-not-deleted keys remain visible because they are durable
records under the current API status definition. The API's name filter follows
the production database's case- and accent-insensitive collation; the provider
must post-filter returned names byte-for-byte so Terraform's exact selector has
stable semantics. Rotation retains an old row with the same name, so name
lookup becomes ambiguous after rotation and callers must use the canonical ID.
It may expose ID, name, masked key, creator, creation/expiration timestamps, and
nullable project metadata. It must never expose plaintext, deleted audit rows,
or provider-authentication identity.

## Unblocking Checklist

- Tag an SDK release containing the exact API-key name filter.
- Add replayable idempotency for API-key Create and Rotate.
- Add a stable current-key/project-scope signal or coded self-rotation
rejection plus effective-scope metadata.
- Verify nullable SDK response fields against API `null` responses.
- Acceptance-test ASCII and multibyte name boundaries against the API's
byte-count validation.
- Acceptance-test a short initial lifetime, unchanged short-lifetime rotation,
rejection of a changed 1-7-day rotation lifetime, and rejection of a known
finite-to-null transition.
- Acceptance-test Create, Read, rename, explicit rotation, metadata-only
import, first rotation after import, project scope, retained rotation history,
near-expiry and already-expired rotation, not-found deletion, and self-use
diagnostics.
- Repeat the sensitive-state and release review before registering the resource.
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ Late or conditional v1 work:

Blocked candidates must remain unimplemented until the API and a tagged SDK expose the required durable contract. Provider code must not guess missing semantics, patch generated SDK code, or add a fallback HTTP client to bypass the durable client module.

The accepted plaintext, import, rotation, and self-use model for API keys is
defined in [API Key Terraform State Design](api-key-state.md). The masked data
source may proceed after the exact-name SDK surface is tagged. The resource
remains blocked on replayable Create/Rotate idempotency and a current-key
rotation guard with effective authenticated project-scope metadata.

`kernel_deployment` remains core v1 scope but is currently blocked until a
tagged SDK exposes source provenance, the API supports deterministic durable
readback, and the write-only environment/token plus metadata-only import design
Expand Down
Loading