Install
Put Atria in front of your backend
Five steps on a Debian or Ubuntu host: install the package, place three certificates, 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
Add the repository and install
Signed with Atria’s own key, pinned to this one source with signed-by — a repository key should only ever be trusted for its own repository.
shellcurl -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-proxyThe 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 three certificates
One is ours, two are yours. The Atria CA is what lets the proxy verify agents offline — pinning it once is what keeps the request path free of any call to us. The server certificate is your own TLS identity for the hostname your agents connect to, and is not issued by Atria; use whatever authority you already use for your own services.
/etc/atria/atria-ca.pem # the Atria CA — verifies every agent certificate server-cert.pem # your own cert, for the hostname agents call server-key.pem # its key — NOT issued by Atria 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.
- 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: 1000.0 # any transaction_value in the bodyA 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. Nothing in atria-proxy.env changes, and the five steps above are the whole install.
# 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;
}
server {
listen 443;
ssl_preread on; # routes by SNI without terminating TLS
proxy_pass $atria_backend;
}
}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 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=/etc/atria/atria-ca.pem # still required — the pin
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: leave unset. nginx holds that identity
# now, and the proxy's own server certificate becomes optional. The CA it
# pins does not.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.