Skip to content

Issue a Cloud Credential ​

A Cloud Credential is a brokered secret owned by a Cloud. Issuing one writes the secret material into OpenBao KV-v2, records a broker row, and returns the credential's lifecycle metadata. The secret payload is accepted inbound only — no read surface ever returns it again, so keep your own copy if you need it elsewhere.

Prerequisites ​

  • An authenticated session — see Log in with plexctl.
  • A Cloud UUID. Issuing a credential needs the manage relation on that Cloud — the Cloud is the residency pivot the issue gate authorises against. A caller without manage is refused with 403 before the payload is ever read.
  • The secret bytes to store, in a file (or piped on stdin).

Steps ​

Write the provider's credentials document ​

The payload is the Crossplane provider's own credentials document. The provisioning broker copies it verbatim into the key credentials of the Secret the Cloud's ProviderConfig reads in each Project namespace, so write it in the format the provider expects. For provider-openstack it is a JSON object with the Keystone endpoint, the region and an application credential scoped to the one OpenStack project the machines land in:

json
{
  "auth_url": "https://keystone.example.com/v3",
  "region": "RegionOne",
  "application_credential_id": "<application-credential-id>",
  "application_credential_secret": "<application-credential-secret>"
}

Keep the file out of version control and remove it once the credential is issued.

Issue with plexctl ​

shell
export PLEXSPHERE_URL="${PLEXSPHERE_URL:-https://localhost:8080}"

plexctl cloud credential create \
  --server       "${PLEXSPHERE_URL}" \
  --cloud-id     <cloud-uuid> \
  --display-name "Acme AWS provisioner secret" \
  --payload-file ./aws-provisioner.json \
  --key-value    role_arn=arn:aws:iam::123456789012:role/acme-provisioner

The command prints the metadata-only projection of the new credential (id, owning Cloud, display name, version 1, derived status active, and the lifecycle timestamps). Repeat --key-value for each key=value pair you want stored alongside the payload.

To avoid writing the secret to a file, pipe it on stdin with --payload-file -:

shell
printf '%s' "${SECRET_JSON}" | plexctl cloud credential create \
  --server       "${PLEXSPHERE_URL}" \
  --cloud-id     <cloud-uuid> \
  --display-name "Acme AWS provisioner secret" \
  --payload-file -

Issue with the HTTP API ​

The payload field is base64-encoded secret bytes; key_values is an optional flat map. Both are accepted inbound only.

shell
curl --silent --show-error --fail-with-body -X POST \
  -H "Authorization: Bearer ${TOKEN}" -H 'content-type: application/json' \
  "${PLEXSPHERE_URL}/v1/clouds/<cloud-uuid>/cloud-credentials" -d '{
  "display_name": "Acme AWS provisioner secret",
  "payload": "'"$(base64 < ./aws-provisioner.json | tr -d '\n')"'",
  "key_values": {"role_arn": "arn:aws:iam::123456789012:role/acme-provisioner"}
}'

A 201 carries the new credential's metadata and a Location header holding its canonical read URL (/v1/cloud-credentials/<credential-uuid>).

Verify ​

Read the credential back — the projection is metadata-only and never includes the payload:

shell
plexctl cloud credential get <credential-uuid> --server "${PLEXSPHERE_URL}"

Or list every credential the Cloud owns:

shell
plexctl cloud credential list --cloud-id <cloud-uuid> --server "${PLEXSPHERE_URL}"

Troubleshooting ​

  • 403 — the caller lacks cloud#manage on the parent Cloud, or the Cloud UUID is wrong. Issuing under an unknown Cloud fails the manage check first, so a missing Cloud also surfaces as 403.
  • 400 invalid_display_name — display_name was empty or whitespace-only.
  • 400 invalid_payload — the payload decoded to zero bytes (an empty file or empty stdin).
  • 413 request_body_too_large — the request exceeded the 8 KiB body ceiling. Large secrets should be referenced indirectly, not inlined.
  • 503 openbao_unavailable — OpenBao, where the credential is stored, is sealed or unreachable. Nothing was stored, so retry once OpenBao answers again; the API log carries the cause under the response's correlation_id.