argocd-server ran --insecure (plain HTTP :8080) while its Helm-managed
Ingress set ssl-passthrough: true, which sends nginx's raw TLS handshake
straight to the pod - HTTP server can't complete a TLS handshake, nginx
logged 502 (peer closed connection in SSL handshake). Compounded by a
second, conflicting Ingress for the same host in
k8s/bootstrap/ingress/ingress.yaml - two Ingress objects on one host is
undefined nginx routing behavior. Disabled the Helm-managed Ingress
(enabled: false) so ingress.yaml's passthrough Ingress is the sole
source of truth, and set server.insecure: false so argocd-server
actually terminates TLS itself, matching passthrough's requirement.
ArgoCD directory.include uses Go filepath.Match glob syntax, not shell
brace expansion - {a,b,c} silently matched nothing, only the original 2
files stayed tracked.
homelab-ca was referenced by 6 manifests (authentik, forgejo-runner,
blackbox-exporter, management-service) as a CA trust ConfigMap but never
existed anywhere - not in git, not live in cluster. Generated a new
10-year self-signed root CA, wired it as a ClusterIssuer (cert-manager
namespace) and distributed the public cert as a ConfigMap to every
consuming namespace (iam, cicd, monitoring, sqs). Private key lives only
in the encrypted Secret. Widened cert-manager-issuers' directory include
glob rather than creating a new Application - destination.namespace is
just a fallback default on a plain directory source, not a transformer,
so it doesn't fight with each ConfigMap's own explicit namespace.
Also adds grafana-oidc secret (GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET),
same pre-existing gap as grafana-admin - was meant to come from a deleted
manual script, value already available in .env.
nodeSelector still targeted az-a/talos-cp-1 from before the 3-CP topology
change. talos-cp-2 (az-b) has the dedicated Longhorn disks now, so the
pod's zone pin and the PVC's only viable replica location never matched
- ReplicaSchedulingFailure: disks are unavailable, pod stuck
ContainerCreating waiting on AttachVolume.
Vault's S3 storage backend needs AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
from vault-minio-creds, previously generated by a helmfile presync hook
that no longer exists post-Terraform/helmfile removal. Sourced from the
same MINIO_ROOT_USER/PASSWORD already in .env. vault-unseal-keys still
missing separately — needs a live 'vault operator init' run, deferred.
PVC sat Pending for 17 days — storageclass.storage.k8s.io "longhorn-wffc"
not found. Only longhorn, longhorn-cnpg, longhorn-static exist. Straight
naming drift, no such class was ever created.
repoServer CPU limit (500m) was too tight once the SOPS sidecar added real
decrypt work under the liveness probe's 1s timeout — repo-server kept
getting killed mid-sync. Raised to 1000m (node has 23+ idle cores, no
scarcity). Separately, the generate script's doc-separator fix exposed
that several .enc.yaml files (cloudflared, temporal, authentik, loki) are
raw Helm-values snippets, not K8s manifests — ArgoCD hard-failed the whole
batch on the first one missing 'kind:'. Script now skips those, so
correctly-shaped Secrets (grafana-admin included) sync independently.
grafana-admin also needed an admin-user key alongside admin-password —
the chart looks up both from the same existingSecret.
Sidecar container was absent from live repo-server Deployment (never in
helm history), causing sops-secrets Application to fail with cmp-server
socket not found — cascaded CreateContainerConfigError across every app
depending on SOPS-decrypted secrets. Also fixes duplicate version field
in plugin ConfigMap that produced a mismatched socket filename, and adds
an initContainer to fetch the sops binary into a writable emptyDir since
the sidecar runs non-root. grafana-secrets.enc.yaml rewritten from a bare
values file (never valid as a K8s Secret) to a proper Secret manifest so
grafana-admin now actually gets created.
- Replace all forgejo.riotpiao.com repo URLs with [email protected] SSH URLs
- Enables immediate GitOps sync without waiting for Forgejo mirror setup
- Includes ingress-nginx now fully ArgoCD-managed (wave 0)
- SOPS secrets can now sync and decrypt TLS certificates
TOPOLOGY CHANGE:
- All 3 control-plane nodes now schedulable (no NoSchedule taints)
- Pod distribution: ~59 on cp-1, ~21 on cp-2, ~23 on cp-3
- Better resource utilization across cluster
ADDED HARD RULE:
- Control-plane scheduling controlled via Terraform
- terraform.tfvars → allow_scheduling = true/false
- Never manual kubectl taint (Talos will revert)
- Workflow: terraform apply → talosctl apply-config
IMPLEMENTATION:
- Terraform: Set allow_scheduling=true for cp-2, cp-3
- Applied via talosctl --mode no-reboot (no disruption)
- Verified: kubectl get nodes shows no taints
ADDED:
- CloudNativePG (CNPG) Database Pattern section
- Explains shared 'app' user model (not per-app roles)
- Documents bootstrap.sh credential distribution pattern
- Working examples (Forgejo, Authentik)
- Prescriptive DO/DON'T guidance for new apps
FIXED:
- Storage topology: 3-node HA (not "sole Longhorn node")
- Verified: all 17 PVCs have replicas across all 3 nodes
- Updated last-modified date
This documents the architectural pattern established during CNPG refactor.
UNIFIED PATTERN: All apps follow same credential distribution
FORGEJO PATTERN (now universal):
1. CNPG creates ddb-cluster-app in ddb namespace (source)
2. bootstrap.sh copies to app namespaces (cicd, iam)
3. Apps reference local copy via secretKeyRef
4. No PostSync Jobs needed
CHANGES:
- bootstrap.sh: Copy ddb-cluster-app to iam namespace (like cicd)
- authentik-values.yaml: Reference local ddb-cluster-app via env vars
- Removed: sync-db-credentials PostSync Job (not needed)
- kustomization.yaml: Removed PostSync Job reference
BENEFITS:
✅ Same pattern as working Forgejo
✅ No complex PostSync Jobs
✅ bootstrap.sh handles setup for future clusters
✅ Simple secretKeyRef, no cross-namespace issues
✅ ArgoCD manages applications, not secrets
Database recreated with app owner (fresh migrations needed).
ARCHITECTURAL CHANGE: Align with CNPG design intent
BEFORE (Complex, broken):
- Per-app roles (authentik, temporal) with Database CR owner field
- Database CR doesn't transfer ownership properly
- Needed manual permission grants (PostSync Job)
- Apps couldn't create tables without grants from 'app' role
AFTER (Simple, works):
- All apps use shared 'app' bootstrap user
- Database CRs: owner: app (matches actual ownership)
- No permission grants needed (owner has full rights)
- Isolation via separate database names only
CHANGES:
- Database CRs: owner changed from app-specific to 'app'
- ddb-cluster.yaml: removed managed.roles section
- Deleted grant-schema-permissions PostSync Job
- Follows Forgejo pattern (already working this way)
MANUAL STEPS REQUIRED:
1. Update authentik-secrets: AUTHENTIK_POSTGRESQL__USER=app
2. Update temporal secrets: similar change
3. Recreate databases with app as owner
4. Restart applications
Benefits:
- Simpler architecture
- No permission grant complexity
- Aligns with CNPG single-cluster design
- Matches working Forgejo implementation
Fresh authentik deployment runs ~100 database migrations which takes 15-20
minutes. Previous startup probe failureThreshold of 60 (10 minutes) killed
the pod before migrations could complete, causing infinite restart loop.
Increased to 120 failures (20 minutes) to allow migrations to finish.
Fixes: nginx 503 due to pod never becoming Ready.
Authentik migrations need to CREATE SCHEMA (not just tables in public schema).
This requires GRANT CREATE ON DATABASE, not just schema-level permissions.
Added to PostSync Job:
- GRANT CREATE ON DATABASE authentik TO authentik
- GRANT CREATE ON DATABASE temporal TO temporal
- GRANT CREATE ON DATABASE temporal_visibility TO temporal
App user can grant these (it owns the databases).
Removes duplicate longhorn-kafka StorageClass managed by Kafka chart.
All applications now use single 'longhorn' StorageClass (3 replicas, Immediate binding).
Changes:
- Kafka chart: use 'longhorn' instead of 'longhorn-kafka'
- Delete Kafka StorageClass template (no longer needed)
- Update longhorn-storageclass.yaml to match deployed config (Immediate, not WaitForFirstConsumer)
Existing Kafka PVCs remain bound to old longhorn-kafka StorageClass (safe - no data loss).
New PVCs will use unified 'longhorn' StorageClass.
ROOT CAUSE: CNPG Database CR creates databases but doesn't grant schema
permissions to the owner role. Bootstrap DB owner 'app' retains CREATE
privilege on public schema, blocking authentik/temporal from creating tables.
SECURITY FIX: Removed insecure 'GRANT TO PUBLIC' from postInitApplicationSQL.
SOLUTION: PostSync Job connects as 'app' (DB owner) and grants schema
permissions to named roles (authentik, temporal) in their respective databases.
Runs after Database CRs reconcile, survives CNPG database recreation.
Pattern: Per-database grants via PostSync, not cluster-wide PUBLIC grants.
Adds SQL to postInitApplicationSQL granting schema permissions to PUBLIC.
Allows any role (authentik, temporal, etc) to create tables in databases.
For existing cluster: run SQL manually (done).
For future bootstrap: automatic via initdb.
Pattern for apps: Database CR + app-specific init Job optional (co-located).
CNPG Database CR creates DBs but doesn't grant schema permissions properly.
Database owner is 'app' instead of specified role (authentik, temporal).
PostSync Job grants ALL on schema public to both app and named roles,
ensuring applications can create tables. Runs after Database CRs reconcile.
Fixes: authentik InsufficientPrivilege error on migration.
Minio operator requires privileged securityContext. Without these labels,
StatefulSet stuck at 0/0 replicas (PodSecurity admission blocks pod creation).
CoreDNS rewrites *.riotpiao.com → ingress-nginx-controller but bootstrap
deployed as ingress-nginx-bootstrap-controller. Service alias makes both work.