TLS and reverse proxies
Native TLS for the daemon, and what it takes to reach the dashboard from another machine.
The daemon listens on 127.0.0.1:3141 over plain HTTP, and it answers loopback clients only.
That is not just a bind address: a request arriving from any other IP is rejected with
403 LOCALHOST_ONLY no matter what server.host is set to. Changing the bind address alone does
not give you remote access.
Native TLS
grith terminates TLS itself. Point [server.tls] at a certificate and key and restart the daemon.
~/.config/grith/config.toml
[server]
host = "127.0.0.1"
port = 3141
[server.tls]
cert_path = "/etc/grith/tls/fullchain.pem"
key_path = "/etc/grith/tls/privkey.pem"$ grith daemon restartBoth keys or neither - the section is parsed as a unit, so setting one without the other fails to
load. It is not reachable through grith config set; edit the file. The daemon says on startup
whether it came up with TLS, and a bind to a non-loopback address without TLS logs a security
warning telling you traffic is going out in plain text.
Reaching the dashboard from another machine
Because the daemon refuses non-loopback clients, remote access means running a reverse proxy on the same host and letting it be the loopback client.
grith.internal.example {
reverse_proxy 127.0.0.1:3141
}Caddy obtains and renews the certificate and upgrades WebSockets without extra configuration. For nginx the WebSocket path needs its own block:
server {
listen 443 ssl;
http2 on;
server_name grith.internal.example;
ssl_certificate /etc/letsencrypt/live/grith.internal.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/grith.internal.example/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3141;
proxy_set_header Host $host;
}
location /ws/live {
proxy_pass http://127.0.0.1:3141;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1d;
}
}Keep Host intact. The WebSocket upgrade is authorised by comparing the request's Origin
against its Host, so a proxy that rewrites one and not the other breaks the live stream.
Authorising the remote browser
Sensitive reads and browser writes both need the dashboard token, carried in an x-grith-csrf
header; the live stream carries it as a ?token= query parameter instead, because a browser
cannot set headers on a WebSocket handshake. A browser gets the token by pairing, and the pairing
code is single use.
$ grith daemon pairThat mints a code and builds a #pair=<code> URL against the loopback address. Take the fragment
and open it against your proxied hostname instead - https://grith.internal.example/#pair=<code>.
The fragment never reaches the server; the page reads it, exchanges it for the real token, stores
it, and strips the URL. A screenshot of the URL afterwards is inert.
Authentication is the proxy's job
grith has no user accounts, no configurable API key, and no roles. The dashboard token is a per-browser bearer, not an identity. If more than one person can reach the proxied hostname, put real authentication in front of it - OIDC, mTLS, or an IP allowlist - and treat anyone who gets past it as holding full control of that machine's daemon.
⚠️Loopback is the security boundary
Everything above moves the boundary from "processes on this machine" to "whatever your proxy lets through". The daemon can approve digest items, change config and shut itself down over the same API the dashboard uses.