Where asd's tunnel-server credentials live, how they're selected at runtime, what client-id means, and what breaks when something is misconfigured.
asd authenticates against the tunnel server with an SSH key + a
short client identifier. This page covers the moving pieces:
credential sources, the priority chain, client-id derivation, and
the failure modes.
For the user-facing commands: cli/auth.
Two things:
A tunnel SSH key (Ed25519 by default). Created by
asd key create, stored in
~/.config/asd/tunnel/credentials.json. The public half is
registered with the tunnel server; the private half stays on
your machine.
A client-id. A short string (typically 4 chars, e.g.
xyz1) that the tunnel server uses to address your machine.
Returned by ${{ macro.tunnelClientId() }} and embedded in
every public subdomain: <service>-<client-id>.<tunnel-host>.
asd auth credentials lists what's available and which one is
active. asd checks sources in this order — first one wins:
| # | Source | When it applies | Persistence |
|---|---|---|---|
| 1 | ASD_TUNNEL_TOKEN environment variable |
Per-shell, per-process | Lost when the shell exits |
| 2 | ~/.config/asd/tunnel/credentials.json |
User-level default | Persistent — written by asd login / asd login key |
| 3 | OS keychain | If the platform supports it and you opted in | Persistent (managed by the OS) |
The first source that returns a usable credential is the one asd
uses for that command. A common confusion: a stale
ASD_TUNNEL_TOKEN set in your .zshrc overrides a fresh
credentials.json. Always check asd auth credentials if
"asd is using the wrong account".
$ asd auth credentials
=== Credential Sources (Priority Order) ===
ℹ️ 1. Environment Token (ASD_TUNNEL_TOKEN)
✗ Not set
ℹ️ 2. Credentials (~/.config/asd/tunnel/credentials.json)
🔐 ACTIVE - API key auth as kelvin@asd.host
Method: API key
ℹ️ 3. OS Keychain
✗ Not configured
$ asd auth whoami
client-id: xyz1
account: kelvin@asd.host
server: eu2.tn.example.com
asd auth status runs an end-to-end probe (credential found +
accepted by the server + token not expired). It returns non-zero
on failure — wire into CI gates.
The client-id comes from the tunnel server during the first
successful authentication. asd doesn't pick it; the server hands
it out:
asd login, asd login key) — the serverasd token create) — the server hands out a${{ macro.tunnelClientId() }} resolves to "" and subdomains<service>.<tunnel-host>.The client-id is public. It appears in every URL your tunnel
serves. Don't treat it as a secret — treat it as part of your
hostname.
| Symptom | Cause | Fix |
|---|---|---|
asd auth status ⛔ "No credentials" |
Nothing in any source | asd login (interactive) or asd login key <KEY> (CI) |
asd auth status ⛔ "Token expired" |
Token past its TTL | asd auth refresh; if it fails, re-login |
| Routes apply but the public URL says "tunnel not found" | Client-id in the subdomain doesn't match the server's record | Likely a stale ASD_TUNNEL_TOKEN. unset ASD_TUNNEL_TOKEN and re-check asd auth whoami |
| Two machines, two different client-ids, same project | Each asd login gets its own client-id by design |
Either use the same persistent credentials across machines (copy ~/.config/asd/tunnel/credentials.json) or accept that each developer sees their own subdomains |
auth whoami shows the right client-id but Apply fails |
Credentials valid but the registered SSH key was rotated server-side | asd key create to register a new key, then asd auth refresh |
For "run this one command as a different identity" (e.g. CI
simulating a deploy):
ASD_TUNNEL_TOKEN="$(cat /path/to/deploy-token)" asd net apply
Sets the token for one process only, overrides the persistent
credentials, leaves your shell environment untouched. No
asd login / asd logout dance.
For long-lived overrides (asd login key <KEY> will persist),
use asd auth switch instead — it's interactive and shows what
you're switching to.
cli/auth — command reference.tunnels — what the credentials authenticate against.cookbook/ci-cd-with-asd — asd login key in headless contexts.