0005 — Control-plane hardening (etcd + kube-apiserver)¶
- Status: proposed
- Status note (2026-07-26): most of the decision has shipped, but the
live control plane diverges from the binding apiserver items, so this stays
proposedrather than flipping to accepted. Shipped and verified: E1 (cluster/monitoring/etcd-snapshot-cronjob.yaml+ansible/playbooks/etcd-snapshot-install.yml; liveetcd-snapshotCronJob on the 6 h schedule), E2 (cluster/monitoring/servicemonitor-control-plane.yaml), E3 (live--quota-backend-bytes=8589934592— later doubled past the decided 4 GiB), A4 (cluster/monitoring/priority-fairness.yaml; FlowSchemas live), M (all five custom alerts incluster/monitoring/alerting-rules.yaml), S1 (terraform/cloudflare.tfAccess application + policy for both API hostnames), R (six skills,ansible/playbooks/control-plane/AGENTS.md,docs/runbooks/etcd-restore.md). Diverged: the entire T2 flag tier is staged but not applied —ansible/playbooks/tune-apiserver-{audit,oidc,encryption,anonymous-auth}.ymlexist in-tree, yet all three live apiservers run zero--audit-*,--oidc-*,--encryption-provider-config, and--anonymous-authflags (verified against the static-pod specs 2026-07-26). A1/A2/A3/A5 therefore remain decisions without live effect until an attended T2 rolling-restart window executes those playbooks. - Date: 2026-04-18
- Deciders: lusoris
Context¶
The etcd + kube-apiserver deep-dive (.workingdir2/research/18-etcd-apiserver/)
flagged nine material gaps on the 4-node kubeadm control plane: no etcd
snapshot backups (defrag-only, manual recovery path), no kube-apiserver audit
trail, no OIDC flow (kubeconfigs are hand-distributed), no encryption-at-rest
for Secrets, --anonymous-auth left at the k8s ≤1.31 default, no dedicated
Grafana dashboard or custom PrometheusRules for the control plane,
no ServiceMonitor pinning scrape targets to real etcd members with mTLS,
no Priority-and-Fairness tuning for known-noisy clients (argocd-repo-server,
kyverno reports-controller, hubble-relay), and the public API endpoints
(k8s-home.infra.cauda.dev, k8s-office.infra.cauda.dev) are reachable
through cloudflared without a Cloudflare Access policy. This ADR records the
decisions taken to close those gaps in one coordinated change set, aligned
with the established deep-dive pattern (config, dashboards, alerts, skills,
hooks, AGENTS.md, runbook) used by the ArgoCD, Hubble, Tetragon, and Kyverno
sweeps.
Decision¶
The control plane is hardened along four axes. Every item below is binding; no hedging.
etcd:
- E1 — Backups: ship etcd snapshots every 6 h, retain 14 days, target is
MinIO Office (Synology-backed S3 endpoint) at
s3://etcd-backups/scheduled/etcd-snapshot-<RFC3339>.db. Same pattern as Longhorn / Loki / CNPG. Not direct NFS — the Synology only speaks NFSv3 and the S3 gateway gives versioning + lifecycle for free. - E2 — Scrape target: declare an explicit
ServiceMonitorfor etcd (port 2381) with mTLS client certs mounted from the kubeadm PKI. Replaces reliance on kube-prometheus-stack chart defaults. - E3 —
quota-backend-bytes: bump to 4 GiB (4294967296). Preemptive headroom for the growing CRD footprint; restore time impact acceptable at that size. - Storage tier: unchanged. The per-node SSDs are fast enough; a dedicated partition is not on the critical path.
kube-apiserver:
- A1 — Audit logging: layered policy —
Metadatadefault,RequestResponsefor the sensitive verb set (secrets,configmaps,roles,rolebindings,clusterrolebindings,serviceaccounts/token). Log to node disk, ship to Loki via the existing Alloy DaemonSet. - A2 — OIDC: wire Authentik as OIDC provider for the apiserver
(
--oidc-issuer-url,--oidc-client-id,--oidc-username-claim=email,--oidc-groups-claim=groups). Ship a/kubeconfig-oidcskill that generates a per-user kubeconfig usingkubectl oidc-login. Authentik Application + Provider declared as an in-repo blueprint. - A3 — Encryption at rest:
EncryptionConfigurationwith AES-GCM onsecretsonly (keeps the plugin-free path; no KMS dependency). Key distributed to CP nodes via Ansible, sealed via sealed-secrets. - A4 — Priority & Fairness: declare
FlowSchema+PriorityLevelConfigurationfor argocd-repo-server, kyverno reports-controller, and hubble-relay so they can't starve the default pool during backlog catch-up. - A5 — anonymous-auth:
--anonymous-auth=false. Health probes are switched to authz'd endpoints during the rollout.
Monitoring:
- M — Full package: enable the chart-bundled Grafana dashboards
(
etcd,kubernetes-system-apiserver) via kube-prometheus-stack values, add cauda-customPrometheusRules(EtcdNoLeader,EtcdDBSizeApproachingQuotaat 80 %,EtcdBackupMissing,ApiserverAuditLogBackpressure,ApiserverRequestThrottled), and declare an Alertmanager inhibit hierarchy —EtcdMembersDowninhibitsApiserverRequestErrors;ApiserverAllDowninhibits downstream workload alerts.
Secure access:
- S1 — Cloudflare Access: add a
cloudflare_zero_trust_access_applicationplus acloudflare_zero_trust_access_policyresource interraform/cloudflare.tffor bothk8s-home.infra.cauda.devandk8s-office.infra.cauda.dev, with Authentik as the SAML/OIDC IDP on the Access side. Audit of the current Terraform state confirmed neither resource exists today.
Runbook / skills / AGENTS.md:
- R — Full: six skills (
/etcd-snapshot,/etcd-restore,/etcd-member-status,/apiserver-flags-audit,/cp-upgrade-checklist,/kubeconfig-oidc), anAGENTS.mdatansible/playbooks/control-plane/consolidatingtune-etcd/fix-home-kubelet-apiserver/ the new snapshot cron and upgrade sequence, and a restore runbook atdocs/runbooks/etcd-restore.md.
Implementation tiers¶
- T0 — manifests-only, ArgoCD-sync-safe:
ServiceMonitor,FlowSchema/PriorityLevelConfiguration, customPrometheusRules, kube-prometheus-stack values update (dashboards + inhibits), etcd snapshotCronJob+ MinIO user + sealed creds, Authentik OIDC Application blueprint. - T1 — Ansible node-side, no apiserver flag change: etcd
quota-backend-bytesbump, audit policy file distribution,EncryptionConfiguration+ key distribution. - T2 — Ansible apiserver-flag changes, requires a rolling CP restart:
--audit-*,--oidc-*,--encryption-provider-config,--anonymous-auth=false. - T3 — Terraform Cloudflare + Authentik IDP coordination: Access Application + Policy resources.
Consequences¶
Positive:
- First genuine etcd recovery path. MTTR moves from "unbounded, manual" to a documented 30–60 min restore for the whole 4-node cluster.
- Every mutation to the cluster becomes attributable through the audit trail; incident triage gains a searchable source of truth via Loki.
- SSO replaces long-lived static kubeconfigs; offboarding a user becomes a single Authentik action instead of tracking down credential copies.
- Secrets gain a second encryption barrier on top of disk encryption.
- Noisy clients can no longer starve the apiserver request queue.
- Dashboards and inhibit hierarchy shorten alert-to-diagnosis time on CP incidents and suppress the downstream alert storm.
- Public API endpoints gain an identity-bound gate; a stolen kubeconfig is no longer sufficient to reach the apiserver from the internet.
Negative:
- Every T2 flag change is a rolling restart of all four CP nodes — ~15 min of degraded-but-not-down apiserver per pass, serialised across nodes. A maintenance window is required.
- The audit log consumes node disk at non-trivial rate (
RequestResponseon sensitive verbs writes bodies). Node disk pressure becomes a monitored quantity; rotation policy must be tuned. - The OIDC rollout has an adoption curve — every existing kubeconfig user
must switch to
kubectl oidc-loginwithin a grace period before static kubeconfigs are retired. Scripts, CI jobs, and iOS (Nautik) configs all need a migration pass. - Cloudflare Access requires a working Authentik IDP hookup on the CF
side; if Authentik is down, the public API endpoints are unreachable
(site-local
10.{1,2}.20.10:6443remains available to admins). - Six new skills, a new
AGENTS.md, dashboards, and rules broaden the surface the deep-dive pattern asks future contributors to keep in sync.
Alternatives considered¶
- Migrate to Talos. Rejected for this pass — scope is unbounded and the kubeadm cluster is stable. Revisit once the T2 work is stable.
- Use Velero for etcd backups. Rejected — Velero snapshots k8s
resources and PVs via the API, not the etcd data store itself. Wrong
tool for a control-plane disaster where the apiserver is the thing that
may be unavailable.
etcdctl snapshot saveis the correct mechanism. - Defer everything and accept the risk. Rejected — the inventory identified nine gaps that compound. In particular, "no etcd backup" is a single-incident-away-from-rebuild failure mode.
References¶
.workingdir2/research/18-etcd-apiserver/01-inventory.md— current-state inventory..workingdir2/research/18-etcd-apiserver/02-decision-log.md— locked decisions (round 1 + round 2)..workingdir2/research/18-etcd-apiserver/99-decision-matrix.md— full option set.docs/adr/0001-record-architecture-decisions.md— ADR process.docs/adr/0002-dual-cloudflared-split-dns.md— cloudflared tunnel topology that fronts the public apiserver hostnames.docs/adr/0003-ipv6-phased-rollout.md— dual-stack Service CIDR kubeadm patch that will land alongside A5's rolling CP restart.docs/adr/0004-latest-tag-remediation.md— pinning discipline that applies to every chart/image this ADR touches.- kube-prometheus-stack chart — bundled etcd + apiserver dashboards and default rule groups.
- kubeadm reference — control-plane configuration, patches, and upgrade flow.
- int128/kubelogin —
kubectl oidc-loginplugin backing the/kubeconfig-oidcskill. - Authentik — Kubernetes provider — Application + Provider configuration for the apiserver OIDC flow.