Each runtime setting has one authoritative value. The chart rejects obsolete
duplicates instead of silently choosing one and allowing the process, Service,
or generated HAProxy configuration to drift.
Previous value
Authoritative value
controller.crdName
controller.configName
controller.debugPort
controller.ports.healthz
controller.config.controller.healthzPort
controller.ports.healthz
controller.config.controller.metricsPort or controller.extraEnv[].name=METRICS_PORT
controller.ports.metrics (0 disables metrics)
controller.config.dataplane.port
haproxy.ports.dataplane
haproxy.dataplane.logLevel
haproxy.agent.logLevel
haproxy.dataplane.resources
haproxy.agent.resources
haproxy.dataplane.extraEnv
haproxy.agent.extraEnv
haproxy.dataplane.service
haproxy.agent.service
haproxy.dataplane.validateConfig
Removed. The pod's own HAProxy binary judges the configuration at reload, and the webhook and the config-load gate still run the full haproxy -c
haproxy.dataplane.debugSocketPath
Removed. Profile the agent through its own metrics and GET /v1/state
haproxy.dataplane.aclFormat
Removed. It formatted the Data Plane API's own access log; the agent logs one structured line per apply instead
haproxy.dataplaneBin
Removed. The agent is the controller's binary, in the controller's image
controller.config.templatingSettings.extraContext.prometheusExporter.excludeMaintServers — HAProxy applies ?no-maint itself, for every scraper
vector.excludeMetrics
controller.config.templatingSettings.extraContext.prometheusExporter.excludeMetrics — same entry names, enabled, families and requires; pattern is gone, HAProxy's exporter filters by exact family name
vector.podMonitor and spoaHub.monitoring.podMonitor
haproxy.monitoring.podMonitor — one PodMonitor for every metrics endpoint on the HAProxy pod
Cache and rate-limit settings introduced after the previous release use their
final ownership from the start: cache.varnish owns the Varnish workload,
cache.haproxy owns HAProxy cache integration, rateLimit.shared owns the
feature, and rateLimit.shared.managedStore owns the optional bundled Valkey
topology. Plugin execution remains under spoaHub.plugins.*.
Helm installs the CRDs in crds/ once and never upgrades them on a subsequent
helm upgrade. This hook Job runs haptic apply-crds (server-side
apply) so additive CRD schema changes reach the cluster on install and upgrade.
Parameter
Type
Default
Description
crds.upgradeJob.enabled
bool
true
Run the pre-install/pre-upgrade hook Job (and its scoped RBAC) that server-side applies the bundled CRDs. Disable if you manage CRDs out-of-band or lack cluster-scoped CRD write permission at upgrade time
crds.upgradeJob.backoffLimit
int
2
Job retry limit (the apply is idempotent, so retries are safe)
crds.upgradeJob.activeDeadlineSeconds
int
300
Job wall-clock deadline
crds.upgradeJob.resources
object
cpu 50m / memory 64Mi–128Mi
Resource requests and limits for the apply Job pod
crds.upgradeJob.annotations
map
{}
Extra annotations for the Job (merged with chart defaults)
crds.upgradeJob.labels
map
{}
Extra labels for the Job (merged with chart defaults)
A pre-install/pre-upgrade hook Job renders the chart embedded in the controller image with this release's values and runs the controller's own load gate over the result — structural validation and the full validationTests suite including haproxy -c — before any object is applied. A failing configuration fails the release; the previous release keeps serving. Argo CD runs it as a PreSync hook. The fail-closed load gate still guards every path that skips hooks (--no-hooks, kubectl, rollback).
Parameter
Type
Default
Description
preRolloutValidation.enabled
bool
true
Run the gate. The Job hard-fails when the controller image's embedded chart version differs from the chart being installed — validating the wrong chart would pass on the wrong input — so disable it when deliberately running a drifted image
preRolloutValidation.backoffLimit
int
1
Job retry limit
preRolloutValidation.activeDeadlineSeconds
int
600
Job wall-clock deadline. Generous: schema fetch, engine compile, and ~700 haproxy -c checks on a possibly cold node
preRolloutValidation.resources
object
cpu 200m / memory 256Mi–512Mi
Resource requests and limits for the validation Job pod
Number of controller replicas (2+ recommended for HA with leader election)
haproxyVersion
string
"3.4"
HAProxy major.minor series. Drives both the controller image tag suffix (:<version>-haproxy<haproxyVersion>) and — combined with haproxyPatchVersions — the HAProxy pod image tag
haproxyPatchVersions
map
See values.yaml
Per-haproxyVersion community patch pins (for example "3.2": "3.2.x"). Maintained by the chart and auto-updated by Renovate
haproxyEnterprisePatchVersions
map
See values.yaml
Per-haproxyVersion enterprise revision pins (for example "3.2": "3.2r1"). Used when haproxy.enterprise.enabled=true
controller.image.repository
string
registry.gitlab.com/haproxy-haptic/haptic
Controller image repository
controller.image.pullPolicy
string
IfNotPresent
Image pull policy
controller.image.tag
string
""
Controller image tag without the HAProxy-series suffix; empty uses the chart appVersion. The rendered reference is always <tag>-haproxy<haproxyVersion>, so the suffix is appended to an explicit value too
nameOverride
string
""
Override chart name
fullnameOverride
string
""
Override full release name
commonLabels
map
{}
Labels added to every chart-rendered resource on top of the standard app.kubernetes.io/* set
commonAnnotations
map
{}
Annotations added to every chart-rendered resource that has an annotations block
controller.deploymentAnnotations
map
{}
Annotations added only to the controller Deployment (in addition to commonAnnotations); useful for hooks like reloader's reloader.stakater.com/auto: "true"
extraDeploy
list or map
{}
Free-form Kubernetes resources to render alongside the chart. Each entry is rendered through tpl so it can reference chart values. Map form (keys → manifests) is convenient for composing across multiple values files
Name of the HAProxyTemplateConfig object the controller watches
controller.logLevel
string
INFO
Initial controller log level (LOG_LEVEL env var) — see Logging and templating for the runtime override
controller.kubeClient.qps
float
-1
Client-side apiserver queries per second (QPS) for the controller's own requests (KUBE_CLIENT_QPS env var). <= 0 disables client-side throttling and relies on apiserver Priority & Fairness; a positive value installs one shared client-side rate limiter across all the controller's clients
controller.kubeClient.burst
int
0
Client-side apiserver burst (KUBE_CLIENT_BURST env var); used only when controller.kubeClient.qps > 0 (0 means 2*qps)
controller.ports.healthz
int
8080
Single source of truth for the controller's /healthz and /debug/* listener, container port, Service, probes, and NetworkPolicy
controller.ports.metrics
int
9090
Single source of truth for the /metrics listener, container port, Service, and monitors; 0 disables metrics and requires all monitor resources to be disabled
Whether the controller writes LoadBalancer addresses back to Ingress/Gateway .status. Disable during a controller migration so the incumbent keeps owning status — with extraContext.statusPatches.enabled: false the status-patch snippets become no-ops
Core HAProxy configuration. Disabling drops the haproxyConfig template the other libraries plug into; leave on unless you supply a complete replacement
controller.templateLibraries.ssl.enabled
bool
true
SSL/TLS and HTTPS frontend support
controller.templateLibraries.ingress.enabled
bool
true
Kubernetes Ingress resource support
controller.templateLibraries.gateway.enabled
bool
true
Gateway API support (HTTP, gRPC, TLS and TCP routes)
Declare that the Gateway API Experimental channel (experimental-install.yaml) is installed. Enables the validationTests that assert experimental HTTPRoute fields (retry per Gateway Enhancement Proposal (GEP) 1731, sessionPersistence per GEP-1619) — Helm can't detect the channel because both installs ship identical CRDs and only HTTPRoute fields differ. The route snippets emit those directives whenever the fields are present, regardless of this flag
Shared ingress-annotations-compat scaffold (level 2.5). Provides parameterized macros consumed by the Ingress vendor annotation libraries below
controller.templateLibraries.governance.enabled
bool
true
Governance rule engine. Enforces declarative constraints over any watched resource; inert until you define controller.config.templatingSettings.extraContext.governance.rules
haproxy-haptic.org/* — HAPTIC's native annotation vocabulary; a best-of-breed superset of the three vendor libraries. The recommended vocabulary for new configs
Worked example of a resource-agnostic library for a custom Route CRD, for learning or exercising the reload-free author contract. Not for production routing (it watches a demo kind) — opt-in
controller.templateLibraries.spoaHub.enabled
bool
false
HAProxy-side Stream Processing Offload Agent (SPOA) hub wiring. Auto-loaded when the SPOA hub sidecar is rendered (any spoaHub.plugins.* enabled, or spoaHub.enabled: true); set this to true to force-load the library standalone
HAProxy bounded-load consistent hashing: cap any one Varnish shard's share to this factor of the mean (0 disables)
cache.haproxy.responseTimeoutMs
int
2000
Inactivity timeout on the Varnish hop. A miss spends it waiting for application response headers. Before headers, a timeout or transport failure retries GET/HEAD directly; after delivery starts, HAProxy terminates an idle partial response because it can't safely replay it. Set this above normal application time to first byte
cache.varnish.enabled
bool
false
Deploy the shared Varnish cache tier and emit the cache routing/backend. Cache-enabled GET/HEAD requests use healthy Varnish shards; other methods and requests observed while every shard is unhealthy go directly to the application backend. The bypass is recorded in cache_degraded and haptic_degraded_cache_total
cache.varnish.loopbackPort
int
8090
Dedicated internal HAProxy port Varnish fetches cache misses from (the "sandwich" backend leg), so the WAF/rate-limit/auth/routing chain runs once on the client request and never on the miss. Reached only by Varnish (via originServiceName, gated by the HAProxy NetworkPolicy); never published on the LoadBalancer
cache.varnish.originServiceName
string
haptic-cache-origin
Name of the internal ClusterIP Service (in the release namespace) that fronts the dedicated backend-fetch port on the HAProxy pods
cache.varnish.workload
string
statefulset
Varnish workload kind: statefulset (ordered rollout keeps 1/N of the cache warm on restart) or deployment (ephemeral accelerator)
cache.varnish.replicas
int
2
Number of Varnish cache shards
cache.varnish.image
string
varnish:9.0
Varnish container image — stock upstream, since the loopback topology needs no custom build. Pin to a digest in production
cache.varnish.imagePullPolicy
string
IfNotPresent
Kubernetes pull policy for the Varnish image (Always, IfNotPresent, or Never)
cache.varnish.malloc
string
256m
Varnish -s malloc,<size> cache size per shard; set to roughly 75% of the pod memory limit
cache.varnish.resources
object
cpu 100m / memory 384Mi
Varnish pod resource requests and limits. A CPU request is required for autoscaling (the HPA's Utilization target is a percentage of the request); keep the memory limit above malloc plus overhead
cache.varnish.podDisruptionBudget
object
enabled true, maxUnavailable1
PodDisruptionBudget settings for the Varnish shards. Pods also prefer separate nodes and use a soft hostname topology spread, so the default still runs on single-node clusters
cache.varnish.networkPolicy.enabled
bool
true
Emit a NetworkPolicy that only allows this release's HAProxy pods to reach Varnish and only allows Varnish egress to DNS plus this release's HAProxy HTTP origin
cache.varnish.autoscaling.enabled
bool
false
Autoscale the Varnish tier with a HorizontalPodAutoscaler. When on, the HPA owns the replica count (the static replicas is ignored)
Enable the native shared rate-limit annotations and auto-enable the bundled rate-limit SPOA plugin. When false, Ingresses carrying haproxy-haptic.org/rate-limit-requests fail the render loudly instead of being silently unprotected. When true, leave rateLimit.shared.managedStore.enabled=true for the chart-managed HA Valkey/Sentinel store or provide rateLimit.shared.externalStore.urls; HAPTIC fails the render instead of silently using a per-pod fallback
rateLimit.shared.failClosed
bool
false
Runtime dependency policy. By default, a Valkey failure degrades both algorithms to a bounded per-sidecar limiter; if the hub/plugin returns no verdict, HAProxy allows the request. These paths set rate_limit_degraded, and plugin metrics distinguish local fallback decisions. Set true to deny when the store or plugin can't answer. Lease mode still spends its existing local lease before applying the store-failure policy
spoaHub.plugins.rate-limit.timeoutMs
int
50
Per-plugin processing timeout for shared rate-limit checks. The chart derives the HAProxy engine's outer deadline from this value; keep it low on public edges so overload reaches the configured lease/exact failure policy quickly
Per-operation Redis/Valkey timeout rendered as the rate-limit plugin's store_timeout_ms. Exact gcra mode waits on this path per request; tune according to measured store round-trip time and failover behavior
rateLimit.shared.managedStore.enabled
bool
true
Deploy chart-managed HA Valkey with Sentinel and inject its store_url into the rate-limit plugin, so budgets are shared across the HAProxy fleet. Leave true for the out-of-box HA store; set false only when you bring your own store via rateLimit.shared.externalStore.urls. Takes effect only when rateLimit.shared.enabled is also true — it's a sub-option of the shared limiter, so on its own it deploys nothing
rateLimit.shared.externalStore.urls
list
[]
One bring-your-own HA Redis/Valkey/Sentinel/Cluster endpoint, used with managedStore.enabled=false (setting both fails the render). Multiple URLs fail validation because the bundled plugin shares one circuit breaker across its shards. Configure the external store with a non-evicting memory policy. The chart owns the generated store_url and rejects a manual store_url/store_urls in spoaHub.plugins.rate-limit.params
rateLimit.shared.managedStore.image
string
valkey/valkey:9.1.1-alpine
Valkey image for the chart-managed shared rate-limit store
rateLimit.shared.managedStore.imagePullPolicy
string
IfNotPresent
Kubernetes pull policy for both the Valkey and Sentinel containers (Always, IfNotPresent, or Never)
rateLimit.shared.managedStore.port
int
6379
Valkey Service port for the chart-managed shared rate-limit store
rateLimit.shared.managedStore.replicas
int
3
Fixed Valkey pod count for the chart-managed Sentinel topology: one writable primary plus replicas for failover. Must be at least 3. This is HA, not automatic horizontal Valkey scaling
rateLimit.shared.managedStore.maxMemory
string
96mb
Valkey --maxmemory for the chart-managed shared rate-limit store
rateLimit.shared.managedStore.maxMemoryPolicy
string
noeviction
Valkey memory policy for the chart-managed shared rate-limit store. This must remain noeviction: evicting a limiter key silently recreates a full budget. At the memory limit, writes fail and follow the configured lease/exact dependency policy instead
rateLimit.shared.managedStore.sentinel
object
port 26379, quorum 2
Sentinel settings for managed-store failover: port, quorum, down-after, failover timeout, parallel syncs, and Sentinel container resources
rateLimit.shared.managedStore.podDisruptionBudget
object
enabled true, maxUnavailable1
PodDisruptionBudget settings for the managed Valkey pods
Emit a NetworkPolicy that only allows HAProxy/SPOA pods and store-internal Valkey/Sentinel traffic to the managed store
rateLimit.shared.managedStore.resources
object
cpu 50m / memory 128Mi
Valkey pod resource requests and limits. The chart-managed store is a fixed-size HA Sentinel topology; use bring-your-own Redis/Valkey infrastructure when you need horizontal store scaling
Request-body inspection and JSON schema validation¶
HAProxy's tune.bufsize, which is also the ceiling on one runtime CLI batch — the controller sizes its map and server batches from it. requestBodyInspection.haproxyBuffer.sizeBytes raises it when body inspection needs a larger buffer; the larger of the two is emitted
HAProxy's tune.cli.max-payload-size, the ceiling on one runtime CLI payload. Emitted on HAProxy 3.4 and above only: the keyword doesn't exist below it, where a payload is capped by tune.bufsize instead
Request-body inspectors need the whole body buffered, so this raises the shared HAProxy tune.bufsize whenever it names the larger of the two. API validation and Coraza policy body caps must fit within sizeBytes - reservedBytes
Wait for the request body before taking a backend connection, so a slow uploader holds an HAProxy buffer instead of a backend server slot. Only requests declaring a Content-Length are held, so gRPC and chunked streaming are never buffered
Default when an Ingress omits request-schema-fail-open. A missing plugin verdict allows the request and sets schema_degraded, exported as haptic_degraded_schema_total; schema validation reduces risk, it doesn't establish identity. Missing referenced Kubernetes objects still fail at render time. Set false where a malformed body reaching the backend is worse than refusing the caller
spoaHub.plugins.api-gateway.timeoutMs
int
25
Hub-side processing timeout for JSON validation. The chart derives the message's outer HAProxy deadline from this value; increase only for measured CPU or scheduling pressure
spoaHub.plugins.api-gateway.maxConcurrency
int/tpl
derived from sidecar memory (16 at the default 256Mi)
Ceiling for concurrent JSON parse/schema evaluations. With adaptiveConcurrency on (default) this is the controller's upper bound, not a fixed limit; derived from spoaHub.resources memory so it self-scales. Set a literal to override
Template-side routing, policy catalogs, and Ingress-author permissions live in the structured extraContext.waf bag so raw HAProxyTemplateConfig users get the same behavior. Process and plugin execution settings live under spoaHub.plugins.coraza; neither tree aliases or overrides the other. A non-empty policies.inline, policies.configMapRefs, or policies.defaultPolicy activates policy governance—there is no second enable flag that can leave a configured catalog inert.
What happens when the SPOA hub or Coraza plugin returns no verdict — a reload, a restart, a timeout. Default allows the request: a WAF reduces risk, it doesn't establish identity, so a blip in HAPTIC's own sidecar mustn't refuse traffic that did nothing wrong. The event is still counted (haptic_degraded_waf_total, and the waf_degraded access-log field), so a WAF silently not inspecting stays visible. Set true only where letting a request past uninspected is worse than refusing it. Requests the WAF did inspect and reject are denied with 403 either way
Which requests the rendered config sends to Coraza: opt-in runs it only on routes carrying a native or compatibility WAF annotation; default-on runs it on every route unless an authorized nginx.ingress.kubernetes.io/enable-modsecurity: "false" opts out. default-on auto-enables the Coraza plugin
WAF enforcement (deny or detect) for requests dispatched by mode: default-on; selected policies and authorized per-route overrides take precedence. Ignored when mode: opt-in
URL of a gzip-compressed tar of an Open Worldwide Application Security Project (OWASP) Core Rule Set (CRS) release. Empty uses the ruleset compiled into the Coraza plugin. When set, HAPTIC fetches and expands the archive, writes the rule files to general storage, and substitutes them for the @crs-setup.conf.example and @owasp_crs/*.conf includes in spoaHub.plugins.coraza.directives — the rest of those directives, including their order, is left alone. Must be https://: the ruleset decides what the WAF blocks, so a plaintext fetch could be replaced in transit and the substituted WAF would still validate. If the ruleset can't be obtained HAPTIC keeps the one already deployed to the fleet, then falls back to the embedded ruleset, so the WAF is never left without rules
How often to re-fetch the ruleset. The request is conditional (If-None-Match / If-Modified-Since), so an unchanged ruleset costs one 304 and triggers no re-render, no push, and no WAF recompile
Highest body cap any policy may request. This is deliberately separate from the default, so approving a larger policy doesn't enlarge every policy. The effective value is enforced by HAProxy and written into that Coraza application
Permit an Ingress to opt out through a compatibility annotation such as enable-modsecurity: "false". This stronger capability is separate from enforcement-mode overrides
Permit arbitrary per-Ingress WAF rules through the nginx-compatible modsecurity-snippet annotation while Coraza governance is active; this grants WAF-policy-author capability
Trusted ConfigMap catalogs keyed by catalog name, each an exact namespace/name/key triple using the same policy schema. A map, like its sibling inline, so adding one catalog keeps the rest
Maximum structured Core Rule Set (CRS) rule-exclusion entries in one reusable policy
spoaHub.plugins.coraza.timeoutMs
int
15
Hub-side processing timeout for WAF evaluation. The chart derives the message's outer HAProxy deadline from this value; this is a failure bound, not expected latency
spoaHub.plugins.coraza.maxConcurrency
int/tpl
derived from sidecar memory (16 at the default 256Mi)
Ceiling for concurrent Coraza evaluations. With adaptiveConcurrency on (default) this is the controller's upper bound, not a fixed limit; derived from spoaHub.resources memory so it self-scales (give the sidecar more memory → higher ceiling). Set a literal to override
spoaHub.plugins.coraza.adaptiveConcurrency
bool
true
The hub resizes the admission semaphore at runtime from Coraza's measured service time (ADR-0002), finding the right concurrency from live latency with no manual tuning; maxConcurrency is then the controller's ceiling. Set false for a fixed cap. Full adaptivity needs a hub image with adaptive support; an older hub ignores the flag and runs maxConcurrency as a fixed cap
Org-wide baselines that namespace teams can't omit. Configured entirely under
extraContext (it creates no Kubernetes resources). The engine is on by default because the annotation library ships one rule (haptic-compress-enable, which makes response compression on-by-default); with that rule disabled and no rules of your own, it does nothing. You
declare a map of generic, JSONPath-driven rules, keyed by a name you choose;
each rule targets a watched resource by name and, per matching resource, either
injects a default when a value is absent or validates the value when
present. Rules apply in sorted key order.
For a step-by-step rollout — audit, fix, then enforce — see the
Governance guardrails how-to. This section is the
field reference.
With enforcement: reject, a new or edited violating resource is denied at the
admission webhook, scoped to that resource's own admission (so one violator never
blocks an unrelated apply), while an already-present violator records a
GovernanceViolation Warning Event and keeps serving. enforcement: audit only
ever warns (a roll-out mode).
Parameter
Type
Default
Description
…extraContext.governance.enabled
bool
true with the default library set
Master switch for the guardrails. The governance library itself declares false; the haptic-annotations library (on by default) merges after it and raises it to true so its shipped haptic-compress-enable rule applies. Set it to false explicitly to switch the engine off
…extraContext.governance.exemptNamespaces
list
[]
Namespaces skipped entirely (infra/system)
…extraContext.governance.rules
map
{}
Admin-declared rules keyed by rule name (see the fields below). A map, not a list, so your rules merge with — rather than replace — the ones the chart ships
Each value of rules is an object:
Field
Type
Description
enabled
bool
Required.true applies the rule, false switches it off. Required rather than defaulted, so a typo fails the render instead of leaving the rule silently inert
resource
string
Required. Watched-resource name the rule targets (ingresses, httproutes, a custom CRD, …)
path
string
Concrete JSONPath into the resource: dotted keys, ['bracket'] keys (for dots/slashes), [n] indices. For example, metadata.annotations['haproxy-haptic.org/rate-limit-rps']
default
string
Inject this value when path is absent. Only a concretepath may carry a default (filtered/wildcard paths are validate-only)
required
bool
The value at path must be present and non-empty
min / max
int
Numeric bounds for the value at path
onViolation
string
reject (default) or clamp — on a min/max violation, rewrite the value to the nearest bound instead of rejecting
allowed
list
Allowed values (enum) for path
pattern
string
Regex the value at path must match
anyOf
list
At least one of the listed JSONPath expressions must be present
satisfiedBy
string
tls — satisfied by spec.tls on the resource or the chart-wide default HTTPS
enforcement
string
reject (default) or audit
message
string
Custom violation message (optional)
governance:enabled:trueexemptNamespaces:[kube-system]rules:# Inject a default per-source rate limit; clamp anything above the ceiling.rate-limit-floor:enabled:trueresource:ingressespath:metadata.annotations['haproxy-haptic.org/rate-limit-rps']default:"100"max:10000onViolation:clamp# Require a WAF policy annotation on every HTTPRoute (cross-resource).httproute-waf-policy:enabled:trueresource:httproutespath:metadata.annotations['haproxy-haptic.org/waf-policy']required:trueenforcement:audit# Require TLS (spec.tls or the chart-wide default HTTPS satisfies it).ingress-tls:enabled:trueresource:ingressessatisfiedBy:tls
To switch off a single rule — including one a template library ships — set its
enabled to false. You don't restate the others:
Add HTTP and HTTPS binds that require a PROXY protocol header, so client_ip, src-keyed rate limiting, the WAF, and IP ACLs see the real client instead of the balancer. Adds the ports to the HAProxy Service, the container ports, and the NetworkPolicy
PROXY-protocol HTTP port. Additional to haproxy.ports.http, which stays open and header-free — a connection reaching this port without the header is dropped, so only the balancer may target it
PROXY-protocol HTTPS port, using the same certificates, ciphers, and protocol negotiation as haproxy.ports.https. With TLS-Passthrough configured it attaches to the SNI-routing frontend instead
Enable default SSL certificate requirement. When the cert-manager API is absent and no inline cert is set, the chart generates a self-signed Secret (never touching an existing one) so the install converges out of the box
defaultSSLCertificate.secretName
string
default-ssl-cert
TLS Secret name containing certificate
defaultSSLCertificate.namespace
string
""
Secret namespace (defaults to Release.Namespace)
defaultSSLCertificate.ecdsaSecretName
string
""
Optional ECDSA companion Secret for the default cert. When set, HAProxy serves ECDSA to modern clients and secretName (RSA) to the rest on the no-SNI / unmatched-SNI path. Must be in the same namespace. Empty = single default cert. See Dual RSA and ECDSA certificates
defaultSSLCertificate.certManager.enabled
bool
true
Use cert-manager for certificate provisioning
defaultSSLCertificate.certManager.createIssuer
bool
true
Create self-signed Issuer (dev/test only)
defaultSSLCertificate.certManager.dnsNames
list
["localdev.me", "*.localdev.me"]
DNS names for the certificate
defaultSSLCertificate.certManager.issuerRef.name
string
""
Issuer name (auto-set when createIssuer=true)
defaultSSLCertificate.certManager.issuerRef.kind
string
Issuer
Issuer kind
defaultSSLCertificate.certManager.duration
duration
8760h
Certificate validity (1 year)
defaultSSLCertificate.certManager.renewBefore
duration
720h
Renew before expiry (30 days)
defaultSSLCertificate.create
bool
false
Create Secret from inline cert/key (testing only). Requires defaultSSLCertificate.certManager.enabled=false so exactly one actor owns the Secret
Enable fleet-wide TLS session resumption. Every HAProxy pod shares one session-ticket encryption key (STEK) so a ticket issued by any pod resumes on any other (TLS 1.2 and 1.3); the key self-rotates daily through a 3-key sliding window with one hitless reload. See TLS session resumption
Shortest start-to-start spacing of the render gate's haproxy -c runs. The gate validates each render off the reconcile path, on a semaphore slot of its own, so this only caps how much CPU a render storm can take from the admission webhook
The CRD block is still called dataplane — it configures the endpoint the
controller applies to, which is now the HAPTIC agent. Four of its fields changed
meaning with the agent; the paths didn't.
Parameter
Type
Default
Description
controller.config.dataplane.minDeploymentInterval
duration
5s
Shortest interval between two reloads of one pod. The chart passes it to the agent as --reload-interval-min: a reload inside the window is scheduled, never dropped, and the controller polls the pod at the scheduled time. With haproxy.enabled=true the agent's 60-second ceiling applies, and the chart fails the render above it
How often the controller asks each pod to re-hash its tree (GET /v1/state?verify=1) and re-applies when a digest disagrees. The same call carries the newest validated plan, so a pod's rollback baseline never lags by more than one interval
How long the agent waits for HAProxy's master to report a reload finished before calling it failed and restoring the last known good file set. The chart passes it to the agent as --reload-timeout; unset, the agent uses its 60-second ceiling
controller.config.dataplane.syncTimeout
duration
2m
How long the controller waits for one pod to answer an apply
controller.config.dataplane.mapsDir
string
/etc/haproxy/maps
HAProxy maps directory. With the bundled fleet (haproxy.enabled=true) it must sit directly under /etc/haproxy, which is where the pod mounts its config volume and resolves every auxiliary path
controller.config.dataplane.sslCertsDir
string
/etc/haproxy/ssl
SSL certificates directory. Same /etc/haproxy constraint as mapsDir when the bundled fleet is enabled; the directory name itself is free
controller.config.dataplane.generalStorageDir
string
/etc/haproxy/general
General storage directory. With the bundled fleet this exact path is required: it's a separate volume the spoa-hub and vector sidecars mount to read rendered files without reaching SSL private keys. The chart fails the render rather than deploy a pod where those sidecars see an empty directory
controller.config.dataplane.configFile
string
/etc/haproxy/haproxy.cfg
HAProxy config file path. Same /etc/haproxy constraint as mapsDir when the bundled fleet is enabled
controller.config.watchedResources.<name> is a map of resource entries. The chart's template libraries contribute most entries (Ingress, Service, EndpointSlice, Secret, plus the Gateway API route kinds when the gateway library is on); operators can add or override entries here. Each entry accepts:
Field
Type
Default
Description
apiVersion
string
one of apiVersion/apiVersions required
API group/version (for example networking.k8s.io/v1, v1 for core). Mutually exclusive with apiVersions
apiVersions
list
[]
Ordered candidate API versions; the controller watches the first one the apiserver serves (for example ["gateway.networking.k8s.io/v1", "gateway.networking.k8s.io/v1alpha2"]). Mutually exclusive with apiVersion
optional
bool
false
Marks the resource non-essential: when no candidate version is served, the watch is dropped and every templateSnippet/validationTest naming it in requires is stripped from the effective config, instead of failing startup
resources
string
required
Plural resource name (for example ingresses)
indexBy
list
[]
JSONPath expressions used to index resources for O(1) template lookup
fieldSelector
string
""
Client-side JSONPath filter (for example spec.ingressClassName=haptic); supports any JSONPath expression unlike Kubernetes' built-in fieldSelector
labelSelector
string
""
Server-side label selector for watch-time filtering (equality-only key=value pairs joined by commas)
enableValidationWebhook
bool
false
Include this resource in the chart-rendered ValidatingWebhookConfiguration
statusPatch
bool
false
Allow the controller to patch this resource's /status subresource
store
string
full
full keeps all resources in memory; on-demand fetches with caching (lower memory, slower lookups). Useful for very large Secret stores
debounceInterval
duration
"" (100ms)
Per-resource debounce window; empty/unparseable falls back to the controller-wide default (DefaultDebounceInterval, 100ms). Avoid raising the value for resources that drive backend membership — EndpointSlices and pods in particular — because the debounce delays Pod removal from the HAProxy server pool by that whole window, so live traffic continues hitting Terminating pods until the next render fires
Own W3C Trace Context in HAProxy itself, so it works with the spoa-hub disabled (the hub has no part in tracing). A valid inbound traceparent is adopted (same trace, its span becomes the parent, its sampling decision honoured); otherwise a trace id and span id are minted. traceparent is then set on the request before it reaches the backend, so your services join the same trace. The access log gains the fields the spans are built from: span_id, parent_span_id, trace_flags, upstream_span_id, the route that matched, the handshake/idle/transfer timings and the exact transaction end. They're removed from the rendered configuration entirely when tracing is off. Off by default because it changes what backends receive and there is no sensible default destination for traces. With it off, an inbound trace id is still adopted into trace_id for log correlation, but nothing is minted or propagated
Percentage of requests sampled when HAPTIC is the trace root (0–100). An inbound traceparent's decision is always honoured instead — re-deciding mid-trace produces half-sampled traces. A request that isn't sampled still propagates traceparent with flags 00, so downstream honours the same decision rather than starting its own
OpenTelemetry Protocol (OTLP) HTTP traces endpoint, for example http://tempo.observability.svc.cluster.local:4318/v1/traces. Empty means propagation-only: HAProxy still sets traceparent so backends join the trace, HAPTIC just contributes no span. Spans are derived from access-log records in the Vector sidecar, so coverage is whatever HAProxy logs — which is everything: a 502/503/504, a WAF deny, a rate-limit 429, a redirect, an error page and a fixed response are all generated by HAProxy itself, and each produces a log record and therefore a span. Requires vector.enabled=true; the render fails otherwise rather than exporting nothing silently. Spans carry the same detail as the access log: HTTP and TLS metadata, the phase timings, and the HAPTIC decision fields (WAF, external auth, rate limit, schema validation, cache, mTLS), each named haptic. + its log-field name. Spans carry no client IP — neither the forwarded address nor the TCP peer — so correlate a trace with the access log through haptic.req_id. Client-side timings stay on the SERVER span; the chosen server, retries and queue wait are on the upstream CLIENT span. When no endpoint is set the span-building transform is dropped from the configuration entirely. Spans are named {method} {host}{route} after the matched route template — never the request URI, which would give every request a distinct span name. A prefix match is marked * (GET example.com/api/*); an exact match isn't (GET example.com/api). The host is included because most deployments serve every Ingress under a single / prefix, where the route alone names every span GET /; this deviates from the OpenTelemetry convention of {method} {http.route}, but the http.route attribute itself is unchanged and carries the path template without the host. With no route matched the name falls back to {method} [namespace/name], and to {method} alone when there is no owning resource either
Cluster name reported as the k8s.cluster.name resource attribute on every exported span. No default: Kubernetes exposes no cluster name to a pod, so the attribute is omitted unless you set it. service.namespace, service.version and k8s.deployment.name need no setting — they come from the release, and identify which HAPTIC emitted a span when a cluster runs several
Extra JSON access-log fields: field name → one HAProxy sample expression, captured at request time and logged as a string. Use str(<value>) for a constant label. Names must match ^[A-Za-z_][A-Za-z0-9_]{0,39}$ and must not collide with a built-in field; expressions must not contain whitespace, #, " or a backslash. See Access logging
{vector: {address: /run/vector/haproxy.sock, format: raw}} while vector.enabled is true (the chart default); {stdout: {address: stdout}} otherwise
Where access-log records go, keyed by a target name you choose; one HAProxy log line per entry, emitted in sorted key order, so several entries fan out. A map, not a list, so adding a target keeps the ones already configured. Each entry takes address (stdout, stderr, fd@<n>, <host>:<port> (UDP), an absolute socket path or ring@<name>), format (defaults to raw for stdout/stderr, rfc5424 otherwise), facility, level (info or debug — anything stricter drops every record), or a ring block (name, address, size, logProto, connectTimeout, serverTimeout, serverOptions) for a buffered TCP client that survives a collector restart. HAProxy's own process messages keep their own stdout target. See Where the logs go
log ... len <bytes>. HAProxy truncates a longer record mid-byte, which makes it invalid JSON; raise it if custom fields or captured request headers push records past the limit (1024–65535)
Opt-in: drop access-log records for 2xx/3xx requests that no gate denied. Denials, 4xx, and 5xx are always kept. Off by default — retaining a full log is lawful under legitimate interest (GDPR Art. 6(1)(f)), and the successful requests either side of a failure are what make a customer's report diagnosable. A record is ~740 bytes, so ~700 MB per million requests if volume forces your hand. See Access logging
Regex every password hash in a basic-auth Secret must match (the auth-secret annotation handlers in the haproxytech and haproxy-ingress libraries). A non-matching hash fails the render with passwordHashValidation.errorMessage; the default accepts all hashes. Example restricting to MD5-crypt (apr1) hashes: "^\$apr1\$". Go RE2 syntax — no lookaheads, so express the policy as the allowed format
HAProxy's built-in Prometheus exporter answers on the stats port (8404, /metrics) on every HAProxy pod and is scraped directly. These values become the query HAProxy applies to a scrape that sends none, so every scraper — the bundled haproxy.monitoring.podMonitor and a hand-written job alike — gets the same exposition; a scraper's own query wins wholesale, so /metrics? returns the raw exposition. See HAProxy data-plane metrics.
Apply HAProxy's ?no-maint, omitting servers in MAINT state. A not-ready or terminating endpoint renders as a disabled server, which HAProxy reports in MAINT, and each still emits a full metric set of zeros; ?no-maint drops those series. How much that saves depends on the deployment — most during a rolling update, when many pods are briefly not-ready or terminating, and little at steady state. No metric name disappears, so name-selecting dashboards and rules keep working, and haproxy_backend_agg_server_status{state="MAINT"} still reports the per-backend count of MAINT servers (which is why haproxy_backend_agg_* isn't in excludeMetrics by default). Set to false if you alert on an individual drained server. Honoured on HAProxy 3.0–3.4
Named exclusions, each with enabled and families — exact metric names HAProxy leaves out of the exposition (metrics=-<name>; the exporter has no regex filter, and a name that isn't a bare metric name is refused at render time). A map rather than a list so you can disable one without restating the others: prometheusExporter.excludeMetrics.backendAggCheckStatus.enabled=false. Add your own with any new key; enabled is required so an entry can't sit inert. An entry may set requires, a dotted extraContext path that must be truthy for the exclusion to apply. The defaults drop HAProxy's host-computed maxima, the agg_check_status family (reproducible by summing haproxy_server_check_status), agg_server_check_status (identical to agg_server_status unless you use agent checks), and the cache and backup-topology families, which are constant unless you enable those features — together about a third of the exposition on a large fleet. The compression exclusion ships off, because response compression is on by default and those series carry real traffic. Two further groups carry requires: vector.requestMetrics.enabled and so drop out on their own if you turn that feature off: haproxy_*_time_average_seconds (1024-connection rolling averages, superseded by real duration histograms) and haproxy_{backend,frontend}_http_{requests,responses}_total (superseded by <prefix>_requests, which carries the exact status code plus route, method, host and service). haproxy_backend_agg_server_status and haproxy_server_http_responses_total are deliberately kept — the first is the only remaining per-backend census of MAINT (not-ready/terminating) servers once excludeMaintServers is on, and the second is per-server, a dimension the request metrics don't carry
API-server timeout for watched-resource admission. HAPTIC configures the controller deadline one second shorter. Allowed range: 2..30
controller.webhook.secretName
string
Auto-generated
Webhook TLS certificate secret name
controller.webhook.service.port
int
443
Webhook service port
controller.webhook.certManager.enabled
bool
false
cert-manager integration for the webhook cert. Default false: the chart issues a self-signed cert itself. Set true for cert-manager-managed issuance and auto-rotation; for manual certs keep false and set controller.webhook.caBundle
controller.webhook.certManager.createIssuer
bool
true
Create a self-signed Issuer for webhook certs
controller.webhook.certManager.issuerRef.name
string
""
Issuer name (auto-set when createIssuer=true)
controller.webhook.certManager.issuerRef.kind
string
Issuer
Issuer kind
controller.webhook.certManager.duration
duration
8760h
Certificate validity (1 year)
controller.webhook.certManager.renewBefore
duration
720h
Renew before expiry (30 days)
controller.webhook.selfSigned.certValidityDays
int
3650
Validity in days of the chart-generated self-signed webhook cert (used when certManager.enabled=false and caBundle is empty). Long by default because the chart doesn't auto-rotate it: the cert is generated once and reused across upgrades via lookup. Rotate manually by deleting the Secret (controller.webhook.secretName) and running helm upgrade, or use cert-manager for automatic rotation
The validator sidecar runs a second haproxy-spoa-hub instance in --validate-socket mode next to the controller. The shared render pipeline consults it before publishing or deploying output, so broken plugin TOML (for example a bad modsecurity-snippet) is rejected regardless of whether a watched resource, config, HTTP refresh, or drift check triggered the render. See Pluggable validators.
Parameter
Type
Default
Description
controller.validators.enabled
bool/null
null
Master enable for the validator sidecar. null auto-derives from the SPOA hub sidecar's own enable; true renders it even when spoaHub is off; false forces it off
controller.validators.socketDir
string
/var/run/haptic-validators
Directory for the validator Unix socket — a shared emptyDir mounted into both the controller container and the validator sidecar
controller.validators.socketName
string
spoa-hub.sock
Socket filename. The controller dials <socketDir>/<socketName>, and the chart writes that path into the auto-wired spec.validators entry
controller.validators.resources.requests.cpu
string
25m
Validator sidecar CPU request; content caching avoids repeat validation for identical renders
controller.validators.resources.requests.memory
string
64Mi
Validator sidecar memory request
controller.validators.resources.limits.memory
string
128Mi
Validator sidecar memory limit
controller.validators.securityContext
map
See values.yaml
Container security context for the validator sidecar. Default runs user and group 65532 (matching the controller's nonroot user) so the Unix socket is readable and writable by the controller without extra fsGroup plumbing; read-only root filesystem, no privilege escalation, all capabilities dropped
controller.validators.extraVolumeMounts
list
[]
Extra volume mounts added to the validator sidecar only (rendered through tpl); for auxiliary data a plugin's validate() needs, such as MaxMind MMDB files or Open Worldwide Application Security Project (OWASP) Core Rule Set (CRS) files. Same shape as spoaHub.extraVolumeMounts
controller.validators.entries
list
[]
Entries appended to the CRD's spec.validators (each: name, socketPath, files glob list, optional timeoutMs/maxConnections). The chart auto-appends a spoa-hub entry validating general/spoa-hub-config.toml when both the SPOA hub sidecar and the validator sidecar are enabled; an operator entry named spoa-hub takes precedence
Agent username. The Secret keys keep their dataplane_ names, so a rotation set up before the agent still works
credentials.dataplane.password
string
""
Agent password. Empty generates a random 32-char password. When lookup works (a normal helm upgrade, or an install against a reachable cluster) the chart reads the existing Secret and preserves the current password across renders. GitOps tools that render without cluster access (ArgoCD/Flux) can't lookup, so an empty value regenerates every sync — set an explicit value (SealedSecret / external secret) in those setups.
Pod-spec scheduling, runtime, and metadata fields for the controller Deployment live under controller.podSpec.*. The chart's _pod-spec.tpl helper renders the universally shared subset; the remaining fields (podAnnotations, podLabels, podSecurityContext) are consumed directly by templates/deployment.yaml.
Named container port the probe targets (declared by controller.ports.healthz)
controller.livenessProbe.initialDelaySeconds
int
10
Initial delay
controller.livenessProbe.periodSeconds
int
10
Probe period
controller.livenessProbe.failureThreshold
int
3
Failure threshold
controller.readinessProbe.httpGet.path
string
/healthz
Readiness probe path
controller.readinessProbe.httpGet.port
string
healthz
Named container port the probe targets
controller.readinessProbe.initialDelaySeconds
int
5
Initial delay
controller.readinessProbe.periodSeconds
int
5
Probe period
controller.readinessProbe.failureThreshold
int
3
Failure threshold
controller.startupProbe.enabled
bool
true
Enable the startup probe; liveness/readiness probes are paused until it succeeds. On by default and load-bearing: controller startup runs the config's embedded validationTests (dozens of haproxy -c checks plus a full template compile), which can exceed the bare liveness budget on slow nodes and crash-loop the pod
controller.startupProbe.httpGet.path
string
/healthz
Startup probe path
controller.startupProbe.httpGet.port
string
healthz
Named container port the probe targets
controller.startupProbe.initialDelaySeconds
int
0
Initial delay
controller.startupProbe.periodSeconds
int
10
Probe period
controller.startupProbe.timeoutSeconds
int
1
Probe timeout
controller.startupProbe.successThreshold
int
1
Success threshold
controller.startupProbe.failureThreshold
int
30
Failure threshold (with periodSeconds: 10 this gives 5 minutes for startup)
Include the HAProxyFleetDiverged warning rule: some HAProxy pods haven't converged to the desired config for 5 minutes. Transient deploy failures self-heal, so sustained divergence is a real fault — this is the noise-free replacement for alerting on raw deployment errors
Include the HAProxyControllerConfigRejected warning rule: the validation gate rejected a config change — the controller keeps serving the last-good config and the latest change isn't live
Include the HAProxyControllerConfigPinned critical rule: HAProxy refused two renders in a row, so the pods keep serving the last configuration it accepted and nothing new reaches them until the input is fixed
Include the HAProxyControllerHAProxyPodsRejected warning rule: discovered HAProxy pods refused admission (often a HAProxy major.minor mismatch with haproxyVersion)
Include the HAProxyAccessLogRecordsDropped warning rule: HAProxy discarded access-log records because the Vector sidecar stopped draining the Unix datagram socket. Traffic is unaffected; the log is incomplete
Include the HAProxyControllerCriticalEventsDropped critical rule: a critical event-bus subscriber's buffer overflowed — reconciliation work was lost and the data plane may be stale
Include the HAProxyAgentApplyRejected warning rule: an HAProxy pod refused an apply and serves its last known good file set; HAProxy's message is in the pod's status condition
Include the HAProxyAgentRecoveryReloadFailed critical rule: a pod rolled back to its last known good file set but the recovery reload failed, so its worker matches neither the rejected apply nor the restored files
Include the HAProxyAgentVersionSkew warning rule: applies degrade to full state plus a reload because a pod's agent doesn't match the controller; expected during a rolling upgrade, a defect after one
controller.monitoring.grafanaDashboard.enabled
bool
false
Create a ConfigMap holding the Grafana dashboard JSON (picked up by the Grafana sidecar via the configured discovery label)
Preserve at least one HAProxy pod during voluntary disruptions. The default is 0 for a one-pod minimum fleet and 1 otherwise. An explicit value must be smaller than the static replica count, or KEDA's minimum replica count when autoscaling is enabled
haproxy.image.repository
string
"" (derived)
HAProxy image repository. Empty selects haproxytech/haproxy-debian or hapee-registry.haproxy.com/haproxy-enterprise from haproxy.enterprise.enabled
haproxy.image.pullPolicy
string
IfNotPresent
Image pull policy
haproxy.image.tag
string
""
HAProxy image tag; empty = derive from haproxyVersion plus the matching entry in haproxyPatchVersions (for example 3.2 → whichever 3.2.x patch the chart currently pins). Override to pin a specific patch yourself.
haproxy.enterprise.enabled
bool
false
Use HAProxy Enterprise. haproxyVersion selects the compatibility series, image revision map, and binary path together
haproxy.haproxyBin
string
Auto-detected
HAProxy binary path
haproxy.initialConfig
string
See values.yaml
HAProxy bootstrap config served until the controller pushes the first rendered config; processed via Helm tpl. Keep the /ready 503 gate or clients hit an empty backend set — see the HAProxy deployment guide
Pod-spec scheduling, runtime, and metadata fields live under haproxy.podSpec.* (the chart's _pod-spec.tpl helper renders the universally shared subset). See also controller.podSpec.* for the controller Deployment.
Parameter
Type
Default
Description
haproxy.podSpec.imagePullSecrets
list
[]
Image pull secrets for the HAProxy pod, which pulls both the HAProxy image and the HAPTIC image its agent container runs from. Empty follows controller.podSpec.imagePullSecrets
haproxy.podSpec.podAnnotations
map
{}
Extra pod annotations for HAProxy pods (supports template expressions)
haproxy.podSpec.shareProcessNamespace
bool
false
Share process namespace between containers (required for signal-based sidecar reload)
haproxy.podSpec.priorityClassName
string
""
Pod priority class
haproxy.podSpec.terminationGracePeriodSeconds
int
30
Termination grace period
haproxy.podSpec.dnsPolicy
string
ClusterFirst
DNS policy
haproxy.podSpec.dnsConfig
map
{}
DNS config
haproxy.podSpec.hostAliases
list
[]
/etc/hosts entries
haproxy.podSpec.runtimeClassName
string
""
Runtime class (for example gVisor, Kata)
haproxy.podSpec.topologySpreadConstraints
list
[]
Topology spread constraints
haproxy.podSpec.nodeSelector
map
{}
Node selector
haproxy.podSpec.tolerations
list
[]
Tolerations
haproxy.podSpec.affinity
map
{}
Affinity rules
haproxy.podSpec.podSecurityContext
map
See values.yaml
Pod-level security context (seccomp, sysctls). UIDs auto-derived from haproxy.enterprise.enabled
Single source of truth for the agent's apply/state API: its listener, the Service, the NetworkPolicy, the container probes, and the controller's connection port
haproxy.ports.agentMetrics
int
5557
The agent's Prometheus endpoint. Scraped by haproxy.monitoring.podMonitor through the named container port agent-metrics, and allowed from the NetworkPolicy's metrics sources
The controller renders the user-facing HAProxy Service from these values (the base library's k8sResources.haproxy-service template) and owns it via Server-Side Apply — the chart itself only creates the internal agent Service. Changes therefore land when the controller reconciles, not at helm upgrade time.
Parameter
Type
Default
Description
haproxy.service.type
string
NodePort
HAProxy service type
haproxy.service.annotations
map
{}
Service annotations
haproxy.service.loadBalancerIP
string
""
LoadBalancer IP (when type: LoadBalancer)
haproxy.service.loadBalancerSourceRanges
list
[]
CIDR allowlist for LoadBalancer traffic
haproxy.service.loadBalancerClass
string
""
LoadBalancer class (Kubernetes 1.24+ multi-LB)
haproxy.service.externalTrafficPolicy
string
""
Cluster or Local; Local preserves client source IP at the cost of uneven distribution
haproxy.service.internalTrafficPolicy
string
""
Cluster or Local for in-cluster traffic
haproxy.service.healthCheckNodePort
int
""
Fixed health-check NodePort for type: LoadBalancer with externalTrafficPolicy: Local; empty lets Kubernetes allocate one
haproxy.service.publishNotReadyAddresses
bool
false
Include not-ready HAProxy pods in the Service endpoints
haproxy.service.http.port
int
80
HTTP service port
haproxy.service.http.nodePort
int
30080
HTTP NodePort
haproxy.service.https.port
int
443
HTTPS service port
haproxy.service.https.nodePort
int
30443
HTTPS NodePort
haproxy.service.stats.port
int
8404
Stats service port
haproxy.service.stats.nodePort
int
30404
Stats NodePort
haproxy.service.extraPorts
list
[]
Additional Service ports (corev1.ServicePort shape) appended to the http/https/stats entries — for example a raw TCP frontend declared via a custom haproxyConfig snippet. Drop a default entry by setting haproxy.service.{http,https,stats}.port: 0
The agent owns the HAProxy pod's file tree and its runtime sockets. It runs the
controller's own image, so its version always matches the controller that talks
to it.
Parameter
Type
Default
Description
haproxy.agent.service.type
string
ClusterIP
Type of the internal Service that fronts the agent port
haproxy.agent.logLevel
string
info
Log level for the agent, which logs JSON on stdout: trace, debug, info, warning, error. At debug the agent logs a line per apply with its verdict, the ops it ran and the reload it performed — raise it when diagnosing an apply the controller reports as failing but HAProxy accepts. The stream carries no end-user data; the only client is the controller
Agent credentials are the top-level credentials.dataplane.* section — see Credentials above.
The agent's probes are fixed: startupProbe on /readyz and livenessProbe on
/healthz. /readyz means "the agent can accept applies" and stays true after
a rejected apply, because a pod that can't be applied to is exactly the pod the
next apply has to reach.
HAProxy nbthread directive value. Default (key absent): if haproxy.resources.limits.cpu is set, ceil(cpu_limit) clamped to ≥1; otherwise the directive is omitted and HAProxy auto-detects all node cores from its CPU affinity. Set 0 to force-omit the directive. Templatable — "{{ mul 2 (...) }}" is allowed
haproxy.shmStats.enabled
bool
false
Persist HAProxy stats counters across reloads via shm-stats-file (HAProxy 3.3+ only)
haproxy.shmStats.path
string
/dev/shm/haproxy-stats
Path to the shared-memory stats file
haproxy.shmStats.maxObjects
int
50000
Maximum object count in the shm-stats file. Each frontend, backend, listen, and server counts as one object — pick a value with headroom; HAProxy can't resize the file on reload
haproxy.shmStats.shmSizeLimit
string
""
/dev/shm emptyDir size limit. Empty auto-calculates from maxObjects (~4 KB/object + 10% overhead, rounded to MiB)
haproxy.lifecycle
map
{}
Container lifecycle hooks for the HAProxy container (preStop, postStart)
One PodMonitor for every metrics endpoint on the HAProxy pod. See Where to scrape.
Parameter
Type
Default
Description
haproxy.monitoring.podMonitor.enabled
bool
false
Create a PodMonitor selecting the HAProxy pods (needs the prometheus-operator CRDs). It declares one endpoint per metrics port the pod exposes: stats (HAProxy's exporter, haproxy_*), with the Vector sidecar on vector-metrics and — while a request-metrics size family is enabled — vector-sizes, and with the sidecar off and the SPOA hub on the hub's metrics port (a hub pinned to a loopback bind fails the render with guidance; spoaHub.hub.metricsAddr: auto resolves to a pod-routable bind in that case). No scrape parameters: HAProxy applies the exclusion policy itself
haproxy.monitoring.podMonitor.interval
duration
30s
Scrape interval, applied to every endpoint
haproxy.monitoring.podMonitor.scrapeTimeout
duration
10s
Scrape timeout, applied to every endpoint
haproxy.monitoring.podMonitor.labels
map
{}
Extra labels on the PodMonitor (for a Prometheus podMonitorSelector)
Master enable. null auto-derives from spoaHub.plugins.*.enabled. Set true to force the sidecar on with no plugins enabled (test rendering); set false to force it off even when a plugin is enabled
Tokio worker thread count; null defaults to CPU count
spoaHub.hub.maxConnections
int
1000
Maximum concurrent connections per listener
spoaHub.hub.blockingThreadKeepAliveSecs
int
30
Keep-alive seconds for blocking-thread workers
spoaHub.hub.maxBlockingThreads
int/null
null
Process-wide blocking-pool cap. Null derives the sum of resolved per-plugin concurrency; an explicit value must be at least that sum. Changing it rolls the HAProxy pods because Tokio fixes this pool at process start
spoaHub.hub.reloadDrainTimeoutMs
int/null
null
Hot-reload quiesce-and-drain budget. Null derives 1.5 times the largest plugin timeout, clamped to 1–30 seconds. 0 restores unsafe legacy immediate retirement and can lose in-flight/background work
spoaHub.hub.metricsAddr
string
auto
Hub Prometheus /metrics listen address. auto binds it where whatever scrapes it can reach: 127.0.0.1:9095 when vector.enabled is true (Vector scrapes over loopback from inside the pod and re-exports on its own port), 0.0.0.0:9095 when it's false (Prometheus scrapes the pod IP directly, so a loopback bind would be a dead target). Set an explicit <ip>:<port> to override, or "" to disable the endpoint (loses per-plugin counters). The metrics carry per-Ingress/route cardinality, so prefer the derived value over exposing it unnecessarily
spoaHub.hub.goGCPercent
int
300
Go GC target percentage (GOGC) for the sidecar's embedded Go runtime (the coraza plugin). Higher than Go's default 100 collects less often under load — fewer stop-the-world pauses and less GC-assist CPU stealing on the request path — for a lower p99 tail. GOMEMLIMIT is derived automatically as a soft cap at 90% of the container memory limit. Set 100 to restore Go's default
spoaHub.haproxy.socketPath
string
/run/spoa/hub.sock
Unix socket path shared between HAProxy and the hub
spoaHub.haproxy.modeSpop
bool
true
Use HAProxy 3.1+ mode spop backend; auto-falls back to mode tcp on 3.0. Set false to force mode tcp on 3.1+
HAProxy's outer per-message processing timeout. Null derives each enabled message's budget plus timeoutProcessingMarginMs; a message budget sums all plugin timeouts sharing that message, covering sequential dependency stages without adding unrelated plugins. An explicit value applies to every message and fails rendering when it's below any enabled message's minimum
spoaHub.haproxy.timeoutProcessingMarginMs
int
100
Scheduling and serialization margin added between each enabled message's plugin budget and its HAProxy deadline
spoaHub.haproxy.poolMaxConn
int
100
Connection pool maximum
spoaHub.haproxy.poolPurgeDelay
duration
30s
Idle-connection purge delay
spoaHub.plugins.<name>.enabled
bool/string
'{{ false }}' (templatable)
Per-plugin enable. Default value is a chart-evaluated tpl string so a plugin can auto-enable when the template libraries that rely on it are on; explicit --set bool always wins
spoaHub.plugins.<name>.timeoutMs
int
per-plugin
Plugin processing timeout in milliseconds
spoaHub.plugins.<name>.maxConcurrency
int/null
plugin default
Maximum plugin calls executing concurrently. Use this as the single owner of plugin CPU admission
spoaHub.plugins.<name>.maxQueue
int/null
plugin default
Maximum calls waiting for a concurrency slot. Coraza defaults to 0, rejecting excess work instead of inflating latency under attack
spoaHub.plugins.<name>.queueTimeoutMs
int/null
plugin default
Maximum queue wait when maxQueue is non-zero
spoaHub.plugins.<name>.adaptiveConcurrency
bool
plugin default (on for coraza/api-gateway)
The latency-feedback concurrency controller (ADR-0002): the hub resizes the plugin's admission semaphore at runtime and maxConcurrency becomes the ceiling. Requires a hub image with adaptive support; an older hub ignores it
spoaHub.plugins.<name>.messages
list
per-plugin
SPOE messages this plugin handles
spoaHub.plugins.<name>.dependsOn
list
[]
Other plugin names this plugin must run after
spoaHub.plugins.<name>.params
string
per-plugin
Free-form TOML blob spliced verbatim under [plugins.params] — use dotted keys (x.y = "...") or fully qualified headers ([plugins.params.x]) for nested values; bare [x] headers close the params scope and break the config
spoaHub.plugins.coraza.directives
string
OWASP CRS includes + SecRuleEngine On
Chart-wide Coraza WAF directives. Reusable policy applications and authorized per-Ingress rules layer on this base. Keep SecRuleEngine Onafter the includes — @coraza.conf-recommended sets DetectionOnly, so an earlier On is silently overridden and the WAF never blocks. Don't also set directives inside params:; the duplicate TOML field breaks the config
spoaHub.plugins.mirror.targetTimeoutMs
int
2000
Per-target timeout for asynchronous mirror requests. It's independent of application backend timeouts so a dead mirror releases shared hub capacity quickly
spoaHub.plugins.mirror.targetRetries
int
0
Retry count for asynchronous mirror requests. The default avoids multiplying work against an unavailable observability target
spoaHub.securityContext
map
See values.yaml
Container security context for the spoa-hub container. Default runs user and group 99, matching the pod's fsGroup, so the Unix socket the hub creates under /run/spoa is accessible to the HAProxy container; read-only root filesystem, no privilege escalation, all capabilities dropped
spoaHub.extraVolumeMounts
list
[]
Extra volume mounts added to the spoa-hub container only (rendered through tpl) — for MMDB files (maxmind), OpenID Connect (OIDC) client secrets (sso-auth), and similar plugin data
Available plugin names (<name>): api-gateway, coraza, external-auth, fingerprinting, maxmind, mirror, rate-limit, sso-auth. See values.yaml for each plugin's defaults and the upstream plugin README for the params: schema.
A Vector container on every HAProxy pod. It receives the access log over a Unix datagram socket, derives per-request metrics from it, and re-exports the SPOA hub's Prometheus metrics alongside its own. HAProxy's own exporter is scraped directly (see Prometheus exporter). See Access logging.
Key
Type
Default
Description
vector.enabled
bool
true
Run the Vector sidecar on each HAProxy pod. When enabled, the access log goes to vector.socketPath instead of stdout and surfaces under kubectl logs <pod> -c vector. Set to false to log straight to the HAProxy container's stdout and scrape the hub directly; HAProxy's exporter is scraped directly either way
vector.image.repository
string
timberio/vector
Vector image repository
vector.image.pullPolicy
string
IfNotPresent
Image pull policy
vector.image.tag
string
0.57.0-debian
Pinned Vector version. Renovate bumps it; keep the # renovate: comment above the value or tracking stops. An empty tag is rejected — a floating tag would change the log pipeline under a running fleet
vector.metricsPort
int
9598
Port serving Vector's /metrics: its own series, the log-derived and request metrics, and the hub's re-exported ones. Rejected at render time if it collides with an haproxy.ports.* entry or the hub's metrics port
vector.sizeMetricsPort
int
9599
Port serving the byte-size histograms from requestMetrics. A second exporter exists because Vector's prometheus_exporter takes one buckets list for every distribution it renders — there is no per-metric override — and bytes and seconds are different domains: a shared list would give each family boundaries it can never fall into. Rendered, along with its container port and PodMonitor endpoint, only while request_size or response_size is enabled. Validated for collisions the same way as metricsPort, including against metricsPort itself
vector.socketPath
string
/run/vector/haproxy.sock
Unix datagram socket HAProxy writes access-log records to. Must be an absolute path with no whitespace — HAProxy's log <path> form requires one
vector.omitEmptyLogFields
bool
true
Strip access-log fields whose value is the empty string, so a record carries only what actually happened. A feature that didn't fire still costs its field on every line: on a measured fleet trace_id, denied_by, waf_matched_var, consumer and cache were empty in 100% of records, and dropping every empty made records 27% smaller (815 to 595 bytes average). Sending nothing rather than an empty value is what Elastic Common Schema and OpenTelemetry both recommend. HAProxy can't do it — its JSON encoder has no omit option and the closest one (+M) substitutes - instead — so Vector strips them in a remap transform that rewrites the line rather than re-encoding it, which preserves field order (re-encoding sorts keys alphabetically and would bury ts). Numbers are untouched: a genuine queue_time_ms: 0 is kept. Set to false if you feed a strongly typed index, or have queries written as field == "", where a stable field set matters more than the bytes
vector.logMetrics
map
eight entries, all on
Metrics derived from the access log, each entry naming a log field and how to project it. Several in-path components expose no scrape endpoint of their own — Varnish has none at all, its counters living in shared memory behind varnishstat — but the access log already carries their verdict per request, so the metric reads a field that's present anyway. Extraction is a regex over the raw record, never a JSON parse, because the pipeline deliberately doesn't parse access-log records. A map rather than a list so adding an entry doesn't replace the ones the chart ships. Each entry sets enabled (required; an entry omitting the flag is refused rather than sitting inert), field, metric, and kind: enum emits a counter tagged with the field's value and must list its accepted values, so an unexpected value can't invent an unbounded label, with tag naming the label (default value); numeric emits a counter incremented by the value. An entry may also set requires, a dotted values path that must be truthy — a metric whose emitter is switched off would otherwise cost a regex on every record. Field, metric, tag, and value names are validated at render time, since they're embedded in the rendered Vector config and an invalid one stops the sidecar child. Four shipped cache entries cover hit rate, object age, storage refusals, and cache bypass. The other four count limiter, WAF, and schema dependency degradation plus every denial reason
vector.requestMetrics.enabled
bool
true
Derive per-request metrics from the access log: one counter and six histograms, dimensioned by route rather than request URI, with the upstream call split into connect, headers and full response. haproxy_* has no equivalent — it offers per-backend counters and non-aggregatable rolling averages, so no per-route dimension, no quantiles and no phase breakdown. On whenever the sidecar is, since the log already carries every field
vector.requestMetrics.prefix
string
haptic_ingress_controller
Metric name prefix, becoming the Vector metric namespace. Set to nginx_ingress_controller to make the output byte-compatible with ingress-nginx, so its dashboards, recording rules and alerts keep working — see Migrating. A trailing underscore is accepted and stripped, since the exporter joins namespace and name with one itself
vector.requestMetrics.controllerClass
string
""
Value of the controller_class label. Empty means ingressClass.controllerName. ingress-nginx puts its --controller-class here (k8s.io/ingress-nginx), so set that if a dashboard selects on it
vector.requestMetrics.terminationStateLabel
bool
true
Add term, HAProxy's 4-character termination state, to every family. It separates a client abort from a server abort, a connect failure, a queue timeout and a response HAProxy generated itself — the one label ingress-nginx has no equivalent of, and usually the fastest route from "requests are failing" to a cause. It's also the most expensive, because it multiplies the histograms too, so it's the first thing to turn off if the series count hurts. Setting false removes the label rather than blanking it, so the remaining series aggregate exactly as they would have without it
vector.requestMetrics.pathLabel
bool
true
Add path, the matched route — the path template you wrote, so it's bounded by the number of rules rather than by traffic. Turning it off also switches off the HAProxy-side route lookup, saving four map lookups per request as well as series
vector.requestMetrics.hostLabel
bool
true
Add host. The equivalent of ingress-nginx's --metrics-per-host
vector.requestMetrics.durationBuckets
list
15 boundaries, 1 ms to 60 s
le boundaries for the four duration histograms, on the metricsPort exporter. A strict superset of ingress-nginx's --time-buckets, so a rule that hardcodes le="0.5" keeps resolving: 1 ms and 2.5 ms are added below, because an in-cluster backend commonly answers in 1–3 ms and upstream's 5 ms floor collapses the whole fast path into one bucket, and 30 s and 60 s above, because upstream stops at 10 s and every longer request lands in +Inf, saturating histogram_quantile well below the server timeouts this chart ships. Must be positive and strictly ascending — Prometheus reads them as cumulative, so an out-of-order list produces silently wrong quantiles
vector.requestMetrics.sizeBuckets
list
12 boundaries, 100 B to 100 MB
le boundaries for the two size histograms, on the sizeMetricsPort exporter. A 1-3-10 ladder, deliberately not ingress-nginx's: it measures request_size and response_size against 10, 20, … 100 bytes, so every real payload lands in +Inf and those _bucket series carry no information — there is no working size-quantile query to stay compatible with. _sum and _count, which is what its network-I/O panel reads, are unaffected either way
vector.requestMetrics.cardinalityLimit.enabled
bool
true
Cap how many distinct values any one label may take, per metric. A backstop for a label going unbounded despite the design — a route matched by regex, a Host header an attacker controls, a path template with an id in it. Applies to these metrics only; the haproxy_* re-export has bounded labels and keeps honor_labels intact. State is in memory and resets when the sidecar restarts, so treat a tripped limit as something to fix
vector.requestMetrics.cardinalityLimit.valueLimit
int
500
Distinct values allowed per label per metric before the limit trips
vector.requestMetrics.cardinalityLimit.action
string
drop_tag
What to do past the limit. drop_tag collapses the offending label onto one series and keeps request totals correct; drop_event discards the requests instead, so a cardinality problem would read as an outage
vector.requestMetrics.metrics
map
seven, all on
Which families to emit; the keys are the emitted name suffixes. An unknown key fails the render rather than sitting inert, and enabling the feature with every entry false is refused. requests counts one per logged request; request_duration_seconds is %Ta, total active time, the client's view; response_duration_seconds is the whole upstream call; connect_duration_seconds is %Tc; header_duration_seconds is %Tr; request_size is %U, request body bytes (so it reads below nginx's $request_length, which counts the request line and headers too) and adds a bytes_in access-log field; response_size is %B. The three upstream timers are only recorded when the phase actually happened — a request HAProxy answered itself contributes to requests and request_duration_seconds and to nothing else
vector.scrapeIntervalSecs
int
15
How often Vector scrapes the hub endpoint it re-exports. Keep at or below Prometheus's own interval, or Prometheus samples a value Vector hasn't refreshed
vector.resources.requests.cpu
string
50m
CPU request for the Vector container
vector.resources.requests.memory
string
256Mi
Memory request for the Vector container. Its memory tracks the request-metrics series (traffic shape, bounded by cardinalityLimit) plus the hub's re-export — measured at 146 MB idle and 364 MB steady with 5,000 distinct routes at 500 records/s. Raise it with the limit for a fleet with far more distinct routes and hosts, or lower cardinalityLimit.valueLimit and the label opt-outs instead
vector.resources.limits.memory
string
1Gi
Memory limit for the Vector container. Covers the measured 698 MB peak of the traffic run above with headroom, and sits above the request so that headroom is burstable rather than reserved. A supervisor running as process 1 restarts an exited or unresponsive Vector child without withdrawing healthy HAProxy traffic; a whole-container OOM can still briefly affect pod readiness.
vector.securityContext.allowPrivilegeEscalation
bool
false
Container security context for Vector
vector.securityContext.readOnlyRootFilesystem
bool
true
Read-only root filesystem; Vector's writable paths are the data_dir and /tmp emptyDir volumes
vector.securityContext.runAsNonRoot
bool
true
Refuse to run as root
vector.securityContext.capabilities.drop
list
[ALL]
Linux capabilities to drop
vector.extraVolumeMounts
list
[]
Extra volume mounts added to the Vector container only (rendered through tpl) — for credentials a downstream sink needs
No CPU limit is set by default to avoid throttling. With no limit, HAProxy's nbthread auto-detects all node cores from its CPU affinity — so HAProxy uses every core on a static node without inflating CPU requests. Set haproxy.resources.limits.cpu to cap both the CPU quota and nbthread to ceil(limit).
NetworkPolicy peers allowed to reach the health port (controller.ports.healthz). The default podSelector: {} admits every pod in the release namespace
controller.networkPolicy.ingress.webhook.enabled
bool
true
Allow webhook access
controller.networkPolicy.ingress.webhook.from
list
IPv4+IPv6 ipBlock catch-alls
NetworkPolicy peers allowed to reach the webhook port (controller.ports.webhook). Defaults to ipBlock catch-alls because the kube-apiserver runs host-network on most distributions — a pod/namespace selector would silently fail to match it and the webhook would return 502 errors. Both 0.0.0.0/0 and ::/0 appear because ipBlock.cidr is single-family. Tighten to your apiserver/node CIDRs for production