Project
Security
Vulnerability reporting, trust boundaries, and secret handling.
Vulnerability reports
Report vulnerabilities privately, as the security policy describes.
Trust boundaries
| Boundary | Protection |
|---|---|
| Tracker and status page | Body and field limits, tracker rate limits, and no internal details in public status output |
| Panel and API | Sessions, Origin checks, scoped tokens, admin and viewer roles, frame denial, content-type protection, and a Content-Security-Policy that allows only the panel’s own files and no inline scripts or styles |
| Sign-in | Argon2id password hashes, per-address rate limits, and a setup token for the first account |
| Agents | Per-host tokens, body and frame limits, per-host and total byte budgets, results accepted only for assigned checks, and no code from the server |
| Heartbeats and deploys | Unguessable ping URLs, deploy-scoped tokens, and constant-time GitHub HMAC verification |
| Checks | A fixed set of probes with no scripts or browsers, timeouts, at most four concurrent runs per target host, and a 1 MiB response-body limit |
| Notifications | Private addresses refused unless allow_private is set, connection to the validated address, no redirects, and SMTP credentials sent only over validated TLS. IPv6 addresses that embed an IPv4 address (mapped, NAT64 and 6to4) are judged by that address, and Teredo and the other special-purpose ranges that are not globally reachable are refused |
Every POST route that works without a session has a byte limit, from 4 KiB for status
page forms and 16 KiB for sign-in to 5 MiB for GitHub webhooks. A larger body gets 413.
Routes that take a token check it before reading the body; the others refuse a body
announced as too large before reading it. kogut never decompresses request bodies, so
the limits count the bytes that arrive.
The status page shows public service names, state, uptime, and neutral incident titles, not check URLs, host names, errors, likely causes, or logs. Tracker responses contain no data. Outbound connections lists the requests kogut makes.
Identity and sessions
- OpenID Connect uses the authorization code flow with PKCE, state, and nonce, and checks the ID token’s signature, issuer, audience, nonce, and expiry. ID tokens must use RS256 or ES256, with one minute of clock tolerance. The state and nonce cookie lasts 10 minutes. Providers need HTTPS except on loopback.
- Provider accounts bind to issuer and subject. Joining an existing account by email
needs a verified address, and another subject cannot take the account over by claiming
the same email. Public providers need
allowed_domains. - Forward auth trusts identity headers only from
configured TCP peers; forwarded addresses and loopback get no implicit trust. Requests
with an
Authorizationheader need a valid bearer token instead. The proxy must strip client-supplied identity headers and block direct access to the panel. - Sessions use
HttpOnly,SameSite=Laxcookies, withSecureon HTTPS. API tokens useAuthorization: Bearerand one scope:read,write,deploy, ormcp. - The setup token is 16 random bytes, logged and stored in
<data>/setup-tokenwith mode 0600. It stops working after the first account exists. - Sign-in and setup share a per-client rate limit of 10 attempts at once, then one per second. The tracker allows 30 events at once, then two per second. The proxy setting for client rate limits is separate from proxy authentication; see the configuration reference.
No code from the server
kogut never runs code that the server sends or that a check carries, and will not add script, browser, or remote command checks: such code becomes remote code execution on every host once someone takes over the server.
- Checks are a fixed set of probes: HTTP, TCP, DNS, ping, and domain expiry, with
declarative assertions on the status, body text, JSON values, and response time. No
check from
kogut.toml, the panel, or the API runs a script, JavaScript, an embedded interpreter, or a browser, on the server or on an agent. - The server never sends an agent anything to run. Its frames set intervals, assign those probes, and ask for log lines. None of them can carry a program or its arguments, and a test fails if one could.
- An agent starts only
systemctlandjournalctl, with fixed arguments, and the command checks in its own file on the host. The one value the server supplies is the unit namejournalctlreads for a log request, checked against a character allowlist. The agent refuses a checks file it could change itself. The server learns the name, service, and interval of each command check, never the command.
Agents and service isolation
Agents open no inbound port. They validate the server certificate with bundled Mozilla roots, not the system store. Plain HTTP is refused outside loopback and Tailscale addresses unless allowed. Optional public-key pins add a check; they do not replace certificate validation. See Agent installation and the environment reference.
Whoever can assign checks to an agent can make it probe any address it can reach with
HTTP, TCP, DNS, and ping checks: admins, write tokens, anyone who can edit kogut.toml,
and an attacker who takes over the server. For an HTTP check with a body assertion, the agent
reads up to 1 MiB of the response and sends it to the server. The server does not store
the body, but a failed json assertion with equals keeps up to 60 characters of the
value it found in its message. The agent that runs a check receives the whole check, with
the URL, request headers, and expected values, so any secret in them reaches that host.
To limit what an agent can connect to, use a firewall or a network namespace.
The install script and NixOS module use an unprivileged dynamic user and systemd
hardening, including a read-only system view and NoNewPrivileges. The distroless
container runs as UID 65532. Ping checks use unprivileged ICMP sockets; the systemd units
keep CAP_NET_RAW.
Docker socket access equals root on the host, even with a read-only mount. The agent
sends only GET requests, but only a socket proxy enforces that. See
Agent permissions.
Secrets and stored data
| Data | Handling |
|---|---|
| File-managed secrets | ${NAME} reads the environment, then a systemd credential. Channel secrets stay out of the database. |
| API tokens, host tokens, and ping URLs | Shown once, stored only as SHA-256 hashes, and replaceable |
| Panel-managed channel secrets | Stored in the database so notifications can be sent, never returned by the API |
| ntfy and webhook URLs | Visible only to admins and write tokens |
| Config and delivery errors | Secrets hidden in output |
| JavaScript errors | Query strings, email addresses, and long tokens scrubbed before storage |
| Agent log excerpts | Stored without redaction, visible in the panel, API, MCP, and Copy for AI |
| Subscriber addresses and link tokens | Stored for status updates. Tokens can only confirm or end their subscription. |
Secrets or personal data in application logs stay in incident excerpts and copied output.
Backups are plain SQLite files with panel-managed channel secrets and subscriber data, but not secrets from the environment or the visitor salt. Protect them like the server’s data.
Visitors and subscribers
The tracker uses no cookies or browser storage. IP addresses and user agents go into a daily visitor hash and are never stored. See Visitor privacy.
Status subscriptions use double opt-in. Confirmation and unsubscribe links show a button first, so following a link alone changes nothing. Subscription requests have per-client and total limits. See Status page.