Skip to content

Troubleshooting

zach115th edited this page Sep 16, 2026 · 11 revisions

Troubleshooting

Symptom first. Each entry states what the message actually means, because several of the most common ones name the wrong thing.

Two scoped guides live elsewhere: post-migration login failure and nginx cannot read the certificate key.

The first move, for almost everything

docker logs --tail 40 iriswebapp_app

A surprising share of "the UI is broken" reports are a Python traceback that the browser could only render as a generic failure. Read the server log before touching client code — the browser-side message is usually a symptom of an HTML error page arriving where JSON was expected, and it names nothing useful.


Upgrading

Boot fails with Can't locate revision <hex>

You are running 1.4.x code against a database that version 2 has already migrated. The v2 migrations are one-way — the choices are completing the upgrade, or restoring the pre-upgrade database backup. Full procedure and rollback commands: Upgrading to Version 2.

git pull fails with Need to specify how to reconcile divergent branches

main is published as a sanitized snapshot whose history can be replaced, so a clone's history diverges from it. Update with:

git fetch --all
git reset --hard origin/main

This discards local edits to tracked files; .env, certificates and the database live outside the tracked tree and are untouched.

After an upgrade, background tasks fail but the app looks fine

Typical shape: a module hook or a case-create task fails with a SQLAlchemy NotImplementedError somewhere inside _indexes_for_keys, before any module code runs. Pages load, the API answers, only background work breaks.

Cause: worker and ai_worker are still running the previous image while app was recreated. Each worker caches its Python imports independently, so a stale worker holds an ORM mapper state that no longer matches the new app.

Fix — --build alone is not enough:

docker compose -f docker-compose.dev.yml up -d --build --force-recreate

Confirm by comparing container IDs before and after, not by reading logs. A container that was not recreated will happily log nothing unusual.

Three-way discriminator when a hook task fails:

Message Cause
NotImplementedError in _indexes_for_keys, before the module runs app/worker code skew — recreate
PGRES_TUPLES_OK and no message from the libpq on a commit, under load Celery fork-safety — fixed since IRIS-NG-v1.0.0
PendingRollbackError at task_hook_wrapper's db.session.commit() A module hit a database error and did not roll back — see below
The app's own list pages also return 500 Schema skew, not a worker problem

PendingRollbackError, with the original error quoted inside it

The traceback names task_hook_wrapper, so it reads like a core failure. It is not. A module hit a database error, caught it, and left the shared session in a failed-flush state; core then commits that same session immediately after the hook returns, and that is the commit you see in the traceback.

The real error is the one quoted as "Original exception was:" further down. Read that, not the top frame.

Consequence to be aware of when triaging: because the session is shared, a single object that could not sync fails the whole hook task, not just itself.

Fixed for the bundled MISP modules in IRIS-NG-v1.3.0, which rolls back per item. (The fix was written for IRIS-NG-v1.2.3, but that version was never tagged and no image was ever built for it, so IRIS-NG-v1.3.0 is the first release that carries it.) If you see this from a third-party or custom module, that module needs a db.session.rollback() on its failure path.

ImagePullBackOff after installing the Helm chart from a clone

The chart derives its image tags from appVersion. If you cloned main at a moment when its declared version had not been tagged, Kubernetes is asking for images that were never built — the version exists in the files and nowhere else.

Fix: install from a release asset rather than a working tree.

helm install iris-ng https://github.com/zach115th/iris-ng/releases/download/<TAG>/iris-web-<ver>.tgz

Every published release attaches a chart whose image tags match its own images. See Kubernetes.


Browser-side messages that mean something else

Unexpected token '<', "<!doctype"... is not valid JSON

This is never a JavaScript parsing bug. The server returned an HTML error page and the client tried to parse it as JSON. Something threw on the server.

docker logs --tail 30 iriswebapp_app

DataTables warning: table id=... - Ajax error

Same class of problem — the endpoint behind the table returned 500. The warning names the table, which is never where the fault is. Check the log.

On /manage/modules specifically, this has one recurring cause: a module configuration entry missing required keys, which raises KeyError while listing modules. Hand-editing iris_module.module_config in the database is the usual way to produce one — an entry needs every field (param_name, param_human_name, type, default, mandatory, section), not just a name and a value.

Every task in the DIM Tasks list shows a red ✗

Fixed in IRIS-NG-v1.3.0. (The fix was written for IRIS-NG-v1.2.3, which was never tagged and produced no image, so v1.3.0 is the first release carrying it.) Before that, the list showed the failure icon on every task, successful ones included — the state it compared against was a raw result blob rather than the task's status, so it never matched. The list was not reporting a problem; it could not report anything else.

Trust the task modal, not the list icon, on any version. Click the task: Task state is celery's view, and Success is what the module itself reported.

