eitri runs an SSH jump gate: a bastion that accepts an ssh -J hop and
forwards you to a VM's SSHd. You authenticate to the gate with a short-lived
SSH user certificate that you sign yourself, using your tenant's own user CA.
Every VM in your tenant trusts your tenant's user CAs—seeded at VM create, so
there's no per-VM key to manage—and the cert carries the principal ubuntu,
the login user on the VM.
Verification runs both ways. Just as the VM trusts your user cert, you
verify what you connect to: the gate and every VM present a host certificate
signed by eitri's host CA. You pin that CA once (@cert-authority) and both
hops are then verified by certificate—no blind trust-on-first-use, and no
host-key-changed warnings when VM names or IPs are recycled.
Two CAs, two directions: your tenant's user CA (private key on your machine) signs what you present; eitri's host CA (private key on the server) signs what the gate and VMs present. eitri deliberately holds no user signing key — a server compromise cannot mint user credentials.
Generate a user CA and register its public key with your tenant. The token
names the tenant (POST /api/v1/user-cas), so no handle is needed; a token that
can act for more than one tenant pins one with eitri ca upload <tenant> <key>
(POST /api/v1/tenants/<tenant>/user-cas):
ssh-keygen -t ed25519 -N '' -f ~/.ssh/eitri_user_ca -C "my tenant user CA"
export EITRI_URL=https://eitri.example.com
export EITRI_TOKEN=<personal-access-token> # mint one in the console → Settings
eitri ca upload ~/.ssh/eitri_user_ca.pub
The CA's private key never leaves your machine; the server stores only the public key. Upload the CA before creating VMs—a VM trusts the tenant user CAs present at its creation. The gate authorizes each connection against the tenant the signing CA was uploaded to.
A caller holding only a token has no CA and no private key, so it cannot sign anything. Such a caller can ask eitri to hold one:
curl -X POST -H "Authorization: Bearer $EITRI_TOKEN" \
https://eitri.example.com/api/v1/managed-ca
eitri generates an ed25519 CA for the tenant, keeps the private half in the
server database, and registers the public half in the tenant's CA set like any
uploaded one—label eitri-managed, visible in the console's user-CA list. It
signs a five-minute certificate per operation and hands it to nobody. This is
what makes the remote MCP endpoint work with nothing but a PAT.
Bring-your-own remains the default: a managed CA exists only for a tenant that
asked, the two coexist in one CA set, and DELETE /api/v1/managed-ca destroys
the key and removes the CA. The same rule applies as for an uploaded CA—enable
it before creating VMs, because a VM trusts the CA set present at its
creation.
export EITRI_URL=https://eitri.example.com
export EITRI_GATE=eitri.example.com:2222 # the gate's ssh_listen address
eitri ssh <vm-name> # opens a shell on the VM
eitri ssh <vm-name> uptime # runs a command and exits
You pass the bare <vm-name>, but the name that reaches the wire is always the
gate connect name <tenant>.<vm-name>—a VM's host cert carries exactly
that one principal, and eitri ssh verifies the dialed name against it under
strict checking, so a bare name would fail host verification. (The gate itself
also resolves a bare name within the connection's tenant, but the VM's cert
does not, so the client sends the namespaced form.) You never need to know your
tenant: eitri ssh derives it from your credential via /me, or takes it from
EITRI_TENANT when set (offline, and the escape hatch for a CA registered in
more than one tenant).
Environment variables:
| Var | Meaning |
|---|---|
EITRI_URL |
Base URL of the eitri server (default the hosted https://console.eitri.sh) |
EITRI_GATE |
Jump gate address for the hop (host:port, ssh_listen; default the hosted gate.eitri.sh:2222) |
EITRI_CA |
Your tenant user-CA private key (default ~/.ssh/eitri_user_ca) |
EITRI_TOKEN |
Personal access token, used only to look up your tenant for the connect name (skipped when EITRI_TENANT is set) |
EITRI_TENANT |
Optional: pins the tenant (offline, and the escape hatch when your CA is registered in more than one); otherwise derived from the token |
EITRI_KEY |
SSH private key path (default ~/.ssh/id_ed25519) |
EITRI_KNOWN_HOSTS |
eitri-managed known_hosts for the CA pin (default ~/.ssh/eitri_known_hosts) |
The SSH session authenticates with no API credential—your signing CA is the
credential. eitri ssh generates ~/.ssh/id_ed25519 if missing, self-signs a
30-minute cert to <key>-cert.pub (which OpenSSH auto-offers), fetches the
eitri host CA and pins it as @cert-authority * in a dedicated known_hosts
file, and execs ssh with both hops verified.
The host
EITRI_GATEpoints at must match the gate's host-cert principal, i.e. the server'sssh_gate_domain(which defaults to the host part ofssh_listen). A mismatch is a hard host-verification failure, by design.
The client is a thin wrapper over three steps you can run by hand:
Self-sign a cert for your public key with your tenant CA—no server involved:
ssh-keygen -s ~/.ssh/eitri_user_ca -I "$(whoami)@$(hostname)" \
-n ubuntu -V +30m ~/.ssh/id_ed25519.pub
Place the cert beside the key. ssh-keygen -s writes
id_ed25519-cert.pub next to the key, and OpenSSH auto-offers a cert named
<key>-cert.pub—nothing further needed, no ssh-add.
Hop through the gate to ubuntu@<tenant>.<vm-name>:
ssh -J "$EITRI_GATE" ubuntu@<tenant>.<vm-name>
The inner user must be ubuntu (the cert principal). The outer gate hop
accepts any username. The gate derives your tenant from the CA that signed
your cert, resolves names within that tenant, and rejects a bare or
foreign-prefixed name.
Self-signed certs should carry a short validity (-V +30m above). When one
expires, ssh is simply rejected—re-run eitri ssh (or the signing step)
to refresh. A specific cert can also be revoked at the gate by serial before it
expires; see credential-revocation.md.
You pin eitri's host CA once and let certificate verification stand in for
trust-on-first-use. Fetch the CA (public material, no token needed) and pin it
in a dedicated known_hosts file—never your main ~/.ssh/known_hosts,
where a * wildcard CA would be trusted for every host you ssh to:
curl -sS "$EITRI_URL/api/v1/ssh-ca" | jq -r .ca \
| sed 's/^/@cert-authority * /' > ~/.ssh/eitri_known_hosts
Then both hops are verified against the CA with StrictHostKeyChecking=yes. A
command-line -o reaches only the final hop, so thread the same options to the
jump hop with an explicit ProxyCommand instead of -J:
GATE_HOST=${EITRI_GATE%%:*}; GATE_PORT=${EITRI_GATE##*:}
[ "$GATE_PORT" = "$EITRI_GATE" ] && GATE_PORT=22
KH=~/.ssh/eitri_known_hosts
ssh \
-o "ProxyCommand=ssh -W %h:%p -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$KH -p $GATE_PORT ubuntu@$GATE_HOST" \
-o StrictHostKeyChecking=yes \
-o "UserKnownHostsFile=$KH" \
ubuntu@<tenant>.<vm-name>
The gate's cert principal is ssh_gate_domain (so $GATE_HOST must match it),
and each VM's cert principal is its <tenant>.<vm-name> connect name (so the
inner ubuntu@<tenant>.<vm-name> host must match). Because verification is by
CA, recycling a VM name or IP never produces a host-key-changed warning—the
new VM simply presents a fresh CA-signed cert for that name. eitri ssh
does all of this for you.