diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index aca39c04..968016df 100755 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -16,6 +16,7 @@ - **Ingress**: `ingress_factory` module. Auth: `protected = true`. Anti-AI: on by default. - **Docker images**: Always build for `linux/amd64` (`docker buildx build --platform linux/amd64`). Pull-through cache serves stale :latest — use versioned tags. - **Node memory changes**: When changing VM memory on any k8s node, update kubelet `systemReserved`, `kubeReserved`, and eviction thresholds accordingly. Config: `/var/lib/kubelet/config.yaml`. Template: `stacks/infra/main.tf`. Current values: systemReserved=512Mi, kubeReserved=512Mi, evictionHard=500Mi, evictionSoft=1Gi. +- **Sealed Secrets**: User-managed secrets go in `sealed-*.yaml` files in the stack directory. Stacks pick them up via `kubernetes_manifest` + `fileset(path.module, "sealed-*.yaml")`. See AGENTS.md for full workflow. ## User Preferences - **Calendar**: Nextcloud at `nextcloud.viktorbarzin.me` diff --git a/AGENTS.md b/AGENTS.md index 54a5ff1d..dffb9f33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,10 +24,26 @@ - **Add a secret**: `sops set secrets.sops.json '["new_key"]' '"value"'` - **Operators** push PRs → Viktor reviews → CI decrypts and applies. No encryption keys needed for operators. +## Sealed Secrets (User-Managed Secrets) +For secrets that users manage themselves (no SOPS/git-crypt access needed): +1. **Create**: `kubectl create secret generic --from-literal=key=value -n --dry-run=client -o yaml | kubeseal --controller-name sealed-secrets --controller-namespace sealed-secrets -o yaml > sealed-.yaml` +2. **Commit**: Place `sealed-*.yaml` files in the stack directory (`stacks//`) +3. **Terraform picks them up** automatically via `fileset` + `for_each`: + ```hcl + resource "kubernetes_manifest" "sealed_secrets" { + for_each = fileset(path.module, "sealed-*.yaml") + manifest = yamldecode(file("${path.module}/${each.value}")) + } + ``` +4. **Deploy**: Push → CI runs `terragrunt apply` → controller decrypts into real K8s Secrets +- Only the in-cluster controller has the private key. `kubeseal` uses the public key — safe to distribute. +- Naming convention: files MUST match `sealed-*.yaml` glob pattern. +- The `kubernetes_manifest` block is safe to add even with zero sealed-*.yaml files (empty for_each). + ## Architecture Terragrunt-based homelab managing a Kubernetes cluster (5 nodes, v1.34.2) on Proxmox VMs. - **70+ services**, each in `stacks//` with its own Terraform state -- **Core platform**: `stacks/platform/modules/` (~22 modules: Traefik, Kyverno, monitoring, dbaas, etc.) +- **Core platform**: `stacks/platform/modules/` (~22 modules: Traefik, Kyverno, monitoring, dbaas, sealed-secrets, etc.) - **Public domain**: `viktorbarzin.me` (Cloudflare) | **Internal**: `viktorbarzin.lan` (Technitium DNS) - **Onboarding portal**: `https://k8s-portal.viktorbarzin.me` — self-service kubectl setup + docs - **CI/CD**: Woodpecker CI — PRs run plan, merges to master auto-apply platform stack diff --git a/stacks/plotting-book/main.tf b/stacks/plotting-book/main.tf index a8bed72f..58bba9d2 100644 --- a/stacks/plotting-book/main.tf +++ b/stacks/plotting-book/main.tf @@ -167,3 +167,9 @@ module "ingress" { "gethomepage.dev/pod-selector" = "" } } + +# Sealed Secrets — encrypted secrets safe to commit to git +resource "kubernetes_manifest" "sealed_secrets" { + for_each = fileset(path.module, "sealed-*.yaml") + manifest = yamldecode(file("${path.module}/${each.value}")) +}