haproxy-ingress library¶
The haproxy-ingress library implements haproxy-ingress.github.io/* annotations compatible with jcmoraisjr/haproxy-ingress, a community HAProxy ingress controller. It supports path matching, backend configuration, session affinity, SSL features, access control, HTTP Strict Transport Security (HSTS) and Cross-Origin Resource Sharing (CORS) headers, and more.
Overview¶
This library is opt-in — disabled by default. Enable it to keep your existing haproxy-ingress.github.io/* annotations working when migrating from jcmoraisjr/haproxy-ingress. For new configuration, the enabled-by-default haproxy-haptic.org/* native library covers the same capabilities (and more); the two coexist, so you can migrate at your own pace.
See the haproxy-ingress.github.io/* annotations render to HAProxy config live:
Migrating from jcmoraisjr/haproxy-ingress
Enable this library (see Configuration below), and your existing haproxy-ingress.github.io/* annotations work without changes. See Migrating from haproxy-ingress for the cutover guide and the per-annotation verdict table, and Annotations for the feature comparison between annotation libraries.
Configuration¶
controller:
templateLibraries:
haproxyIngress:
enabled: true # Set to enable this opt-in library (disabled by default)
Extension points¶
The haproxy-ingress library hooks into these extension points. Snippet names encode priority via a numeric prefix — the 600-range deliberately runs after the core haproxytech 100–500 range so haproxy-ingress.github.io/* annotations override haproxy.org/* when both are set on the same Ingress.
features-* (shared-state initialization)¶
| Snippet | Purpose |
|---|---|
features-100-haproxy-ingress-ssl-passthrough |
Scans ingresses annotated with haproxy-ingress.github.io/ssl-passthrough and registers backends in gf["sslPassthroughBackends"] |
features-105-haproxy-ingress-ssl-redirect |
Processes ssl-redirect, ssl-redirect-code — registers hosts into the shared ssl-redirect-<code>.map (ssl.yaml emits the redirect rule) |
features-135-haproxy-ingress-redirect-to |
Processes redirect-to, redirect-to-code — registers host→location in the shared redirect-loc-<code>.map |
features-145-haproxy-ingress-app-root |
Processes app-root — registers host→path into the shared app-root.map (base.yaml emits the gated rule) |
features-155-haproxy-ingress-hsts |
Processes hsts, hsts-max-age, hsts-include-subdomains, hsts-preload — registers host→value into the shared hsts.map (base.yaml emits the response-header rule) |
map-path-* (path-map extension points)¶
| Snippet | Extension Point | Purpose |
|---|---|---|
map-path-regex-600-haproxy-ingress |
map-path-regex-* |
Regex path-map entries for path-type: regex |
map-path-exact-600-haproxy-ingress |
map-path-exact-* |
Exact path-map entries for path-type: exact |
map-path-prefix-600-haproxy-ingress |
map-path-prefix-* |
Prefix path-map entries for path-type: begin |
map-pfxexact-600-haproxy-ingress |
map-pfxexact-* |
Prefix-exact entries for path-type: prefix |
backend-directives-* (per-backend directives)¶
| Snippet | Annotations Processed |
|---|---|
backend-directives-600-haproxy-ingress-timeouts |
timeout-connect, timeout-server, timeout-queue, timeout-http-request, timeout-keep-alive, timeout-tunnel |
backend-directives-610-haproxy-ingress-load-balance |
balance-algorithm |
backend-directives-620-haproxy-ingress-maxconn |
maxconn-server |
backend-directives-630-haproxy-ingress-health-checks |
health-check-uri, backend-check-interval, health-check-port, health-check-fall-count, health-check-rise-count |
backend-directives-640-haproxy-ingress-proxy-protocol |
proxy-protocol |
backend-directives-650-haproxy-ingress-ssl-backend |
secure-backends, backend-protocol, secure-sni, secure-verify-hostname, secure-verify-ca-secret, secure-crt-secret |
backend-directives-660-haproxy-ingress-server-options |
initial-weight, other server-line options |
backend-directives-670-haproxy-ingress-session-affinity |
affinity, session-cookie-* |
backend-directives-680-haproxy-ingress-auth |
auth-secret, auth-realm (attaches userlist to the backend) |
backend-directives-685-haproxy-ingress-rate-limiting |
limit-rps, limit-rpm, limit-whitelist |
backend-directives-690-haproxy-ingress-rewrite-target |
rewrite-target (capture-group rewrites; literal rewrites go to path-rewrite.map) |
backend-directives-695-haproxy-ingress-agent-check |
agent-check-port, agent-check-addr, agent-check-interval, agent-check-send |
backend-directives-900-haproxy-ingress-config-backend |
config-backend |
frontend-filters-* (HTTP-frontend request/response filters)¶
| Snippet | Annotations Processed |
|---|---|
frontend-filters-600-haproxy-ingress-forwardfor |
forwardfor |
frontend-filters-610-haproxy-ingress-access-control |
allowlist-source-range (or its deprecated alias whitelist-source-range), denylist-source-range |
frontend-filters-660-haproxy-ingress-cors |
cors-enable, cors-* |
frontend-filters-670-haproxy-ingress-headers |
headers |
frontend-filters-680-haproxy-ingress-default-backend-redirect |
default-backend-redirect, default-backend-redirect-code |
Other extension points¶
| Snippet | Extension Point | Purpose |
|---|---|---|
global-top-600-haproxy-ingress-auth |
global-top-* |
Emits a userlist auth_<secretNs>_<secretName> per unique auth secret (deduplicated) |
backends-501-haproxy-ingress-ssl-passthrough |
backends-* |
TCP-mode passthrough backends for hosts with ssl-passthrough: "true" |
map-host-650-haproxy-ingress-alias |
map-host-* |
server-alias hostnames → the primary host's routing key in host.map |
map-hostregex-650-haproxy-ingress-alias |
map-hostregex-* |
server-alias-regex patterns → the primary host's routing key in host-regex.map |
map-body-size-680-haproxy-ingress |
map-body-size-* |
Per-backend body-size.map entries for proxy-body-size limits |
map-path-rewrite-690-haproxy-ingress |
map-path-rewrite-* |
Per-backend path-rewrite.map entries for literal rewrite-target values |
frontend-extra-650-haproxy-ingress-config-frontend |
frontend-extra-* |
config-frontend raw directives in the HTTP frontend |
global-settings-650-haproxy-ingress-config-global |
global-settings-* |
config-global raw directives in the global section |
defaults-settings-650-haproxy-ingress-config-defaults |
defaults-settings-* |
config-defaults raw directives in the defaults section |
Path matching¶
haproxy-ingress.github.io/path-type¶
Status: ✅ Supported
Description: Controls how path matching is performed for paths with pathType: ImplementationSpecific.
Valid values: regex, exact, prefix, begin
Usage:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-matching-example
annotations:
haproxy-ingress.github.io/path-type: "regex"
spec:
ingressClassName: haptic
rules:
- host: api.example.com
http:
paths:
- path: "^/api/v[0-9]+/users/[0-9]+$"
pathType: ImplementationSpecific
backend:
service:
name: users-service
port:
number: 80
Path type behaviors:
| Value | Behavior | Example Path | Matches |
|---|---|---|---|
regex |
Regular expression matching | ^/api/v[0-9]+/ |
/api/v1/, /api/v2/users |
exact |
Exact string match | /api/users |
Only /api/users |
prefix |
Path prefix (with segment boundaries) | /api/ |
/api/, /api/users, /api/v1/ |
begin |
Legacy prefix (simple string prefix) | /api |
/api, /api/users, /apikey |
Standard Path Types
For pathType: Exact or pathType: Prefix, the annotation is ignored. The annotation only affects pathType: ImplementationSpecific paths.
Backend Configuration¶
Timeouts¶
Status: ✅ Supported
Annotations:
| Annotation | Description | Default |
|---|---|---|
timeout-connect |
Backend connection timeout | - |
timeout-server |
Backend response timeout | - |
timeout-queue |
Queue wait timeout | - |
timeout-tunnel |
Tunnel/WebSocket timeout | - |
timeout-http-request |
HTTP request timeout | - |
timeout-keep-alive |
Keep-alive timeout | - |
Usage:
annotations:
haproxy-ingress.github.io/timeout-server: "60s"
haproxy-ingress.github.io/timeout-connect: "10s"
haproxy-ingress.github.io/timeout-queue: "30s"
Generated HAProxy Configuration:
backend my-backend
timeout connect 10s
timeout queue 30s
http-request set-timeout server var(txn.backend_name),concat(|server),map_str_int(maps/backend-timeouts.map) if { var(txn.backend_name),concat(|server),map_str_int(maps/backend-timeouts.map) -m found }
timeout-connect and timeout-queue render as literal backend directives. timeout-server (and timeout-tunnel) are reload-free: the value moves into backend-timeouts.map as milliseconds keyed <backend>|server (here my-backend|server 60000), read by the uniform set-timeout line every backend carries.
haproxy-ingress.github.io/balance-algorithm¶
Status: ✅ Supported
Description: Load balancing algorithm for the backend.
Valid values: roundrobin, leastconn, source, first, random, static-rr, uri, url_param, hdr, rdp-cookie
Usage:
Generated HAProxy Configuration:
Connection limits¶
Annotations:
| Annotation | Status | Description |
|---|---|---|
limit-connections |
✅ Supported | Backend fullconn limit |
maxconn-server |
✅ Supported | Per-server maxconn on the default-server line |
maxqueue-server |
✅ Supported | Per-server maxqueue on the default-server line |
maxconn-server and maxqueue-server append maxconn <n> / maxqueue <n> to
serverOpts["flags"] in backend-directives-620-haproxy-ingress-maxconn, which
BuildServerOptions (base library) emits onto the default-server line.
Usage:
annotations:
haproxy-ingress.github.io/limit-connections: "1000"
haproxy-ingress.github.io/maxconn-server: "50"
haproxy-ingress.github.io/maxqueue-server: "100"
Health checks¶
Annotations:
| Annotation | Status | Description |
|---|---|---|
health-check-uri |
✅ Supported | HTTP health check path — emitted as option httpchk GET <uri> |
backend-check-interval |
✅ Supported | Emitted as inter <value> on the default-server line |
health-check-port |
✅ Supported | Emitted as port <n> on the default-server line |
health-check-fall-count |
✅ Supported | Emitted as fall <n> on the default-server line |
health-check-rise-count |
✅ Supported | Emitted as rise <n> on the default-server line |
backend-directives-630-haproxy-ingress-health-checks appends inter / port /
fall / rise to serverOpts["flags"], which BuildServerOptions (base
library) emits onto the default-server line.
Usage:
annotations:
haproxy-ingress.github.io/health-check-uri: "/healthz"
haproxy-ingress.github.io/backend-check-interval: "5s"
haproxy-ingress.github.io/health-check-port: "8082"
haproxy-ingress.github.io/health-check-fall-count: "3"
haproxy-ingress.github.io/health-check-rise-count: "2"
Generated HAProxy Configuration:
backend my-backend
option httpchk GET /healthz
default-server check inter 5s port 8082 fall 3 rise 2
server my-app-pod-1 10.0.0.1:8080 guid srv:my-backend:my-app-pod-1 # Pod: my-app-pod-1
check and its tuning (inter, port, fall, rise) live on default-server (not on individual server lines) so endpoint changes can be applied via the runtime API without a HAProxy reload. Each server is named after its pod (ADR-0011) and carries a stable guid.
haproxy-ingress.github.io/agent-check-port¶
Status: ✅ Supported
Description: Enable HAProxy's auxiliary agent check: HAProxy connects to an agent on each server at the given port, and the agent's reply can adjust the server's weight or state. agent-check-port is the enabler — setting agent-check-addr, agent-check-interval, or agent-check-send without it fails the render, because HAProxy disables an agent check that has no port.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
agent-check-port |
Port the agent listens on (required to enable) | - |
agent-check-addr |
Address the agent listens on | server address |
agent-check-interval |
Interval between agent checks | HAProxy's agent-inter default |
agent-check-send |
String sent to the agent on connect | - |
Usage:
annotations:
haproxy-ingress.github.io/agent-check-port: "9998"
haproxy-ingress.github.io/agent-check-addr: "10.0.0.50"
haproxy-ingress.github.io/agent-check-interval: "5s"
haproxy-ingress.github.io/agent-check-send: "check"
Generated HAProxy Configuration:
backend my-backend
default-server check agent-check agent-port 9998 agent-addr 10.0.0.50 agent-inter 5s agent-send check
Like the health-check options, the agent-check parameters live on the default-server line, so they're runtime-safe.
haproxy-ingress.github.io/proxy-protocol¶
Status: ✅ Supported
Description: Enable PROXY protocol when connecting to backend servers.
Valid values: v1, v2, v2-ssl, v2-ssl-cn
Usage:
Generated HAProxy Configuration:
backend my-backend
default-server check send-proxy-v2
server my-app-pod-1 10.0.0.1:8080 guid srv:my-backend:my-app-pod-1 # Pod: my-app-pod-1
send-proxy-v2 lives on default-server, not on individual server lines, so pods can be added or removed over the runtime API without a HAProxy reload.
Backend SSL¶
Status: ✅ Supported
Annotations:
| Annotation | Description |
|---|---|
secure-backends |
Enable SSL to backend (true/false) |
backend-protocol |
Protocol: h1, h2, h1-ssl, h2-ssl |
secure-sni |
SNI value for backend connection |
secure-verify-hostname |
Hostname for certificate verification |
secure-verify-ca-secret |
CA secret for backend verification |
secure-crt-secret |
Client certificate for mTLS |
ssl-ciphers-backend |
Cipher list (TLS ≤ 1.2) for the backend connection |
ssl-cipher-suites-backend |
Cipher suites (TLS 1.3) for the backend connection |
Usage:
annotations:
haproxy-ingress.github.io/secure-backends: "true"
haproxy-ingress.github.io/backend-protocol: "h2"
haproxy-ingress.github.io/secure-verify-ca-secret: "backend-ca"
haproxy-ingress.github.io/secure-crt-secret: "client-cert"
haproxy-ingress.github.io/ssl-ciphers-backend: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
Generated HAProxy Configuration:
backend my-backend
default-server check ssl alpn h2 ca-file /path/to/ca.pem crt /path/to/client.pem verify required ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
server my-app-pod-1 10.0.0.1:8443 guid srv:my-backend:my-app-pod-1 # Pod: my-app-pod-1
The SSL keywords live on default-server, not on individual server lines, so pods can be added or removed over the runtime API without a HAProxy reload. ssl-ciphers-backend (HAProxy's ciphers server option) and ssl-cipher-suites-backend (ciphersuites) apply only when TLS to the backend is on — secure-backends: "true" or an -ssl backend-protocol. Without backend TLS they're ignored, because HAProxy rejects the keywords on a plaintext server line.
haproxy-ingress.github.io/initial-weight¶
Status: ✅ Supported
Description: Initial weight for backend servers (0-256). The library reads and validates the value (0-256 range), then appends weight <n> to serverOpts["flags"] in backend-directives-660-haproxy-ingress-server-options, which BuildServerOptions (base library) emits onto the default-server line.
Usage:
haproxy-ingress.github.io/proxy-body-size¶
Status: ✅ Supported
Description: Maximum allowed request body size. Requests exceeding the limit receive a 413 response.
Valid values: Plain number (bytes), or with k/m/g suffix (case-insensitive). Value 0 (the upstream default) means unlimited — no limit is emitted.
Usage:
Generated configuration: the per-backend limit is written to body-size.map
(keyed on the resolved backend), not into the backend section. A shared,
resource-agnostic frontend rule (base.yaml frontend-filters-250-request-body-size)
enforces it, so adding or changing the limit is a map-only, reload-free update.
# frontend (shared, static — emitted once regardless of how many backends set a limit)
http-request set-var(txn.haptic_body_limit) var(txn.backend_name),map(maps/body-size.map,0),add(0)
http-request deny deny_status 413 if { var(txn.haptic_body_limit) -m int gt 0 } { req.body_size,sub(txn.haptic_body_limit) -m int gt 0 }
Rate limiting¶
haproxy-ingress.github.io/limit-rps¶
Status: ✅ Supported
Description: Reject a source IP's requests with HTTP 429 once it exceeds the configured rate.
Related annotations:
| Annotation | Description |
|---|---|
limit-rps |
Maximum requests per second per source IP |
limit-rpm |
Maximum requests per minute per source IP |
limit-whitelist |
Comma-separated CIDRs exempt from the limit |
The cap is hard — jcmoraisjr/haproxy-ingress grants a burst allowance on top of the configured rate, so expect stricter enforcement at the same value after migrating. HAProxy stores one request-rate counter per backend, so when both are set limit-rps wins and limit-rpm is ignored (the rendered config notes it in a comment). Invalid CIDRs in limit-whitelist fail the render.
Usage:
annotations:
haproxy-ingress.github.io/limit-rps: "10"
haproxy-ingress.github.io/limit-whitelist: "10.0.0.0/8"
Generated HAProxy Configuration:
backend my-backend
stick-table type ip size 100k expire 1s store http_req_rate(1s) peers localinstance
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 10 } !{ src 10.0.0.0/8 }
The peers localinstance reference carries the per-source counters across HAProxy reloads, so accumulated rates survive config churn.
URL rewriting¶
haproxy-ingress.github.io/rewrite-target¶
Status: ✅ Supported
Description: Rewrite the request path before forwarding to the backend. Capture groups written in the nginx-compatible $1–$9 form are translated to HAProxy's \1–\9 backreferences.
Usage:
Generated HAProxy Configuration:
Capture rewrites (the value contains $N) stay as a per-backend replace-path:
A literal rewrite (no capture, for example rewrite-target: "/new") is instead written to
path-rewrite.map (<backend_name> /new) and applied by a shared frontend set-path rule,
so changing the value is a map-only, reload-free update.
Raw Configuration Injection¶
The four config-* annotations inject operator-authored HAProxy directives verbatim into a configuration section. HAPTIC validates the resulting config before deploying it, but the directives are yours — a typo fails the render.
haproxy-ingress.github.io/config-backend¶
Status: ✅ Supported
Description: Raw HAProxy directives injected into each of the Ingress's backend sections.
Usage:
annotations:
haproxy-ingress.github.io/config-backend: |
http-send-name-header X-Backend-Server
retries 5
haproxy-ingress.github.io/config-global¶
Status: ✅ Supported
Description: Raw HAProxy directives injected into the global section. The section is process-wide: every Ingress carrying the annotation contributes its block (prefixed with an # Ingress: <namespace>/<name> comment), and deduplication across Ingresses is your responsibility.
Usage:
haproxy-ingress.github.io/config-frontend¶
Status: ✅ Supported
Description: Raw HAProxy directives injected into HAPTIC's shared HTTP frontend — not a per-Ingress frontend, so the directives apply to all HTTP traffic. They render before the routing logic, so captures, ACLs, and early http-request rules you inject are in scope for the routing that follows. Like config-global, every annotated Ingress contributes its block.
Usage:
annotations:
haproxy-ingress.github.io/config-frontend: |
capture request header X-Request-Id len 64
haproxy-ingress.github.io/config-defaults¶
Status: ✅ Supported
Description: Raw HAProxy directives injected into the defaults section. Same process-wide semantics as config-global.
Usage:
Session affinity¶
Cookie-based session affinity — also called sticky sessions — pins a client to the same backend server across requests.
haproxy-ingress.github.io/affinity¶
Status: ✅ Supported
Description: Enable cookie-based session affinity.
Valid values: cookie
Related annotations:
| Annotation | Description | Default |
|---|---|---|
session-cookie-name |
Cookie name | INGRESSCOOKIE |
session-cookie-strategy |
insert, rewrite, prefix |
insert |
session-cookie-dynamic |
Use dynamic cookie key | true |
session-cookie-keywords |
Additional cookie options | - |
session-cookie-domain |
Cookie domain | - |
session-cookie-same-site |
None, Lax, Strict |
- |
session-cookie-preserve |
Preserve backend cookies | - |
Usage:
annotations:
haproxy-ingress.github.io/affinity: "cookie"
haproxy-ingress.github.io/session-cookie-name: "SERVERID"
haproxy-ingress.github.io/session-cookie-strategy: "insert"
haproxy-ingress.github.io/session-cookie-same-site: "Lax"
Generated HAProxy Configuration:
backend my-backend
cookie SERVERID insert indirect nocache dynamic attr "SameSite=Lax"
dynamic-cookie-key <generated-key>
Access control¶
haproxy-ingress.github.io/allowlist-source-range¶
Status: ✅ Supported
Description: Comma-separated list of CIDRs allowed to access this ingress.
Usage:
Generated HAProxy Configuration:
haproxy-ingress.github.io/whitelist-source-range¶
Status: ✅ Supported (deprecated alias)
Description: Deprecated alias of allowlist-source-range, honoured only when allowlist-source-range is absent on the same Ingress. Prefer allowlist-source-range for new Ingresses.
haproxy-ingress.github.io/denylist-source-range¶
Status: ✅ Supported
Description: Comma-separated list of CIDRs denied access to this ingress.
Usage:
Redirects¶
haproxy-ingress.github.io/ssl-redirect¶
Status: ✅ Supported
Description: Redirect HTTP requests to HTTPS.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
ssl-redirect |
Enable SSL redirect | - |
ssl-redirect-code |
HTTP status code | 302 |
Usage:
annotations:
haproxy-ingress.github.io/ssl-redirect: "true"
haproxy-ingress.github.io/ssl-redirect-code: "301"
haproxy-ingress.github.io/app-root¶
Status: ✅ Supported
Description: Redirect requests to root path (/) to the specified path.
Usage:
haproxy-ingress.github.io/redirect-to¶
Status: ✅ Supported
Description: Redirect all requests to the specified URL.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
redirect-to |
Target URL | - |
redirect-to-code |
HTTP status code | 302 |
Usage:
annotations:
haproxy-ingress.github.io/redirect-to: "https://new.example.com"
haproxy-ingress.github.io/redirect-to-code: "301"
haproxy-ingress.github.io/default-backend-redirect¶
Status: ✅ Supported
Description: Redirect requests that match one of the Ingress's hosts but none of its paths, instead of letting them fall through to the default backend.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
default-backend-redirect |
Target URL | - |
default-backend-redirect-code |
HTTP status code: 301, 302, 303, 307, or 308 (other values fail the render) | 302 |
Usage:
annotations:
haproxy-ingress.github.io/default-backend-redirect: "https://landing.example.com"
haproxy-ingress.github.io/default-backend-redirect-code: "301"
Generated HAProxy Configuration:
http-request redirect location %[var(txn.host),map(maps/default-backend-redirect-301.map)] code 301 if !{ var(txn.backend_name) -m found } { var(txn.host),map(maps/default-backend-redirect-301.map) -m found }
The host→URL pairs live in a per-code map, so changing the target URL is a map-only, reload-free update. The !{ var(txn.backend_name) -m found } guard fires exactly when the routing cascade matched no path — the same condition that otherwise selects the default backend.
HSTS¶
haproxy-ingress.github.io/hsts¶
Status: ✅ Supported
Description: Enable HTTP Strict Transport Security (HSTS) headers.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
hsts |
Enable HSTS | - |
hsts-max-age |
Max-age in seconds | 15768000 |
hsts-include-subdomains |
Include subdomains | - |
hsts-preload |
Enable preload | - |
Usage:
annotations:
haproxy-ingress.github.io/hsts: "true"
haproxy-ingress.github.io/hsts-max-age: "31536000"
haproxy-ingress.github.io/hsts-include-subdomains: "true"
haproxy-ingress.github.io/hsts-preload: "true"
Generated HAProxy Configuration:
http-response set-header Strict-Transport-Security %[var(txn.host),map(maps/hsts.map),url_dec(1)] if { ssl_fc } { var(txn.host),map(maps/hsts.map) -m found }
One shared rule for all hosts; each host's value (here max-age=31536000; includeSubDomains; preload) lives in hsts.map keyed by host, so adding or changing an HSTS value is a map-only, reload-free update.
CORS¶
haproxy-ingress.github.io/cors-enable¶
Status: ✅ Supported
Description: Enable Cross-Origin Resource Sharing (CORS) handling for the ingress.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
cors-enable |
Enable CORS | - |
cors-allow-origin |
Allowed origins — comma-separated list, single-level *. wildcards; matched Origin is echoed back |
* |
cors-allow-methods |
Allowed methods | GET, PUT, POST, DELETE, PATCH, OPTIONS |
cors-allow-headers |
Allowed headers | Common headers |
cors-allow-credentials |
Allow credentials | - |
cors-expose-headers |
Exposed headers | - |
cors-max-age |
Preflight cache time | 86400 |
Usage:
annotations:
haproxy-ingress.github.io/cors-enable: "true"
haproxy-ingress.github.io/cors-allow-origin: "https://example.com"
haproxy-ingress.github.io/cors-allow-credentials: "true"
Headers¶
haproxy-ingress.github.io/forwardfor¶
Status: ✅ Supported
Description: Configure X-Forwarded-For header handling.
Valid values: add, update, ignore, ifmissing
Usage:
haproxy-ingress.github.io/headers¶
Status: ✅ Supported
Description: Add request headers. Pipe-separated name:value pairs.
Usage:
Server alias¶
haproxy-ingress.github.io/server-alias¶
Status: ✅ Supported
Description: Comma-separated extra exact hostnames that route like the Ingress's first rule host. Each alias becomes a host.map entry pointing at the primary host's routing key, so every path already registered for that host applies to the alias — no backend or path duplication.
Usage:
Generated configuration (host.map entries, for an Ingress whose first rule host is example.com):
haproxy-ingress.github.io/server-alias-regex¶
Status: ✅ Supported
Description: A regular expression matching extra hostnames, routed to the Ingress's first rule host via host-regex.map. The routing cascade consults host-regex.map after an exact host.map miss, so one entry routes every matching hostname. The value is emitted verbatim and must be a HAProxy-compatible Perl Compatible Regular Expression (PCRE).
Usage:
Generated configuration (host-regex.map entry):
SSL features¶
haproxy-ingress.github.io/ssl-passthrough¶
Status: ✅ Supported
Description: Enable TCP-level SSL passthrough (Layer 4) where HAProxy routes based on SNI without terminating SSL.
Usage:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ssl-passthrough-example
annotations:
haproxy-ingress.github.io/ssl-passthrough: "true"
spec:
tls:
- hosts:
- secure.example.com
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-backend
port:
number: 443
Implementation notes:
- Uses SNI-based routing in TCP mode
- Backend receives encrypted traffic and terminates SSL
- HTTP-level features (headers, path rewriting) aren't available for passthrough traffic
- Mixed passthrough and termination on different hosts is supported
Authentication¶
haproxy-ingress.github.io/auth-secret¶
Status: ✅ Supported
Description: Enable basic authentication using credentials from a Kubernetes Secret.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
auth-secret |
Secret name (or namespace/name) |
- |
auth-realm |
Authentication realm | Restricted |
Usage:
annotations:
haproxy-ingress.github.io/auth-secret: "basic-auth"
haproxy-ingress.github.io/auth-realm: "Protected Area"
Secret format:
apiVersion: v1
kind: Secret
metadata:
name: basic-auth
type: Opaque
data:
# Key: username, Value: base64-encoded bcrypt hash (NOT htpasswd format)
admin: JDJ5JDA1JG1OMVdWazVRbmJnNFF3ZEFkWGJmei44YjNjZUg2UTVLT1ZDS3hSMklrTkFmSmdMaTVwSUtX
Generate password hash:
External authentication¶
The library wires the haproxy-ingress.github.io/auth-* annotation family to the Stream Processing Offload Agent (SPOA) hub's external-auth plugin (v0.3.0+). When set, each request hits an HTTP auth subrequest before reaching the backend; the auth service's status code decides whether HAProxy forwards the request, redirects to a sign-in URL, or returns 401.
Prerequisites¶
The SPOA hub sidecar with the external-auth plugin must be enabled:
The hub auto-enables when any plugin is on, and the spoa-hub template library auto-loads when the hub is enabled. See the SPOA Hub operations guide for the full deployment surface.
Not auto-enabled with this library
Unlike the nginx-ingress library, enabling the haproxy-ingress library doesn't auto-enable the external-auth plugin. Without the plugin, auth-url is silently not enforced — set spoaHub.plugins.external-auth.enabled=true explicitly.
Host-less rules error at render time
All external-auth annotations key their per-route lookup tables by host+path. An Ingress rule without an explicit host can't be enforced — silently skipping auth on a route the operator marked protected would be a security failure mode. The chart fails the Helm render with an explicit error identifying the offending Ingress; add a host: to the rule to fix.
haproxy-ingress.github.io/auth-url¶
Status: ✅ Supported
Description: Auth service URL the SPOA hub calls per request. The plugin appends the original request path, sends a GET (overridable via auth-method), and gates the request based on the response status: 2xx allows, 3xx with auth-signin redirects, anything else returns 401.
Usage:
Generated HAProxy Configuration:
http-request set-var(txn.auth_url) var(txn.host_match),concat(,txn.path,),map(maps/auth-url.map)
http-request send-spoe-group spoa-hub-check-auth check-auth-group if { var(txn.auth_url) -m found }
http-request deny deny_status 401 if { var(txn.auth_url) -m found } !{ var(txn.hub.external_auth.allowed) -m bool }
The matching auth-url.map entry:
haproxy-ingress.github.io/auth-signin¶
Status: ✅ Supported
Description: Browser-flow sign-in URL. When set, an auth failure produces a 302 redirect instead of a 401 — the standard pattern for OpenID Connect (OIDC) / Security Assertion Markup Language (SAML) flows where unauthenticated users go to a login page. The deny rule still emits, so routes without auth-signin keep the API-friendly 401.
Usage:
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-signin: "https://login.example.com/oauth2/start"
Generated HAProxy Configuration:
http-request redirect location %[var(txn.auth_signin)] code 302 if { var(txn.auth_url) -m found } !{ var(txn.hub.external_auth.allowed) -m bool } { var(txn.auth_signin) -m found }
http-request deny deny_status 401 if { var(txn.auth_url) -m found } !{ var(txn.hub.external_auth.allowed) -m bool }
haproxy-ingress.github.io/auth-method¶
Status: ✅ Supported
Description: HTTP method for the auth subrequest. Defaults to GET (or whatever the plugin's TOML config sets); set this to override per-route.
Valid values: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS
Usage:
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-method: "POST"
Body-having methods carry an empty body
POST / PUT / PATCH go to the auth service with an empty body — the plugin doesn't forward the original request payload. Auth services that need the body should read it via sample-fetch args (see SPOA hub operations) or use header-based auth.
haproxy-ingress.github.io/auth-headers-request¶
Status: ✅ Supported
Description: Comma-separated list of request header names to forward to the auth service. The chart auto-extends the Stream Processing Offload Engine (SPOE) message body to capture every header listed across ingresses (deduped, the six standard headers — Authorization, Cookie, X-Forwarded-{For,Proto,Host,Uri} — are always captured), and the plugin then narrows the per-route forwarded set to exactly the headers the annotation lists.
Usage:
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-headers-request: "Authorization, X-Tenant-Id, X-Request-Id"
Generated HAProxy Configuration:
http-request set-var(txn.auth_forward_headers) var(txn.host_match),concat(,txn.path,),map(maps/auth-forward-headers.map)
The corresponding entries in the SPOE message:
spoe-message check-auth
args ... forward_headers=var(txn.auth_forward_headers) ... hdr_authorization=req.hdr(Authorization) hdr_x_tenant_id=req.hdr(X-Tenant-Id) hdr_x_request_id=req.hdr(X-Request-Id)
Header names are validated against the RFC 7230 token grammar; values containing whitespace, fetch syntax (%[var(...)]), or other non-tchar characters fail the Helm render.
haproxy-ingress.github.io/auth-headers-succeed¶
Status: ✅ Supported
Description: Comma-separated list of response header names from the auth service to forward to the upstream backend on auth success. Common pattern: the auth service returns X-Auth-User: alice on 200, this annotation makes that header available to the backend application.
Usage:
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-headers-succeed: "X-Auth-User, X-Auth-Roles"
Generated HAProxy Configuration:
http-request set-header X-Auth-User %[var(txn.hub.external_auth.x_auth_user)] if { var(txn.hub.external_auth.x_auth_user) -m found } { var(txn.hub.external_auth.allowed) -m bool }
http-request set-header X-Auth-Roles %[var(txn.hub.external_auth.x_auth_roles)] if { var(txn.hub.external_auth.x_auth_roles) -m found } { var(txn.hub.external_auth.allowed) -m bool }
One set-header directive per unique header across all ingresses; the per-route gating happens via the plugin's per-ingress extract_headers SPOE arg — routes that didn't list a header have its txn var unset, so the var ... -m found gate skips them.
haproxy-ingress.github.io/auth-headers-fail¶
Status: ✅ Supported
Description: Comma-separated list of response header names from the auth service to forward to the client on auth failure. Drives, for example, WWW-Authenticate for Bearer challenges or X-Error-Reason for diagnostics on 401 / 5xx.
Usage:
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-headers-fail: "WWW-Authenticate, X-Error-Reason"
Generated HAProxy Configuration:
http-after-response set-header WWW-Authenticate %[var(txn.hub.external_auth.www_authenticate)] if { var(txn.auth_url) -m found } !{ var(txn.hub.external_auth.allowed) -m bool } { var(txn.hub.external_auth.www_authenticate) -m found }
http-after-response set-header X-Error-Reason %[var(txn.hub.external_auth.x_error_reason)] if { var(txn.auth_url) -m found } !{ var(txn.hub.external_auth.allowed) -m bool } { var(txn.hub.external_auth.x_error_reason) -m found }
The conditions ensure the directive only fires on the deny response (auth path ran, not allowed, plugin actually extracted the header). The plugin v0.3.0+ extracts headers on every reply path (2xx, 3xx, 4xx, 5xx, fail-policy), so 401 and 5xx replies populate the txn vars too.
Why http-after-response rather than http-response
HAProxy's http-request deny short-circuits the request flow — the 401 response is generated internally, so http-response rules (which fire only on responses received from a backend) never apply to it. http-after-response runs after every response, including HAProxy-generated ones, and is the only directive that lets the extracted fail-path headers reach the client.
haproxy-ingress.github.io/oauth¶
Status: ✅ Supported
Description: Convenience wiring for oauth2-proxy. Setting oauth: "oauth2_proxy" (or "oauth2-proxy" — the only accepted values) desugars onto the external-auth machinery: it derives the auth URL, sign-in redirect, subrequest method, and success-header forwarding that you would otherwise wire via auth-url, auth-signin, auth-method, and auth-headers-succeed. The Ingress must route the oauth-uri-prefix path (default /oauth2) to the oauth2-proxy Service — the auth URL is derived from that path's Service, and the render fails without such a path.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
oauth |
oauth2_proxy / oauth2-proxy (other values fail the render) |
- |
oauth-uri-prefix |
The Ingress path routing to the oauth2-proxy Service | /oauth2 |
oauth-headers |
Auth-reply headers forwarded to the backend on success | X-Auth-Request-Email |
Usage:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oauth-protected
annotations:
haproxy-ingress.github.io/oauth: "oauth2_proxy"
spec:
ingressClassName: haptic
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-backend
port:
number: 80
- path: /oauth2
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 4180
Effective external-auth configuration (what the desugaring derives):
auth-url http://oauth2-proxy.<namespace>.svc:4180/oauth2/auth
auth-signin /oauth2/start?rd=%[path]
auth-method HEAD
forwarded success headers: X-Auth-Request-Email
An explicit auth-url on the same Ingress takes precedence and disables the oauth desugaring entirely; an explicit auth-signin, auth-method, or auth-headers-succeed overrides only its derived value.
Plaintext auth URL
The derived auth URL is plain http:// toward the in-cluster oauth2-proxy Service, so the external-auth plugin must allow plaintext: set allow_plaintext = true in spoaHub.plugins.external-auth.params.
Combined example¶
End-to-end: a protected API route with browser sign-in, custom request header forwarding, identity propagation to the backend, and a Bearer challenge on failure.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: protected-api
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-signin: "https://login.example.com/oauth2/start"
haproxy-ingress.github.io/auth-method: "GET"
haproxy-ingress.github.io/auth-headers-request: "Authorization, X-Tenant-Id"
haproxy-ingress.github.io/auth-headers-succeed: "X-Auth-User, X-Auth-Roles"
haproxy-ingress.github.io/auth-headers-fail: "WWW-Authenticate"
spec:
ingressClassName: haptic
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-backend
port:
number: 80
Client certificate auth (mTLS)¶
The library wires the haproxy-ingress.github.io/auth-tls-* annotations for incoming client-cert verification. The CA bundle from the referenced Secret is written to the SSL cert dir and referenced from the HAProxy crt-list line for the matching SNI as [ca-file <path> verify <mode>] — HAProxy verifies incoming client certs at the TLS layer. Companion annotations react to the verification result via ssl_c_verify and ssl_fc_has_crt.
haproxy-ingress.github.io/auth-tls-secret¶
Status: ✅ Supported
Description: Reference to a kubernetes.io/tls Secret whose ca.crt field contains the CA bundle that signs the clients' certificates. The chart writes the CA to ssl/<ns>-<secret>-client-ca.pem and adds [ca-file <path> verify <mode>] to the crt-list line for every host on the annotated Ingress.
Format: name (resolves in the Ingress namespace) or namespace/name.
Usage:
The Secret:
apiVersion: v1
kind: Secret
metadata:
name: client-ca
type: kubernetes.io/tls
data:
ca.crt: <base64 PEM CA bundle>
Host-less rules error at render time
SNI-keyed verification can't be enforced on Ingress rules without a host:. The chart fails the Helm render with a descriptive error.
haproxy-ingress.github.io/auth-tls-verify-client¶
Status: ✅ Supported
Description: Client certificate verification mode.
Valid values:
| Value | HAProxy verify mode | Behaviour |
|---|---|---|
on (default) |
required |
Reject connections without a valid client cert |
off |
(no-op) | Don't enable verification on this host — the entry is skipped, falling through to the default crt-list line |
optional |
optional |
Verify when a cert is presented; allow connections without |
optional_no_ca |
optional |
Same as optional — HAProxy doesn't have a distinct mode for "verify but accept invalid"; operators wanting to accept self-signed certs should set optional and inspect ssl_c_used in their auth-tls-error-page logic |
Other values fail the render.
Usage:
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-verify-client: "optional"
auth-tls-strict not separately wired
haproxy-ingress's auth-tls-strict annotation overlaps with auth-tls-verify-client: optional — operators wanting "soft" verification (allow connections without cert, defer to backend header inspection) should use auth-tls-verify-client: optional directly.
haproxy-ingress.github.io/auth-tls-error-page¶
Status: ✅ Supported
Description: URL to redirect to (302) when client certificate verification fails.
Usage:
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-error-page: "https://example.com/cert-required"
Generated HAProxy Configuration:
http-request redirect location https://example.com/cert-required code 302 if { ssl_c_verify gt 0 } { hdr(host) -i example.com }
The ssl_c_verify gt 0 condition matches any verification error (including missing cert when verify required is set).
haproxy-ingress.github.io/auth-tls-cert-header¶
Status: ✅ Supported
Description: When "true", forwards the verified client certificate (base64-encoded DER), subject CN, and full subject DN to the upstream backend as HTTP headers.
Usage:
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-cert-header: "true"
Generated HAProxy Configuration:
http-request set-header X-SSL-Client-CN %[ssl_c_s_dn(CN)] if { ssl_fc_has_crt } { hdr(host) -i example.com }
http-request set-header X-SSL-Client-DN %[ssl_c_s_dn] if { ssl_fc_has_crt } { hdr(host) -i example.com }
http-request set-header X-SSL-Client-Cert %[ssl_c_der,base64] if { ssl_fc_has_crt } { hdr(host) -i example.com }
The ssl_fc_has_crt gate ensures the headers only flow when a cert was actually presented (relevant when auth-tls-verify-client: optional is in effect — connections without a cert get no headers rather than empty ones).
Web application firewall (ModSecurity / Coraza)¶
Opt an Ingress into the SPOA hub Coraza Web Application Firewall (WAF) with haproxy-ingress.github.io/waf: "modsecurity" (the only supported value). Each <host><path> is added to haproxy-ingress-waf.map, whose value is the enforcement mode from haproxy-ingress.github.io/waf-mode: deny (default) blocks on a rule hit, detect runs the WAF in shadow mode (logs without blocking). Setting waf-mode to anything other than deny/detect, or setting it without waf, fails the render. The Coraza plugin auto-enables when the haproxy-ingress or nginx-ingress library is on. See the SPOA Hub operations guide for the plugin's configuration surface.
Unsupported annotations¶
The library reads only the annotations documented on this page; it ignores any other haproxy-ingress.github.io/* key. Two annotations are accepted for compatibility but have no effect:
| Annotation | Reason |
|---|---|
auth-tls-strict |
Upstream defaults it to true (fail-closed on a missing/invalid client CA); here a missing CA skips mTLS for the Ingress (fail-open), and this annotation can't restore fail-closed. For soft verification use auth-tls-verify-client: optional instead. |
docs |
A pointer to jcmoraisjr/haproxy-ingress documentation, not a configuration key. |
Before cutting over, paste your manifests into the playground migration report to get a per-annotation verdict for exactly the annotations you use — see Check what changes.
Watched resources¶
This library watches the following additional resources:
- Secrets (
v1/secrets) — read for basic-auth credentials (auth-secret), backend TLS material (secure-verify-ca-secret,secure-crt-secret), and incoming client-CA bundles (auth-tls-secret)
Annotation inventory¶
The machine-readable source of truth for this page is the library's migration coverage declaration in the chart (charts/haptic/charts/haproxy-ingress/90-migration-coverage.yaml). It classifies every haproxy-ingress.github.io/* annotation the library reads, and CI checks that each annotation it classifies as carried over has a reference entry above. The migration guide renders the same data as a per-annotation support table.
Access-log fields¶
The library contributes mtls_verify and mtls_cn to the
structured access log when any Ingress
sets auth-tls-secret or auth-tls-cert-header: the certificate verification
result (0 on success, otherwise an X509 error code) and the client's CN. The
other annotation libraries contribute identical fields, so several can be enabled
at once.
See also¶
- Template Libraries Overview - How template libraries work
- Base Library - Path matching infrastructure
- Ingress Library - Standard Ingress support
- HAProxyTech Library -
haproxy.org/*annotations - HAProxy Ingress Documentation - Original annotation reference