diff --git a/README.md b/README.md index 1bd64e0..477f4cf 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,16 @@ GitHub OAuth provides SSO for ArgoCD, Grafana, AWX, and kubectl/Headlamp (via OI Secrets are age-encrypted with field-level selective encryption. The `.sops.yaml` `encrypted_regex` targets only sensitive values (tokens, passwords, OAuth client secrets) so metadata stays diffable. +### sops-secrets-operator validation path + +`bootstrap/` installs `sops-secrets-operator` as the highest-priority child Application (`sync-wave: "-2"`) and waits for its CRD before the existing KSOPS-dependent waves continue. This is intentionally additive: KSOPS remains active until KMS-backed `SopsSecret` resources are proven and every existing age-encrypted Secret has been ported. + +Future KMS-backed `SopsSecret` manifests should use the SOPS KMS recipient managed by the [`makeitworkcloud/tfroot-aws`](https://github.com/makeitworkcloud/tfroot-aws) repository with `encrypted_suffix: Templates`. Do not copy raw KMS key identifiers into docs or chat; use the applied OpenTofu output locally when encrypting migration manifests. + +For a full KSOPS deprecation with no secret bootstrap loop, the operator should use ambient AWS auth, not static AWS access keys in a Kubernetes Secret. Because the cluster API is not publicly reachable, the target design is a small public static Kubernetes ServiceAccount OIDC issuer endpoint for AWS STS discovery/JWKS, not public cluster access. The endpoint is hosted outside the cluster by the `makeitworkcloud/www` static site at `https://makeitwork.cloud/oidc`, with discovery metadata at `/oidc/.well-known/openid-configuration` and JWKS at `/oidc/openid/v1/jwks`. + +That endpoint serves only public metadata: `/.well-known/openid-configuration` and `/openid/v1/jwks`. The private ServiceAccount signing key remains host-local to k3s/provisioning, k3s issues projected ServiceAccount tokens with the public issuer URL, and AWS IAM reads only the public discovery document and JWKS. This removes the Kubernetes/GitOps AWS-credential bootstrap loop, but it does not eliminate the host-level trust root required for k3s to sign ServiceAccount tokens. Static access keys are acceptable only for short-lived validation, not as the target endstate. + ```bash sops -e -i secret.yaml # encrypt in place sops -d secret.yaml # decrypt to stdout diff --git a/bootstrap/kustomization.yaml b/bootstrap/kustomization.yaml index 39ef060..aabe143 100644 --- a/bootstrap/kustomization.yaml +++ b/bootstrap/kustomization.yaml @@ -2,6 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: + # Wave -2/-1: Install sops-secrets-operator before every other GitOps child. + - sops-secrets-operator-app.yaml + - wait-for-sops-secrets-operator.yaml # Wave 0: Configure ArgoCD (KSOPS + GitHub OAuth + RBAC) # NOTE: cluster-admin ClusterRoleBinding is managed by ansible-role-crc # (ArgoCD cannot grant itself permissions it doesn't have) diff --git a/bootstrap/sops-secrets-operator-app.yaml b/bootstrap/sops-secrets-operator-app.yaml new file mode 100644 index 0000000..c477a94 --- /dev/null +++ b/bootstrap/sops-secrets-operator-app.yaml @@ -0,0 +1,45 @@ +--- +# Highest-priority child Application for validating a KSOPS replacement path. +# The operator must exist before any future KMS-backed SopsSecret workloads are +# introduced, so it deliberately syncs before the ArgoCD/KSOPS wave 0 resources. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: sops-secrets-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + project: default + destination: + server: https://kubernetes.default.svc + namespace: sops-secrets-operator + source: + chart: sops-secrets-operator + repoURL: https://isindir.github.io/sops-secrets-operator/ + targetRevision: 0.27.1 + helm: + releaseName: sops-secrets-operator + valuesObject: + namespaced: false + resources: {} + extraEnv: + # SOPS can infer the region from the KMS ARN, but setting the SDK + # region keeps AWS client startup deterministic in the k3s cluster. + - name: AWS_REGION + value: us-west-2 + - name: AWS_DEFAULT_REGION + value: us-west-2 + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 30s + maxDuration: 5m + factor: 2 diff --git a/bootstrap/wait-for-sops-secrets-operator.yaml b/bootstrap/wait-for-sops-secrets-operator.yaml new file mode 100644 index 0000000..dd75de1 --- /dev/null +++ b/bootstrap/wait-for-sops-secrets-operator.yaml @@ -0,0 +1,55 @@ +--- +# Sync hook that blocks lower-priority bootstrap waves until the replacement +# SOPS operator CRD is available. Do not wait for SopsSecret reconciliation here: +# KMS-backed SopsSecret migration is a later step after ambient AWS auth exists. +apiVersion: batch/v1 +kind: Job +metadata: + name: wait-for-sops-secrets-operator + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded + ignore-check.kube-linter.io/non-existent-service-account: "SA created by GitOps operator" + ignore-check.kube-linter.io/latest-tag: "bitnami/kubectl:latest is acceptable for a one-shot bootstrap wait Job" +spec: + ttlSecondsAfterFinished: 300 + backoffLimit: 30 + activeDeadlineSeconds: 900 + template: + spec: + serviceAccountName: argocd-argocd-application-controller + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: wait + image: bitnami/kubectl:latest + command: + - /bin/bash + - -c + - | + set -euo pipefail + + echo "Waiting for sops-secrets-operator CRD..." + until kubectl get crd sopssecrets.isindir.github.com >/dev/null 2>&1; do + echo "Waiting for sopssecrets.isindir.github.com CRD..." + sleep 10 + done + + echo "sops-secrets-operator CRD is ready" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 128Mi