Those two are genuinely different, which is worth knowing even after the fix. The list icon reflects the task, not the module inside it — a task can complete normally while the module it ran reports a failure, so a green check plus Success: Failure in the modal is a coherent combination, not a contradiction. The module's verdict is only available in the modal.

A list page is blank and permanently shows "Updates available"

A JavaScript exception during rendering aborted before the code that clears the banner ran, so the banner is stuck as a side effect rather than a real state mismatch. Open the browser console: the first error is the actual fault. Typically a renderer assuming a nested object exists on a row where it is NULL.

400 Invalid CSRF token on a POST that looks correct

The validator reads the token as a form or JSON body field and never looks at the X-CSRFToken header. Sending only the header fails. Details and the correct pattern for both JSON and multipart bodies: Development Guide → CSRF.


MISP

Every push fails with SSLCertVerificationError: self-signed certificate

Reaching this error is good news: it means the request got as far as the MISP API call, so configuration and connectivity are correct and TLS trust is the only blocker.

Options, least to most secure:

  1. Uncheck Verify TLS in the module configuration under /manage/modules. Read per-call, so no restart is needed. Skips MITM protection — reasonable on an internal lab MISP, not on anything crossing a network you do not control.
  2. Add the MISP CA to the app and worker container trust stores.

A tag never appears on a synced attribute

MISP's GET /tags/search/<term> returns an empty list for any tag name containing a colon — which is every taxonomy tag, tlp:green included. Use POST /tags/index with searchall. The bundled client already does; this matters if you are writing your own.

The MISP event has fewer indicators than the cluster card showed

Expected, and reported rather than silent. The correlation dashboard displays indicators at every TLP, while the push publishes TLP:GREEN and TLP:CLEAR only. Check tlp_withheld_count on the push response. Full rule: IOC Correlation → TLP handling.

IOC sync fails with duplicate key value violates unique constraint "misp_attribute_link_misp_attribute_id_key"

Fixed in IRIS-NG-v1.3.0 (Alembic d1a7c93f5e64; written for the never-tagged IRIS-NG-v1.2.3, so v1.3.0 is the first release that carries it) — upgrade, and the migration applies itself on the next start.

The cause is worth knowing because it explains which of your IOCs are affected. The link table treated a MISP attribute as belonging to exactly one IRIS IOC. MISP deduplicates attributes within an event by (type, value, category), so it hands back the same attribute for two IOCs that share a value and type in one case — and for an IOC you deleted and recreated, since that mints a new internal id while MISP still holds the original attribute.

To see which case it is, on the affected instance:

-- who already owns the attribute named in the error?
SELECT l.ioc_id, l.date_created, i.ioc_value, i.ioc_type_id, i.case_id
FROM misp_attribute_link l JOIN ioc i ON i.ioc_id = l.ioc_id
WHERE l.misp_attribute_id = <the id from the error>;

-- every duplicate waiting to hit the same thing
SELECT case_id, ioc_value, ioc_type_id, count(*), array_agg(ioc_id)
FROM ioc GROUP BY 1,2,3 HAVING count(*) > 1;

Nothing needs cleaning up by hand: after upgrading, several IRIS IOCs may legitimately point at one MISP attribute, which is what MISP models.


Authentication

A user sees "ACCESS DENIED … case #1" right after logging in, and the switcher fails with a CSRF error

Fixed in IRIS-NG-v2.1.0; on earlier versions the workaround was to grant every user access to the lowest-numbered case. Two inherited defects stacked: the session's starting case was the lowest case id in the database, chosen with no access check, so anyone without that case landed on the access-denied page on their first case-scoped click (the sidebar Case link, a bare /case); and the case switcher on that page — the only way out — posted no CSRF token because the page renders no form, so every switch returned 400 Invalid CSRF token. Since 2.1.0 the starting case is the lowest one the user can actually open (a user with no case access lands on Home) and the switcher carries its own token on every page. If you granted everyone case #1 as a workaround, you can revoke it after upgrading.

There is no MFA enrolment QR code on "My settings"

There is not meant to be one. The profile page shows a buttonSet up MFA, or Reset MFA once you have enrolled — and the QR code is on the page it links to.

Two reasons the button itself can appear to be missing:

  • Enforcement is off. Every MFA control is hidden until Enforce MFA for all users is saved under Advanced → Server Settings → Security. Before that the interface genuinely looks as though it has no MFA support.
  • It is below the fold. Up to and including IRIS-NG-v1.4.0, the account actions sit after the whole cyber-security skills catalogue — roughly 34 checkboxes further down the page — next to Change password. Scroll to the very bottom. Fixed in IRIS-NG-v1.4.1.

The simpler route is to log out and back in: with enforcement on, login takes you straight into enrolment. Have your authenticator and your password ready — confirming enrolment needs a current code and the password.

