diff --git a/config/platform-infra/secret-plane.yaml b/config/platform-infra/secret-plane.yaml index cab4042b..5cd1af3b 100644 --- a/config/platform-infra/secret-plane.yaml +++ b/config/platform-infra/secret-plane.yaml @@ -43,6 +43,7 @@ vault: tokenLengthBytes: 32 syncProbe: secretStoreName: hwlab-secret-plane-vault + clusterSecretStoreName: hwlab-secret-plane-vault-cluster externalSecretName: hwlab-secret-plane-poc targetSecretName: hwlab-secret-plane-poc-sync refreshInterval: 15s diff --git a/scripts/src/platform-infra-secret-plane.ts b/scripts/src/platform-infra-secret-plane.ts index 44ec1a0d..44a233a7 100644 --- a/scripts/src/platform-infra-secret-plane.ts +++ b/scripts/src/platform-infra-secret-plane.ts @@ -62,6 +62,7 @@ interface VaultConfig { interface SyncProbeConfig { secretStoreName: string; + clusterSecretStoreName: string; externalSecretName: string; targetSecretName: string; refreshInterval: string; @@ -227,6 +228,7 @@ function parseSyncProbe(record: Record): SyncProbeConfig { if (!/^[A-Za-z0-9._-]+$/u.test(remoteProperty)) throw new Error(`${configLabel}.syncProbe.remoteProperty must be a simple key`); return { secretStoreName: y.kubernetesNameField(record, "secretStoreName", "syncProbe"), + clusterSecretStoreName: y.kubernetesNameField(record, "clusterSecretStoreName", "syncProbe"), externalSecretName: y.kubernetesNameField(record, "externalSecretName", "syncProbe"), targetSecretName: y.kubernetesNameField(record, "targetSecretName", "syncProbe"), refreshInterval: y.stringField(record, "refreshInterval", "syncProbe"), @@ -526,6 +528,27 @@ spec: key: ${vault.tokenSecretKey} --- apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: ${probe.clusterSecretStoreName} + labels: + app.kubernetes.io/name: ${probe.clusterSecretStoreName} + app.kubernetes.io/component: cluster-secretstore + app.kubernetes.io/part-of: platform-infra + app.kubernetes.io/managed-by: unidesk +spec: + provider: + vault: + server: "http://${vault.serviceName}.${target.namespace}.svc.cluster.local:${vault.port}" + path: ${probe.vaultMountPath} + version: v2 + auth: + tokenSecretRef: + name: ${vault.tokenSecretName} + key: ${vault.tokenSecretKey} + namespace: ${target.namespace} +--- +apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: ${probe.externalSecretName} @@ -615,7 +638,7 @@ for doc in re.split(r"^---\\s*$", text, flags=re.M): continue match = re.search(r"^\\s*kind:\\s*([A-Za-z0-9]+)\\s*$", doc, flags=re.M) kind = match.group(1) if match else "" - if kind in {"SecretStore", "ExternalSecret"}: + if kind in {"SecretStore", "ClusterSecretStore", "ExternalSecret"}: custom_docs.append(doc.strip() + "\\n") else: core_docs.append(doc.strip() + "\\n") @@ -637,7 +660,7 @@ if kubectl get crd ${secretPlane.eso.crds.map((name) => name).join(" ")} >/dev/n else custom_rc=0 custom_disposition=skipped-crds-missing - printf '%s\\n' 'SecretStore/ExternalSecret dry-run skipped because ESO CRDs are not installed yet; real apply installs CRDs first.' >"$custom_out" + printf '%s\\n' 'SecretStore/ClusterSecretStore/ExternalSecret dry-run skipped because ESO CRDs are not installed yet; real apply installs CRDs first.' >"$custom_out" : >"$custom_err" fi if kubectl get namespace ${target.namespace} >/dev/null 2>&1; then @@ -804,6 +827,7 @@ capture_json pods kubectl -n ${target.namespace} get pods -l app.kubernetes.io/p capture_json services kubectl -n ${target.namespace} get service ${secretPlane.vault.serviceName} capture_json crds kubectl get ${crdArgs} capture_json secretstore kubectl -n ${target.namespace} get secretstore ${secretPlane.syncProbe.secretStoreName} +capture_json clustersecretstore kubectl get clustersecretstore ${secretPlane.syncProbe.clusterSecretStoreName} capture_json externalsecret kubectl -n ${target.namespace} get externalsecret ${secretPlane.syncProbe.externalSecretName} capture_json targetsecret kubectl -n ${target.namespace} get secret ${secretPlane.syncProbe.targetSecretName} capture_json tokensecret kubectl -n ${target.namespace} get secret ${secretPlane.vault.tokenSecretName} @@ -857,6 +881,8 @@ def condition_summary(item): {"type": c.get("type"), "status": c.get("status"), "reason": c.get("reason"), "message": c.get("message")} for c in status.get("conditions", []) ] +def ready_condition(item): + return any(c.get("type") == "Ready" and c.get("status") == "True" for c in condition_summary(item)) def secret_key_summary(item, keys): data = (item or {}).get("data") or {} out = {"name": (item or {}).get("metadata", {}).get("name"), "ready": bool(data), "keys": sorted(data.keys()), "missingKeys": [key for key in keys if key not in data], "fingerprints": {}, "valuesPrinted": False} @@ -876,14 +902,17 @@ crd_names = sorted([item.get("metadata", {}).get("name") for item in crds if isi target_secret = secret_key_summary(load("targetsecret"), ["${secretPlane.syncProbe.remoteProperty}"]) token_secret = secret_key_summary(load("tokensecret"), ["${secretPlane.vault.tokenSecretKey}"]) secretstore = load("secretstore") or {} +clustersecretstore = load("clustersecretstore") or {} externalsecret = load("externalsecret") or {} eso_ready = all(deployment_ready.get(name) is True for name in ${JSON.stringify(esoDeployments)}) vault_ready = deployment_ready.get("${secretPlane.vault.deploymentName}") is True consumer_ready = deployment_ready.get("${secretPlane.syncProbe.consumer.deploymentName}") is True crds_ready = all(name in crd_names for name in ${JSON.stringify(secretPlane.eso.crds)}) +secretstore_ready = ready_condition(secretstore) +clustersecretstore_ready = ready_condition(clustersecretstore) synced = target_secret["fingerprints"].get("${secretPlane.syncProbe.remoteProperty}") == "${secretPlane.syncProbe.expectedFingerprint}" payload = { - "ready": eso_ready and vault_ready and crds_ready, + "ready": eso_ready and vault_ready and crds_ready and secretstore_ready and clustersecretstore_ready, "target": "${target.id}", "route": "${target.route}", "namespace": "${target.namespace}", @@ -891,11 +920,14 @@ payload = { "crdsReady": crds_ready, "esoReady": eso_ready, "vaultReady": vault_ready, + "secretStoreReady": secretstore_ready, + "clusterSecretStoreReady": clustersecretstore_ready, "consumerReady": consumer_ready, "syncReady": synced, "deployments": deployments, "services": [service_summary(item) for item in list_items("services")], "secretStore": {"name": "${secretPlane.syncProbe.secretStoreName}", "conditions": condition_summary(secretstore)}, + "clusterSecretStore": {"name": "${secretPlane.syncProbe.clusterSecretStoreName}", "conditions": condition_summary(clustersecretstore)}, "externalSecret": {"name": "${secretPlane.syncProbe.externalSecretName}", "conditions": condition_summary(externalsecret), "refreshTime": (externalsecret.get("status") or {}).get("refreshTime")}, "targetSecret": target_secret, "tokenSecret": {"name": token_secret["name"], "ready": token_secret["ready"], "keys": token_secret["keys"], "missingKeys": token_secret["missingKeys"], "valuesPrinted": False}, @@ -978,13 +1010,15 @@ else fi kubectl -n ${target.namespace} get secretstore ${secretPlane.syncProbe.secretStoreName} -o json >"$tmp/secretstore.json" 2>"$tmp/secretstore.err" secretstore_rc=$? +kubectl get clustersecretstore ${secretPlane.syncProbe.clusterSecretStoreName} -o json >"$tmp/clustersecretstore.json" 2>"$tmp/clustersecretstore.err" +clustersecretstore_rc=$? kubectl -n ${target.namespace} get externalsecret ${secretPlane.syncProbe.externalSecretName} -o json >"$tmp/externalsecret.json" 2>"$tmp/externalsecret.err" externalsecret_rc=$? -python3 - "$vault_put_rc" "$vault_metadata_rc" "$sync_rc" "$consumer_rollout_restart_rc" "$consumer_rollout_rc" "$consumer_env_rc" "$secretstore_rc" "$externalsecret_rc" "$secret_fingerprint" "$tmp" <<'PY' +python3 - "$vault_put_rc" "$vault_metadata_rc" "$sync_rc" "$consumer_rollout_restart_rc" "$consumer_rollout_rc" "$consumer_env_rc" "$secretstore_rc" "$clustersecretstore_rc" "$externalsecret_rc" "$secret_fingerprint" "$tmp" <<'PY' import json, os, sys -vault_put_rc, vault_metadata_rc, sync_rc, consumer_rollout_restart_rc, consumer_rollout_rc, consumer_env_rc, secretstore_rc, externalsecret_rc = [int(value) for value in sys.argv[1:9]] -secret_fingerprint = sys.argv[9] -tmp = sys.argv[10] +vault_put_rc, vault_metadata_rc, sync_rc, consumer_rollout_restart_rc, consumer_rollout_rc, consumer_env_rc, secretstore_rc, clustersecretstore_rc, externalsecret_rc = [int(value) for value in sys.argv[1:10]] +secret_fingerprint = sys.argv[10] +tmp = sys.argv[11] def text(name, limit=3000): try: return open(os.path.join(tmp, name), encoding="utf-8", errors="replace").read()[-limit:] @@ -999,7 +1033,7 @@ def conditions(obj): status = (obj or {}).get("status") or {} return [{"type": c.get("type"), "status": c.get("status"), "reason": c.get("reason"), "message": c.get("message")} for c in status.get("conditions", [])] payload = { - "ok": vault_put_rc == 0 and vault_metadata_rc == 0 and sync_rc == 0 and consumer_rollout_restart_rc == 0 and consumer_rollout_rc == 0 and consumer_env_rc == 0 and secretstore_rc == 0 and externalsecret_rc == 0, + "ok": vault_put_rc == 0 and vault_metadata_rc == 0 and sync_rc == 0 and consumer_rollout_restart_rc == 0 and consumer_rollout_rc == 0 and consumer_env_rc == 0 and secretstore_rc == 0 and clustersecretstore_rc == 0 and externalsecret_rc == 0, "target": "${target.id}", "namespace": "${target.namespace}", "checks": { @@ -1027,6 +1061,7 @@ payload = { "stderrTail": text("consumer-env.err"), }, "secretStore": {"exitCode": secretstore_rc, "conditions": conditions(load("secretstore.json")), "stderrTail": text("secretstore.err")}, + "clusterSecretStore": {"exitCode": clustersecretstore_rc, "conditions": conditions(load("clustersecretstore.json")), "stderrTail": text("clustersecretstore.err")}, "externalSecret": {"exitCode": externalsecret_rc, "conditions": conditions(load("externalsecret.json")), "stderrTail": text("externalsecret.err")}, }, "boundary": "platform-infra only; no HWLAB namespace or workload integration was created", @@ -1089,8 +1124,9 @@ function vaultSummary(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget) function syncProbeSummary(secretPlane: SecretPlaneConfig): Record { const probe = secretPlane.syncProbe; return { - dataFlow: "Vault KV v2 -> ESO SecretStore -> ExternalSecret -> Kubernetes Secret -> consumer env", + dataFlow: "Vault KV v2 -> ESO SecretStore/ClusterSecretStore -> ExternalSecret -> Kubernetes Secret -> consumer env", secretStoreName: probe.secretStoreName, + clusterSecretStoreName: probe.clusterSecretStoreName, externalSecretName: probe.externalSecretName, targetSecretName: probe.targetSecretName, refreshInterval: probe.refreshInterval, @@ -1117,7 +1153,7 @@ function policyChecks(secretPlane: SecretPlaneConfig, target: SecretPlaneTarget, { name: "no-hwlab-workloads", ok: !/namespace:\s*hwlab/iu.test(yaml) && !/hwlab-v0?3/iu.test(yaml), detail: "This PoC must not integrate into HWLAB v0.3 yet." }, { name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Secret plane services stay ClusterIP-only." }, { name: "no-hardcoded-token", ok: !yaml.includes("VAULT_DEV_ROOT_TOKEN_ID:") && secretPlane.vault.bootstrap.tokenMode === "generated-if-missing", detail: "Vault dev token is generated into a Kubernetes Secret when missing and never committed." }, - { name: "required-objects-rendered", ok: kinds.includes("Deployment") && kinds.includes("SecretStore") && kinds.includes("ExternalSecret"), detail: "Vault backend, ESO SecretStore and ExternalSecret PoC objects are rendered from YAML." }, + { name: "required-objects-rendered", ok: kinds.includes("Deployment") && kinds.includes("SecretStore") && kinds.includes("ClusterSecretStore") && kinds.includes("ExternalSecret"), detail: "Vault backend, ESO SecretStore/ClusterSecretStore and ExternalSecret PoC objects are rendered from YAML." }, ]; } @@ -1139,10 +1175,13 @@ function compactStatus(parsed: Record, full: boolean): Record): RenderedCliResult { ["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)], ["ESO", stringValue(eso.version), "manifest", stringValue(eso.manifestUrl)], ["VAULT", stringValue(vault.deploymentName), "service", stringValue(vault.serviceDns)], + ["STORE", stringValue(syncProbe.secretStoreName), "clusterStore", stringValue(syncProbe.clusterSecretStoreName)], ["SYNC", stringValue(syncProbe.externalSecretName), "targetSecret", stringValue(syncProbe.targetSecretName)], ["POLICY", failed.length === 0 ? "ok" : `failed=${failed.length}`, "valuesPrinted", "false"], ]; @@ -1205,6 +1245,8 @@ function renderStatus(result: Record): RenderedCliResult { ["crds", boolText(summary.crdsReady), "ESO API installed"], ["eso", boolText(summary.esoReady), "controller/webhook/cert-controller"], ["vault", boolText(summary.vaultReady), "Vault dev KV v2 backend"], + ["secretStore", boolText(summary.secretStoreReady), stringValue(record(summary.secretStore).name)], + ["clusterStore", boolText(summary.clusterSecretStoreReady), stringValue(record(summary.clusterSecretStore).name)], ["sync", boolText(summary.syncReady), `secret=${stringValue(targetSecret.name)} key=${stringValue(arrayValues(targetSecret.keys).join(","), "-")}`], ["token", boolText(tokenSecret.ready), `secret=${stringValue(tokenSecret.name)} valuesPrinted=false`], ]),