Outcome
This guide puts Arion in front of a non-HTTP service — PostgreSQL here — as a pure L4 proxy: a listener on port 5432 that reads PROXY protocol from the load balancer in front of it, caps concurrent connections at 1000, admits only clients from 10.0.0.0/8, and streams bytes to a health-checked pair of database servers. Use it whenever the traffic is not HTTP but you still want one hardened entry point.
Prerequisites
- An
arionbinary — see install. - A TCP service to front. The verify section substitutes throwaway echo listeners, so none is required to follow along.
ncandsocatfor the local verification.
The architecture
- A listener on
0.0.0.0:5432whose listener filter parses the PROXY protocol header at accept time, recovering the real client address. - One filter chain of three network filters, in order: connection limit, RBAC allowlist, TCP proxy. No HTTP connection manager — nothing here parses the byte stream.
- The
postgrescluster holds two static endpoints, each probed with a TCP connect check every five seconds.
The complete config
# arion.yaml, composed from 6 features.
logging:
log_level: info
envoy_bootstrap:
static_resources:
listeners:
- name: tcp_5432
address:
socket_address: { address: 0.0.0.0, port_value: 5432 }
listener_filters:
- name: envoy.filters.listener.proxy_protocol
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
filter_chains:
- name: tcp_5432_chain
filters:
- name: envoy.filters.network.connection_limit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit
stat_prefix: cx_limit_tcp_5432
max_connections: 1000
- name: envoy.filters.network.rbac
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC
rules:
action: ALLOW
policies:
allowed-clients:
permissions:
- any: true
principals:
- direct_remote_ip: { address_prefix: 10.0.0.0, prefix_len: 8 }
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
cluster: postgres
clusters:
- name: postgres
connect_timeout: 5s
type: STATIC
load_assignment:
cluster_name: postgres
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 10.0.3.11, port_value: 5432 }
- endpoint:
address:
socket_address: { address: 10.0.3.12, port_value: 5432 }
health_checks:
- timeout: 1s
interval: 5s
healthy_threshold: 3
unhealthy_threshold: 2
tcp_health_check: {}Walkthrough
PROXY protocol at accept
listener_filters runs before any network filter, at connection accept. The proxy_protocol filter consumes the PROXY v1/v2 header that an edge load balancer prepends, and from then on Arion treats the address inside the header as the client's address — the allowlist below sees the real client, not the load balancer. A connection that arrives without a valid header is rejected outright, so enable this filter only behind something that always sends it. See listeners.
The connection limit
connection_limit is the first network filter: at most max_connections: 1000 concurrent connections through this filter chain, with stat_prefix: cx_limit_tcp_5432 naming its stats. Databases hold state per connection, so capping at the proxy protects the upstream's memory and file descriptors from a connection flood.
The allowlist
The network rbac filter carries action: ALLOW and a single policy, allowed-clients: any traffic (permissions: any: true) from principals whose direct_remote_ip falls in 10.0.0.0/8. With an ALLOW ruleset, anything no policy matches is denied — a connection from outside the range is closed without a byte reaching the database. Policy shapes are covered in RBAC.
The TCP proxy
tcp_proxy is the terminal filter: it opens a connection to the postgres cluster and copies bytes in both directions until either side closes. There is no routing table at L4 — the filter names its cluster directly. See TCP proxy.
The cluster
postgres is a STATIC cluster with two endpoints and a 5-second connect_timeout. Its health check is tcp_health_check: {} — success means the endpoint accepted a TCP connection within timeout: 1s — every 5 seconds, ejecting after 2 failures and readmitting after 3 passes.
Run and verify
Fake the two database servers with echoing TCP listeners:
socat TCP-LISTEN:15432,reuseaddr,fork EXEC:/bin/cat &
socat TCP-LISTEN:15433,reuseaddr,fork EXEC:/bin/cat &
For a local run, change the two cluster endpoints to 127.0.0.1:15432 and 127.0.0.1:15433, then start Arion:
arion --config arion.yaml
A well-formed connection sends a PROXY header naming an allowed client, then its payload:
printf 'PROXY TCP4 10.0.0.5 10.0.3.1 41000 5432\r\nping\n' | nc -w 2 127.0.0.1 5432ping
The echo proves the path end to end — and that only the payload crossed it: Arion consumed the PROXY line, so the upstream never saw it. Now claim a source outside the allowlist:
printf 'PROXY TCP4 192.0.2.7 10.0.3.1 41000 5432\r\nping\n' | nc -w 2 127.0.0.1 5432
The connection closes with no output — RBAC denied it. Finally, connect with no PROXY header at all (printf 'ping\n' | nc -w 2 127.0.0.1 5432): the listener filter rejects the connection and Arion logs that no valid PROXY protocol header was detected.
In a real deployment the edge load balancer adds the header itself and psql -h <arion-host> -p 5432 connects straight through.
Production notes
- The allowlist is only as trustworthy as the PROXY header. Expose port 5432 solely to the load balancers that add the header; a client that can reach Arion directly can write any source address into it.
- If nothing in front of Arion speaks PROXY protocol, drop the listener filter — with it enabled, plain connections are rejected, which is exactly what the header-less
nctest shows. - A TCP check proves the endpoint accepts connections, not that the database can serve queries. Alert on the database's own signals too.
- Size
max_connectionsfrom the upstream's connection budget (for PostgreSQL,max_connectionsserver-side) rather than from expected load. - To route TLS connections by SNI without terminating TLS — one listener fanning out to different clusters per hostname — add the
tls_inspectorlistener filter and give each filter chain afilter_chain_matchonserver_names, each ending in its owntcp_proxy. See TCP proxy and the TLS inspector reference.
Next
Field detail for the filters in this chain lives in the tcp_proxy, connection limit and network RBAC references. For the HTTP equivalent of this front door, start from the L7 load balancer guide.