Unreleased documentation. Choose your installed release in the version menu. Features described here may be absent from that release.
haproxy-ingress library¶
Use this library when migrating Ingresses with haproxy-ingress.github.io/*
annotations from haproxy-ingress. It covers
path matching, backend settings, session affinity, TLS, access control, HTTP
Strict Transport Security (HSTS), and Cross-Origin Resource Sharing (CORS).
The library is disabled by default. Enable it to retain supported annotations, and review the caveats below before switching traffic. For new configuration, use native HAPTIC annotations.
See the haproxy-ingress.github.io/* annotations render to HAProxy config live:
Before moving traffic, check annotation compatibility and follow the migration guide.
Configuration¶
Apply the following Helm values through your values file.
The annotation examples on this page belong under an Ingress's metadata.annotations;
quote all annotation values.
controller:
templateLibraries:
haproxyIngress:
enabled: true # Set to enable this opt-in library (disabled by default)
Path matching¶
haproxy-ingress.github.io/path-type¶
Controls how path matching is performed for paths with pathType: ImplementationSpecific.
Valid values: regex, exact, prefix, begin
A regex path matches from the start of the path, on the rule's host only. HAPTIC rejects a regex path whose groups or [ ] classes don't close within it, or that uses \Q, \E, \c or (*.
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 |
This annotation affects only pathType: ImplementationSpecific. It's
ignored for Exact and Prefix paths.
Backend configuration¶
Timeouts¶
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 | - |
annotations:
haproxy-ingress.github.io/timeout-server: "60s"
haproxy-ingress.github.io/timeout-connect: "10s"
haproxy-ingress.github.io/timeout-queue: "30s"
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¶
Load balancing algorithm for the backend.
Valid values: roundrobin, leastconn, source, first, random, static-rr, uri, url_param, hdr, rdp-cookie
Connection limits¶
Annotations:
| Annotation | Description |
|---|---|
limit-connections |
Backend fullconn limit |
maxconn-server |
Per-server maxconn on the default-server line |
maxqueue-server |
Per-server maxqueue on the default-server line |
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 | Description |
|---|---|
health-check-uri |
HTTP health check path — emitted as option httpchk GET <uri> |
backend-check-interval |
Emitted as inter <value> on the default-server line |
health-check-port |
Emitted as port <n> on the default-server line |
health-check-fall-count |
Emitted as fall <n> on the default-server line |
health-check-rise-count |
Emitted as rise <n> on the default-server line |
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"
haproxy-ingress.github.io/agent-check-port¶
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 | - |
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"
haproxy-ingress.github.io/proxy-protocol¶
Enable PROXY protocol when connecting to backend servers.
Valid values: v1, v2, v2-ssl, v2-ssl-cn
Backend SSL¶
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 |
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"
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¶
Initial weight for backend servers, from 0 to 256.
haproxy-ingress.github.io/proxy-body-size¶
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.
Changing a body-size limit deploys without reloading HAProxy.
Rate limiting¶
haproxy-ingress.github.io/limit-rps¶
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. A route carries one request-rate counter, so when both are set limit-rps wins and HAPTIC records a RateLimitCapIgnored Event on the Ingress for the ignored limit-rpm. Invalid CIDRs in limit-whitelist fail the render.
annotations:
haproxy-ingress.github.io/limit-rps: "10"
haproxy-ingress.github.io/limit-whitelist: "10.0.0.0/8"
Rate-limit settings and source-IP exemptions are stored in shared maps. Updating
an existing route's settings changes those maps without changing its backend.
Counters are keyed by route and client address, so each route has a separate
per-client budget. The peers localinstance section preserves counters across
HAProxy reloads.
URL rewriting¶
haproxy-ingress.github.io/rewrite-target¶
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.
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¶
Raw HAProxy directives injected into each of the Ingress's backend sections.
annotations:
haproxy-ingress.github.io/config-backend: |
http-send-name-header X-Backend-Server
retries 5
haproxy-ingress.github.io/config-global¶
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.
haproxy-ingress.github.io/config-frontend¶
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.
annotations:
haproxy-ingress.github.io/config-frontend: |
capture request header X-Request-Id len 64
haproxy-ingress.github.io/config-defaults¶
Raw HAProxy directives injected into the defaults section. Same process-wide semantics as config-global.
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¶
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 | - |
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"
Access control¶
haproxy-ingress.github.io/allowlist-source-range¶
Comma-separated list of CIDRs allowed to access this ingress.
haproxy-ingress.github.io/whitelist-source-range¶
Status: Supported (deprecated alias)
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¶
Comma-separated list of CIDRs denied access to this ingress.
Redirects¶
haproxy-ingress.github.io/ssl-redirect¶
Redirect HTTP requests to HTTPS.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
ssl-redirect |
Enable SSL redirect | - |
ssl-redirect-code |
HTTP status code | 302 |
annotations:
haproxy-ingress.github.io/ssl-redirect: "true"
haproxy-ingress.github.io/ssl-redirect-code: "301"
haproxy-ingress.github.io/app-root¶
Redirect requests to root path (/) to the specified path.
haproxy-ingress.github.io/redirect-to¶
Redirect all requests to the specified URL.
Related annotations:
| Annotation | Description | Default |
|---|---|---|
redirect-to |
Target URL | - |
redirect-to-code |
HTTP status code | 302 |
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¶
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 |
annotations:
haproxy-ingress.github.io/default-backend-redirect: "https://landing.example.com"
haproxy-ingress.github.io/default-backend-redirect-code: "301"
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¶
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 | - |
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"
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¶
Enable Cross-Origin Resource Sharing (CORS) handling for the ingress. The headers come from per-route maps read by one frontend rule block, so adding or removing a CORS route is reload-free.
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 |
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¶
Configure X-Forwarded-For header handling.
Valid values: add, update, ignore, ifmissing
haproxy-ingress.github.io/headers¶
Add request headers. Pipe-separated name:value pairs.
Server alias¶
haproxy-ingress.github.io/server-alias¶
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. Each alias must be a valid hostname, optionally starting with *.; case doesn't matter. A hostname an older Ingress already claims stays with that Ingress; see Host aliases claim hostnames.
haproxy-ingress.github.io/server-alias-regex¶
A regular expression matching extra hostnames, routed to the Ingress's first rule host. The expression must match the whole hostname, and applies only when no rule host, wildcard host, or exact alias matches. It must be a HAProxy-compatible Perl Compatible Regular Expression (PCRE) whose groups and [ ] classes close within it.
SSL features¶
haproxy-ingress.github.io/ssl-passthrough¶
Enable TCP-level SSL passthrough (Layer 4) where HAProxy routes based on SNI without terminating SSL.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ssl-passthrough-example
annotations:
haproxy-ingress.github.io/ssl-passthrough: "true"
spec:
ingressClassName: haptic
tls:
- hosts:
- secure.example.com
rules:
- host: secure.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: secure-backend
port:
number: 443
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¶
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 |
annotations:
haproxy-ingress.github.io/auth-secret: "basic-auth"
haproxy-ingress.github.io/auth-realm: "Protected Area"
Create the Secret in the Ingress namespace. Each key is a username; its value is
that user's password hash, without the username: prefix. With htpasswd installed:
umask 077
htpasswd -n -B admin | cut -d: -f2 > admin.hash
kubectl -n default create secret generic basic-auth --from-file=admin=admin.hash
rm admin.hash
HAPTIC refuses $apr1$, $1$, Data Encryption Standard (DES) crypt, and plaintext credentials by default;
see accepted formats.
Use HTTPS for the protected route; HTTP Basic authentication doesn't encrypt credentials.
External authentication¶
The haproxy-ingress.github.io/auth-* annotations use the bundled external-auth plugin. 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.
Set an explicit host on every protected Ingress rule. HAPTIC rejects
protected rules without one.
haproxy-ingress.github.io/auth-url¶
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.
The matching auth-url.map entry:
haproxy-ingress.github.io/auth-signin¶
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.
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¶
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
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-method: "POST"
POST, PUT, and PATCH auth requests have an empty body. Use header-based
authentication; the original request payload isn't forwarded.
haproxy-ingress.github.io/auth-headers-request¶
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.
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"
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. Names containing _ aren't forwarded: SPOE argument names can't contain -, so X_Api_Key and X-Api-Key would share one capture. The admission webhook rejects them, and an Ingress that already has one gets an InvalidAuthHeader Warning Event; if no other name is listed, the Ingress forwards the default headers. Use the dashed name.
haproxy-ingress.github.io/auth-headers-succeed¶
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.
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"
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¶
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.
annotations:
haproxy-ingress.github.io/auth-url: "https://auth.example.com/check"
haproxy-ingress.github.io/auth-headers-fail: "WWW-Authenticate, X-Error-Reason"
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.
haproxy-ingress.github.io/oauth¶
Use oauth: "oauth2_proxy" or oauth: "oauth2-proxy" with
oauth2-proxy. HAPTIC configures the
authentication URL, sign-in redirect, subrequest method, and identity headers.
The Ingress must route oauth-uri-prefix (default /oauth2) to the oauth2-proxy
Service; rendering fails if that path is missing.
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 |
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¶
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)¶
Use auth-tls-* annotations to require client certificates signed by a trusted
CA. The Ingress also needs an HTTPS certificate for its hosts.
haproxy-ingress.github.io/auth-tls-secret¶
Reference to a 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.
Create the Secret from your client CA bundle in the Ingress's namespace:
Set an explicit host on every protected Ingress rule. HAPTIC rejects
protected rules without one.
haproxy-ingress.github.io/auth-tls-verify-client¶
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; a certificate from an unknown CA still fails verification. Add the issuing CA to the trusted bundle to accept it |
Other values fail the render.
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-verify-client: "optional"
auth-tls-strict has no separate effect. To allow clients without a
certificate, use auth-tls-verify-client: optional; invalid certificates
are still rejected.
haproxy-ingress.github.io/auth-tls-error-page¶
URL to redirect to (302) when client certificate verification fails.
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-error-page: "https://example.com/cert-required"
The redirect can only run after a successful TLS handshake. With the default
verify required, a missing or invalid certificate aborts the handshake: the
client sees a TLS error, not this page. optional still rejects an invalid
certificate; it only permits clients that send no certificate.
haproxy-ingress.github.io/auth-tls-cert-header¶
When "true", forwards the verified client certificate (base64-encoded DER), subject CN, and full subject DN to the upstream backend as HTTP headers.
annotations:
haproxy-ingress.github.io/auth-tls-secret: "client-ca"
haproxy-ingress.github.io/auth-tls-cert-header: "true"
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)¶
Set haproxy-ingress.github.io/waf: "modsecurity" to inspect requests with the
Coraza Web Application Firewall (WAF). This is the only supported waf value.
Choose waf-mode: deny (the default) to block matches or waf-mode: detect to
record them. Other modes, or waf-mode without waf, fail validation.
This annotation library enables Coraza automatically. See the plugin settings to configure it.
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)
See the annotation compatibility table for migration differences.
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.
For custom behavior, use the base extension points and write a template snippet.
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