setup-secret · diff

git:20260728.f0c4328 to git:20260925.7bf99b8

52 added, 101 removed. Audit A to A.

---
name: setup-secret
description: Secret access wiring and manifest authoring. Use when a workload needs to read a secret, the user asks to create a secret or generate secret YAML, configure a pull secret, or fix a deployment paused on a secret reference.
---
# Secret Access Setup
- > **Tool availability:** secrets are read-only through MCP on every profile — `list_resources` / `get_resource` (kind="secret") show existence and metadata, never values. No tool creates, edits, deletes, or reveals a secret: secret data and lifecycle are managed by the user (Console, CLI, Terraform, Pulumi, or the API); you draft manifests with placeholders, the user fills the values and applies. `grant_workload_secret_access` (every profile) grants a workload access — it never returns values.
-
- Secret access is the #1 thing users get wrong: a workload reads a secret only when **three** things are all in place. Miss any one and the value is silently absent at runtime — or the deployment pauses on an unresolved reference.
-
- ## The mandatory chain
+ A workload reads a secret only when three things are in place. Miss one and the value is silently absent, or the deployment pauses on the unresolved reference.
| Step | What must be true | Without it |
|---|---|---|
- | **1. Identity** | an identity exists and is linked to the workload (`spec.identityLink`) | workload has no API credential — reads nothing |
- | **2. Policy** | a policy grants that identity `reveal` on the secret | reference resolves to empty |
- | **3. Reference** | the secret is injected as `cpln://secret/NAME` (env or volume) | nothing to read |
+ | **Identity** | an identity linked to the workload (`spec.identityLink`) | the workload reads nothing |
+ | **Policy** | a policy granting that identity `reveal` on the secret | the reference resolves to empty |
+ | **Reference** | `cpln://secret/NAME` or `cpln://secret/NAME.KEY` in env or a volume | nothing to read |
- `reveal`, **not** `view` — `view` exposes only metadata. This is the single most common mistake.
+ `reveal`, not `view`: `view` shows metadata only. This is the most common mistake.
- ## Pull secrets are different — no identity/policy
+ ## Creating and rotating
- To pull images from a private registry, don't build the chain. Add the registry secret to the **GVC's** `pullSecretLinks` and every workload in that GVC can pull. Pull secrets are registry credentials — `docker`, `ecr`, or `gcp` types.
+ `mcp__cpln__create_secret` creates secrets without a value passing through the chat, and `mcp__cpln__rotate_secret` rotates them; their descriptions cover both paths. Values Control Plane generates and values the user enters belong in separate secrets. Confirm a secret exists (`get_resource`, kind "secret") before anything references it.
- ```yaml
- kind: gvc
- spec:
- pullSecretLinks:
- - //secret/my-registry
- ```
+ ## Wiring access
- ## Authoring a secret manifest — the user applies it
+ - **`deploy_app`** wires all three steps for the references in its env.
+ - **`grant_workload_secret_access`** (`gvc`, `workloadName`, `secretName`) creates the identity if missing (default `{gvc}-{workloadName}`), links it, and creates or updates a `reveal` policy (default `{gvc}-{workloadName}-secrets-policy`). It does not add the reference. The workload must exist first: create it (its deployment pauses on the reference), then grant, and it resumes.
+ - **By hand:** `create_identity`, `update_workload` to set `spec.identityLink`, then `create_policy` with target kind `secret` and a `reveal` binding for the identity (`access-control` skill).
+ - Identities are GVC-scoped: one per workload, shareable within a GVC, never across GVCs.
- Drafting the manifest is an expected part of the job — users ask for a scaffold, fill in the real values themselves, and apply it. Generate the YAML with UPPERCASE placeholders, then always hand back the next steps:
+ ## References
- 1. **Fill in the placeholders locally** — the value never enters the chat.
- 2. **Apply it**: `cpln apply -f secret.yaml --org ORG`, the Console's **cpln apply** button (paste the YAML), or the per-type CLI command that reads the value from a file (`cpln secret create-docker --name NAME --file config.json`).
- 3. **Treat the filled file as a live credential** — keep it out of git and delete it after applying.
- 4. **Say when it's done** — verify with `get_resource` (kind="secret") and continue with the access chain below.
+ | Type | Keys | Example |
+ |---|---|---|
+ | opaque | `payload` | `cpln://secret/api-key.payload` |
+ | userpass | `username`, `password` | `cpln://secret/creds.password` |
+ | tls | `key`, `cert`, `chain` | `cpln://secret/web-tls.cert` |
+ | keypair | `secretKey`, `publicKey`, `passphrase` | `cpln://secret/deploy-key.secretKey` |
+ | dictionary | user-defined | `cpln://secret/cfg.DB_HOST` |
+ | aws / ecr | `accessKey`, `secretKey`, `roleArn`, `externalId` | `cpln://secret/aws.accessKey` |
- Never ask for the real value in chat, and never apply the manifest yourself.
+ Inject as an env var or a volume (`{ uri: "cpln://secret/NAME", path: "/secrets/x" }`). Secret mounts are read-only, at most 15 volumes per container, and `/dev`, `/dev/log`, `/tmp`, `/var`, and `/var/log` are rejected. A workload reads a secret when it starts, so a value changed any way other than `rotate_secret` needs a redeploy.
- `data` has a fixed shape per `type`, validated by the backend on create. The trap: **for `docker`, `gcp`, and `azure-sdk`, `data` is a single JSON string** (a `>-` block scalar in YAML), never a YAML mapping — an object is rejected.
+ ## Pull secrets need no identity or policy
+ To pull from a private registry, add a secret of type `docker`, `ecr`, or `gcp` to the GVC's `spec.pullSecretLinks`, and every workload in that GVC can pull. The user enters the registry credentials through the Console link from `create_secret`.
+
+ ## Manifests for CLI and GitOps users
+
+ Users who keep secrets as code may want a manifest instead of the Console link. Write it with UPPERCASE placeholders, then tell them to fill it in locally, apply it with `cpln apply -f secret.yaml --org ORG` or the Console's cpln apply button, keep the filled file out of git, and delete it after. Never ask for the real value, and never apply the file yourself.
+
+ `data` has a fixed shape per type. The trap: for `docker`, `gcp`, and `azure-sdk`, `data` is one JSON string, never a YAML mapping.
+
```yaml
kind: secret
name: my-registry
type: docker
data: >-
{"auths":{"REGISTRY_HOST":{"username":"USERNAME","password":"PASSWORD"}}}
```
- | `type` | `data` | Backend validation |
+ | `type` | `data` | Validation |
|---|---|---|
- | `opaque` | object `{payload, encoding?}` | `payload` valid base64 when `encoding: base64` (default `plain`) |
+ | `opaque` | `{payload, encoding?}` | `payload` valid base64 when `encoding: base64` (default `plain`) |
| `dictionary` | object of string values | keys match `[-._a-zA-Z0-9]+` |
- | `userpass` | object `{username, password, encoding?}` | — |
- | `tls` | object `{cert, key?, chain?}` | `cert` and `key` must be valid PEM |
- | `keypair` | object `{secretKey, publicKey?, passphrase?}` | `secretKey` a valid PEM private key |
- | `aws` | object `{accessKey, secretKey, roleArn?, externalId?}` | `accessKey` starts `AKIA`, `roleArn` starts `arn:` |
- | `ecr` | aws fields + `repos` (1–20) | each `ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com[/REPO]` |
- | `azure-connector` | object `{url, code}` | `url` must be https |
- | `nats-account` | object `{accountId, privateKey}` | `accountId` a public nkey (`A…`), `privateKey` a seed (`SA…`) |
- | `docker` | **JSON string** | must parse with an `auths` object keyed by registry host, at least one entry |
- | `gcp` | **JSON string** | full service-account key: `type`, `project_id`, `private_key_id`, `private_key`, `client_email`, `client_id`, `auth_uri`, `token_uri`, `auth_provider_x509_cert_url`, `client_x509_cert_url` |
- | `azure-sdk` | **JSON string** | `subscriptionId` / `tenantId` / `clientId` (UUIDs) plus `clientSecret` |
-
- `mcp__cpln__get_resource_schema` (kind="secret") returns the apply schema and REST endpoints.
-
- ## Workflow
-
- ### 1 — Identify the secret
-
- The secret must already exist — the user creates and rotates it through any Control Plane surface: Console, CLI (value-in-a-file, never an inline flag), Terraform, Pulumi, or the API. Confirm it exists with `list_resources` or `get_resource` (kind="secret") before wiring anything; never ask for the value in chat and never invent a placeholder. If it does not exist yet, author the manifest (section above) and wait until the user has applied it.
-
- ### 2 — Grant the workload access
-
- **Preferred — one call.** `grant_workload_secret_access` (`gvc`, `workloadName`, `secretName`) creates the identity if missing (default `{gvc}-{workloadName}`), links it to the workload, and creates/updates a `reveal` policy (default `{gvc}-{workloadName}-secrets-policy`). It never returns secret values, and it does **not** inject the reference — step 3 still applies.
-
- **Manual alternative** (granular control): `create_identity` → `update_workload` to set `spec.identityLink` → `create_policy` (targetKind `secret`, a `reveal` binding naming the identity). Policy shape lives in **access-control**.
-
- **Ordering matters.** The workload must already exist. For a new workload that references a secret: `create_workload` first (its deployment pauses on the unresolved reference), then grant — the deployment resumes.
-
- Identities are **GVC-scoped**: one per workload, shareable across workloads in the same GVC, never across GVCs.
-
- ### 3 — Inject the reference
-
- `update_workload` (read current state with `get_resource` first) to add `cpln://secret/NAME` — the whole secret — or `cpln://secret/NAME.KEY` for one property:
-
- | Type | Keys | Example |
- |---|---|---|
- | opaque | `payload` | `cpln://secret/api-key.payload` |
- | userpass | `username`, `password` | `cpln://secret/creds.password` |
- | tls | `key`, `cert`, `chain` | `cpln://secret/web-tls.cert` |
- | dictionary | user-defined | `cpln://secret/cfg.DB_HOST` |
- | aws / ecr | `accessKey`, `secretKey`, `roleArn` | `cpln://secret/aws.accessKey` |
-
- Inject as an **env var** or a **volume mount** (`{ uri: "cpln://secret/NAME", path: "/secrets/x" }`). Mounts are read-only (except Azure Files), max **15** per container, and these knative-reserved paths are rejected: `/dev`, `/dev/log`, `/tmp`, `/var`, `/var/log`.
-
- ### 4 — Verify and redeploy
+ | `userpass` | `{username, password, encoding?}` | none |
+ | `tls` | `{cert, key?, chain?}` | `cert` and `key` valid PEM |
+ | `keypair` | `{secretKey, publicKey?, passphrase?}` | `secretKey` a valid PEM private key |
+ | `aws` | `{accessKey, secretKey, roleArn?, externalId?}` | `accessKey` starts with `AKIA`, `roleArn` with `arn:` |
+ | `ecr` | aws fields plus `repos` (1 to 20) | each `ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com[/REPO]` |
+ | `azure-connector` | `{url, code}` | `url` is https |
+ | `nats-account` | `{accountId, privateKey}` | `accountId` a public nkey (`A…`), `privateKey` a seed (`SA…`) |
+ | `docker` | **JSON string** | parses with an `auths` object keyed by registry host |
+ | `gcp` | **JSON string** | a full service-account key: `type`, `project_id`, `private_key_id`, `private_key`, `client_email`, `client_id`, `auth_uri`, `token_uri`, `auth_provider_x509_cert_url`, `client_x509_cert_url` |
+ | `azure-sdk` | **JSON string** | `subscriptionId`, `tenantId`, `clientId` (UUIDs) and `clientSecret` |
- - `get_resource` (kind="workload") → `spec.identityLink` is set and the env/volume reference reads `cpln://secret/…`.
- - `get_resource` (kind="policy") → the binding grants `reveal` to that identity.
- - Updating a workload spec redeploys automatically; via CLI use `cpln apply --ready` to block until healthy. **A rotated secret value needs a redeploy** — running replicas keep the old value until then.
+ `get_resource_schema` (kind "secret") has the apply schema and REST endpoints.
- ## Quick reference — MCP tools
+ ## Verify
- | Tool | Purpose |
- |---|---|
- | `grant_workload_secret_access` | Composite — identity + `reveal` policy + link, in one call |
- | `create_identity` / `create_policy` | Build the access chain manually (granular control) |
- | `update_workload` | Set `identityLink`; inject the env / volume reference |
- | `list_resources` / `get_resource` (kind="secret") | Confirm a secret exists / read its metadata |
+ - `get_resource` (kind "workload"): `spec.identityLink` is set and the env or volume reads `cpln://secret/…`.
+ - `get_resource` (kind "policy"): the binding grants `reveal` to that identity.
## Common mistakes
- - **Object `data` on a docker / gcp / azure-sdk secret** — those three types take one JSON string; a YAML mapping fails validation.
- - **No identity** — a workload with no `identityLink` reads no secrets.
- - **`view` instead of `reveal`** — metadata only, no value.
- - **Bad reference** — must be `cpln://secret/NAME`, not the bare name.
- - **Granting before the workload exists** — the workload comes first.
- - **Sharing an identity across GVCs** — they are GVC-scoped.
- - **Over-engineering pull secrets** — registries need only `pullSecretLinks`, no identity/policy.
- - **Skipping the redeploy after rotation** — running replicas keep the old value.
-
- ## Related skills
-
- | Need | Skill |
- |---|---|
- | Policy shape, permissions, principals | `access-control` |
- | Workload identities, cloud / private-network access | `native-networking` |
- | Workload spec, deploy, env vars | `workload` |
-
- ## Documentation
-
- - [Secret Reference](https://docs.controlplane.com/reference/secret.md)
+ - A YAML mapping as `data` on a docker, gcp, or azure-sdk secret.
+ - No `identityLink` on the workload, or `view` instead of `reveal`.
+ - The bare secret name instead of `cpln://secret/NAME`.
+ - Granting before the workload exists, or sharing an identity across GVCs.
+ - An identity and policy for a pull secret, which needs only `pullSecretLinks`.