feat(azure): sync Key Vault to AKS via External Secrets Operator - #360
Conversation
Install ESO (workload-identity to Key Vault) and a ClusterSecretStore on AKS when external_secrets_enabled is set, so Key Vault secrets sync into native k8s Secrets. team-operator is unchanged. Also fix CreateSecretIfNotExists quoting string values, and add a migration guide.
Prefix Azure site secret Key Vault names with the compound (workload) name so they follow the <compound>-<site>-<field> convention that ESO selects per site via dataFrom.find. Greenfield only; existing clusters migrate per docs/guides/external-secrets-aks.md.
For each site, create an ExternalSecret using dataFrom.find on ^<compound>-<site>- with a rewrite to strip the prefix, producing the native Secret team-operator reads. Introduce azureSiteSecretName so the Site CR's secret.vaultName and the ExternalSecret target share one source of truth (hyphen form, matching live).
workloadSecret.vaultName now uses azureWorkloadSecretName (<compound>-posit-team), matching the live cluster naming and the site secret convention. AWS naming unchanged.
dev-admin-token and dev-user-token have empty values and therefore cannot be stored in Key Vault or matched by dataFrom.find. Emit them as empty literals via target.template with mergePolicy: Merge so the synced Secret keeps the same shape as the hand-applied one.
Add tables for which Key Vault secrets PTD creates vs which must be created by hand (licenses, Workbench tokens, workload main-database-url, grafana-postgres-admin-secret, dockerhub creds), the reserved 'workload' site name, the Kubernetes Secrets that must stay out of ESO, and a post-migration cleanup checklist. Cross-reference from CONFIGURATION.md and the docs index.
|
@claude review please |
|
Claude finished @amdove's task in 1m 29s —— View job PR Review
This is a well-structured PR with a clear description and good test coverage for the new Findings1.
|
Reduce the ESO-related inline commentary from ~87 to ~39 lines, keeping only non-obvious rationale (RBAC-vs-access-policy, namespace ownership, CRD ordering, empty-value handling) and pointing at docs/guides/external-secrets-aks.md for the rest.
- EnsureWorkloadSecret now uses encodeSecretValue for consistency with CreateSecretIfNotExists - extract the always-empty site secret keys to a named var so the ExternalSecret template has one source of truth - reject a site named 'workload' when external secrets are enabled (collides with the reserved Key Vault prefix) - correct refreshInterval in the guide example to match the generated value (1h)
Owner sets an ownerReference on the target Secret, so deleting the ExternalSecret (or the ESO CRDs) would cascade-delete a live product Secret. Orphan adopts, updates and prunes identically but omits the ownerReference. Verified on a test cluster: adoption, update-on-Key-Vault-change, pruning, and Secret survival after ExternalSecret deletion.
The <compound>-<site>-<field> Key Vault naming is now used only when a cluster in the workload has external secrets enabled; otherwise bootstrap keeps the historical <site>-<field> name. This leaves vaults on unmigrated Azure workloads untouched when this change merges. Adds AnyClusterExternalSecretsEnabled and azureSiteSecretKeyVaultName with unit tests, and documents that the flag must be set before the first bootstrap run on a new workload.
|
Claude finished @amdove's task in 6m 51s —— View job PR Review
SummaryDesign is sound and the description is accurate. Five inline comments posted; two are blocking. Blocking:
Important: Nice-to-have: Confirmed correct:
|
The chart renders CRDs as templates, so an Atomic rollback of a failed upgrade deletes them, cascade-deleting every ExternalSecret and ClusterSecretStore in the cluster (observed during testing). Set crds.annotations.helm.sh/resource-policy=keep, matching the team-operator release's crd.keep. Also document running --refresh after a failed apply, since a rollback can remove resources Pulumi still believes exist.
- bootstrap: type-assert site secret field values instead of fmt.Sprintf("%v"), so a future non-string field fails loudly rather than being stored Go-formatted
- use azRoleDefID for the Key Vault Secrets User assignment, consistent with the other role assignments in this file
- document what breaks when the OIDC issuer is absent and the federated credential is skipped
- explain why the always-empty site secret key list is curated rather than derived
|
Both blocking items are resolved. 1. It is also already deployed and running from this branch on an internal Azure staging cluster — the release reports 2. The remaining items are answered inline: the Separately, one real robustness gap this review prompted me to look at: the chart renders CRDs as templates, so an |
Description
Adds External Secrets Operator (ESO) support for AKS so Azure Key Vault secrets sync into native Kubernetes Secrets instead of being applied by hand — the AKS counterpart to the AWS Secrets Store CSI driver. team-operator needs no changes: it keeps reading native Secrets by name (
SecretType: kubernetes) and is unaware of ESO.Opt-in per cluster via
external_secrets_enabled. AKS only; AWS still uses the CSI driver.Code Flow
clusters_azure.go) — per release: managed identity + federated identity credential (workload identity) with Key Vault Secrets User scoped to the workload vault; ESO Helm release + controller ServiceAccount inposit-team-system(reusing the namespace team-operator's release creates, viaDependsOn); aClusterSecretStoreusingauthType: WorkloadIdentity.dataFrom.findon^<compound>-<site>-with arewritethat strips the prefix, so keys land exactly as team-operator reads them.dev-admin-token/dev-user-tokenare always empty and can't be stored in Key Vault, so they're emitted as empty literals viatarget.template+mergePolicy: Merge.creationPolicy: Orphaninstead of ESO's defaultOwner.Ownersets anownerReference, so deleting the ExternalSecret (e.g. flipping the flag off) would garbage-collect a live product Secret.Orphanadopts, updates and prunes identically but never destroys.Atomicrollback of a failed upgrade would delete them and cascade-delete everyExternalSecretandClusterSecretStorein the cluster (observed while testing).crds.annotations."helm.sh/resource-policy": keepprevents that, matching the team-operator release'scrd.keep.bootstrapnames Azure site secrets<compound>-<site>-<field>, gated on the flag; without it the historical<site>-<field>name is kept.azure/secretstore.go) —CreateSecretIfNotExistsranjson.Marshalon strings, storing them wrapped in literal quotes. Harmless while values were only stored statically, corrupting once ESO syncs them verbatim (a quoted DB password fails auth). Strings now store verbatim; non-strings still JSON-encode.docs/guides/external-secrets-aks.md: naming, which Key Vault secrets PTD creates vs. hand-created, Secrets to keep out of ESO, lifecycle policies, migration procedure, cleanup checklist.Why the site-secret name helpers are inert
buildAzureSiteSpeccomputed<compound>-<site>.posit.team(dotted) while every live Site CR uses hyphens — alarming, but it never reaches the cluster.applySiteOverridesmerges eachsite_<name>/site.yamlover the computed spec withmergo.WithOverride, and all 10 Azure site configs explicitly setsecret,workloadSecretandmainDatabaseCredentialSecret(type: kubernetes+ hyphenatedvaultName). Confirmed empirically:sites --dry-runon an Azure workload reports no changes with either anorigin/mainor a branch binary.So
azureSiteSecretName/azureWorkloadSecretNamechange nothing today. They fix the default for a future workload shipping novaultNameoverride (previously pointing team-operator at a nonexistent Secret) and give the Site CR and ExternalSecret target one shared source of truth.Merge safety
Every change is gated or inert:
bool, defaults false).site.yamloverrides on all existing Azure workloads.Validation
Verified on an internal Azure staging cluster: store reports
Valid;find+rewritework (theKey Vault Secrets Userrole already grants thereadMetadataaction listing needs); emptytemplatevalues persist underMerge;Orphanadopts a hand-applied Secret, updates on Key Vault change, prunes sourceless keys, and survives ExternalSecret deletion (also verified against a full ESO teardown); add → update → delete Key Vault lifecycle propagates; migration reproduced the live Secret with every value hash-identical.Category of change
Checklist