Agent Gateway (TCP Relay)

The Athena Agent Gateway is a small, standalone TCP passthrough relay you run on a host inside a customer network. Hermes agents on that network connect to the gateway, and the gateway forwards their traffic to an upstream Athena agent channel. Only the gateway host needs egress to the upstream server, and TLS stays end to end between each agent and Athena the whole way — the gateway copies encrypted bytes and holds no agent credentials.

The gateway is an optional deployable component, separate from the Athena server and from a managed agent. You only need it when the machines running agents cannot (or should not) reach the upstream Athena server directly and you want to funnel their traffic through one controlled egress point.

When to use a gateway#

A managed agent normally makes a single outbound connection to the Athena server's agent channel (TCP 8444 by default; see Network Ports & Firewall). In some networks you don't want every agent host to have that egress. The gateway gives you a middle hop:

Pointing agents at the gateway is a DNS change, not a protocol change. Agents keep the Athena hostname, so their TLS hostname verification still holds; you simply make that hostname resolve to the gateway on the agent network. Because TLS is not terminated at the gateway, the relay never sees agent identities or report contents — it cannot decrypt, inspect, or alter the traffic it carries.

How it works#

One gateway instance forwards to exactly one configured upstream destination. There is deliberately no request-supplied destination — a relay that could be told where to connect would be an open proxy. For each incoming agent connection the gateway opens a fresh socket to the upstream and copies bytes through unchanged in both directions.

The UpstreamHost the gateway forwards to must not resolve through the DNS override that points the Athena hostname at the gateway — otherwise the relay would connect to itself in a loop. Point the upstream at the true origin server, bypassing that override (for example with a direct IP or a separate resolver). The gateway rejects the obvious local self-route at startup, but it cannot detect a loop that forms through NAT or a load balancer, so plan the two names deliberately.

Configuration#

All settings live under a Gateway section in the gateway's appsettings.json, alongside the binary. Every key can also be supplied as an environment variable using the standard double-underscore form, e.g. Gateway__ListenAddress and Gateway__UpstreamHost — convenient for containers and secret injection.

SettingDefaultPurpose
Gateway:ListenAddressrequiredThe local interface address agents connect to (a LAN or overlay address). No default: a wildcard bind would expose the listener on every interface of the host and must be a deliberate choice. Must be a literal IP address.
Gateway:ListenPort8444The port agents connect to on the gateway.
Gateway:UpstreamHostrequiredThe single upstream destination the gateway forwards to — the true Athena server address, not the DNS-overridden hostname.
Gateway:UpstreamPort8444The upstream agent-channel port.
Gateway:MaxConnections1000Hard ceiling on established relay connections. A starting value, not a tested capacity promise — each connection costs roughly two sockets and two copy buffers.
Gateway:ConnectTimeoutSeconds10How long to wait when establishing the upstream connection before giving up.
Gateway:DrainTimeoutSeconds30How long a graceful shutdown lets existing streams finish. Zero cuts them immediately.
Gateway:CopyBufferBytes65536Copy-buffer size per direction. Two buffers are held per connection, so this multiplied by MaxConnections is the relay's memory floor. Capped at 1 MiB.
Gateway:HealthListenAddress127.0.0.1Interface for the liveness, readiness, and metrics endpoints. Loopback by default: this surface reports internal state and should be exposed deliberately, never reachable from the agent network just because the relay is.
Gateway:HealthPort9080Port for the health and metrics endpoints. Must not collide with the listen port on the same address.
Gateway:UpstreamProbeIntervalSeconds30How often readiness re-checks that the upstream accepts a TCP connection. Kept well above a per-second cadence so a fleet of gateways cannot turn readiness into load on the upstream.

A minimal appsettings.json for a gateway on a LAN address forwarding to a cloud server:

{
  "Gateway": {
    "ListenAddress": "10.20.0.5",
    "ListenPort": 8444,
    "UpstreamHost": "203.0.113.10",
    "UpstreamPort": 8444
  }
}

Validating the configuration#

The gateway validates its whole configuration before it binds a listener, so a bad config is a clean startup failure that names every problem at once rather than a runtime surprise. You can run that check on demand without starting the relay:

# Check a config change before restarting a live gateway
Athena.Gateway --validate

On success it prints the resolved listen and upstream endpoints and exits 0. On a bad config it prints each problem on its own line and exits 78 (EX_CONFIG), which makes it easy to gate deployments in CI or in a service's pre-start step. Common messages you may see:

The gateway pins its working directory to the binary's own folder so it finds appsettings.json reliably whether it runs as a service, a container entrypoint, or a bare executable. If validation reports a required value you plainly set, confirm the config file is sitting next to the binary.

Deploying the gateway#

