Running castiel on a server with Tailscale
castiel is a desktop app, but nothing stops you from running it on a Linux server and reaching it from your phone, tablet, or laptop. For this purpose, castiel provides the --auth and --bind flags. Security matters, since castiel is effectively a remote code-execution machine. The --auth flag protects every API call with an access token: on first start, castiel generates a cryptographically random 256-bit token, stores it owner-only in ~/.config/castiel/auth.json, and prints it once to the log. Devices log in with the token once and receive a short-lived session cookie; scripts can pass it directly as a bearer token.
This is pretty secure, but for further security the recommended approach is Tailscale: you reach the server through a tailnet, a private, encrypted network only your own devices can join. This guide takes a first-time user from an empty server to a working, phone-accessible castiel in about ten minutes.
The architecture looks like this:
The request flow: your device connects through the tailnet, tailscale serve terminates HTTPS for https://<your-server>.<tailnet>.ts.net and forwards to castiel which never sees anything but loopback traffic on 127.0.0.1:8081.
Why this is the recommended way to expose castiel:
- castiel never leaves the loopback interface. It listens on
127.0.0.1:8081, so nothing on the server's LAN or the internet can reach it directly. Only the tailscale daemon, which proxies it. - Real HTTPS inside the tailnet.
tailscale serveterminates TLS with a valid certificate forhttps://<your-server>.<tailnet>.ts.net. castiel detects the proxy via theX-Forwarded-Proto: httpsheader and marks its login cookieSecureaccordingly. - Token auth stays on. castiel runs with
--auth, so even a tailnet device needs the access token to drive the API. Which executes commands on the server, so this matters. - No public exposure. Unlike
tailscale funnel(which publishes a service to the public internet, do not use it for castiel),tailscale serveis tailnet-only.
Prerequisites
- A Linux server (x86_64) with SSH access.
- A Tailscale account.
- The Tailscale app on the device you'll access castiel from (e.g. your phone).
curlandtaron the server (present on virtually every distribution).
Install Tailscale on the server
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale uptailscale up prints an authentication URL. Open it in a browser, log in, and approve the device. The server now appears in your Tailscale admin console.
Two tailnet settings must be on for the next steps (both are under the admin console, DNS tab):
- MagicDNS: gives the server a stable name like
castiel.tail1234.ts.net. - HTTPS Certificates: lets
tailscale serveissue valid certificates for those names.
If you don't enable them here, tailscale serve will offer to enable HTTPS for you when you first run it.
Install castiel from the latest release
The easiest way is the installer script, which downloads the latest Linux release, installs a self-contained app image into your home directory, and puts a castiel command on your PATH:
curl -fsSL https://iso53.github.io/castiel/install.sh | bashThis installs to ~/.local/share/castiel and symlinks the launcher to ~/.local/bin/castiel. Make sure ~/.local/bin is on your PATH (the installer tells you if it isn't, add export PATH="$HOME/.local/bin:$PATH" to ~/.bashrc), then verify:
castiel --versionManual alternative (without the installer script)
Download the latest bundle directly from the release:
curl -L -o /tmp/castiel.tar.gz \
https://github.com/ISO53/castiel/releases/latest/download/castiel-linux-x64.tar.gz
mkdir -p ~/.local/share
tar -xzf /tmp/castiel.tar.gz -C ~/.local/share
rm /tmp/castiel.tar.gz
ln -sf ~/.local/share/castiel/bin/castiel ~/.local/bin/castielLinux bundles are x86_64-only; there is no arm64 Linux build yet.
The bundle is self-contained: no Java, Node, or other runtime dependencies.
Run castiel as a daemon
For a server, castiel should run as a systemd service: it survives SSH disconnects, restarts on failure, and starts on boot.
Create the unit file (replace nothing, the variables expand to your own user):
sudo tee /etc/systemd/system/castiel.service > /dev/null <<EOF
[Unit]
Description=castiel pentesting harness
After=network-online.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$HOME
ExecStart=$HOME/.local/share/castiel/bin/castiel --auth --log-file $HOME/castiel.log
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now castielCheck that it is running:
systemctl status castiel --no-pagerThe important flags in ExecStart:
--auth: require token authentication for every API call. Mandatory here: castiel executes commands on the server, and a daemonized instance must never accept anonymous commands. On first boot with--auth, a 256-bit access token is generated and stored owner-only in~/.config/castiel/auth.json(and printed once in the boot log).--log-file ~/castiel.log: mirror logs to a file next to the console output, rotated automatically.
Don't double-run
Either castiel runs as the daemon or you start it manually in a terminal. Never both at once. The second instance fails to bind port 8081. To run it interactively for a while, stop the service first (sudo systemctl stop castiel), and start it again when done.
Get the access token
castiel --show-tokenThis prints the stored access token (run it as the same user the service runs as. The token lives in ~/.config/castiel/auth.json). Copy it; you'll paste it into the login screen on your phone. You can rotate it later with castiel --reset-token (takes effect on the next start and drops all open sessions).
Expose castiel with tailscale serve
One command, run once. The configuration is persistent inside the tailscale daemon, so it survives reboots and SSH disconnects:
sudo tailscale serve --bg 8081This proxies https://<your-server>.<tailnet>.ts.net to the local http://127.0.0.1:8081. Verify:
sudo tailscale serve statusUnder the hood, the proxy rewrites the Host header to 127.0.0.1:8081 (which castiel's host allowlist accepts as a loopback name) and adds X-Forwarded-Proto: https (which makes castiel's login cookie Secure). No reverse-proxy configuration on castiel's side is needed for this part.
Allowlist your ts.net hostname
castiel rejects requests whose Host header doesn't name the app. A defense against DNS rebinding. Depending on your Tailscale version, the proxy may forward the original Host: <your-server>.<tailnet>.ts.net to the backend instead of rewriting it. If that happens, your phone shows:
error: unexpected host headerand the castiel log confirms it:
sudo journalctl -u castiel -n 30 --no-pager | grep -i "host header"
# Rejected request with unexpected Host header "castiel.tail1234.ts.net"The fix is one environment variable that opts the proxy hostname in castiel.web.allowed-hosts, set through a systemd drop-in so it survives package updates of the unit file:
sudo systemctl edit castielIn the editor, add the override between the two marker comment lines (anything below the second marker is discarded):
[Service]
Environment=CASTIEL_WEB_ALLOWED_HOSTS=castiel.tail1234.ts.net(Use the exact hostname from the log line; multiple names can be comma-separated.) Then apply and restart:
sudo systemctl daemon-reload
sudo systemctl restart castielOpen it on your device
- Connect the Tailscale app on your device to the same tailnet.
- Open
https://<your-server>.<tailnet>.ts.netin the browser (find the name withtailscale dns statuson the server, or in the admin console's Machines tab). - The castiel login screen appears, paste the token.
You're logged in with a session cookie (HttpOnly, SameSite=Strict, Secure). Sessions are kept in memory: they last 24 hours of inactivity (30 days absolute) by default, and a castiel restart logs you out again just re-paste the token.
Connect a provider over the server's own internet
Tailscale only constrains inbound traffic; everything castiel does outbound (LLM provider APIs, web search, CVE lookups, --check-update) uses the server's normal internet connection. Cloud providers like OpenRouter or Z.ai work as-is. If you run Ollama on the same server, point the provider at http://localhost:11434. See LLM providers for details.
Day-to-day operation
journalctl -u castiel -f # follow live logs
tail -f ~/castiel.log # the same logs from the file
sudo systemctl restart castiel # restart (logs out all sessions)
sudo systemctl stop castiel # stop it temporarily
sudo systemctl disable castiel # stop auto-starting on boot
sudo tailscale serve status # check the proxy mappingLeaving it idle is safe. castiel uses a few hundred MB of RAM while idle and makes no connections when unused; the security posture is identical whether you're connected or not.
Reboots come back automatically. Tailscale and its persistent serve config start with the system, castiel is enabled, and the token in ~/.config/castiel/auth.json persists. The only thing you redo after a restart is logging in with the token, because sessions are in-memory.
Security notes
Never use
tailscale funnelfor castiel, that publishes the service to the public internet.tailscale servekeeps it tailnet-only.By default, every device in your tailnet can reach the server. If you share nodes with other people, restrict access with a Tailscale ACL (admin console → Access controls):
json{ "acls": [{ "action": "accept", "src": ["you@example.com"], "dst": ["castiel:443"] }] }--bindbeyond localhost requires--auth. The setup above never uses it (loopback + proxy is strictly better), but if you ever bind a non-loopback address, e.g.castiel --bind 0.0.0.0 --auth, token auth is mandatory and castiel refuses to boot without it.Rotate the token if it ever leaks:
castiel --reset-token, then restart the service and log in again on your devices.Tighten sessions for a shared or high-value server by setting the
castiel.auth.session-idleandcastiel.auth.session-absoluteproperties (e.g.Environment=CASTIEL_AUTH_SESSIONIDLE=2hin the same systemd drop-in as Step 6).
For the complete list of launcher flags, see the CLI reference. To keep castiel up to date, run castiel --check-update on the server. It compares this build with the latest GitHub release and swaps itself in place.