diff --git a/Makefile b/Makefile index 1f6047e..1ee997f 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ OPENBAO_UI_OVERLAY_K8S ?= helm/openbao-ui-overlay-k8s.yaml OPENBAO_VERIFY_AUTH_ARGS ?= OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-drill/evidence.json +EXTERNAL_SECRETS_NAMESPACE ?= external-secrets ARGOCD_NAMESPACE ?= argocd ARGOCD_BOOTSTRAP_DIR ?= argocd/bootstrap ARGOCD_REPOSITORY_SECRET ?= @@ -157,6 +158,11 @@ openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/au KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS) +openbao-configure-external-secrets-issue-core: ## Configure OpenBao policy/role for issue-core ESO pilot + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + OPENBAO_RELEASE=$(OPENBAO_RELEASE) ESO_NAMESPACE=$(EXTERNAL_SECRETS_NAMESPACE) \ + scripts/openbao-apply-external-secrets-issue-core.sh + openbao-validate-restore-evidence: ## Validate non-secret OpenBao restore-drill evidence JSON OPENBAO_RESTORE_EVIDENCE='$(OPENBAO_RESTORE_EVIDENCE)' \ scripts/openbao-validate-restore-evidence.sh @@ -180,9 +186,9 @@ argocd-repo-apply: ## Apply a SOPS-encrypted ArgoCD repository Secret (set ARGOC argocd-status: ## Show Railiance ArgoCD projects, root app, and registered repos $(KUBECTL) get appprojects.argoproj.io -n $(ARGOCD_NAMESPACE) \ - railiance-bootstrap railiance-tenants + railiance-bootstrap railiance-tenants railiance-platform-addons $(KUBECTL) get applications.argoproj.io -n $(ARGOCD_NAMESPACE) \ - railiance-apps-root + railiance-apps-root external-secrets openbao-secretstore issue-core $(KUBECTL) get secrets -n $(ARGOCD_NAMESPACE) \ -l argocd.argoproj.io/secret-type=repository @@ -198,4 +204,4 @@ help: ## Show this help /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) -.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help +.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-validate-restore-evidence openbao-validate-emergency-evidence argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup help diff --git a/argocd/applications/external-secrets.application.yaml b/argocd/applications/external-secrets.application.yaml new file mode 100644 index 0000000..c06457a --- /dev/null +++ b/argocd/applications/external-secrets.application.yaml @@ -0,0 +1,35 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: external-secrets + namespace: argocd + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: external-secrets + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + project: railiance-platform-addons + source: + repoURL: https://charts.external-secrets.io + chart: external-secrets + targetRevision: 0.16.1 + helm: + releaseName: external-secrets + values: | + installCRDs: true + serviceAccount: + create: true + name: external-secrets + destination: + server: https://kubernetes.default.svc + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - ApplyOutOfSyncOnly=true + - PruneLast=true diff --git a/argocd/applications/openbao-secretstore.application.yaml b/argocd/applications/openbao-secretstore.application.yaml new file mode 100644 index 0000000..f3ff4c3 --- /dev/null +++ b/argocd/applications/openbao-secretstore.application.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: openbao-secretstore + namespace: argocd + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: external-secrets + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + project: railiance-platform-addons + source: + repoURL: https://gitea.coulomb.social/coulomb/railiance-platform.git + targetRevision: main + path: argocd/platform-addons/openbao-secretstore + destination: + server: https://kubernetes.default.svc + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + - PruneLast=true diff --git a/argocd/bootstrap/02-railiance-platform-addons-project.yaml b/argocd/bootstrap/02-railiance-platform-addons-project.yaml new file mode 100644 index 0000000..609f06b --- /dev/null +++ b/argocd/bootstrap/02-railiance-platform-addons-project.yaml @@ -0,0 +1,48 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: railiance-platform-addons + namespace: argocd + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: gitops +spec: + description: Platform-owned cluster add-ons required by tenant workloads. + sourceRepos: + - https://gitea.coulomb.social/coulomb/railiance-platform.git + - https://charts.external-secrets.io + destinations: + - server: https://kubernetes.default.svc + namespace: "*" + clusterResourceWhitelist: + - group: "" + kind: Namespace + - group: apiextensions.k8s.io + kind: CustomResourceDefinition + - group: admissionregistration.k8s.io + kind: MutatingWebhookConfiguration + - group: admissionregistration.k8s.io + kind: ValidatingWebhookConfiguration + - group: rbac.authorization.k8s.io + kind: ClusterRole + - group: rbac.authorization.k8s.io + kind: ClusterRoleBinding + - group: external-secrets.io + kind: ClusterSecretStore + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: Secret + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: rbac.authorization.k8s.io + kind: Role + - group: rbac.authorization.k8s.io + kind: RoleBinding + orphanedResources: + warn: true diff --git a/argocd/bootstrap/kustomization.yaml b/argocd/bootstrap/kustomization.yaml index 03db897..57835eb 100644 --- a/argocd/bootstrap/kustomization.yaml +++ b/argocd/bootstrap/kustomization.yaml @@ -3,5 +3,6 @@ kind: Kustomization resources: - 00-railiance-bootstrap-project.yaml - 01-railiance-tenants-project.yaml + - 02-railiance-platform-addons-project.yaml - 10-railiance-apps-root.application.yaml diff --git a/argocd/platform-addons/openbao-secretstore/kustomization.yaml b/argocd/platform-addons/openbao-secretstore/kustomization.yaml new file mode 100644 index 0000000..2b05892 --- /dev/null +++ b/argocd/platform-addons/openbao-secretstore/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - openbao.clustersecretstore.yaml diff --git a/argocd/platform-addons/openbao-secretstore/openbao.clustersecretstore.yaml b/argocd/platform-addons/openbao-secretstore/openbao.clustersecretstore.yaml new file mode 100644 index 0000000..2771fcb --- /dev/null +++ b/argocd/platform-addons/openbao-secretstore/openbao.clustersecretstore.yaml @@ -0,0 +1,23 @@ +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: openbao + labels: + app.kubernetes.io/part-of: railiance-gitops + railiance-platform/component: external-secrets +spec: + provider: + vault: + server: http://openbao.openbao.svc.cluster.local:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: external-secrets-issue-core + serviceAccountRef: + name: external-secrets + namespace: external-secrets + conditions: + - namespaces: + - issue-core diff --git a/docs/argocd-gitops.md b/docs/argocd-gitops.md index 5131f89..88d6a9b 100644 --- a/docs/argocd-gitops.md +++ b/docs/argocd-gitops.md @@ -174,3 +174,41 @@ ClusterRoleBindings, or other cluster-admin resources. If a tenant needs a cluster-scoped platform resource, create a new platform-owned workplan instead of broadening the tenant project by default. + +## Platform Add-ons + +External Secrets Operator is a platform-owned add-on because it installs CRDs, +webhooks, and cluster RBAC. Tenant Applications must not install or upgrade it. + +The GitOps contract uses: + +- `railiance-platform-addons` AppProject for cluster add-ons. +- `external-secrets` ArgoCD Application for the public Helm chart. +- `openbao-secretstore` ArgoCD Application for the OpenBao + `ClusterSecretStore`. +- OpenBao Kubernetes auth role `external-secrets-issue-core` for the + issue-core pilot. + +The initial `ClusterSecretStore/openbao` is intentionally limited to the +`issue-core` namespace. Broaden it only with a new platform review when another +tenant is ready to consume OpenBao through ESO. + +Configure the OpenBao side without printing token values: + +```bash +OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \ + make openbao-configure-external-secrets-issue-core +``` + +The helper keeps Kubernetes auth in local-reviewer mode: OpenBao rereads its +own mounted service-account token and CA file instead of storing an expiring +reviewer JWT. + +Then sync ArgoCD and verify: + +```bash +make argocd-bootstrap-deploy +make argocd-status +kubectl -n external-secrets get deploy,pod +kubectl get clustersecretstore.external-secrets.io openbao +``` diff --git a/openbao/policies/external-secrets-issue-core.hcl b/openbao/policies/external-secrets-issue-core.hcl new file mode 100644 index 0000000..6e6a856 --- /dev/null +++ b/openbao/policies/external-secrets-issue-core.hcl @@ -0,0 +1,13 @@ +# Least-privilege policy for the External Secrets Operator issue-core pilot. +# +# The matching Kubernetes auth role binds only the ESO service account in the +# external-secrets namespace. ClusterSecretStore usage is separately limited to +# the issue-core namespace. + +path "platform/data/workloads/issue-core/issue-core/*" { + capabilities = ["read"] +} + +path "platform/metadata/workloads/issue-core/issue-core/*" { + capabilities = ["read", "list"] +} diff --git a/scripts/openbao-apply-external-secrets-issue-core.sh b/scripts/openbao-apply-external-secrets-issue-core.sh new file mode 100755 index 0000000..be2812d --- /dev/null +++ b/scripts/openbao-apply-external-secrets-issue-core.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}" +OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}" +KUBECTL="${KUBECTL:-kubectl}" +TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}" +ROLE_NAME="${OPENBAO_ESO_ROLE:-external-secrets-issue-core}" +POLICY_NAME="${OPENBAO_ESO_POLICY:-external-secrets-issue-core}" +ESO_NAMESPACE="${ESO_NAMESPACE:-external-secrets}" +ESO_SERVICE_ACCOUNT="${ESO_SERVICE_ACCOUNT:-external-secrets}" +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +POLICY_FILE="${POLICY_FILE:-$REPO_DIR/openbao/policies/external-secrets-issue-core.hcl}" +DRY_RUN=0 + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-apply-external-secrets-issue-core.sh [--dry-run] + +Configures OpenBao for the issue-core External Secrets Operator pilot: + - refreshes Kubernetes auth config for in-cluster short-lived tokens + - writes the external-secrets-issue-core read policy + - writes the Kubernetes auth role bound to external-secrets/external-secrets + +The script reads an OpenBao operator token from OPENBAO_TOKEN_FILE or an +interactive hidden prompt. It never prints or stores the token. +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +pod="${OPENBAO_RELEASE}-0" + +read_token() { + if [ "$DRY_RUN" -eq 1 ]; then + printf 'dry-run-token\n' + return + fi + if [ -n "$TOKEN_FILE" ]; then + if [ ! -f "$TOKEN_FILE" ]; then + echo "ERROR: OPENBAO_TOKEN_FILE does not exist: $TOKEN_FILE" >&2 + exit 1 + fi + head -n 1 "$TOKEN_FILE" + return + fi + local token + read -r -s -p "OpenBao token: " token + printf '\n' >&2 + printf '%s\n' "$token" +} + +kubectl_exec() { + # shellcheck disable=SC2086 + $KUBECTL "$@" +} + +remote_bao() { + local token="$1" + shift + if [ "$DRY_RUN" -eq 1 ]; then + printf 'DRY-RUN: bao %s\n' "$*" + return 0 + fi + printf '%s\n' "$token" | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \ + sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@" +} + +remote_sh() { + local token="$1" + local script="$2" + if [ "$DRY_RUN" -eq 1 ]; then + printf 'DRY-RUN: remote shell: %s\n' "$script" + return 0 + fi + printf '%s\n%s\n' "$token" "$script" | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \ + sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; sh' +} +write_policy() { + local token="$1" + if [ ! -f "$POLICY_FILE" ]; then + echo "ERROR: missing policy file: $POLICY_FILE" >&2 + exit 1 + fi + if [ "$DRY_RUN" -eq 1 ]; then + printf 'DRY-RUN: bao policy write %s %s\n' "$POLICY_NAME" "$POLICY_FILE" + return 0 + fi + { printf '%s\n' "$token"; cat "$POLICY_FILE"; } | kubectl_exec exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \ + sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$POLICY_NAME" +} + +token="$(read_token)" +if [ -z "$token" ]; then + echo "ERROR: empty token" >&2 + exit 1 +fi + +remote_bao "$token" status +remote_sh "$token" 'bao write auth/kubernetes/config \ + kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \ + disable_iss_validation=true' +write_policy "$token" +remote_bao "$token" write "auth/kubernetes/role/${ROLE_NAME}" \ + "bound_service_account_names=${ESO_SERVICE_ACCOUNT}" \ + "bound_service_account_namespaces=${ESO_NAMESPACE}" \ + "policies=${POLICY_NAME}" \ + ttl=15m + +remote_bao "$token" read "auth/kubernetes/role/${ROLE_NAME}" + +cat <<'NEXT' + +External Secrets OpenBao role configured. + +Next steps: + 1. Sync the external-secrets and openbao-secretstore ArgoCD Applications. + 2. Provision platform/workloads/issue-core/issue-core/issue-core-runtime + with ISSUE_CORE_API_KEY and GITEA_BACKEND_TOKEN without printing values. + 3. Confirm ExternalSecret/issue-core-runtime becomes Ready. +NEXT diff --git a/scripts/openbao-apply-initial-config.sh b/scripts/openbao-apply-initial-config.sh index f0bfad5..4892622 100755 --- a/scripts/openbao-apply-initial-config.sh +++ b/scripts/openbao-apply-initial-config.sh @@ -188,8 +188,7 @@ enable_optional "$token" "kubernetes/ auth method is already enabled." auth enab remote_sh "$token" 'bao write auth/kubernetes/config \ kubernetes_host="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}" \ - token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \ - kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt' + disable_iss_validation=true' write_policy "$token" platform-admin "$POLICY_DIR/platform-admin.hcl" write_policy "$token" platform-readonly "$POLICY_DIR/platform-readonly.hcl" diff --git a/workplans/RAILIANCE-WP-0004-argocd-gitops-bootstrap.md b/workplans/RAILIANCE-WP-0004-argocd-gitops-bootstrap.md index 16adf14..f85f405 100644 --- a/workplans/RAILIANCE-WP-0004-argocd-gitops-bootstrap.md +++ b/workplans/RAILIANCE-WP-0004-argocd-gitops-bootstrap.md @@ -10,7 +10,7 @@ topic_slug: railiance planning_priority: high planning_order: 4 created: "2026-06-19" -updated: "2026-06-19" +updated: "2026-06-25" state_hub_workstream_id: "e57e487b-8557-439d-8093-0457c73ede93" --- @@ -149,6 +149,21 @@ platform/operators/argocd/repositories/ External Secrets Operator for values that become Kubernetes Secrets, CSI for file-reference workloads, and no OpenBao injector in the current deployment. + +## Follow-up Progress (2026-06-25) + +- Added a platform-owned `railiance-platform-addons` AppProject for + cluster-scoped add-ons. +- Added the `external-secrets` ArgoCD Application for External Secrets + Operator and the `openbao-secretstore` Application for + `ClusterSecretStore/openbao`. +- Added the least-privilege OpenBao policy and Kubernetes auth role helper for + the issue-core ESO pilot. The role binds only the + `external-secrets/external-secrets` service account and reads only + `platform/workloads/issue-core/issue-core/*`. +- Limited the initial `ClusterSecretStore/openbao` to the `issue-core` + namespace; broaden only through a later platform review. + ## Target State - `argocd/bootstrap/` contains the two AppProjects and root app-of-apps