Authentication
RunWisp has two ways in:
- The CLI and TUI on the same machine use the control socket. No password.
- The Web UI and anything else over HTTP need the password.
Local clients (CLI, TUI)
Section titled “Local clients (CLI, TUI)”runwisp, runwisp tui, runwisp status, runwisp run, and the other
commands talk to the daemon over the control socket.
Only the user who started the daemon can open it, so there’s nothing to log in
to. Anyone who can read the data directory controls the daemon.
With --url (or RUNWISP_URL), runwisp run and runwisp tui connect over
HTTP instead and log in like any network client.
If the CLI prints daemon is not reachable at <path> while a daemon is up, the
two are using different data directories. Check --data and which user you’re
running as.
Network clients (Web UI, remote REST)
Section titled “Network clients (Web UI, remote REST)”The Web UI asks for the password once, then keeps you logged in with a session
cookie for 24 hours. The password itself never crosses the network: login is a
challenge and response. To log in from a script, use runwisp run --url or the
script in Remote triggers.
The session cookie and run output are only as private as the connection. When you bind beyond loopback, turn on TLS or put the daemon behind a reverse proxy that does.
The TUI’s Open in browser logs the browser in with a single-use ticket that
expires after 60 seconds. When runwisp tui --url points at a plain http://
address that isn’t loopback, the TUI warns you first, because the ticket would
cross the network unencrypted.
The password
Section titled “The password”RUNWISP_PASSWORD='long-random-string' runwisp daemon- Set
RUNWISP_PASSWORD. The daemon reads it from the environment and keeps it in memory. Browser sessions survive a restart as long as the value doesn’t change. Keep the value in whatever secret store you already use. - Leave it unset. The daemon makes up a new random password on every boot, so each restart logs everyone out. Fine for a daemon you start by hand; annoying for one that runs for months.
runwisp service install
generates a stable password for you, so an installed daemon doesn’t have this
problem.
To rotate the password, change RUNWISP_PASSWORD (or unset it) and restart.
Every network session ends.
RunWisp never writes the password or the session signing key to disk.
Getting the generated password
Section titled “Getting the generated password”runwisp password | wl-copy # Linux, Waylandrunwisp password | pbcopy # macOSIn the TUI, open Home, focus the Password row, and press Enter
to copy it.
runwisp password works only over the control socket, and only for a generated
password. It exits 1 when RUNWISP_PASSWORD is set (RunWisp never hands out a
password you chose) and 5 when authentication is off. Piping it into a
clipboard tool keeps it out of your scrollback.
Running without a password
Section titled “Running without a password”RUNWISP_AUTH=off runwisp daemonThe API and Web UI answer every request without a password. Anyone who can reach the port can read every log and run any task as the user that started the daemon. Use it for local development or on an isolated network, never on a port the internet can reach. If you need remote access without a RunWisp login, put a proxy that does its own authentication in front.
RUNWISP_AUTHacceptsofforon(any case). Any other value stops the daemon at startup.- It can’t be combined with
RUNWISP_PASSWORD; the daemon refuses to start with both. - The daemon prints a warning at startup, the Web UI shows an Auth disabled badge, and the TUI shows the password as disabled.
- The control socket works the same as before.
The official Docker image won’t start until you choose RUNWISP_PASSWORD or
RUNWISP_AUTH=off. For a passwordless local container, publish the port on
loopback only:
docker run --rm -e RUNWISP_AUTH=off -p 127.0.0.1:9477:9477 \ -v ./runwisp.toml:/etc/runwisp/runwisp.toml:ro runwisp/runwisp:latestA CI job or webhook can control one task or service without the password. Give
it a hook_tokens entry, then send the
token as a bearer header:
curl -fsS -X POST -H "Authorization: Bearer $CI_DEPLOY_TOKEN" \ "https://runwisp.example.com:9477/api/hooks/tasks/deploy/run?wait=true"Each hook is a session route under /api/hooks/, with the same query, body, and
response:
| Hook | Same as |
|---|---|
POST /api/hooks/tasks/{name}/run |
POST /api/tasks/{name}/run |
POST /api/hooks/tasks/{name}/start |
POST /api/tasks/{name}/start |
POST /api/hooks/tasks/{name}/stop |
POST /api/tasks/{name}/stop |
POST /api/hooks/tasks/{name}/restart |
POST /api/tasks/{name}/restart |
Add ?wait=true to hold the request until the work is done:
run,start,restarton a task: returns the finished run with itsexitCodeandendReason.starton a task that is already running waits for that run.stop: returns once every run has ended, or409if some are still ending whenwaitTimeout(seconds, default120) runs out.start,restarton a service:400, since a service instance doesn’t end on its own.
Runs started this way show as triggered by hook.
If a webhook sender can only set a URL, pass the token as a query parameter instead. The header wins when both are sent.
https://runwisp.example.com:9477/api/hooks/tasks/deploy/run?token=<token>A missing or wrong token gets 401, and so does an unknown task, so hooks don’t
reveal which tasks exist. A valid token whose allow list leaves out the action
gets 403. Tokens are checked even with RUNWISP_AUTH=off, and
manual_trigger doesn’t apply. A rotated
token takes effect on runwisp reload.
Rate limiting
Section titled “Rate limiting”Login requests are limited per client IP: 5 requests in any 5-minute window,
counting the challenge, the login, and launch-ticket redemptions together. Over
the limit, the daemon answers 429 Too Many Requests until the window moves on.
A restart resets the count. The control socket isn’t limited.
Scripts should log in once and reuse the session token; runwisp run --url
does this for you.
Hooks are limited by failures only: after 20 rejected tokens from one
client IP within a minute, that IP gets 429 (even with a valid token) until
the window moves on. Successful calls never count.
Live log streams are capped at 128 open at once, and 16 per client IP. Past the
cap, new streams get 503 Service Unavailable. Close some log views and try
again.
Reverse proxies
Section titled “Reverse proxies”Use a reverse proxy (nginx, Caddy, Traefik, Cloudflare Tunnel) when you need a publicly trusted certificate. Let the proxy handle TLS, keep RunWisp on plain HTTP behind it, and tell RunWisp which addresses the proxy connects from:
[daemon]trusted_proxies = ["127.0.0.1/32", "10.0.0.0/8"]From those addresses only, RunWisp trusts:
X-Forwarded-For, so logs and the rate limit see the real client IP.X-Forwarded-Proto: https, so the session cookie is markedSecure.
Keep the list tight: an address in it can claim to be any client IP, and so use
up another client’s login limit. The
RUNWISP_TRUSTED_PROXIES environment variable (comma-separated) overrides the
key. See trusted_proxies.
For a LAN or a tailnet, you don’t need a proxy:
tls = "auto" serves HTTPS with
a self-signed certificate.
Public endpoints
Section titled “Public endpoints”These answer without a password:
GET /health: always200 OK. Use it as a liveness probe.GET /metrics: when metrics are on and share the main listener./api/auth/*: login status, challenge, login, and launch-ticket redemption.POST /api/hooks/*: needs one of the unit’s hook tokens instead, in the header or the URL.GET /api/daemon/identity: tells a startingrunwispwhich daemon holds the port. Loopback and socket callers only;403for everyone else.- The Web UI’s static files.
Everything else under /api/ needs a session or the control socket.
The login response is PBKDF2-HMAC-SHA256 (600,000 rounds) of the password, salted with a single-use challenge, so a captured login is slow to brute-force. It doesn’t protect the session token; TLS does.