"Speaking up takes courage. Staying silent shouldn't be the safer option."
A secure, self-hosted internal reporting channel β because every employee deserves protection.
OpenWhistle is a self-hosted whistleblower platform that fulfils the mandatory reporting channel requirements under the German Hinweisgeberschutzgesetz (HinSchG) and EU Directive 2019/1937. Any company with 50 or more employees and all public authorities are legally required to provide an internal reporting channel. OpenWhistle provides a fully open source solution β free of charge, zero vendor lock-in, and privacy-first by design.
π Live demo: demo.openwhistle.net π Documentation: openwhistle.net/en/docs/
- Full anonymity β No IP addresses logged at any layer. An employee submitting from the office network leaves no trace. Times the reporter causes (submission, their messages, their files) are stored as the day only, so no exact time can be matched to who was at their desk.
- Two-factor whistleblower access β Case number + secret UUID4 PIN with brute-force protection. No accounts, no email β nothing to tie the report back to a person.
- Multi-step submission wizard β Guided 5β6 step form with back/next navigation and Redis-backed session state. Anonymous or confidential mode selectable at step 1.
- Anonymous / confidential mode β Anonymous leaves no personal data. Confidential encrypts name, contact info, and optional secure email with Fernet.
- Identity on request β a confidential reporter's name is hidden by default and shown only to the case handler (an admin of the case's organisation while it is unassigned), after a reason that the audit log records.
- Multi-location / branch selection β Optional location selector shown when the operator has configured active branches or offices; full admin management UI included.
- Bidirectional communication β Required by HinSchG Β§17. The whistleblower can reply to admin messages using only their case number and PIN.
- HinSchG SLA tracking β 7-day acknowledgement and 3-month feedback deadlines with days remaining shown in both the admin dashboard and the whistleblower status page.
- Role-based access control β
ADMINandCASE_MANAGERroles. Case managers can process their assigned reports; only admins manage users, categories, and deletions. The admin sidebar shows each role only the pages it may open. - Case assignment β Assign reports to any active staff member; "My Cases" dashboard filter for case managers.
- Status workflow β
received β in_review β pending_feedback β closed; only valid transitions allowed server-side. - 4-eyes deletion β Hard deletion requires two different admins (request + confirm); the same admin, or an account one of them made, gets HTTP 409. GDPR Art. 17 compliant.
- Immutable audit log β Every admin action recorded with timestamp and username, shown as readable labels in all four languages; CSV export keeps the machine codes; required by HinSchG Β§11 Abs. 5.
- Search by case number or content β Find a case by any part of its number or by a word in its description or messages. Content is decrypted in memory for that request only; no searchable index is stored, and the confidential name never matches.
- Internal notes β Admin-only notes on cases; never visible to the whistleblower.
- Case linking β Link related cases with bidirectional normalization constraint.
- Custom categories β DB-driven report categories; full management UI at
/admin/categories. - PDF export β Case export including SLA compliance section (HinSchG Β§17); the confidential identity is left out by default and only included via the same audited reveal as the case page.
- Dashboard statistics β SLA compliance rate, status distribution, category breakdown.
- "Signal" design system β documented, token-driven identity (
DESIGN.md); app + site, light + dark. - Mandatory MFA β TOTP (compatible with any authenticator app) required for every admin account. No exceptions, no bypass.
- Own password, own account β every admin changes their password on
/admin/accountwith the current password and a TOTP code; every other session ends. A password an admin, a superadmin reset or the host set must be replaced before any other admin page opens. - Self-service recovery β admins link their own single sign-on identity; a superadmin resets a lost authenticator, and the host's script resets any account, the last superadmin included.
- Lockout-proof whistleblower login β a correct case number and PIN always open the case; wrong guesses are counted but can never lock the rightful whistleblower out.
- Password-spraying alarm β failed admin logins are counted instance-wide (no username, no IP);
crossing
ADMIN_FAILED_LOGIN_ALERT_THRESHOLDsends one alert and writes an audit entry. - Object-level authorization β Every report endpoint enforces per-record access: case managers see only their assigned reports, admins are scoped to their organisation. Role assignment enforces privilege tiers (only superadmins grant superadmin; no self-role-change; the last admin cannot be demoted away).
- Hardened HTTP security β Strict, per-response nonce-based Content-Security-Policy (no
unsafe-inline), consolidated single-source security headers (HSTS,X-Frame-Options, nosniff), and strict username validation. - Absolute admin session lifetime β An admin session can be kept alive by refreshing it, but
never past
SESSION_MAX_HOURS(default 12) from login; the refresh endpoint itself requires a CSRF token, not just a valid cookie. - OIDC / SSO support β Optional single sign-on via any OpenID Connect provider (Keycloak,
Authentik, Azure AD, Google, β¦), with PKCE and a verified ID token (signature, issuer,
audience, expiry, nonce). Each admin links their own account while signed in; TOTP is still
required. LDAP supports LDAPS and StartTLS (
LDAP_START_TLS). - Authenticator recovery β a superadmin resets a lost authenticator on
/admin/users(the account gets a new temporary password and enrols a new app at the next login), or the operator runsscripts/reset_admin_password.py --reset-totp <username>. Both end the account's sessions. - File attachments β Whistleblowers can attach evidence files (PDF, images,
.docx,.xlsx, CSV, TXT β up to 10 MB each, 5 per report; legacy.doc/.xlsare refused, since their author cannot be removed). Identifying metadata (photo GPS/EXIF, PDF and Office author fields) is removed on upload, and files are encrypted with the report's own key. - Optional virus scanning β Attachments can be checked against a ClamAV
clamddaemon before they are stored (CLAMAV_HOST). Fail-closed: if the scanner is unreachable, the upload is refused rather than stored unscanned. - Internationalisation β English, German, French and Brazilian Portuguese UI; language picker in the nav bar; a test keeps every key and placeholder present in every locale.
- WCAG 2.1 AA β Skip-to-content link, ARIA labels, live regions, visible focus indicators, and keyboard-accessible language picker.
- Setup wizard β Web-based first-run wizard creates the initial admin account with TOTP setup.
No manual database steps. It asks for a one-time setup token (
SETUP_TOKEN, or a random one logged at first start), so reaching/setupfirst is not enough to own the installation. - Encrypted second factor β TOTP secrets are stored encrypted; a database dump alone yields no account's second factor.
- Hardened containers β read-only root file system, no capabilities,
no-new-privileges, the image's base images and the bundled nginx and ClamAV pinned by digest (PostgreSQL and Redis by tag), nocurlin the image; the Helm chart sets the same. - IP leakage detection β The admin dashboard warns when upstream proxies forward IP headers.
- Hard deletion β Reports can be permanently deleted including all messages, attachments, and Redis session data. DSGVO-compliant.
- DSGVO compliant β All resources are self-hosted. No external CDN calls, no tracking.
- Multi-registry Docker β Published to GHCR, Docker Hub, and Quay.io on every release.
- Health-check v2 β
/healthreports database and Redis status; suitable for Kubernetes liveness and readiness probes. - Version & update check β the admin System page shows the installed version and,
when
UPDATE_CHECK_ENABLED=true, whether a newer release is available on GitHub. Opt-in and off by default; a daily background job caches the result and no instance data is sent out. - Voluntary installation count β off unless an admin agrees in the setup wizard or on the
System page: once a day a random identifier and the version go to
telemetry.wdkro.de, nothing else.TELEMETRY_ENABLED=falselocks it off; the demo is never counted. - Structured JSON logging β
LOG_FORMAT=jsonproduces structured log output for aggregation pipelines;LOG_FORMAT=textfor human-readable development output. - Slack / Teams webhooks β
NOTIFY_WEBHOOK_TYPEselects Block Kit (Slack) or Adaptive Card (Teams) payload formats so no custom integration work is needed. - SLA reminders β Background scheduler automatically sends reminders when the 7-day and 3-month HinSchG deadlines approach; Redis dedup keys prevent duplicate notifications.
- S3-compatible storage β Optional
STORAGE_BACKEND=s3routes new attachments to any S3-compatible bucket (AWS, MinIO, Hetzner Object Storage) instead of PostgreSQL BLOBs. - LDAP / Active Directory login β Admin accounts can authenticate via corporate LDAP;
first login auto-provisions the user; TOTP enrollment still required. LDAP uses python-ldap
(OpenLDAP client); installing from source needs
libldap2-dev libsasl2-devandpip install '.[ldap,s3]'. The container image includes both. - Helm chart β Official
charts/openwhistle/Helm chart for Kubernetes deployments. - Ansible role β Official
ansible/roles/openwhistle/Ansible role for bare-metal / VM deployments with Docker CE, systemd unit, and optional Certbot TLS. - Encrypted report storage β All report descriptions and messages are encrypted at-rest using per-report envelope encryption (HKDF-SHA256 MEK + Fernet DEK); the key is never stored in the database; pre-encryption rows are transparently readable (backward compat).
- Separate encryption key with rotation β
ENCRYPTION_KEYis the root of at-rest encryption, independent of the session-signingSECRET_KEY; old keys stay readable viaENCRYPTION_KEY_PREVIOUSwhilescripts/rotate_encryption_key.pyre-encrypts under the new one. - Data retention (GDPR / HinSchG) β on by default (
RETENTION_ENABLED): closed reports are deletedRETENTION_DAYSdays after closure (default 1095 = 3 years); satisfies GDPR Art. 5(1)(e) and HinSchG Β§11 Abs. 5; each deletion recorded in the audit log. - Batched notifications β new reports and whistleblower messages are announced in one
digest every
NOTIFICATION_BATCH_MINUTES(default 1440, once a day), so the notice's timing cannot be matched to who was at their desk, and is no more precise than the stored day. Webhooks (Slack, Teams, generic) carry counts only; the email to your own admins also names the case numbers. - Encrypted attachment names and drafts β filenames are encrypted with the report key; a submission draft in Redis is encrypted with a key held only in the whistleblower's cookie; Office comment and tracked-change authors are anonymised on upload.
- Multi-tenancy β
MULTI_TENANCY_ENABLED=truelets a single deployment serve multiple independent organisations with isolated data, per-tenant categories, locations, and users. - Per-organisation reporting link β with multi-tenancy, each organisation's employees report
at
/submit/<org-slug>, which offers only that organisation's categories and locations and files the report under it. Admins copy the link from their dashboard. - Superadmin role β New
superadminrole aboveadminfor managing organisations in multi-tenant deployments; existing admin permissions are unchanged. - Telephone channel compliance guide β Admin page (
/admin/telephone-channel) provides a HinSchG Β§ 16 Abs. 3 checklist (oral and text form), implementation options, and the Β§ 11 Abs. 2 rule that a call is recorded only with consent. - TLS on by default β the bundled nginx in
docker-compose.prod.ymlserves HTTPS out of the box: copyfullchain.pem/privkey.pemintonginx/certs/(no symlinks; key root-owned 0600 or 0644), or a self-signed certificate forTLS_HOSTNAMEis generated on first start; plain HTTP only redirects. The self-signed certificate is for first boot/testing only β install a real one before real users arrive, since the app's HSTS header pins a browser that clicked through the warning. To renew, drop the new certificate intonginx/certs/and rundocker compose up -d tls-init nginx(tls-initis one-shot; restartingnginxalone keeps the old certificate). Behind an external TLS terminator,docker-compose.behind-proxy.ymlmakes nginx proxy plain HTTP on port 80. - Tor onion address β
ONION_LOCATIONadds anOnion-Locationheader (Tor Browser offers to switch) and a note on the submit page for reporters on a monitored network; see the documentation "Offering an onion address".
A live demo is available at demo.openwhistle.net
| Role | Username | Password | TOTP Code |
|---|---|---|---|
| Admin | demo |
demo |
000000 |
Demo case numbers and PINs are shown after logging in to the demo admin account. The demo resets automatically every 6 hours.
git clone https://github.com/openwhistle/OpenWhistle.git
cd OpenWhistle
cp .env.example .env # SECRET_KEY, ENCRYPTION_KEY: openssl rand -hex 32 each (fresh install)
docker compose up -d
docker compose logs app | grep "Setup token" # read the one-time setup token
# Open http://localhost:4009/setup to create the first admin accountAccessing from other devices on the same network? Add
SECURE_COOKIES=falseto your.env(ordocker-compose.yml) when the app is served over plain HTTP. Browsers refuse to sendSecurecookies over HTTP, causing session failures on remote devices. Always keepSECURE_COOKIES=true(the default) behind HTTPS in production.
For full installation instructions, environment variable reference, reverse proxy configuration, and administration guide, see openwhistle.net/en/docs/.
Pre-built multi-arch images (linux/amd64, linux/arm64) are published to three registries:
| Registry | Image |
|---|---|
| GitHub Container Registry | ghcr.io/openwhistle/openwhistle |
| Docker Hub | kermit1337/openwhistle |
| Quay.io | quay.io/jp1337/openwhistle |
All GHCR images are signed with Cosign (keyless, Sigstore).
OpenWhistle is designed to comply with:
Disclaimer: OpenWhistle is a technical tool. Operators are responsible for ensuring their deployment meets all applicable legal requirements in their jurisdiction.
Contributions are welcome. Please open an issue before submitting a pull request, and read CONTRIBUTING.md first.
OpenWhistle is released under the GNU General Public License v3.0.
OpenWhistle is developed in free time. If you find it useful, consider supporting the project.