Skip to content
🛡️ adminadmin@example.com · Domain admin in Acme Corp

Chapter 8 — The provisioning catalog

Three things before anything can be provisioned

Blueprint — the recipe. What a bucket or a node is.

Cloud — the provider account to build it in.

Credential — the vaulted secret that pays for it.

This chapter builds all three, and it takes two different hats to do it.

Register a catalog — and verify who signed it

bash
CATALOG_ID=$(plexctl blueprint catalog add \
  --name "Official Blueprint catalog" \
  --ref ghcr.io/plexsphere/blueprints:v1 \
  --domain-id "$DOMAIN_ID" \
  --verify-identity '^https://github\.com/plexsphere/blueprints/\.github/workflows/release\.yml@refs/tags/v.*$' \
  --verify-issuer https://token.actions.githubusercontent.com \
  --output json | jq -r '.id')
echo "$CATALOG_ID"

The two --verify-* flags are the point.

A catalog is an OCI artifact, and plexsphere will not import from it unless the signature chains to the identity you named. Not "we pulled it over TLS" — "this was built by that workflow, on a tag, in that repo".

Browse what is in it

bash
plexctl blueprint catalog browse "$CATALOG_ID"

Browsing reads the source. Nothing has entered your Domain yet — this is the shop window, not the shelf.

Import one

bash
plexctl blueprint import "$CATALOG_ID" aws-s3-bucket
bash
plexctl blueprint list --all

Import is deliberate and per-Blueprint. Registering a catalog does not dump its whole contents into your Domain — you choose what your tenant carries.

Capture the version id

bash
BLUEPRINT_ID=$(plexctl blueprint list --all --output json \
  | jq -r '.items[] | select(.slug == "aws-s3-bucket") | .id')
BLUEPRINT_VERSION_ID=$(plexctl blueprint get "$BLUEPRINT_ID" --output json \
  | jq -r '.versions[] | select(.version == "v1alpha1") | .id')
echo "$BLUEPRINT_VERSION_ID"

Resources are provisioned from a version, not from a Blueprint.

A Blueprint that ships a fix does not silently change what your existing Resources were built from.

👤 membermember@example.com · ordinary member of Acme Corp

Optional — an ordinary member can read it

bash
plexctl login --profile-name member --domain-id "$DOMAIN_ID"
bash
plexctl blueprint catalog list --profile member --output json \
  | jq -r '.items[] | {id, name, domain_id}'

member@example.com holds no authoring grant and sees the catalog anyway. Authoring and reading are separate rights.

Presenter note: costs a browser sign-in for one point. Skip when short on time.

🏗️ operatoroperator@example.com · platform grant, no Domain

Change hats

bash
plexctl login --profile-name operator --platform

Everything so far was the tenant hat — Domain admin in Acme Corp.

Clouds and Credentials are not tenant objects. They are catalog-global, and they gate on a platform grant that no Domain admin holds.

A genuinely different principal

bash
plexctl whoami --profile operator
text
PRINCIPAL  SCOPE     SUBJECT                               DOMAIN  ACR  AMR
user       platform  019100c0-0d0c-7000-8000-000000000002  -       -    -

SCOPE = platform. DOMAIN = -.

This session belongs to no tenant at all. Compare it with the whoami from chapter 2, which named Acme Corp in that column.

This is not a bigger admin. It is a different axis of authority.

Create the Cloud

bash
CLOUD_ID=$(plexctl cloud create \
  --slug demo-cloud \
  --display-name "Demo Cloud" \
  --provider aws \
  --external-id 000000000000 \
  --endpoint '{"region":"us-east-1","partition":"aws"}' \
  --region-defaults '{"default_region":"us-east-1"}' \
  --provider-package xpkg.upbound.io/upbound/provider-aws-s3@v2.6.1@sha256:7e00c910bdbf39b2dcc8d7632153b6be1c31d202ede0f2d12ade053eb77aae96 \
  --provider-config-api-version aws.m.upbound.io/v1beta1 \
  --profile operator --output json | jq -r '.id')
echo "$CLOUD_ID"

An AWS account, as far as the platform is concerned. Locally it is backed by an emulator, so there is no real account, no real token, and no bill.

--provider-package declares one Crossplane provider package as <source>@<version>, and it is repeatable: a Cloud can name several. The one above is the exact package the local stack runs.

Read it back

bash
plexctl cloud list --all --profile operator --output json \
  | jq '.items[] | {id, slug, provider, external_id}'
json
{
  "id": "019fb9c7-97ba-7d4a-8867-29e77d21c5e6",
  "slug": "demo-cloud",
  "provider": "aws",
  "external_id": "000000000000"
}

Creating a Cloud granted its creator manage on it, which folds into the observe relation reads gate on. The list is relation-filtered, same as every other list in this demo.

Vault the Credential

bash
printf '{"access_key_id":"AKIAEXAMPLE","secret_access_key":"example"}' > /tmp/demo-credential.json
CREDENTIAL_ID=$(plexctl cloud credential create \
  --cloud-id "$CLOUD_ID" \
  --display-name demo-credential \
  --payload-file /tmp/demo-credential.json \
  --profile operator --output json | jq -r '.id')
echo "$CREDENTIAL_ID"

The payload goes in and never comes back out. It is accepted inbound only; the material lives in the secrets engine.

What a read gives you

bash
plexctl cloud credential list --cloud-id "$CLOUD_ID" --profile operator --output json
json
{
  "items": [
    {
      "cloud_id": "019fb9c7-97ba-7d4a-8867-29e77d21c5e6",
      "created_at": "2026-06-24T18:00:36.153507Z",
      "display_name": "demo-credential",
      "expires_at": "2026-06-25T18:00:36.146475Z",
      "id": "019fb9c7-fe03-7dcf-b671-382f2b307d6f",
      "status": "active",
      "updated_at": "2026-06-24T18:00:36.153507Z",
      "version": 1
    }
  ]
}

Lifecycle metadata. No access_key_id, no secret_access_key.

There is no read surface anywhere that returns them — not for the operator who created it, not for anyone.