make operator own AppRole secret - #55
Merged
Merged
Conversation
Signed-off-by: Travis Cotton <trcotton@lanl.gov>
ensureAppRoleSecretID now takes the live RoleID and binds the generated SecretID to it via a new annotation openchami.org/vault-approle-role-id. The regeneration decision became three-way (internal/reconcilers/vault.go): - Absent / empty id → generate. - Non-empty id bound to the current RoleID → preserve (unchanged normal-reconcile behavior; no VSO churn). - Non-empty id bound to a different RoleID, or no binding annotation → regenerate. This handles your primary concern — the "Secret survives, Vault/AppRole recreated → new RoleID → stale 403" case now self-heals. A hand-created Secret with no annotation is treated as unbound and adopted (operator mints a SecretID of known provenance and stamps the annotation). As you noted, a manually revoked SecretID with an unchanged RoleID is not auto-detected; that would need active login-probing and is left as a separate concern. 2. Ownership / lifecycle of the operator-created Secret The operator-written Secret now carries the standard managed labels (openchami.org/managed-by=operator, app.kubernetes.io/name=vault-approle), matching the tokensmith_bootstrap.go convention. On the owner reference question: I deliberately did not add one. The OpenCHAMIControlPlane is namespaced and lives in a different namespace than the openchami-<cluster> control-plane namespace, and Kubernetes forbids cross-namespace owner references. Cleanup is already handled correctly — the controller's deletion path deletes the entire openchami-<cluster> namespace (openchamicontrolplane_controller.go:285), which garbage-collects this Secret along with everything else. I documented this rationale in the function godoc. 3. Vault RBAC documentation - docs/install-production.md §6.3 (new) — documents the operator's own Vault policy, explicitly including auth/approle/role/<role>/secret-id with create/update, flagged as the new capability required by this fix. - §8 rewritten — the manual staging is now marked "the operator provisions this for you," with §8.1 kept as the legacy/air-gapped path (including how to stamp the binding annotation for a fully admin-managed SecretID). - hack/local-dev/seed-vault.sh — dropped the manual kubectl create secret step (operator does it now); prints a note instead. - test/fixtures/production-controlplane.yaml.example and internal/admin/init.go — stale "create out-of-band" comments updated. Tests Updated/added in vault_test.go (all passing): - AppRoleSecretIDIdempotent — now seeds the matching RoleID annotation; asserts preserve + no GenerateSecretID. - AppRoleSecretIDRegeneratesOnRoleIDMismatch (new) — stale RoleID → regenerate + rebind annotation. - AppRoleSecretIDAdoptsUnboundSecret (new) — no annotation → regenerate + stamp annotation. - AppRoleSecretIDBootstrap — now also asserts the annotation and managed-by label. Signed-off-by: Travis Cotton <trcotton@lanl.gov>
Signed-off-by: Travis Cotton <trcotton@lanl.gov>
travisbcotton
force-pushed
the
bug/54-vso-approle-management
branch
from
September 18, 2026 20:17
2862c7a to
37a9009
Compare
…reated OIDC client not authorized on provider Signed-off-by: Travis Cotton <trcotton@lanl.gov>
…DC provider uri Signed-off-by: Travis Cotton <trcotton@lanl.gov>
Signed-off-by: Travis Cotton <trcotton@lanl.gov>
alexlovelltroy
approved these changes
Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
When
authMethod: appRole, the operator previously owned only half of theVault AppRole credential. It created the Vault policy, the AppRole, and threaded
the RoleID into the VSO
VaultAuthCR — but it never created the KubernetesSecret (named by
spec.platform.vault.appRoleSecretRef) that VSO reads theSecretID from.
That split ownership left a bootstrap/lifecycle gap:
403 permission denied, so noVaultStaticSecretever materialized and the control plane stalled withBucketReady=False/DatabaseReady=False.Kubernetes Secret was lost, requiring a manual
vault write .../secret-id+kubectl create secretworkaround.This PR makes the operator own the complete AppRole credential lifecycle so
fresh installs and namespace recreation are deterministic without any
out-of-band steps.
What changed
Operator now provisions the AppRole SecretID
internal/reconcilers/vault.go— a newensureAppRoleSecretIDstep runs in theappRolebranch of the Vault reconciler, immediately afterEnsureAppRole:secret_idvia the existingvault.Client.GenerateSecretID.key
id(VSO's required key name) in the per-cluster namespace.the other VSO resources.
The RoleID continues to be supplied directly in
VaultAuth.spec.appRole.roleId,so this Secret carries only the SecretID — a single, deterministic ownership
model.
RoleID-bound regeneration (self-healing, no churn)
The stored SecretID is bound to the RoleID it was minted against via a new
annotation,
openchami.org/vault-approle-role-id. The regeneration decision isthree-way:
ididbound to the current RoleIDidbound to a different RoleID, or no binding annotationreconciles — the operator never rotates a working credential out from under
VSO.
SecretIDTTL="0"means it stays valid indefinitely.403" case now self-heals on the next reconcile.unbound and adopted: the operator mints a SecretID of known provenance and
stamps the annotation.
Ownership / cleanup of the operator-created Secret
(
openchami.org/managed-by=operator,app.kubernetes.io/name=vault-approle),matching the
tokensmith_bootstrap.goconvention.OpenCHAMIControlPlaneisnamespaced and typically lives in a different namespace than the
openchami-<cluster>control-plane namespace, and Kubernetes forbidscross-namespace owner references. Cleanup already happens correctly: on CP
deletion the controller tears down the entire
openchami-<cluster>namespace,which garbage-collects this Secret. This rationale is captured in the function
godoc.
New Vault permission requirement
The operator's Vault identity now needs
create/updateonauth/approle/role/<role>/secret-id. Documented in the install guide (seebelow).
Documentation
docs/install-production.md§6.3 (new) — the operator's own Vault policy,explicitly including the new
auth/approle/role/<role>/secret-idcapability.docs/install-production.md§8 (rewritten) — manual SecretID staging isnow "the operator provisions this for you"; §8.1 retains the legacy /
air-gapped flow, including how to stamp the binding annotation for a fully
admin-managed SecretID.
hack/local-dev/seed-vault.sh— dropped the manualkubectl create secretstep (the operator does it now); prints an explanatory note instead.
test/fixtures/production-controlplane.yaml.exampleandinternal/admin/init.go— stale "create out-of-band" comments updated.Tests
Added/updated in
internal/reconcilers/vault_test.go(all passing):TestVaultReconciler_AppRoleSecretIDBootstrap— fresh install generates theSecretID, creates the Secret with key
id, and stamps the RoleID annotation +managed-by label. Exactly one
GenerateSecretIDcall.TestVaultReconciler_AppRoleSecretIDIdempotent— a SecretID bound to thecurrent RoleID is preserved;
GenerateSecretIDnot called.TestVaultReconciler_AppRoleSecretIDRegeneratesWhenEmpty— emptyidtriggers regeneration (namespace-recreation case).
TestVaultReconciler_AppRoleSecretIDRegeneratesOnRoleIDMismatch(new) — staleRoleID binding → regenerate + rebind annotation (Vault/AppRole recreation).
TestVaultReconciler_AppRoleSecretIDAdoptsUnboundSecret(new) — unboundhand-created Secret → regenerate + stamp annotation.
TestVaultReconciler_AppRoleVaultAuthWiring— VaultAuth carries the liveRoleID and references the SecretRef by name.
make test(including the envtest controller suite) is green.Reconcile flow (issue #54's requested model)
Backward compatibility / upgrade notes
Fixes #54
Checklist
make test(or equivalent) locally and all tests passgit commit -s) with my real name and email<filename>.licensesidecarLICENSES/directoryType of Change