The MFA button is greyed out

The account is exempt and is never prompted for a second factor, so there is nothing to set up or reset. The tooltip says which case applies: the built-in administrator (user #1), or a service account — the latter cannot log in interactively at all and authenticates by API key, which has no MFA step. Exempt accounts also cannot reach /auth/mfa-setup directly. See Multi-Factor Authentication.

Enforcing MFA locked everyone out

There are no recovery codes. Disable enforcement directly and restart — the restart is required, because server settings are cached in the process and are not re-read:

docker exec iriswebapp_db psql -U postgres -d iris_db \
  -c "UPDATE server_settings SET enforce_mfa = false;"
docker restart iriswebapp_app

Since IRIS-NG-v1.4.1 the built-in administrator is exempt, so it can still log in and reset another user's MFA from Access Control → Users without touching the database. On IRIS-NG-v1.4.0 and earlier that account is subject to enforcement like any other, and the database route above is the only way out.

Changing IRIS_MFA_ENABLED in .env does nothing

Correct behaviour. That variable only seeds the setting when the settings row is first created, and is never read again. On an existing instance the checkbox on Server Settings owns the value. The MFA enabled/MFA disabled line in the startup log reflects the environment variable, not the current database value.

SSO users are never asked for MFA

Expected — OIDC logins are exempt by design. Enforce a second factor at your identity provider instead. Enforcement still applies to local and LDAP logins, so an instance using OIDC with local fallback gets MFA on the fallback path only. See Single Sign-On.


AI surfaces

"No AI backend configured"

Expected when none is set. Every other feature works normally — the AI layer is optional. Configure under Settings → AI.

A summary or chat request never completes

AI summary and chat are queued rather than run inline: the POST returns 202 with a task_id and the client polls. If jobs stay queued forever, the ai_worker container is not running or is not consuming ai_queue.

docker ps --filter name=iriswebapp_ai_worker
docker logs --tail 40 iriswebapp_ai_worker

An AI panel shows an authentication error, but the backend is fine

Typical shape: a panel reads "Failed to authenticate. API Error: 401 …" or "OAuth session expired", while the configured backend answers normally and nothing is failing right now.

Read the panel footer first. If it says cached, you are looking at a stored record of a past failure, not a live one. When a generation fails, the error text can be saved as the panel's content and then served indefinitely — long after the outage that caused it ended. The timestamp in the footer tells you when it actually happened.

Fix: click Re-run on the panel. That regenerates from current data and replaces the stored error.

Why it matters beyond the panel: a cluster narrative is also published outward — it becomes the campaign name and description in the STIX export and in a MISP cluster push. A cached error there is not a cosmetic problem; re-run the narrative before exporting or pushing a cluster whose panel has ever shown an error.

If it is genuinely failing now, the error repeats immediately after a Re-run. Then check the backend under Settings → AI, and — if you use the Claude proxy sidecar — that its OAuth credentials are still valid on the host.

The panel header shows an old prompt version after an upgrade

Not a failure. The version is stamped when the output was generated, and the panel renders the newest cached artifact. It changes on the next regeneration, not on upgrade. Use Re-run if you want it now.

An AI job fails with Unknown AI feature '<name>' after an upgrade

A worker started before that feature existed is answering from a stale in-memory registry. Both worker and ai_worker keep their own copy, so restart both, not just the one you would expect to run AI jobs:

docker compose -f docker-compose.dev.yml restart worker ai_worker

An AI pass reports "AI backend returned no JSON object" and the reply was empty

Seen with small reasoning models on a large input: the model spends its whole completion budget thinking and emits nothing, so the reply is empty at finish_reason: length. The app logs the head of the reply, which for this case is ''. The ICS forms pass already retries once on a trimmed payload; if it still comes back empty, use a larger model for that surface (Settings → AI → per-feature overrides) or a backend with a higher completion limit. Nothing is written to the forms on a failed call.


Database

After migrating from vanilla DFIR-IRIS, no account can log in

Covered in detail at Getting Started. Short version: the secrets in .env did not match the ones the password hashes were made with. Applies when importing with a bundle produced by an older copy of the migration script.

The database is empty after a restart

Check whether the Postgres volume is still attached, and — on Kubernetes — whether the chart is using a node-local hostPath volume. A hostPath volume does not follow a pod to another node, so a rescheduled database comes up silently empty rather than failing. The shipped chart defaults to a normal PersistentVolumeClaim; hostPath is opt-in.


Still stuck

Open an issue with the output of docker logs --tail 60 iriswebapp_app, your release version from Settings → System, and whether the instance was upgraded or installed fresh. For anything security-relevant, follow Security rather than filing publicly.

Clone this wiki locally