SitRep is a self-hosted public status page server. It ships as single binary with the web frontend and an embedded database file. An optional Dockerfile is available.
- One instance serves many status pages, reachable below a base domain path, as subdomain, or on their own domain.
- Fully localized UI: German and English ship with the binary, adding a language means adding one catalog file (contributions welcome).
- Panels fed by Prometheus queries: single values, up/down states by thresholds, and charts. SitRep polls the data sources and serves visitors from memory; visitors never cause queries.
- Incidents and maintenance with a timeline of updates written in Markdown, shown on the status page, shaded in charts, in an archive, in an Atom feed per language and as JSON for other websites to embed.
- Status pages refresh themselves and fetch only what changed.
- Imprint and privacy statement per status page or for the whole instance, as Markdown text or link, and a landing page text or a status page as start page.
- A brand color and an SVG logo per status page; the favicon shows the status.
- Status pages can be taken offline or paused, and exported to and imported from YAML files.
- Light, dark and system color schemes, remembered per visitor.
- Admin console with sign-in by single sign-on (OpenID Connect, limited to a group) or by username and password (bcrypt or argon2id hashes, with login throttling). It manages data sources, status pages with their panels, incidents and a live preview, and instance settings for languages, the default color scheme, legal pages and the landing text.
- Roles per status page and for the whole instance, managed in the console.
- No third-party requests, no tracking, no consent banner needed.
Building needs Go and Node.js.
make build
./sitrep hash-password -user admin > users # asks for the password twice
SITREP_BASE_DOMAINS=status.example.com \
SITREP_AUTH=basic \
SITREP_BASIC_USERS_FILE=users \
./sitrep serveImages for amd64 and arm64 are published as ghcr.io/digineo/sitrep:
latest and v<version> for releases, latest-dev for the main branch.
make docker-build builds one locally, tagged sitrep.
docker run --rm -it ghcr.io/digineo/sitrep hash-password -user admin
# copy the printed line into ./users
docker run -d -p 2607:2607 \
-v sitrep:/data \
-v ./users:/etc/sitrep/users:ro \
-e SITREP_BASE_DOMAINS=status.example.com \
-e SITREP_AUTH=basic \
-e SITREP_BASIC_USERS_FILE=/etc/sitrep/users \
ghcr.io/digineo/sitrepThe image runs as an unprivileged user (UID 65532) in /data, which holds
the database and is read for .env files. Mounted files must be readable
by that user. Its health check runs sitrep healthcheck, which requests
/healthz at SITREP_LISTEN.
The console is at http://status.example.com:2607/admin. Whoever signs
in first becomes its owner, see Accounts and roles.
For a local try,
use sitrep.localhost as base domain: browsers resolve every
*.localhost name to your machine. Note: SITREP_BASE_DOMAINS=localhost
will not work.
All configuration comes from environment variables. Optional .env.local
and .env files in the working directory are read too, in that order of
priority; real environment variables always win. The files hold lines of
KEY=VALUE, optionally prefixed with export; values may be quoted with
single or double quotes (without escape sequences), and unquoted values end
at #. For a quick setup, copy the documented .env.sample to
.env.local and fill in the blanks.
Invalid or missing values are reported together, and the server does not
start. A variable that is set but empty is an error, except for the two
optional secrets, which then count as unset. Booleans accept
true/false/1/0/yes/no/on/off. Durations combine the units d, h, m
and s in this order, e.g. 90s, 1h30m or 7d. Only the variables of
the selected auth provider are read.
| Variable | Default | Meaning |
|---|---|---|
SITREP_LISTEN |
:2607 |
HTTP listen address. |
SITREP_DB |
sitrep.db |
Database file. One instance per file. |
SITREP_SECRET_KEY |
Base64-encoded 32-byte key for data source secrets, see Data sources. Generate with openssl rand -base64 32. |
|
SITREP_DEFAULT_REFRESH |
30s |
Default poll interval, 5s to 24h. |
SITREP_BASE_DOMAINS |
required | Comma-separated base domains, e.g. status.example.com: lowercase hostnames with at least two labels, each listed once. |
SITREP_TRUST_PROXY |
Comma-separated IP addresses and CIDR networks of trusted reverse proxies, see Running behind a reverse proxy. true is deprecated. |
|
SITREP_AUTH |
oidc |
Auth provider: oidc or basic. |
SITREP_SESSION_TTL |
12h |
Admin session lifetime, 5m to 30d. |
SITREP_OIDC_ISSUER |
required for oidc | Issuer URL, exactly as the identity provider reports it. |
SITREP_OIDC_CLIENT_ID |
required for oidc | Client ID. |
SITREP_OIDC_CLIENT_SECRET |
Client secret, for confidential clients. | |
SITREP_OIDC_REDIRECT_URL |
required for oidc | https://<base domain>/auth/oidc/callback; the host must be a base domain. |
SITREP_OIDC_SCOPES |
openid profile email |
Space-separated scopes; openid is always requested. |
SITREP_OIDC_GROUPS_CLAIM |
groups |
Claim with the user's groups: a list or a single string. |
SITREP_OIDC_GROUP |
required for oidc | Group required to sign in. |
SITREP_OIDC_ADMIN_GROUP |
Deprecated name of SITREP_OIDC_GROUP, read only if that is unset. |
|
SITREP_BASIC_USERS_FILE |
required for basic | Users file, see below. |
SITREP_LOG_LEVEL |
info |
debug, info, warn or error. |
SITREP_LOG_FORMAT |
text |
text, json or pretty (colored console output). |
The startup log shows the effective configuration, with secrets and credentials in URLs redacted.
With SITREP_AUTH=oidc (the default), admins sign in at an OpenID
Connect identity provider. Register SitRep there as a client with the
authorization code flow and the redirect URL
https://<base domain>/auth/oidc/callback. A confidential client needs
SITREP_OIDC_CLIENT_SECRET; a public client works without one, since
SitRep always uses PKCE.
Only members of SITREP_OIDC_GROUP may sign in. SitRep reads
the groups from the claim SITREP_OIDC_GROUPS_CLAIM of the ID token, or
from the user info endpoint if the ID token lacks the claim. Admins are
shown by their name claim, else preferred_username, else their subject.
SitRep keeps the email claim only if email_verified is true. What
admins may do depends on their roles.
- Group changes are checked only at sign-in. Someone removed from the
group keeps access until their session expires, at most
SITREP_SESSION_TTLafter they signed in, unless an owner deletes their account. - Signing out ends the SitRep session, but not the session at the identity provider, which may sign the admin in again without asking.
- Startup does not wait for the identity provider. SitRep fetches its configuration in the background and retries at growing intervals of up to a minute; until then, the login screen says that sign-in is temporarily unavailable.
- Several base domains: sign-in always completes on the host of
SITREP_OIDC_REDIRECT_URL, and the console continues there.
Notes for common identity providers:
- Keycloak: the issuer is
https://<host>/realms/<realm>. Keycloak sends no groups by default: add a "Group Membership" mapper to the client with the token claim namegroups, included in the ID token. With "Full group path" on, groups read/admins; turn it off or setSITREP_OIDC_GROUP=/admins. - authentik: the issuer is
https://<host>/application/o/<application slug>/, with the trailing slash. The defaultprofilescope mapping sends the group names ingroups. - Microsoft Entra ID: the issuer is
https://login.microsoftonline.com/<tenant ID>/v2.0. Use app roles rather than groups: define a role with the valuesitrep-adminin the app registration, assign it to the admins, and setSITREP_OIDC_GROUPS_CLAIM=rolesandSITREP_OIDC_GROUP=sitrep-admin. A groups claim would carry group object IDs, and Entra ID leaves it out entirely for users in more than 200 groups, which SitRep then treats as not being a member. Entra ID sends noemail_verifiedclaim, so accounts added by email are never bound: let people sign in first, then an owner assigns their roles in the user list, orgrant-ownernames their account by its ID.
SitRep creates an account for everyone at their first sign-in. The first account of the active auth provider becomes the instance's owner; switching providers makes the next first account an owner again. Other accounts start without a role and see no status page until they get one.
Roles build on each other:
| Role | German | May |
|---|---|---|
| Responder | Redakteur | manage the incidents of a status page |
| Maintainer | Leiter | also edit its panels and settings, export and import it, and add and remove its members |
| Admin | Admin | do all of this on every status page, create and delete status pages, and edit data sources and the instance settings |
| Owner | Eigentümer | also add and remove admins and owners, and delete accounts |
Responders and maintainers hold their role per status page; admins and owners hold theirs for the instance. Nobody changes their own roles or deletes their own account, so an owner always remains. Maintainers can read every metric of the data sources their panels use, since data sources are shared.
Maintainers add members to a status page in the console, and owners add
admins and owners, by email address, or by username with
SITREP_AUTH=basic. An account added by email is bound to whoever signs
in first with that verified email address. A domain other than one's own
is pointed out, as it may be a typo. The user list shows owners every
account with its roles and last sign-in, and marks accounts that cannot
sign in any more: those of another auth provider and, with
SITREP_AUTH=basic, those no longer in the users file. Accounts are only
deleted by owners.
If no owner can sign in any more, stop the server and make an account
an owner from the command line, by email address, or by username with
SITREP_AUTH=basic. An account that does not exist yet is created, and
an email address is bound to the account that next signs in with it:
./sitrep grant-owner alice@example.comAn account without a verified email address is named by its ID instead.
The log line "created an account on first sign-in" shows it, and the
signed-in person finds it as id at /auth/session.
With SITREP_AUTH=basic, admins are listed in an htpasswd-style file, one
username:hash per line. Blank lines and lines starting with # are
ignored. Every user in the file may sign in. SitRep reloads the file when
it changes; a broken file keeps the previous users and logs an error.
Removing a user from the file ends their sessions.
Create argon2id hashes with SitRep, or bcrypt hashes with Apache's
htpasswd:
./sitrep hash-password -user alice >> users
htpasswd -B -C 12 users bobbcrypt needs a cost of at least 10. argon2id hashes need at least m=19456, t=2 and p=1, and at most m=1048576, t=10 and p=16.
Failed attempts are counted per username and, independently, per client address, or per /64 network for IPv6. After five failures within 15 minutes for a username, or from an address, further attempts for that username, or from that address, are refused for 15 minutes. SitRep keeps addresses only as keyed hash under a secret that changes daily; the change resets the address counters. It keeps at most 10,000 counters in memory; while all are in use, attempts that would need a new one are refused, too.
At most four password verifications run at a time; an attempt that cannot
start one within five seconds is refused as too many attempts. Each argon2id
verification allocates its memory parameter, so hashes with m=1048576 (1 GiB)
can make logins use up to 4 GiB. The default of sitrep hash-password,
m=65536, needs 64 MiB each. Usernames are limited to 200 bytes.
Panels query data sources, which you configure in the console under "Data sources". SitRep ships the Prometheus type: a base URL (a path prefix is allowed), optional basic or bearer authentication, and a request timeout of 1s to 2m (default 10s). Requests never follow redirects, and responses over 10 MiB are refused. Panels whose responses exceed 1 MiB still work, but the console warns about them: they load the backend and enlarge the public page.
Passwords and tokens are stored encrypted with SITREP_SECRET_KEY and are
never shown again. They are bound to the URL they were entered for: saving
another URL removes them unless you enter them again, so credentials are
never sent to another host. Without the key, data sources cannot store
secrets. If the key is lost or changed, data sources with secrets become
unusable: their panels show no new data and the console marks them until
you enter the secrets again. Public pages keep working.
Admins can point a data source at any URL the server reaches. That is fine
because admins are trusted anyway: they also run arbitrary queries.
Requests to data sources and to the identity provider honor the proxy
environment variables HTTPS_PROXY, HTTP_PROXY and NO_PROXY.
Each panel is polled at once and then at its refresh interval, by default
SITREP_DEFAULT_REFRESH. Panels of a status page are polled whether or
not anyone visits it.
SitRep speaks plain HTTP and neither terminates TLS nor compresses
responses; a reverse proxy in front of it does both. Set
SITREP_TRUST_PROXY to the proxy's address, e.g. 127.0.0.1,::1, so that
SitRep takes the scheme, host and client address from the proxy's
X-Forwarded-* headers: it needs the scheme to set secure cookies and to
accept the console's requests. Requests from other addresses have these
headers ignored. The client address is the last X-Forwarded-For entry
that is not a listed proxy, so with a CDN in front of the proxy, list the
CDN's networks, too.
Warning
SITREP_TRUST_PROXY=true is deprecated and logs a warning at startup.
It trusts the headers of every client, which can then forge their
address to evade the login throttle, and their scheme and host. Replace
it with the proxy's address.
With Caddy, on-demand TLS gets a certificate
for each host the first time it is visited. Before it requests one, Caddy
asks /tls/authorize?domain=<host>, which answers 200 only for the base
domains and the hosts of existing status pages, whatever their
availability:
{
on_demand_tls {
ask http://127.0.0.1:2607/tls/authorize
}
}
https:// {
tls {
on_demand
}
encode zstd gzip
reverse_proxy 127.0.0.1:2607
}Everyone who can change a status page's route, maintainers included, can thus make Caddy request certificates for domains pointed at SitRep.
Caddy passes the original Host header and sets the X-Forwarded-*
headers by default. SitRep sends no Strict-Transport-Security header;
add one in the proxy if you want it. /healthz answers ok while the
database is readable, for health checks.
- Base domains: point them at the proxy with A and AAAA records.
- Subdomain mode: add a wildcard record, e.g.
*.status.example.com, pointing at the proxy. Every subdomain-mode page answers below every base domain. - Custom domains: whoever owns the domain points it at the proxy,
usually with a CNAME record to a base domain, e.g.
status.customer.example CNAME status.example.com. SitRep does not check who owns a domain; entering it in the status page's settings is enough.
A base domain host serves the landing page at /, the console at /admin
and path-mode status pages at /<slug>/. Subdomain-mode pages answer on
<slug>.<base domain>, custom-domain pages on their own host. A status
page promoted to start page in the global settings answers on the base
domains instead, at /, /incidents and so on. Its own route redirects
there: path-mode and subdomain-mode pages to the same base domain, custom
domains to the first base domain.
With one enabled language, URLs carry no language: /, /incidents,
/imprint. With several, they start with it: /en/, /de/incidents,
/de/impressum. Legal pages use slugs in their language. Unprefixed URLs
redirect to the visitor's language: the one chosen in the language switcher
(cookie lang), else the best match of the browser's languages, else the
primary language. Following a link to another language does not change the
stored choice.
Admins open an incident with a title and a first update that sets the status to planned (maintenance) or active, and add updates as things change. Each update sets a status, a severity (minor, major or critical) or both, and has a Markdown description. Ongoing incidents count in the status of the page: a critical one as an outage, any other as degraded.
Visitors see upcoming and ongoing incidents, and finished ones for seven
days after their last update; the archive at /incidents lists all of
them. Charts shade the time an incident was ongoing in its severity's
color. Each language has an Atom feed at /feed.atom with one entry per
update.
A status page can delete finished incidents a number of days after their last update ("Keep finished incidents" in its settings). Upcoming and ongoing incidents are never deleted. Retention runs at startup and hourly.
/incidents.json (with the language prefix on pages with several
languages, e.g. /de/incidents.json) lists the incidents visitors see, in
the URL's language: ID, title, phase (upcoming, ongoing, finished),
current status and severity, and every update with its time in UTC, status,
severity and description as HTML. Responses are cached for 60 seconds. It
is not linked from any page.
Other websites can read it in the browser when their origin is listed under
"Allowed origins for incidents.json" in the status page's settings, e.g.
https://www.example.com. SitRep then answers with
Access-Control-Allow-Origin for that origin; it never allows credentials.
The global settings set an imprint and a privacy statement, each either
none, a link to a page elsewhere, or a Markdown text that SitRep shows at
/imprint and /privacy (in German /impressum and /datenschutz). Each
status page uses them unless it sets its own. The footer links them.
The landing page on the base domains shows the landing text from the global settings, or without one a neutral page with a link to the console. The settings can also promote a status page to start page; the base domains then show it, with its own legal pages, instead.
A status page can have a brand color, which colors its header and footer with black or white text, whichever contrasts more, and an SVG logo of at most 64 KiB. SitRep sanitizes logos when they are saved: it keeps only plain SVG shapes, text, gradients, masks and filters, and removes scripts, event handlers, links to other documents and anything that loads other resources. Logos are served with a content security policy that sandboxes them.
A status page is online, offline or paused. Visitors of an offline or paused page see its name, logo and legal pages and a notice that it is unavailable; everything else answers 503, including the feed and incidents.json. SitRep keeps polling offline pages, so their data is current when they come back, but stops polling paused ones. The console previews every page regardless of its availability.
The settings of a status page export it as a YAML file: its settings and panels in display order, with localized texts by language. Panels name their data source, so a file can be imported on another instance with data sources of the same names and types. Exports leave out IDs, timestamps, availability, incidents and the data sources themselves.
Importing a file creates a new status page ("Import status page" in the
sidebar), or replaces the settings and panels of an existing one, keeping
its incidents and availability. Files must start with version: 1, may
not contain unknown keys and are validated completely before anything is
saved.
None of these cookies is needed to read public pages, and none tracks anyone:
| Cookie | Set when | Content | Lifetime |
|---|---|---|---|
theme |
a visitor picks a color scheme | light, dark or system |
1 year |
lang |
a visitor picks a language | the language code | 1 year |
sitrep_session, on HTTPS __Host-sitrep_session |
an admin signs in | a random session token | the session lifetime |
sitrep_oidc |
an admin starts single sign-on | random values that secure the sign-in, and the console page to return to | 10 minutes, deleted when the sign-in completes |
SitRep stores an account for everyone who signed in or was added: the subject, display name, the email address if the identity provider verified it, and the times of creation and of the last sign-in. Accounts are kept until they are deleted. Accounts added by email that never signed in are deleted once they lose their last role.
Sessions only refer to the account. Signing out deletes the session.
Sessions expire after SITREP_SESSION_TTL, and expired ones are deleted
at startup and hourly.
Incidents and their updates store the subject and display name of the admin who created them and of the admin who last edited each update. Only the console shows them; status pages, feeds and incidents.json never do. They are deleted with their incident, either by an admin or by the status page's incident retention. Deleting an account replaces them with "System".
Logs contain the usernames of failed sign-ins and the subjects of sign-ins refused by single sign-on, of new accounts, and of admins who change roles or delete accounts, but no email addresses and no client addresses.
make dev runs the server with live reload of the backend and the
frontend, make lint runs every linter, and make test runs the Go,
Vitest and Playwright tests. make help lists all targets.
CONTRIBUTING.md explains the setup, the conventions and
how to add a language, an auth provider or a data source type.
MIT, see LICENSE.

