IaC, GitOps & Helm hardening
What this is
Infrastructure as code (IaC) means your deployment is a reviewed, versioned
file instead of a sequence of console clicks; GitOps runs that idea on a
loop — a controller inside the cluster watches a Git repository and continually
reconciles the cluster to match the file. Like a thermostat: you set the
temperature in Git, the controller keeps nudging the room until it matches, and
a hand-edit on the live cluster is an opened window the controller quietly
closes again. probectl ships that whole stack, declarative (you state the
desired end state; the tooling works out the steps) and secure-by-default: a
hardened Helm chart, Terraform modules that wrap it, and ArgoCD/Flux manifests
so that a git push is the only deploy action you take. Everything is
HTTPS-by-default and refuses to run with default credentials.
%%{init: {'theme':'base','themeVariables':{'background':'#0d1117','primaryColor':'#161b22','primaryTextColor':'#e6edf3','primaryBorderColor':'#3b82f6','lineColor':'#8b949e','secondaryColor':'#21262d','tertiaryColor':'#0d1117','clusterBkg':'#161b22','clusterBorder':'#30363d','fontFamily':'ui-monospace, SFMono-Regular, Menlo, monospace'},'flowchart':{'curve':'basis','nodeSpacing':55,'rankSpacing':55,'padding':12}}}%%
flowchart LR
subgraph Git
V[values overlay\nsmall/medium/large/multitenant]
end
V --> TF[Terraform module] --> H[Helm chart]
V --> GO[ArgoCD / Flux] --> H
H --> K[Kubernetes:\nDeployment · Service · Ingress(HTTPS)\nNetworkPolicy · PDB · HPA]
Three ways in (one chart)
| Path | Use it when | Where |
|---|---|---|
| Helm | manual / scripted installs | deploy/helm/probectl |
| Terraform | infra-as-code alongside the cluster + DB | deploy/terraform/ |
| Terraform resources | reviewed API resource changes after install | terraform-provider-probectl or deploy/terraform/modules/probectl-resources |
| GitOps (ArgoCD/Flux) | continuous reconcile from Git | deploy/gitops/ |
All deployment paths deploy the same hardened chart — Terraform and GitOps
just wrap it. After deployment, the native terraform-provider-probectl manages
served /v1 and /provider/v1 resources directly; the
modules/probectl-resources module is the no-plugin fallback that shells out to
the checked-in probectl api command.
Hardened Helm chart
values.schema.json types every key (Helm validates input against it on
install/upgrade). Three Kubernetes objects in the table deserve a one-line
introduction: a NetworkPolicy is the pod's firewall (which traffic may come
in, where traffic may go out); a PodDisruptionBudget (PDB) is the floor on
how many replicas voluntary maintenance — a node drain, an upgrade — may take
down at once; a HorizontalPodAutoscaler (HPA) adds and removes replicas to
follow load. The secure defaults below are enforced by the CI hardening gate
(make helm-gate), which renders the chart and greps for each invariant — a
regression here fails the build:
| Control | Default |
|---|---|
| Pod identity | non-root (runAsNonRoot: true), uid pinned to 65532, fsGroup set |
| Container | readOnlyRootFilesystem, allowPrivilegeEscalation: false, capabilities drop ALL, seccomp RuntimeDefault |
| Service account | token automount off (automountServiceAccountToken: false) — no Kubernetes API access |
| Ingress | HTTPS-only, HSTS, HTTP→HTTPS redirect, ClusterIP service (no plaintext API) |
| Credentials | render fails without an envelope key or session-HMAC key (no default creds); secrets supplied via existingSecret |
| Probes | readiness /readyz (flips to 503 while draining, for zero-downtime rollouts), liveness /healthz |
| Network | NetworkPolicy is on by default in every profile; API ingress is restricted to the ingress-controller namespace by default (see the note below) |
| Disruption | PodDisruptionBudget (medium / large / multitenant) for zero-downtime upgrades |
| Scale | HorizontalPodAutoscaler (large profile; it then owns the replica count instead of the Deployment) |
About the default NetworkPolicy. It is on in every profile. API ingress is
already limited to the named ingress-controller namespace, so ordinary
in-cluster pods cannot bypass the TLS ingress and talk to the plaintext API
listener. Adjust networkPolicy.ingressFrom to your ingress controller's
labels. The remaining deliberate hole is egress: empty egressTo allows all
non-DNS egress until you name your datastore, bus, IdP, and feed destinations.
values-large.yaml ships the filled-in reference egress shape; copy that
pattern and narrow the CIDRs. The hardening gate checks the default ingress
selector and the strict profile's fully narrowed egress policy.
Reference sizing
The size overlays differ only in runtime sizing — every one of them gets the same HTTPS-by-default, hardened-pod, NetworkPolicy-on posture above.
| Profile | File | Replicas | PDB | HPA | NetworkPolicy |
|---|---|---|---|---|---|
| small | values-small.yaml |
1 | – | – | on (holes open; close them) |
| medium | values-medium.yaml |
3 | minAvailable 2 | – | on (holes open; close them) |
| large | values-large.yaml |
4 → HPA 4–12 | minAvailable 3 | on | on (filled allow-lists) |
| multitenant | values-multitenant.yaml |
3 + anti-affinity | minAvailable 2 | – | on (holes open; close them) |
Two more overlays ship for specialized profiles: values-strict.yaml (the
regulated/hardened profile the gate renders) and values-multiregion.yaml
(active-active HA — see multi-region.md).
The selected two-region reference is executable configuration, not prose:
deploy/terraform/examples/two-region
instantiates the module against two Kubernetes providers, while
applicationset-multiregion.yaml
declares the same two releases for continuing ArgoCD reconciliation. PostgreSQL
writer and local-read DSNs remain Secret-only. Synchronous metadata replication,
RPO 0, and RTO <=60s are the acceptance thresholds; ClickHouse and object
recovery remain distinct backup/replication measurements and never inherit the
metadata number by implication.
helm install probectl deploy/helm/probectl -f deploy/helm/probectl/values-medium.yaml \
--set ingress.host=probectl.example.com --set ingress.tlsSecretName=probectl-tls \
--set control.tls.existingSecret=probectl-tls \
--set-string image.digest='sha256:<release-digest>' \
--set secrets.envelopeKey="$(openssl rand -base64 32)"
Private PKI. control.trustBundle.existingConfigMap names a ConfigMap holding
the operator's CA bundle; the chart mounts it read-only into both containers and
sets SSL_CERT_FILE, so an IdP, SIEM, CMDB or sslmode=verify-full database
behind an enterprise CA verifies without any skip-verify switch (DPR-026). See
the chart README.
Config-as-code
The declarative config is the Helm values: control.*, oidc.*,
database.url, and control.extraEnv map to PROBECTL_* environment variables
via the chart's ConfigMap; the size overlays are the reference config. Commit
your overlay, point Terraform or Argo/Flux at it, and the cluster converges to it.
Terraform
deploy/terraform/modules/probectl deploys the chart plus a Kubernetes
Secret for the sensitive config — so credentials never land in the ConfigMap or
release values. It requires image_digest; the deprecated image_tag input is
accepted only in <version>@sha256:<digest> form and discards the mutable tag.
Pre-create the module's ingress_backend_tls_trust_secret with the complete
ingress-nginx proxy-ssl Secret contract: tls.crt, tls.key, and the control
listener issuer in ca.crt. The module enables ingress-nginx certificate and
name verification and derives the expected name from ingress_host unless
overridden. This is ingress-nginx's outbound identity/trust Secret, not the
public ingress or control-listener TLS Secret.
It's cloud-agnostic: point the providers at any kubeconfig. The
module interface (inputs / outputs / secret handling) is documented in
deploy/terraform/README.md. The native
terraform-provider-probectl then manages tests, alert routes, Provider/MSP
tenants, and advanced API resources against the self-hosted control plane. Its
read-only data sources look up or page through tenants, tests, and agents, so
modules can compose with existing probectl objects without importing them as
managed resources. Tenant and agent/test reads remain tenant-scoped; provider
tenant inventory uses the separate operator privilege domain. make terraform-gate runs terraform fmt -check and terraform validate against the
single-region and two-region roots; provider data-source
examples live in deploy/terraform/examples/provider-data-sources.
GitOps
deploy/gitops/ has an ArgoCD Application (argocd/application.yaml), a
two-region ArgoCD ApplicationSet
(argocd/applicationset-multiregion.yaml), and a Flux OCIRepository +
HelmRelease (flux/) that pin and cosign-verify the signed OCI chart. All reference
secrets.existingSecret rather than inlining credentials — Git history is
forever, so a secret must never enter it. Manage that Secret
with Sealed Secrets or the External Secrets Operator (both keep only an
encrypted or referenced form in Git; a cluster-side controller materializes the
real value). The examples also reference a probectl-backend-ca Secret; manage
its tls.crt, tls.key, and ca.crt through the same controller so
ingress-nginx can authenticate and verify the HTTPS control listener. Replace
each manifest's invalid image.digest placeholder with the
signed release or approved-mirror digest before syncing; leaving it unchanged
fails Helm validation. ArgoCD automated
sync (prune deletes resources removed from Git; selfHeal re-applies the
desired state over hand-edits) and Flux's install/upgrade remediation.retries
together give a self-correcting, auto-rolling-back deployment. See
deploy/gitops/README.md. make gitops-gate
structurally validates the manifests (every doc has an apiVersion + kind).
CI gates
helm-gate— lints every profile, asserts the hardening invariants above, and validates the GitOps manifests and the compose config. (This is the CI job name to require in branch protection;make gitops-gateruns inside it.)terraform-gate—terraform fmt -check+validateof the module through both deployment roots.gitops-gate— amaketarget: the ArgoCD/Flux manifests are well-formed (apiVersion+kind).
Scope
This stack includes single-cluster and exact two-region IaC/GitOps roots over a secure-by-default chart. Active-active multi-region topology and DR is documented separately (multi-region.md, ops/dr.md, runbooks/region-failover.md) and is an Enterprise entitlement (the validated failover runbooks and support, not the fence itself). The FIPS build is likewise the Enterprise-distributed artifact; the STIG/CIS-style hardening checklist itself is public — see hardening.md.