Proxy Config Reference
QHx Proxy forwards HTTP, TCP, and MQTT traffic and uses SPIFFE workload identities for mTLS. This page describes configuration fields, defaults, and deployment requirements. For complete configurations, see Proxy Configuration Examples.
Before starting a proxy, make the SPIFFE Workload API socket accessible to it, register the workload so it receives an X.509-SVID, and configure the trust bundles needed to authenticate its peers.
Load a configuration
Section titled “Load a configuration”Supply a YAML file with -config:
qhx-proxy -config /etc/qhx/proxy.yamlAlternatively, supply the configuration as the value of QHX_PROXY_CONFIG:
export QHX_PROXY_CONFIG="$(cat /etc/qhx/proxy.yaml)"qhx-proxyThe environment variable contains YAML or JSON text, not a filename. A nonempty
-config path takes precedence; a file read error does not fall back to the
environment. The proxy exits if neither source is supplied or validation fails.
Restart the process to apply configuration changes.
Run qhx-proxy -help for logging options and defaults. Common options are
-zap-log-level=debug, -zap-encoder=json or -zap-encoder=console, and
-zap-devel.
Global settings
Section titled “Global settings”| Field | Required / default | Meaning |
|---|---|---|
spiffe.workload_socket_path | Required | Workload API address visible inside the proxy, for example unix:///spiffe-workload-api/agent.sock. The socket must exist and be accessible to the proxy user. |
listeners | Required, at least one | List of independently configured listeners. |
metricsAddr | ":7600" | Bind address for the HTTP /metrics endpoint. Omitted or empty selects this default. Use "127.0.0.1:7600" to bind to loopback or "0" to disable it. |
keylog | Empty; disabled | File for TLS session keys, created or appended to by the proxy. Use only for controlled debugging: anyone with this file and captured traffic can decrypt the sessions. The parent directory must exist and be writable. |
notary.enable | false | Enable the notary for this proxy’s listeners; see Notary for prerequisites. |
notary.database.path | No default | Writable database file, required when the notary is enabled. The file is created if absent; its parent directory must already exist. |
Listeners and targets
Section titled “Listeners and targets”| Listener field | Required / default | Meaning |
|---|---|---|
name | Required | Nonempty name, unique within this configuration. Identifies the listener in logs. |
address | Required | Bind address in host:port form, such as 127.0.0.1:8081 or :8443. An omitted host binds on all interfaces. |
mode | Required | client, server, or central; see the table below. |
protocol | http | http, tcp, or mqtt. |
target.url | Required | Destination URL with a scheme and host. Include an explicit port for TCP and MQTT. Use the scheme table below. |
source.spiffe_ids | Empty list | Incoming peer identity patterns, used in server and central modes. |
target.spiffe_ids | Empty list | Outgoing peer identity patterns, used for the mTLS hop in client and central modes. |
timeouts | See Timeouts and connection reuse | Optional read, write, and idle durations. |
connection.disable_keepalive | false | Disable outgoing HTTP connection reuse. TCP and MQTT do not use this setting. |
middlewares | Empty list | Protocol-specific middleware entries with a type and inline options. |
Modes and URL schemes
Section titled “Modes and URL schemes”The caller connects to this listener; the target is the next service or proxy that the listener connects to.
| Mode | Caller → listener | Listener → target | Typical placement |
|---|---|---|---|
client | Plaintext | SPIFFE mTLS | Beside the calling application |
server | SPIFFE mTLS | Plaintext | Beside the receiving application |
central | SPIFFE mTLS | SPIFFE mTLS | Between a client proxy and a server proxy |
Use these target schemes for that topology:
| Protocol | client / central target | server target |
|---|---|---|
http | https://peer.example.org:8443 | http://127.0.0.1:8080 |
tcp | tls://peer.example.org:9443 | tcp://127.0.0.1:5432 |
mqtt | mqtts://peer.example.org:8883 | mqtt://127.0.0.1:1883 |
For HTTP, set an https:// target for SPIFFE mTLS; mode: client alone does not
upgrade an http:// target. An HTTPS target in server mode uses ordinary HTTPS
certificate verification, without the proxy’s outgoing SPIFFE identity or
target.spiffe_ids restriction.
Peer identity restrictions
Section titled “Peer identity restrictions”source.spiffe_ids and target.spiffe_ids are lists of Go regular expressions
matching the peer’s SPIFFE URI. A nonempty list allows a peer when at least one
expression matches. Use ^ and $ to match the entire URI, escape literal dots,
and use YAML single quotes to preserve backslashes. For example,
'^spiffe://example\.org/ns/app/sa/api-server/.*$' permits workloads under that
namespace and service account.
An empty or omitted list allows any SPIFFE identity authenticated using the
available trust bundles. Certificate and trust validation still apply. Setting
source.spiffe_ids on a plaintext client listener does not authenticate its
application caller; bind that listener to loopback or restrict network access.
In a central topology, the server proxy authenticates the central proxy, not
the original client.
Timeouts and connection reuse
Section titled “Timeouts and connection reuse”Set durations under each listener’s timeouts, using Go duration strings such
as 500ms, 30s, or 2m. Omission or 0s selects the defaults below; zero does
not disable a timeout.
| Field | Default | HTTP behavior |
|---|---|---|
timeouts.read | 15s | Time allowed to read the incoming request, including its body. |
timeouts.write | 60s | Time allowed to write the response to the caller. |
timeouts.idle | 120s | Wait for the next request on an incoming keep-alive connection. |
For long-running HTTP requests, adjust the read and write timeouts to suit the
application. These fields do not set outgoing dial or TLS handshake timeouts.
connection.disable_keepalive: true opens new outgoing HTTP connections instead
of reusing them.
TCP does not use these timeout fields. Buffered MQTT uses timeouts.idle to
close an idle upstream connection; the next publish reconnects. MQTT does not
use read or write, and unbuffered MQTT does not use idle. Use positive
durations for normal operation.
Middleware
Section titled “Middleware”Configure middleware in the middlewares array. Each entry has a type, with
its options at the same level; do not add a config wrapper. For example,
allowUnknown belongs alongside type: openai. Middleware is protocol-specific;
TCP does not support it.
HTTP middleware
Section titled “HTTP middleware”The first middleware entry processes incoming requests first.
| Type | Options and defaults | Behavior |
|---|---|---|
notary-query | No options | Serves local GET /.qhx/workload/{id}, /.qhx/certificate/{id}, and /.qhx/receipt/{id} queries. Requires notary.enable: true. |
openai | allowUnknown: false | Processes POST /v1/chat/completions. Other methods and paths receive HTTP 403 unless allowUnknown: true, which forwards them without OpenAI processing. |
dummy | message: "" | Diagnostic middleware that logs requests and adds a nonempty message to the Qhx-Dummy request and response headers. |
When combining notary-query and openai, put notary-query first so notary
queries are not blocked by OpenAI endpoint filtering. The listener’s incoming
identity restrictions also control access to its notary query routes.
The openai middleware removes JSON fields outside its supported model. It
reads at most 1 MiB of each request body and buffers the complete response;
streaming responses are not supported. Without the notary, it produces no
receipts. With the notary enabled, only successfully parsed HTTP 200 JSON
responses are notarized; empty, error, and non-JSON responses receive no receipts.
For notarization, use a client or central listener with an authenticated upstream
workload; see Notary.
MQTT buffering
Section titled “MQTT buffering”The MQTT buffer middleware queues outgoing publishes in memory and retries
sending them after connection failures. Put storage, overflow-policy, and
reconnect-backoff alongside type: buffer.
| Option | Default | Meaning and limits |
|---|---|---|
storage.type | in-memory | The only supported backend. |
storage.max-bytes | 33554432 (32 MiB) | Maximum queued payload bytes; excludes packet metadata and other process memory. Zero selects the default. |
storage.max-messages | 0 | Maximum queued messages; zero means no count limit. |
storage.max-ttl | 0s | Maximum queued-message age from enqueue; zero disables expiry. Enforcement depends on the deployed build; see below. |
overflow-policy | reject | reject, drop-oldest, or drop-newest; see below. |
reconnect-backoff.initial | 1s | Initial retry delay; zero selects the default. |
reconnect-backoff.max | 30s | Maximum retry delay and time budget for each replay attempt; zero selects the default. Must be at least initial. |
storage.max-ttl is enforced only in builds containing the
queued-message expiry fix.
Without it, expired messages may still be sent. With the fix, message age is
checked before each upstream send or retry, including after reconnection and
retry backoff. An in-progress send may finish after expiry: max-ttl is not a
deadline for broker receipt. Check your deployed build before relying on it.
Sizes, counts, and durations must not be negative. Retry delays double after failures, up to the configured maximum, and reset after a successful replay.
Use buffering only for publishing connections. Buffered MQTT supports QoS 0 and 1, but not subscriptions or QoS 2. A successful CONNECT acknowledgement is local to the proxy and does not confirm broker acceptance.
For QoS 0, failed sends are retried without broker acknowledgement. For QoS 1, the proxy normally acknowledges a publish after enqueueing it, then waits for the target’s PUBACK when forwarding it. A local PUBACK does not confirm broker delivery. Duplicate delivery is possible, including after reconnection.
Buffers are per client session and held in memory; queued messages do not
survive process restarts. With reject, a full queue causes the publish to fail
and the connection to close. drop-oldest evicts queued messages to make room;
an oversized message still fails. drop-newest discards the new message and can
still return a QoS 1 PUBACK. Buffered MQTT does not support notarization.
Notary
Section titled “Notary”Set notary.enable: true to enable a shared notary and database for this proxy’s
listeners. The proxy must run in Kubernetes with in-cluster API credentials,
permission to get the referenced Pods, a usable JWT-SVID source, and a writable
database directory. Use persistent storage to retain records across pod
replacement.
For workload statements, the authenticated upstream SPIFFE identity must use
the QHx pod form:
spiffe://trust-domain/ns/namespace/sa/service-account/pod/pod-name/pod-uid.
The referenced Pod must be accessible through the notary’s Kubernetes API, with
a matching UID and service account.
HTTP notarization requires openai middleware. Requests select workload
(the default), logRequest, or signRequest with the Qhx-Notarization-Level
header. Add notary-query to retrieve statements, certificates, and receipts
over HTTP.
Unbuffered MQTT supports notarization when an authenticated upstream pod
identity is available. MQTT 5 clients can select the same levels with the
qhx.notarization user property on CONNECT or PUBLISH and receive the resulting
notary identifiers. TCP and buffered MQTT do not support notarization. Enabling
the notary does not automatically add HTTP middleware.
Validation and startup checks
Section titled “Validation and startup checks”Configuration validation checks required fields, unique listener names,
supported modes and protocols, TCP/MQTT target schemes, and MQTT buffer limits.
Unknown fields can be silently ignored: use source.spiffe_ids,
target.spiffe_ids, and middlewares exactly as shown.
Valid configuration does not establish connectivity. Also check socket access, identity registration, peer trust, listening ports, and notary permissions and storage. Check the proxy logs and test application traffic after deployment.
Complete examples
Section titled “Complete examples”See Proxy Configuration Examples for HTTP, TCP, and MQTT configurations. For Kubernetes manifests, see the manual sidecar deployment guide and the TCP deployment example.