-
-
Notifications
You must be signed in to change notification settings - Fork 0
Security
What is hardened by default, what you must change before exposing an instance, and the security-relevant configuration. For the developer-facing patterns (CSRF placement, output escaping, injection sinks) see Development Guide.
IRIS-NG holds incident data — indicators, evidence custody, client names. Treat an internet-reachable instance as something to plan for, not something to arrive at by accident.
| Do this | Why |
|---|---|
Change the database credentials in .env
|
POSTGRES_PASSWORD and POSTGRES_ADMIN_PASSWORD ship as literal __MUST_BE_CHANGED__ placeholders |
| Use a real TLS certificate | The default is self-signed. See TLS Certificates |
Set SERVER_NAME to the real hostname |
A mismatch warns in the browser even with a valid certificate |
| Rotate the initial admin password |
IRIS_ADM_PASSWORD is commented out by default, so the password is generated and printed to the container log on first boot — and container logs are rarely treated as secret. Setting it in .env instead just moves the secret into a file |
| Enable MFA | Per user, plus an instance-wide enforce_mfa toggle on Server Settings |
| Review case access grants | Default is deny_all — new users see nothing until granted, so grants are the thing that accumulates |
| Restrict who can reach port 443 | A VPN or allowlist in front of the instance is worth more than any single setting below |
IRIS_SECRET_KEY is not on that list, because it now handles itself — see below.
You do not need to configure these. They are listed so you can verify them, and so you do not weaken one by accident.
The Flask SECRET_KEY self-heals. It signs the session cookie, and a known key means
forgeable sessions. If the shipped placeholder is still in place, a unique key is generated
on first boot and persisted in the runtime_secret table, so every replica agrees on it
and it survives restarts. A key you set explicitly always wins and is never touched. The
swap is logged at WARNING, and existing sessions are invalidated once when it happens.
Session cookies are Secure, HttpOnly, SameSite=Lax.
CORS is off. No cross-origin headers are sent at all, and the websocket accepts same-origin connections only.
No outbound telemetry. Analytics are opt-in and empty by default. The one component
that contacts the internet on its own is the Sponsor tab on Server Settings, which reads
the project's FUNDING.yml from GitHub when you open that tab.
Passwords use bcrypt, which generates a per-password salt. .iris-case exports use
AES-256-GCM with PBKDF2-HMAC-SHA256 at 600,000 iterations, and the iteration count carried
in the file is range-checked on import so a crafted file cannot force either a weak key or
a CPU exhaustion.
Security headers are sent on every response, including error responses:
Content-Security-Policy (see below)
X-Frame-Options DENY
X-Content-Type-Options nosniff
X-XSS-Protection 1; mode=block
Strict-Transport-Security max-age=31536000
The
alwayskeyword on those directives is load-bearing. Without it nginx appliesadd_headeronly to 2xx/3xx responses, so error pages ship bare. That was a real defect — fixed inIRIS-NG-v1.2.0.
All of these live in .env and are empty or safe by default. Change them only for a
specific reason.
Empty by default: no CORS headers, websocket same-origin only.
Most deployments never need this. CORS is a browser policy. The IRIS-NG UI calls
same-origin /api/v2/ paths, and API clients, n8n workflows and LLM backends are all
server-side HTTP, where CORS does not apply at all. If an integration is not a browser,
CORS is not what is blocking it.
Set it only when a browser application served from another origin must call this instance's API:
IRIS_CORS_ALLOWED_ORIGINS=https://soc.example.com,https://tools.example.comScheme and host must match exactly. A matching origin is echoed back verbatim; the wildcard
* is never sent, because a browser refuses to attach credentials to a wildcard response —
which is why the wildcard this replaced never worked for authenticated calls in the first
place.
All three empty by default: nothing is loaded and nothing is contacted. The tag shape is Plausible/Umami compatible.
All three must agree on the host. The first two render the <script> tag; the third is
what nginx allowlists in the Content-Security-Policy. Set the script but not the origin and
your own policy blocks your own analytics.
Worth thinking about for a DFIR deployment. Most analytics report the page path. IRIS-NG case ids travel as query parameters (
?cid=42), which are usually not sent — so paths tend to leak which features are used rather than which cases. Verify that for your vendor rather than assuming it.
See TLS Certificates.
The CSP allows unsafe-inline and unsafe-eval.
default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline'; img-src 'self' data:; worker-src blob:;
unsafe-inline is required because the application renders a great deal of inline
JavaScript from Jinja templates. unsafe-eval is required by vis.js, which the case
Graph tab uses for layout — without it that tab is blank.
The practical consequence: the CSP will not save you from an XSS bug, so output
escaping has to be right on its own. It still constrains where scripts, images and styles
may be loaded from, which is worth having. Removing unsafe-inline would mean moving the
inline JavaScript out of the templates — a large change, not currently planned.
worker-src blob: is needed by the ACE editor, which spawns its syntax-checking worker
from a blob URL.
The tree had never had a security pass before August 2026. It has had one since, plus an external scan.
| When | What | Released in |
|---|---|---|
| 2026-08-05 | First internal review — 10 findings, all fixed | IRIS-NG-v1.2.0 |
| 2026-08-10 | External scanner — 3 further defects, all inherited from upstream | IRIS-NG-v1.2.0 |
The two that mattered most, both from the internal review:
- Stored XSS in the correlation drawer. Analyst note Markdown was rendered into the DOM raw. The renderer follows CommonMark, which passes HTML through verbatim, so note content became script — and the audience for the cross-case correlation dashboard is typically someone with broader case access than the note's author.
-
A publicly known default
IRIS_SECRET_KEY, shipped in every deployment path and therefore identical across every install that never changed it.
From the external scan: a CORS wildcard sent alongside Allow-Credentials; security
headers missing always, so error pages carried none of them; and a login CSRF guard whose
inverted condition meant the token was never validated.
Upgrade rather than rebuilding from an older tag.
IRIS-NG-v1.1.1and everything before it predate all of the above, so no image published under those tags contains any of these fixes.
It was driven by trust boundaries — authentication, authorization, deserialization, injection sinks, secrets and deployment posture — not a line-by-line audit of every file. Areas explicitly not covered include the working-timeline parsers (which read attacker-supplied forensic files) and alert ingestion. Absence of findings there means absence of review, not absence of bugs.
Please do not open a public issue for a security vulnerability. A public report is a public disclosure, and this project is deployed by people who cannot patch on your timetable.
Use GitHub's private vulnerability reporting on the repository if it is available to you; otherwise contact the maintainer directly and ask for a private channel before sending details. Fixed issues are described in the Changelog once a release carrying the fix exists.
If you are reporting something you found by scanning a deployed instance, please include whether the scan was authenticated — it changes the interpretation substantially, and unauthenticated scanners routinely report one finding many times, once per URL.