Skip to content

Security

zach115th edited this page Aug 11, 2026 · 1 revision

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.


Before you expose an instance

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.


Secure by default

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 always keyword on those directives is load-bearing. Without it nginx applies add_header only to 2xx/3xx responses, so error pages ship bare. That was a real defect — fixed in IRIS-NG-v1.2.0.


Security-relevant configuration

All of these live in .env and are empty or safe by default. Change them only for a specific reason.

Cross-origin access (IRIS_CORS_ALLOWED_ORIGINS)

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.com

Scheme 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.

Analytics (IRIS_ANALYTICS_SCRIPT_URL, IRIS_ANALYTICS_SITE_ID, ANALYTICS_ORIGIN)

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.

TLS (CERT_DIR, CERT_FILENAME, KEY_FILENAME)

See TLS Certificates.


Known trade-offs

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.


Security review history

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.1 and everything before it predate all of the above, so no image published under those tags contains any of these fixes.

Scope of the review

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.


Reporting a vulnerability

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.

Clone this wiki locally