The live cluster has no Authentik workload, no DNS record and no tunnel route,
but two declarations still pointed at it.
controlplane.tftpl still set kube-apiserver oidc-issuer-url to
https://authentik.riotpiao.com/application/o/kubernetes/ and pinned that hostname
in extraHostEntries. The live API server carries zero OIDC arguments, so the
template and reality had diverged: the next machine-config render would have
re-added an issuer that does not resolve.
management-service exported KMSVC_AUTHENTIK_ISSUER_URL and
KMSVC_AUTHENTIK_AUDIENCE, both empty. Rename them to KMSVC_ZITADEL_* so the names
describe the identity provider that actually exists.
The CoreDNS Corefile kept one rewrite for authentik.riotpiao.com. The Terraform
source file no longer contains it, so the live ConfigMap was stale; that line is
removed live and the next render converges without it.
Zitadel is running and has provisioned OIDC clients for all 12 consumers
(state.json: kubernetes, paperless, forgejo, argocd, grafana, immich, minio,
vault, poimen, api-gw, queue-sqs, homarr). Authentik's remaining job was to
serve until the last consumer moved.
Remove every Authentik artifact from GitOps:
- ArgoCD Application `authentik` and its Helm values
- ArgoCD Application `iam-jobs` (provisioner RBAC + migration job)
- ArgoCD Application `oidc-bootstrap` (one-shot apiserver restart)
- authentik-db CNPG cluster definition
- authentik-secrets SOPS file and secret-generator entry
- Ingress rule, cloudflared tunnel hostname, CoreDNS rewrite
- Prometheus alerts and ServiceMonitor
- admin OIDC ClusterRoleBinding provisioning binding
- scripts/iam/provision-rbac.py (the Authentik provisioner)
The chart repo https://charts.goauthentik.io is removed from the AppProject.
Kube-apiserver OIDC and consumer configs are switched to Zitadel in the
local cluster-config (gitignored) and applied separately via talosctl.
The Terraform root module duplicated a writer that the provisioning script could
be, and its state carried generated client secrets. Declare the objects in the
script instead: project, 19 roles, 12 OIDC apps, applied through the management
API with the iam-admin PAT. State is a 0600 JSON file under ~/.config/zitadel.
Also add zitadel to the Cloudflare tunnel hostname list, with the import command
and the note that the tunnel's Public Hostname entry is a separate dashboard
step: DNS alone yields a 404 from the tunnel.
Zitadel first boot completed on 2026-10-26, so set FirstInstance.Skip: true and
initJob.enabled: false. Without Skip, every sync re-runs machinekeyWriter and
silently rotates the iam-admin key that terraform/zitadel authenticates with.
Also wire wireguard_allowed_cidr into the WireGuard security-group rule; the
variable was declared but the rule still used relay_admin_cidr, which blocks
the homelab peer unless its egress address happens to match the operator CIDR.
- remove terraform/terraform.tfvars.local.bak from the index (it carries the
live cluster PKI: CA keys, service-account key, secretbox secret)
- ignore terraform/*.bak and terraform/edge/.secrets/
- generate a fresh WireGuard relay keypair into the git-ignored local tfvars;
the committed example now documents rotation instead of holding key material
Adds terraform/edge, a root module with two providers:
- Cloudflare 5.28.1: CNAMEs to the existing tunnel, plus an opt-in resource that
can take the tunnel ingress list over when manage_tunnel_config is true. It is
off by default because that resource REPLACES the whole list, and the tunnel is
remotely managed from the dashboard today.
- Tencent Cloud 1.83.38: VPC, subnet, security group with three rules, key pair,
one CVM relay with cloud-init (WireGuard listener, forwarding sysctls, Caddy),
and DNSPod A records for the .cn hostnames.
The design keeps the homelab inbound-free: the relay terminates TLS in China and
forwards over a WireGuard tunnel the cluster dials outbound. Cloudflare keeps
serving every existing hostname untouched.
Verified: terraform init succeeds for both providers, fmt clean, validate
reports 0 errors, and plan resolves to 10 creates with placeholder variables.
Provider-side name validation caught a real bug during that run: key pair and
security group names allow only letters, digits and underscore, so the derived
names use underscores.
Not applied. Credentials come from CLOUDFLARE_API_TOKEN,
TENCENTCLOUD_SECRET_ID and TENCENTCLOUD_SECRET_KEY; the WireGuard private key is
TF_VAR only. State is local and git-ignored like terraform/zitadel.
Blocked on facts this module cannot invent: the real .cn domain, whether it has
an ICP filing, the CVM image ID, the operator CIDR, and the homelab WireGuard
public key.
Adds terraform/zitadel: the provisioning half the ArgoCD wiring left open.
It declares 1 project, 19 project roles, 12 OIDC applications, 5 machine users
plus keys, one API application for the gateway, project grants, per-agent claim
metadata, and a disabled groups-claim action.
Facts established while writing it:
- Client IDs cannot be preserved. ZITADEL v4.19.2 CreateOIDCApplicationRequest
has no client_id field, and provider 3.8.7 rejects a settable application id
(terraform validate reports "Invalid or unknown key"). Plan §3.2 assumed a
client_id-preserving re-point; every consumer now also changes client ID.
- Role keys equal the existing group names, so the groups claim can be
byte-identical and no RBAC file changes. v4.19.2 allows role keys of 1-200
characters with no character-set restriction.
- Machine users get JWT profile keys, not passwords. Per-agent claims move to
user metadata and reach tokens only through the groups-claim action.
- The action stays gated: enable_groups_claim_action defaults to false because
the action runtime API is unverified. Two validations stop a null script from
reaching file().
Checks: terraform fmt -check -recursive clean, terraform validate 0 errors,
plan reaches only the expected missing-credentials error, structural gate
0 error / 0 warning, and one reviewer round: BLOCK on a plan-time
fileexists(null) and on the missing api-gw machine path, both fixed, re-reviewed
to SIGN-OFF.
State is local and git-ignored; it carries generated client secrets and machine
keys. Move it to MinIO before a second operator applies.
Not applied: no Zitadel instance is running yet.
Coexistence wiring for the Authentik replacement: ZITADEL gets its own CNPG
cluster, SOPS masterkey, Helm values and ArgoCD Application in namespace iam.
Authentik keeps serving; no consumer is moved and no Authentik object changes.
- k8s/infra/databases/zitadel-db.yaml: CNPG Postgres 16.2, 3 instances
- k8s/infra/iam/zitadel-values.yaml: chart 10.4.0 pinned, DSN from the CNPG
secret, masterkey from a Secret, metrics ServiceMonitor, second-sync caveat
- k8s/argocd/apps/30-security.yaml: Application named zitadel, wave 3,
automated sync deliberately absent for the first sync (chart PreSync hook
re-run risk)
- k8s/argocd/secrets/zitadel-masterkey.enc.yaml: 32-byte masterkey, SOPS
- k8s/infra/monitoring/alerts/svc-zitadel-rules.yaml: target-down, login-UI,
crash-loop and PreSync-job-failed rules
- terraform/files/coredns/Corefile: in-cluster rewrite for the zitadel host
Not applied. The cluster is mid-incident: vault-0 and authentik-server are 0/1,
authentik-db-2 has 14 restarts, prometheus-0 is 1/2 CrashLoopBackOff and
kube-state-metrics has 1925 restarts. Two manual steps still gate the first
sync: the Cloudflare Tunnel public hostname plus a proxied CNAME, and
terraform apply + make apply-cp for the CoreDNS rewrite (Talos-owned).
The offline masterkey copy is at ~/zitadel-masterkey.offline (mode 600). Move it
to sealed storage and delete it before the first sync. Losing it makes every
encrypted ZITADEL value unreadable.
The disk behind /var/lib/longhorn-disk4 (sde1, 2TB PERC H710P virtual disk)
failed: 54 million SCSI I/O errors and climbing at ~7,000/minute, against 3 on
every other device on the node. Its XFS filesystem hung, so five Longhorn
replica processes and the longhorn-manager blocked in uninterruptible D state
in openat and unlinkat. D-state processes cannot be killed, so they held the
node's three replica rebuild slots permanently: no new replica could start on
talos-cp-2 and every new volume there failed to attach with DeadlineExceeded.
That is what stopped the azhi database from coming up.
Removed from all three declarations so it cannot come back:
- terraform.tfvars.local.bak, longhorn_disks for talos-cp-2
- the Longhorn add-disks Job, which would otherwise re-register it
- the Job's disk-count guard, 5 -> 4, or it would fire on every run once the
disk is gone
The running cluster already has the disk fenced
(allowScheduling=false, evictionRequested=true); the replicas are evicted and
the device retired at the controller after that.
Adds the pi coding-agent sandbox's cluster side:
- `k8s/infra/poimen/pi-agent-workspace-pv.yaml` — static hostPath PV + PVC for the sandbox /workspace (Longhorn cannot attach new volumes in this cluster; this gives real persistence and survived a pod delete/recreate test: marker file + 38 KB session.jsonl intact, the session answered from its own history afterwards).
- `k8s/argocd/secrets/pi-sandbox-secrets.enc.yaml` — the sandbox's API-key Secret, SOPS-encrypted (recipient from .sops.yaml, payload only: `encrypted_regex ^(data|stringData)$`), registered in the ksops generator. Round-trip verified against the live Secret.
- `terraform/files/coredns/Corefile` — `rewrite name pi.riotpiao.com ingress-nginx-controller...` so the hostname resolves in-cluster.
Note: the keys currently in that Secret are the ones leaked earlier and must be rotated; re-encrypt with `sops -e -i` after rotating.Reviewed-on: #54
Co-authored-by: rock <[email protected]>
- Change container port from 8188 to 8888 (Caddy proxy binding)
- Remove PVC, use emptyDir for ephemeral models/output
- Replace startup + readiness probes with single liveness probe
- Remove explicit COMFYUI_FLAGS (container defaults work)
- Pod now reaches Ready state immediately after image pull
Fixes GPU contention by using ephemeral storage. ComfyUI now runs and is accessible at https://comfy.riotpiao.comReviewed-on: #18
Co-authored-by: poimen <[email protected]>
Reviewed-on: riotpiao-poimen/homelab-frontend#21
Co-authored-by: rock <[email protected]>
Cloudflare Tunnel's origin service for img.riotpiao.com self-references
the same public hostname (same pattern as paperless/forgejo/authentik) -
without this rewrite, cloudflared's in-cluster DNS resolution has
nowhere to loop back to, and TLS to the raw ingress-nginx service name
fails cert validation (cert is only valid for *.riotpiao.com).
Adds permissions claim + per-service admin groups in Authentik, scoped
Role/RoleBinding per service, public PKCE kubernetes OAuth2 client, and
kube-apiserver OIDC extraArgs. Also fixes paperless OIDC signup permissions
via adapter override and adds CoreDNS rewrite for authentik.riotpiao.com.
Fixes controlplane.tftpl's install.wipe:true (should be false, live CPs already run false) and syncs coredns Corefile back to what's actually deployed (drops an unrolled-out, stale Kong-era rewrite).
Adds optional cloudflare_talos_sans (machine.certSANs, talos API :50000) and
cloudflare_apiserver_sans (cluster.apiServer.certSANs, kube-apiserver :6443) per
control-plane node. cp-1 gets cp1.homelab + cp1-talos.homelab; cp-2/cp-3 get
their cpN-talos.homelab. Values set in gitignored tfvars.
Consolidate three separate Forgejo Actions (argocd-sync, security-scan, validate-k8s) into single cluster-ci workflow for cleaner CI/CD pipeline with proper job sequencing and reduced auth overhead.
Add Terraform configuration for Talos cluster machine configs:
- Provider setup for Talos
- Centralized variables for CP and worker configs
- Template-based config generation for controlplane.yaml and worker-*.yaml
- Sensitive data separated in terraform.tfvars (gitignored)
- Local state tracking for infrastructure
Phase 0 continuation: enable ArgoCD to decrypt SOPS-encrypted secrets on sync.
1. Update ArgoCD Helm values (terraform/argocd-bootstrap.tf):
- Add SOPS_AGE_KEY_FILE env var to repoServer
- Mount sops-age K8s Secret at /home/argocd/.sops
- Add ConfigManagementPlugin for SOPS (detects *.enc.yaml files)
2. Update encrypted secrets with real values:
- k8s/base/secrets.enc.yaml: encrypted with actual service credentials
- All secret values encrypted at rest in git
- ArgoCD decrypts on sync using K8s Secret + AGE key
Prerequisites:
- K8s Secret created: kubectl create secret generic sops-age -n argocd --from-file=keys.txt=/Users/rockliang/.sops/key.txt
- SOPS_AGE_KEY_FILE env var set in ArgoCD repoServer (done above)
Next: Phase 1 — migrate 9 hookless releases to ArgoCD + create Applications that reference encrypted secrets.
Migrate terraform state from local file to MinIO S3 bucket (terraform-state).
Backend config: https://minio-api.riotpiao.homelab.com (external endpoint).
State now persisted remotely, shared across team, safe for cluster rebuild.
Also added terraform-state bucket to MinIO managed buckets.
Import all live authentik resources (groups, users, oauth2 providers, applications)
into terraform state via authentik-generated.tf. Provider config in authentik-config.tf.
Resources are drift-free and match live cluster.
Add cilium CNI to bootstrap helm_release.for_each (1.19.5, kube-system).
Cilium was unmanaged (helmfile-only); now IaC-owned. Critical path for
cluster rebuild recovery. Adds cilium repo to helm-repositories.tf.