Required step in the main walkthrough (its step 7) — this
tutorial has no hand-filled clouds.yaml with a static, non-expiring
OpenStack password anywhere ("one OpenStack user account per cluster,
password that never expires" is exactly the pattern a real platform
shouldn't do long-term). Every OpenStack credential the tutorial uses is
minted on demand, short-lived, by OpenBao + VEXXHOST's
vault-plugin-secrets-openstack,
delivered via External Secrets Operator's
VaultDynamicSecret generator, consumed by CAPO's
OpenStackClusterIdentity.
One narrowly-scoped static credential remains at the root (OpenBao's own
root identity) — everything downstream is ephemeral.
See least-privilege-openstack-credentials.md for an alternative that keeps a static root credential instead of OpenBao.
-
Create one narrowly-scoped project + root user in DevStack — this is the one static credential left in the whole chain, deliberately scoped to nothing beyond what it needs:
source /opt/stack/devstack/openrc admin admin # on the devstack VM openstack project create capo-poc openstack user create vault-broker --password '<pick one>' --project capo-poc openstack role add --user vault-broker --project capo-poc member
-
Deploy OpenBao and the OpenStack secrets engine:
kubectl apply -f openbao/ kubectl -n openbao rollout status deploy/openbao --timeout=120s OS_AUTH_URL="http://<devstack-ip>/identity/v3" \ VAULT_BROKER_USER_ID="$(openstack user show vault-broker -f value -c id)" \ VAULT_BROKER_PASSWORD='<the password from step 1>' \ CAPO_POC_PROJECT_ID="$(openstack project show capo-poc -f value -c id)" \ MEMBER_ROLE_ID="$(openstack role show member -f value -c id)" \ ./openbao/setup.sh
Sanity check it mints a real, working credential before moving on:
POD=$(kubectl -n openbao get pod -l app=openbao -o jsonpath='{.items[0].metadata.name}') kubectl -n openbao exec "$POD" -- env BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN="$(cat /tmp/openbao-root-token.txt)" \ bao read openstack/creds/capo-poc-member
-
Install External Secrets Operator:
helm repo add external-secrets https://charts.external-secrets.io helm upgrade --install external-secrets external-secrets/external-secrets \ -n external-secrets --create-namespace --set installCRDs=true
-
Optional sanity check — apply the consumer side — generator,
ExternalSecret,OpenStackClusterIdentity, and a minimal infra-onlyOpenStackCluster/Cluster(noMachineDeployment/control plane on purpose — this proves the credential path, not a bootable node). Skip this if you're heading straight to the real target, the Talos cluster in the main walkthrough's step 9 — that's the same proof, on real infrastructure:kubectl apply -f capo-poc/ kubectl -n capo-poc get externalsecret capo-poc-clouds # STATUS: SecretSynced kubectl -n capo-poc get openstackcluster capo-poc -o wide # READY: true
capo-controllerreconciles the network/subnet/router/security-group using a credential that, at the time it's used, is seconds old. Watch it rotate:watch kubectl -n capo-poc get secret capo-poc-clouds -o jsonpath='{.data.clouds\.yaml}' \ -o go-template='{{index .data "clouds.yaml" | base64decode}}'
The
application_credential_idembedded in the Secret changes everyrefreshInterval(60s incapo-poc/01-external-secret.yaml, comfortably under the 300s lease TTL inopenbao/setup.sh); old credentials disappear fromopenstack application credential list --user vault-brokeronce their lease expires — Vault/OpenBao revokes them in Keystone, not just locally.
Gotcha if apiServerLoadBalancer.enabled: true: capo-poc's
infra-only OpenStackCluster never creates an Octavia load balancer, so it
never noticed this — but capo-controller's reconcile holds one
authenticated client for the whole reconcile, including its internal LB
active-wait poll (~2min observed on this DevStack). The original 120s lease
TTL in openbao/setup.sh was too close to that window — Vault could revoke
the credential mid-poll, failing with a confusing 404 Could not find Application Credential instead of a clean retry. Bumped to 300s for this
reason; if your Octavia is slower, go higher. This applies to
talos-devstack01/ too (also apiServerLoadBalancer.enabled: true).
Next: back to the main walkthrough, step 8, to get a Talos node image and apply the real cluster.