Install
Put Atria in front of your backend
Five steps on a Debian or Ubuntu host: install the package, place the proxy certificate, name your agents in policy, start the service. Nothing to compile, and no call to us on the request path.
This page is for the team running the proxy. If you are writing an agent that needs to call one, you want the sandbox walkthrough instead — no install, no account.
- 1
Install the package
On Debian or Ubuntu, add the apt repository — signed with Atria’s own key, pinned to this one source with signed-by, since a repository key should only ever be trusted for its own repository. On RHEL, AlmaLinux or Rocky Linux 9 and later, install the .rpm directly; upgrade later by running the same two commands. Both packages install the same files and the same service, so every step below applies to either.
Debian, Ubuntu — aptcurl -fsSL https://apt.trustatria.com/trustatria.gpg.key \ | sudo gpg --dearmor -o /usr/share/keyrings/trustatria.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/trustatria.gpg] \ https://apt.trustatria.com atria main" \ | sudo tee /etc/apt/sources.list.d/atria.list sudo apt update && sudo apt install atria-proxyRHEL, AlmaLinux, Rocky Linux 9+ — dnfcurl -fsSLO https://apt.trustatria.com/rpm/atria-proxy-latest.x86_64.rpm sudo dnf install ./atria-proxy-latest.x86_64.rpmThe service is enabled but deliberately does not start. It has no certificates and no real policy yet, and a proxy that started anyway would either refuse everything or — worse — accept traffic it cannot properly judge. The package tells you exactly which files are missing.
- 2
Place the proxy certificate
The Atria CA ships with the package — it is what lets the proxy verify agents offline, with no call to us on the request path. The proxy’s own certificate is issued by the same CA: generate it on dash.trustatria.com with the hostname your agents connect to and the destinations it may forward to (both default to localhost). It is the proxy’s TLS identity and the key it signs its own calls to us with — the only certificate the proxy holds. It lives two weeks: generate the next one there and run sudo systemctl reload atria-proxy.
/etc/atria/server-cert.pem # the proxy's certificate — generate on dash.trustatria.com server-key.pem # its key — shown once when generated, kept only here # the Atria CA is already at /usr/share/atria/atria-ca.pem sudo chown root:atria /etc/atria/server-key.pem sudo chmod 0640 /etc/atria/server-key.pemThe package creates the atria system user and locks /etc/atria to 0750 on install, so the private key is readable by the service account and nobody else. A renewed certificate is applied with sudo systemctl reload atria-proxy — no restart.
- 3
Point it at your backend
Read by systemd as an EnvironmentFile, so plain KEY=value — no shell quoting, no expansion. dpkg keeps your edits across upgrades. Set the default route; the defaults are sane for everything else.
/etc/atria/atria-proxy.envATRIA_LISTEN_ADDR=0.0.0.0:8443 # where agents reach you ATRIA_UPSTREAM_URL=http://127.0.0.1:9090 # the default route — set it: unset # means the public sandbox, not you # Optional: let an agent pick a backend per request with a # TA-Proxy-Pass: <host> header. The host must be in BOTH the # certificate's scope set (fixed when its API key was issued) and # the list below. Unset = *, i.e. trust whatever the certificate allows. #ATRIA_ALLOWED_DESTINATIONS=*.internal.acme.com,api.partner.com ATRIA_CRL_URL=https://api.trustatria.com/crl CRL_PULL_INTERVAL=60s # Loopback on purpose — this listener has no authentication of its own. ATRIA_ADMIN_ADDR=127.0.0.1:8081Without ATRIA_CRL_URL the proxy cannot learn which certificates were revoked, and stamps crl_stale on every affected audit line rather than refusing traffic. Revocation fails open; policy fails closed.
- 4
Say what each agent may do
Keyed by agent id, taken from the subject of the agent’s Atria certificate. An agent with no entry here is denied everything — there is no implicit allow, so nothing works until you name one.
/etc/atria/policies.yamlagents: acme-procurement-v2: allowed_endpoints: - method: POST path: /api/v1/orders # this path, not the subtree - method: GET path: /api/v1/orders/* # write /* if you mean below it rate_limit_per_minute: 100 # per agent, across all its certificates max_transaction_value_usd: 1000.0 # any transaction_value in the body, in USDA path that only matches after normalisation — //api/v1/orders, /api/v1/../admin — is rejected rather than guessed at, so the proxy and your backend can never disagree about what a request addresses.
- 5
Start it
The proxy validates its whole configuration before it touches the network, so a missing file or a mismatched certificate and key is reported as that — not as a confusing connection error later.
shellsudo systemctl start atria-proxy journalctl -u atria-proxy -n 20 # one line says why, if it did not
Behind nginx
When something in front of you already terminates TLS
The agent’s side does not change. It holds the same Atria certificate and opens the same mutual TLS connection it would to a proxy on its own port. What changes is only whether that handshake reaches the proxy — and there are two ways to put nginx in front, of which the first needs nothing from anybody.
1. nginx forwards TCP — mTLS reaches the proxy untouched
If nginx is there to share port 443, route by hostname, or sit at the edge — not to inspect HTTP — let it forward the connection without opening it. The handshake then happens between the agent and the proxy exactly as it does without nginx: same certificate, same client auth, same per-request revocation check. The only proxy setting that changes is where it listens.
sudo apt install libnginx-mod-stream # Debian, Ubuntu
sudo dnf install nginx-mod-stream # RHEL, AlmaLinux, Rocky# A stream block, not an http one: nginx never opens the connection.
stream {
map $ssl_preread_server_name $atria_backend {
agents.example.com 127.0.0.1:8443; # the proxy
default 127.0.0.1:8444; # every other HTTPS site
}
server {
listen 443;
listen [::]:443;
ssl_preread on; # routes by SNI without terminating TLS
proxy_pass $atria_backend;
}
}
# Port 443 is the stream block's now: each existing HTTPS site's
# http server { } moves to the default route —
# listen 127.0.0.1:8444 ssl;
# (no other HTTPS sites on this host: drop the default line)ATRIA_LISTEN_ADDR=127.0.0.1:8443 # only nginx reaches the proxysudo setsebool -P httpd_can_network_connect 1 # nginx connects onward
sudo semanage port -a -t http_port_t -p tcp 8444 # and may listen on 8444
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart atria-proxyconf.d/ is included inside http { } on both families, where a stream block is an error — hence nginx.conf itself. The proxy still terminates TLS with its own certificate, so that certificate must name the hostname agents dial (entered when you generate it). Leave proxy_protocol off: the proxy does not read it, and the handshake would fail.
2. nginx terminates TLS — the certificate travels in a header
Sometimes it must: a WAF, a CDN, an ALB, or an nginx already serving other paths on that hostname. Then whatever completed the handshake saw the client certificate, and the proxy behind it sees plaintext HTTP with nothing attached. The agent still uses the same certificate — it presents it as a proof in the TA-Authorization header, signed per request and bound to the method, the URL and the body, so a copied header is useless anywhere else. Verified against the same pinned CA and the same revocation list as before; nothing about issuance changes, and neither does what the proxy decides.
ATRIA_AUTH_MODE=proof
ATRIA_CA_CERT=/usr/share/atria/atria-ca.pem # still required — ships with the package
ATRIA_PUBLIC_URL=https://agents.example.com # what your agents address
ATRIA_LISTEN_ADDR=127.0.0.1:8443 # nginx is the only caller
# ATRIA_TLS_CERT / ATRIA_TLS_KEY: keep them. Not served here — nginx
# holds TLS — but still the proxy's identity: it signs its calls to us
# and names where the proxy may forward. The CA it pins is required too.ATRIA_PUBLIC_URL is the one easy thing to get wrong. Every request carries a proof bound to the URL it was minted for, and the proxy compares against this value — never against the request’s own Host header, which the caller controls. Point it at the internal address and every request is refused.
server {
listen 443 ssl;
server_name agents.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
# Off, deliberately: nginx folds // into / by default, which would hide
# an ambiguous path from the proxy. The proxy refuses those rather than
# guessing, so that it and your backend can never disagree about what a
# request addresses.
merge_slashes off;
location / {
# No trailing slash, no rewrite, no sub_filter. The proof binds the
# method, the URL and a hash of the body — anything that edits one
# of them invalidates it, and the request is refused as forged.
proxy_pass http://127.0.0.1:8443;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Authorization carries the proof and is forwarded by default.
# Only set this if something above cleared it:
# proxy_set_header Authorization $http_authorization;
# At least ATRIA_UPSTREAM_TIMEOUT, or nginx gives up first and your
# audit log records a decision the caller never saw.
proxy_read_timeout 60s;
}
}Two things this mode deliberately does not do. It does not accept a client certificate that nginx forwarded in a header ($ssl_client_escaped_cert) — a header any intermediary can set is not proof of possession, and the signed proof exists precisely because it is. And it does not fall back to mTLS: the header is required on every request, so there is no weaker path to strip down to. Having nginx verify the certificate as well (ssl_verify_client on against the Atria CA) is worth doing — it turns anything without an Atria certificate away at your edge — but it is a gate in front, not a substitute.
Day to day
Changing policy, and seeing what happened
Policy edits apply without a restart. A document that fails to parse is refused and the previous policy keeps enforcing — so check the reply rather than assuming it applied.
curl -X POST http://127.0.0.1:8081/admin/policy/reload
curl http://127.0.0.1:8081/admin/policy # what it currently believes
curl http://127.0.0.1:8081/admin/crl # revocation list statusEvery decision, allowed or blocked, is one JSON line in /var/log/atria/audit.log — the agent id from the certificate subject, the reason code, and whether the revocation list was stale when the call was judged. It is append-only, and it is deliberately not removed when the package is purged: it is your record of every authorisation decision the proxy made, and deleting it should be something you decide to do.
The admin listener has no authentication of its own. Keep it on loopback — /admin/crl/pull in particular is a request amplifier pointed at Atria Cloud.