| Version | Supported |
|---|---|
latest Docker tag / master branch |
✅ |
| Older tags | Best-effort, please upgrade |
Open a GitHub Security Advisory or issue with:
- Affected version / image digest
- Steps to reproduce
- Impact assessment
Do not open a public issue with exploit details for RCE / auth bypass. Prefer a private advisory; we aim to respond within 72h.
SSH Sentinel is designed to run on a private tailnet or localhost:
- Agents push outbound only (no inbound ports on members).
- Bearer per-host tokens are stored in
data/agents.json(0600). Rotate withdocker exec ssh-sentinel /srv/ssh-sentinel gentoken <host>. - Never expose port 8079 directly to the internet. Put SSO in front (Tailscale Serve, Cloudflare Access, Authelia, Traefik + Authentik).
- Login modes (
AUTH_MODE, defaultlocalfail-closed):local= HTTP Basic againstAUTH_USER+AUTH_PASS_HASH(mint withssh-sentinel genhash) orAUTH_PASSWORD;forward= trust SSO identity headers from Authentik-via-Traefik ForwardAuth (AuthentikX-authentik-username/X-authentik-email, also AutheliaRemote-User, Cloudflare Access email) only fromAUTH_TRUSTED_PROXIES(default loopback + RFC1918 + Tailscale), with optionalAUTH_ALLOWED_USERS; - Built-in SSO (
AUTH_MODE=oidc): authorization-code flow againstOIDC_ISSUERwith RS256 ID-token verify (issuer, audience, expiry, nonce), state/nonce anti-replay, server-side sessions (random token in HttpOnly cookie,OIDC_SESSION_TTL), optionalAUTH_ALLOWED_USERS. RS256 check is covered by the Rust RFC 7515 vector test (cargo test -p ssh-sentinel crypto); setOIDC_COOKIE_SECURE=1on direct HTTPS;none= explicit open mode for private networks/demo only./healthzand/api/authstay open; agent push keeps its own per-host Bearer tokens. /api/abusersexposes attacker IPs only (hits, first/last, attempted users, geo/org/ASN) — accepted logins, hostnames, internal/self IPs and raw lines can never appear. Still gated by login unlessAUTH_MODE=none; paginated, cached 60s, rate-limited per client IP.- Treat
AGENT_TOKENlike a password: pass via env / secret store, never commit to git, never paste in screenshots. - First setup (
AUTH_MODE=localwith no credential): the server mints a one-time token (ADMIN_SETUP_TOKENenv ordata/setup.token,0600, printed once in the log).POST /api/admin/setupwith that token creates the admin login (data/admin.json,0600, pbkdf2-sha256). Setup closes after first success. Env credential skips the wizard. - Admin APIs (
/api/admin/*,/api/banlist) always need login. Ban endpoints refuse private, reserved, self, and whitelisted IPs. SELF_PUBLIC_IPSexcludes your own scanners/VPN egress from attacker stats.- Demo data (
demo/auth.log.sample) is fully synthetic — safe to share.
- This repo contains no credentials, private keys, or customer data.
Gate checks:
.envis git-ignored, only.env.exampleis committed,data/is git-ignored, tokens are generated at runtime viasecrets.token_urlsafe.
auth.log contains more than attacker IPs: Accepted lines enumerate real
accounts + admin source IPs, and sudo lines leak PWD/COMMAND args.
Defaults are chosen so compromise detection never goes blind:
- Allowlist at ingest (
SHIP_FILTER=sshd-only, default on agent + central): onlysshd+pam_unix(sshd:session)lines are shipped/stored/served.sudo/CRON/systemdnoise is dropped.SHIP_FILTER=fullfor debugging. - Attacker IPs always fully visible — no masking on failed/probe stats, map, tables, or log chains. No PII redaction there by design.
- Accepted usernames always masked: normal
Acceptedlogins showd****y-style names in every mode. Raw names never reach the API.fail-then-accept(IP had prior fails) is alwayssuspicious; withTRUSTED_IPS/TRUSTED_USERSset, unknown-IP/user accepts are alsosuspicious. Suspicious entries show full user + full IP + red banner. - Screenshot-safe mask toggle (header, off by default, remembered per browser): hides IPs, usernames, host names, and session lines on every page, including suspicious rows. Raw values stay in props and API calls. Only rendered text is masked.
PRIVACY_MODE=strictadditionally masks theself_ipslist.- Set
TRUSTED_IPS(admin/home/runner IPs) +TRUSTED_USERS(e.g.ubuntu,deploy) for strongest signal. Without them, onlyfail-then-acceptflags. - Stored agent logs are
0600plaintext JSONL capped atMAX_LINES_PER_HOST. After enabling filtering, purge old noise:docker exec ssh-sentinel /srv/ssh-sentinel scrub.