By default /api, /v1 and /metrics are loopback-only and the panel is http://localhost:8545/app/. For a node on a server you can instead publish the real panel on one hostname of your own, protected by a login. It is off unless you set two environment variables, and nothing else about the loopback rules changes.
Status: implemented and covered by 12 automated tests (
router/test/remote.test.mjs: no session, wrong token, forged and expired cookies, cross-site writes, proxy headers, rate limiting, metrics, p2p, login Origin). Enabled on the project's own node (2026-11-06) through a Cloudflare quick tunnel and exercised end to end from the internet and in a browser (sign in, all views, a chat answered by Ollama). No external security review. Treat the token as full control of the node, including its wallet.
Why a login is mandatory#
A tunnel or reverse proxy connects to the node from loopback, so the peer-address check proves nothing for proxied traffic. Merely allowing another Host would publish the wallet, upstream API keys and your model server to anyone who finds the URL. Remote admin therefore requires, in this order:
- The exact admin host in the
Hostheader. Every other host keeps the loopback rules. - A valid session cookie:
HttpOnly; Secure; SameSite=Strict, signed with HMAC-SHA256 under a random per-boot key (a restart signs everyone out), valid 12 hours. - For browser writes, an
Originequal tohttps://<admin host>(and no cross-siteSec-Fetch-Site).
Also enforced: the token is compared in constant time and is never logged or returned; five failed logins lock that client for 10 minutes and 30 failures lock all logins for 10 minutes; once enabled, a request carrying proxy headers (cf-connecting-ip, x-forwarded-for, ...) can never use the localhost path; /metrics is never reachable through the admin host; /p2p/* stays open as it always was.
Enable#
Pick a hostname you control, e.g. admin.example.com (the domain you plan to use for the project), and a long random token:
python3 -c 'import secrets;print(secrets.token_urlsafe(32))' # keep it in a password manager
Add to /etc/elyra/env (root-owned, chmod 600):
ELYRA_ADMIN_HOST=admin.example.com
ELYRA_ADMIN_TOKEN=<the token>
sudo systemctl restart elyra. The node keeps listening on 127.0.0.1:8545 only. If either variable is missing or the token is under 24 characters, remote admin stays off and the log says why.
Publish the hostname with a Cloudflare named tunnel#
The node must not listen on a public port; the tunnel dials out. On the server:
cloudflared tunnel login # authorise the zone once
cloudflared tunnel create elyra-admin
cloudflared tunnel route dns elyra-admin admin.example.com
/etc/cloudflared/elyra-admin.yml:
tunnel: elyra-admin
credentials-file: /etc/cloudflared/<tunnel-id>.json
ingress:
- hostname: admin.example.com
service: http://127.0.0.1:8545
- service: http_status:404
Run it as its own systemd unit (cloudflared --config /etc/cloudflared/elyra-admin.yml tunnel run elyra-admin). Use a dedicated tunnel: do not edit tunnels that serve other products. Cloudflare passes the original Host, which is what the node checks. Optionally put Cloudflare Access in front as a second factor; the node's login stays in place.
Quick tunnels (trycloudflare.com) work for a test but the hostname changes on every restart, so ELYRA_ADMIN_HOST would have to follow it. Use a named tunnel for anything lasting.
Verify (from your laptop, not the server)#
H=https://admin.example.com
curl -s -o /dev/null -w '%{http_code}\n' $H/api/status # 401 (no session)
curl -s -o /dev/null -w '%{http_code}\n' $H/metrics # 403
curl -s -o /dev/null -w '%{http_code}\n' $H/app/ # 302 -> /admin/login
curl -s -o /dev/null -w '%{http_code}\n' $H/p2p/hello # 200 (open, by design)
Then open $H/ in a browser, sign in with the token, and the normal panel loads, talking to your real node and your real Ollama. POST /admin/logout (the panel does not have a button yet) or clearing the cookie signs you out.
Lesson from the first real login#
curl and the unit tests passed, but a real browser was refused with cross-site request refused. The login page had Referrer-Policy: no-referrer, and with that policy Chrome sends Origin: null on a same-origin form POST. The Origin check was right to refuse null (sandboxed iframes and cross-origin redirects send it), so the page now uses same-origin and a test pins both facts. Anything that adds a stricter referrer policy in front (a proxy, a WAF rule) will break sign-in the same way.
Quick tunnel as the interim (what runs today)#
/usr/local/bin/elyra-quick-tunnel.sh under elyra-quick-tunnel.service starts a dedicated cloudflared tunnel --url http://127.0.0.1:8545, waits for the *.trycloudflare.com hostname, writes it to ELYRA_ADMIN_HOST in /etc/elyra/env and restarts the node. The hostname changes whenever the tunnel restarts (and signs everyone out). The current one is in /run/elyra-admin-url on the server. Replace it with a named tunnel as soon as the domain exists, then systemctl disable --now elyra-quick-tunnel and set ELYRA_ADMIN_HOST by hand.
Limits and honest risks#
- One shared token, no accounts, no 2FA. Rotate it by editing the env file and restarting.
- Anyone with the token can spend the node wallet, change sources and use your model server. Keep only testnet funds in a node you expose.
- The global login lock lets an attacker who can reach the URL stop you from logging in for 10 minutes by guessing. Cloudflare Access or a WAF rule in front removes that.
- Sessions live in memory keyed per boot; a restart logs everyone out.
- The public try page below is separate from the login: it never exposes
/api,/v1,/metricsor the panel.
Public try page#
Optional. With remote admin on, set ELYRA_PUBLIC_TRY=1 and a signed-out visitor to your admin hostname gets a one-page chat with a local model on this machine instead of the login redirect (the login stays at /admin/login).
What it can and cannot do:
- It only uses a local, non-hosted upstream (for example Ollama). It never routes to a peer (that would spend your escrow), never uses a hosted API key, never serves simulated answers. With no such upstream it reports the model as offline.
- Limits:
ELYRA_TRY_PER_IP_HOUR(default 20),ELYRA_TRY_DAILY(300, all visitors),ELYRA_TRY_MAX_TOKENS(256),ELYRA_TRY_CONCURRENCY(2).ELYRA_TRY_MODELpicks one model when several are connected. - The server adds a fixed system prompt, accepts only user and assistant messages (at most 12, 4000 characters) and streams the answer. The router never logs or stores prompt text.
- Same-origin only, strict content security policy, no cookies read. Client identity for the hourly limit comes from Cloudflare's
CF-Connecting-IP, so run it behind Cloudflare (or another proxy that sets it) or all visitors share one limit. - It costs you CPU: every visitor request runs on your machine. Lower the limits if the box is small.