The gateway runs equally as a Linux systemd service, a container, or a Windows service. It needs no privileged port (it listens on 8444 by default), no elevated capabilities, and writes nothing outside its own log directory.

Linux (systemd)

A ready-made unit file ships with the gateway. Assuming the published output lives in /opt/athena-gateway with its appsettings.json alongside:

# Install and enable the service
sudo install -m 0644 athena-gateway.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now athena-gateway

# Follow the logs
journalctl -u athena-gateway -f

The unit runs a config validation as a pre-start step, so it refuses to start on a bad config rather than restart-looping against one. It runs as a dedicated athena-gateway user, is hardened with the usual sandboxing directives, and sets a stop timeout above the drain window so systemd does not kill the process mid-drain. Keep the unit's stop timeout above Gateway:DrainTimeoutSeconds, and keep the file-descriptor limit above twice Gateway:MaxConnections (each connection uses two sockets).

Docker

A runtime-only container image is provided. Inside a bridged container the listen and health addresses refer to the container's interface — a host LAN or overlay address is not automatically reachable inside the container — so the image defaults both to 0.0.0.0 and expects you to publish the ports and constrain exposure at the host/network layer.

docker run -d --name athena-gateway \
  -p 8444:8444 \
  -p 127.0.0.1:9080:9080 \
  -e Gateway__UpstreamHost=203.0.113.10 \
  -e Gateway__UpstreamPort=8444 \
  athena-gateway:latest

The container stops on SIGTERM and honours the drain window, so allow enough stop grace for established sessions to finish. The image's own health check runs the --validate configuration check.

Windows service

The same binary registers as a Windows service (service name AthenaGateway) when run under the service control manager, reading appsettings.json from its install directory. Configure it the same way and set Gateway:ListenAddress to the interface agents reach it on.

Health & readiness#

The gateway serves liveness, readiness, and metrics on the separate health port (loopback by default). The endpoints return JSON:

EndpointStatusMeans
GET /health/live (also /health)200The relay's accept loop is running. Liveness is about this process only, never the upstream.
GET /health/ready200 / 503Ready only when the listener is bound and a recent bounded TCP connect to the upstream succeeded. Reports upstreamReachable, when it was last checked, and any error.
GET /metrics200Prometheus text exposition (see below).

Readiness recovers; it never restart-loops. An upstream outage makes the gateway not-ready (HTTP 503), not unhealthy — established connections keep being served throughout, and the gateway returns to ready on its own when the upstream comes back. Wire orchestrators to the readiness probe for traffic gating and to liveness only for process health, so a temporary upstream blip does not kill a perfectly good relay. A green readiness probe proves the upstream endpoint accepts TCP — it does not prove the Athena server is healthy, that its certificate is valid, or that an agent would authenticate; all of that happens inside the end-to-end TLS session the gateway cannot see.

Prometheus metrics#

GET /metrics returns standard Prometheus text. Every series carries an instance label identifying the gateway instance. The counters measure sockets and ciphertext bytes, never agents or message contents — the gateway cannot see inside TLS, so no metric is labelled with an agent identity or a remote address.

MetricTypeMeaning
athena_gateway_active_connectionsgaugeEstablished relay connections right now (sockets, not agents).
athena_gateway_accepted_totalcounterDownstream connections accepted.
athena_gateway_rejected_at_capacity_totalcounterConnections closed immediately because MaxConnections was reached.
athena_gateway_upstream_connect_failures_totalcounterAttempts to reach the upstream that failed or timed out.
athena_gateway_upstream_connects_totalcounterUpstream connections successfully established.
athena_gateway_upstream_connect_milliseconds_avggaugeMean time to establish an upstream connection since start.
athena_gateway_abnormal_closes_totalcounterRelay connections that ended on a transport fault rather than a clean close.
athena_gateway_clean_closes_totalcounterRelay connections that ended cleanly.
athena_gateway_bytes_to_upstream_totalcounterCiphertext bytes relayed from agents to Athena.
athena_gateway_bytes_to_downstream_totalcounterCiphertext bytes relayed from Athena to agents.

Useful signals to alert on: a rising rejected_at_capacity_total means you are hitting MaxConnections; a rising upstream_connect_failures_total (or a not-ready readiness probe) means the gateway cannot reach the upstream server.

Logging & troubleshooting#

The gateway logs to the console and to a rolling daily file under its logs/ directory (kept for the last 14 days by default). Payload bytes are never logged — connections are identified only by the gateway instance id plus an ephemeral connection id, never by a guessed agent id. Typical checks:

Keep the health/metrics surface off the agent network. It defaults to loopback for a reason: it reports internal state and should be exposed only to your monitoring, never reachable by agents just because the relay is.