AstroDeck's control surface lets a client slew the mount, toggle power, and start/abort sequences. A fresh server has no authentication configured, but the CLI binds to loopback by default and refuses an unauthenticated non-loopback bind. This keeps convenient local setup without silently publishing an admin-for-all controller to the LAN or internet.
Auth is unconfigured by default. Set the ASTRODECK_TOKEN environment variable to a
non-empty secret to require that token on direct REST requests and on the direct
WebSocket. Relay-tunneled scopes deliberately cannot use this local transport
credential; they use home-verified sessions instead. When the variable is unset
(or empty), direct loopback behavior remains open for initial setup.
When a token is set, clients must supply it via any one of:
- HTTP header
X-Auth-Token: <token> - HTTP header
Authorization: Bearer <token> - query string
?token=<token>(the only option a browser has for the WebSocket)
Requests without a valid token get 401. The WebSocket upgrade is closed with
code 1008 (policy violation) before the client joins the event bus.
The UI shell (/, /index.html, /assets/*, and SPA deep links) is served
openly so the browser can load the bundle; the bundle then attaches the token to
its /api/* and /ws calls. No part of the control surface (/api, /ws) is
ever served without the token.
# PowerShell
$env:ASTRODECK_TOKEN = "a-long-random-secret"
python -m astrodeck
# bash
ASTRODECK_TOKEN="a-long-random-secret" python -m astrodeckThe comparison is constant-time. This is a single shared secret, not a user
system; it is the minimum bootstrap bar. Prefer headers over ?token= for REST
calls because URLs are commonly retained in browser history and proxy logs. The
browser WebSocket uses the query form because browser WebSocket APIs cannot set
an authorization header.
If only the machine running AstroDeck needs to reach it (e.g. a kiosk browser on the same box), bind the loopback interface so nothing off-box can connect at all:
python -m astrodeck --host 127.0.0.1The default bind is 127.0.0.1. To serve a LAN tablet, deliberately select a
non-loopback bind and configure a shared token or local/Google authentication:
ASTRODECK_TOKEN="a-long-random-secret" \
python -m astrodeck --host 0.0.0.0An unauthenticated non-loopback bind exits with status 2. The explicit
--allow-insecure-open (or ASTRODECK_ALLOW_INSECURE_OPEN=1) override exists
only for isolated development networks; do not ship or supervise production
with that override.
AstroDeck treats the configuration tree as secret-bearing state: it can contain
session keys, login credentials, relay/update tokens, exact site coordinates,
and driver/profile extras. On Windows, startup requires that tree to reside on
NTFS or ReFS and applies a protected DACL with exactly three trustees: the
process account, SYSTEM, and BUILTIN\Administrators. Broad inherited access,
reparse points, unexpected ownership, an ACL API error, or a FAT/exFAT config
volume makes startup fail before the server listens. This protects the state
from other standard local accounts; it does not protect it from administrators,
SYSTEM, same-account malware, kernel compromise, or offline disk access.
For a Windows service, use a dedicated non-administrator service account and
set ASTRODECK_CONFIG_DIR to an NTFS/ReFS directory created and owned by that
identity. Do not run AstroDeck as Administrator or LocalSystem, and do not let
other standard users write the installed executable, its supervisor, or its
current-release directory: protecting data does not help if another account can
replace the code that reads it.
Separately from the bind interface above: as long as no sign-in method is
actually enabled (auth.methods empty), every direct caller — loopback
included — is granted admin automatically. Creating local user accounts does
NOT change this by itself; the open-admin provider stays active until a method
is enabled.
To verify operator/viewer gating from the machine running the server, the
primary mechanism is: enable the local method, create a disposable
operator/viewer account, and sign in as it — the logged-in session carries its
own role regardless of trust_loopback.
auth.trust_loopback: false (via POST /api/auth/config or the Settings →
Sign-in methods panel) is an additional strict mode on top of that: it
removes the automatic open-admin fallback for loopback callers, so an
unauthenticated loopback client is denied exactly like a remote one. Use it
to verify unauthenticated-loopback denial, or to ensure a loopback browser
can never silently fall back to admin while methods are empty. Flipping the
flag alone, with no method enabled, only produces 401s — it does not create a
login path. Default is true (today's behavior, unchanged). Footgun:
disabling it while no method is enabled and no session cookie already exists
locks that browser out immediately — recover by editing trust_loopback back
to true in the server's config file and restarting, or by seeding an
account with python -m astrodeck create-admin.
A shared token over plain HTTP is still sent in the clear. For anything beyond a trusted LAN:
- Set
ASTRODECK_TOKENto a long random secret. - Front AstroDeck with a TLS reverse proxy (Caddy, nginx, a tunnel such as
Tailscale/Cloudflare) so the token and all traffic are encrypted in transit.
Bind AstroDeck itself to
127.0.0.1and let the proxy be the only listener on the public interface. - Consider IP allow-listing at the proxy.
There is currently no TLS, no rate-limiting, and no audit log inside AstroDeck itself — those belong at the reverse proxy for now.
- The public WebSocket relay must use
wss://(plainws://is accepted only for a loopback development relay). The relay can observe and replay the home's signed session cookie, so operate it as a trusted bearer-token intermediary. RawASTRODECK_TOKENheaders and query parameters are never valid tunnel credentials. Authentication-configuration, session-revocation, token-login, relay-configuration, factory-reset, and self-update mutation routes are direct-only at the home. Configuration, alert-destination, driver/profile mutation, discovery, and connection-setup routes are also direct-only because they can select or probe host/LAN resources. The one exception in that family isPOST /api/profiles/<id>/activate: reconnecting a profile that is already saved takes only a saved id and no destination, so it is allowed over the relay for an account that holdscontrol.reconnect, which the admin and operator roles hold and the viewer and syncer roles do not. That capability is deliberately narrower thanconfig.backend: it covers this one route and nothing else, so an operator can bring a dropped rig back but still cannot save, edit or delete a profile, apply one with its overrides, connect a rig from a caller-chosen specification, or scan the network, all of which keepconfig.backend. The route still answers 409 while a sequence, capture loop, polar alignment or the auto-resume ladder is running. Itsforceoption, which aborts a running sequence and disarms auto-resume, stays aconfig.backenddecision (admin only, 403forbiddenfor an operator) and is direct-only for every role (403local_only). - People management over the relay is open in a narrowed form, behind a step-up.
Listing users needs only the admin session. Every change needs a sign-in less
than 300 seconds old, local or Google, completed through the relay; a session
minted from the break-glass access token never counts. With that, a relayed
admin can create a Google-only viewer or operator account, change a non-admin
between viewer and operator, enable or disable a non-admin, and delete a
non-admin. Password resets, any admin grant, any edit or deletion of an admin,
and username or email edits stay direct-only (403
local_only). The routes under/api/usersare a fail-closed allow-list at the fence: a route added there later is direct-only until it is listed. A relayed request that fails the step-up is 403step_up_requiredand leaves a line in the audit log. The step-up exists because the relay can replay a cookie it has seen, and a replayed cookie is always older than the moment it was captured, so a relay that only replays cookies cannot change who may sign in. It does NOT defend against a malicious relay that is present during the step-up: a TLS-terminating relay sees the password POST and theSet-Cookiethat answers it, and serves the page that drives the sign-in, so it can obtain the fresh session and, within the narrowed operations above, create an operator or viewer account for a Google identity of its choosing. That is why the relay stays a trusted intermediary, and why admin grants and password resets are kept off this path. - The home accepts only the Host names it expects on its listener: loopback,
the bind address, and whatever
ASTRODECK_ALLOWED_HOSTSadds (a comma-separated list of exact names, no wildcards). Anything else is answered 421, which is what stops a DNS-rebinding page from reaching the LAN listener. Relay-tunneled requests are exempt: they never touched the listener (the home dialed out to its configured relay over TLS, and the relay client marks the scope in ASGI state that no network client can set), and the Host they carry is the relay's public name, which may be a custom domain in front of it. So a relay rename needs no allowlist change, and a reverse proxy in front of the LAN listener needs its public name inASTRODECK_ALLOWED_HOSTS. update.repoandupdate.signing_pubkeyare code-execution trust roots. The HTTP API cannot set or rotate them. Provision them offline in the persistent config or installer, protect that file with OS permissions, and keep the Ed25519 private signing key outside every AstroDeck host and relay.