Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bootstrap/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions bootstrap/sops-secrets-operator-app.yaml
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions bootstrap/wait-for-sops-secrets-operator.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading