Skip to content

Guest Portal

zach115th edited this page Sep 30, 2026 · 4 revisions

Guest Portal

Since IRIS-NG-v2.4.0. Upgrading to it applies four one-way migrations — back the database up first — and rebuilds the nginx image. The portal itself stays off until the tunnel agent's key is set (see How the portal is exposed below); an instance that never sets it is unchanged.

War rooms can admit guests: people outside your IRIS-NG instance — the affected organisation's incident lead, a partner agency's liaison — who take part in one room and nothing else. A guest is not a user account. They never see the login page, hold no permissions, have no case access of their own, and cannot reach any page or API outside the room they were invited to. Inside the room they take part at the same level as a responder, and they see the cases the room's leads have attached, read-only. They enter through the guest portal, a stripped-down view of the room, reached through a Cloudflare tunnel that exposes only that portal.

What a guest is

Member (IRIS-NG user) Guest
Account user row, roles, permissions, case ACL none: a row in the room's guest list
Signs in at /login (password, MFA, SSO) the portal, with email + password
Sees the whole product per their permissions one war room, in the portal layout
Case data per case ACL every case attached to the room, read-only, through the room only
Room role lead / responder / observer responder level, never lead
Ends account deactivation expiry (14 days by default), revoke, or the room closing

A guest does what a responder does with the room's own content: read and post in the stream (threads, pins, decisions and notes included), create polls and close their own, vote, create, update and delete room tasks, create, edit and delete room notes and folders, draft and edit SitReps (with preview and the revision trail), add room timelines and their events, create and edit teams, seed the ICS forms and export them as PDF, and read the operational summary and the member list.

A guest also sees every case attached to the room, read-only: the Cases tab lists each with its name, state, open date and attachment note (never the customer, the owner or task counts, and no link into the case pages); the Timelines tab shows the cases' events; the Tasks tab lists the case tasks; the Notes tab lists the case notes and opens them; the stream carries the case activity lane. Attaching a case to a room with guests is the sharing decision — there is no per-case switch, so a lead attaches a case only when its content may reach every guest in the room.

A guest cannot publish or delete a SitRep, delete a timeline, manage members or guests, attach, detach or annotate cases, open a case peek, change the room's status, name or settings, use correlation, STIX or MISP, or run any AI feature (SitRep draft, ICS draft, operational summary); the Correlation tab is not shown. A guest who seeds the ICS forms gets the plain forms, and the AI pass waits for an analyst. The server enforces all of this; the portal only hides what would be refused.

Guest-authored content — messages, polls, SitReps and their revisions, tasks, notes, timelines and events, teams — is attributed as Name (Organisation), with a guest badge in the stream, and stays in the room after the guest is removed.

Inviting a guest

On the room's Members tab, + Add member → Guest:

  • Name, organisation and email, how many days the invitation is valid (1–90, default 14), and an optional password. Leave the password blank for a random 16-character one (letters and digits); a password you type must satisfy the instance's own password policy from Server settings.
  • Invite creates the guest and shows the invitation link and the password once, each with a Copy button, plus the room's address. When SMTP is configured on Server settings the same information is emailed to the guest; when it is not, relay it yourself. The link and the password are never displayed again — New link and Reset password on the Members tab issue replacements.

The Members tab lists guests under Guests with their status (active, expired, revoked, closed), expiry, last sign-in and a locked chip after too many failed sign-ins. Leads have per-guest actions: +14d extends the expiry, New link issues a fresh invitation link (the old one stops working, the password stays), Reset password issues a new random password (emailed and shown once; the link stays), Revoke ends access on the guest's next request, Remove deletes the guest and the invitation. Closing the room ends every guest's access; reopening it restores them.

Signing in as a guest

Email and password are the credential. The invitation link opens the sign-in form with the email filled in and the room fixed; a forwarded link admits nobody without the password. A guest who lost the email can open the room's address directly (see Room addresses below) and sign in with email and password there. Every failure shows one generic message; ten failures lock the guest for fifteen minutes, which a password reset clears. Expired, revoked and closed invitations show a plain explanation, never a login redirect. Leave in the portal banner ends the session; the session also ends when the browser closes.

Opening an invitation in a browser that is signed in to IRIS-NG as a user signs that user out first: a guest session never sits under a user session.

Room addresses

Every room has a slug, derived from its name when the room is created and editable by a lead in the room's Edit dialog (lowercase letters, digits and hyphens; blank derives it again from the name). On the portal hostname the room lives at https://<portal hostname>/<slug>, and an invitation is https://<portal hostname>/join/<secret>. On the instance's own address the same pages are /portal/r/<slug> and /portal/join/<secret>. The Members tab shows the room's current address.

How the portal is exposed

The feature is designed for a Cloudflare tunnel, so that nothing but the portal ever becomes reachable from outside:

  • nginx serves a second, portal-only server block on PORTAL_PORT (default 8081) on the Docker network only. It proxies the portal pages, the war-rooms API the portal page uses, and static assets. Everything else, including /login, answers 404. Sign-in and invitation paths are rate-limited.
  • Requests through that block carry a marker the app recognises: a user session or an API key arriving through the portal is refused, so the tunnel cannot be used to reach IRIS-NG as an analyst even with valid credentials. The main server block strips the marker, so it cannot be forged from the normal origin.
  • A tunnel agent container (docker-compose.portal.yml, built from docker/tunnel/) runs cloudflared pointed at that port, asks the instance every fifteen seconds which tunnel mode to run, and reports its status back. It authenticates with a shared key, PORTAL_TUNNEL_AGENT_KEY in .env (generate one with openssl rand -hex 32); without the key the agent endpoints are off and the Settings page says so.
# .env: PORTAL_PORT=8081 and PORTAL_TUNNEL_AGENT_KEY=<random hex>
docker compose -f docker-compose.dev.yml -f docker-compose.portal.yml up -d --build tunnel

Or let the update script do it: bash scripts/update.sh --enable-portal generates the key into .env, recreates the app so it reads it, and starts the agent — also on an install that is already up to date. Once the key is in .env, every later run of the script keeps the agent rebuilt alongside the rest (see Getting Started).

The Helm chart does not carry this: it has no nginx pod, so the portal server block and the tunnel overlay are Docker Compose features.

Settings → Guest Portal

A page on the Manage IRIS rail (server administrators):

  • Tunnel mode. Quick tunnel (testing only): an anonymous trycloudflare.com tunnel with no account. Its hostname is random and changes whenever the agent restarts, so invitation links stop working after a restart, and nothing can be put in front of it. Named tunnel: a tunnel from your own Cloudflare account, with a fixed hostname that can sit behind Cloudflare Access. The agent switches within about fifteen seconds of a save.
  • Public URL. The base address guests use. Required for a named tunnel; leave it empty for a quick tunnel and the agent's reported hostname is used. Invitation links and room addresses are built from it, in this order: this field, then a connected agent's report, then the instance's own address.
  • Tunnel token. Write-only. Blank keeps the stored token; typing clear removes it. A named tunnel cannot be saved without one.
  • Tunnel status. The agent's last report: reporting or silent, the mode it runs, whether the tunnel is connected, the hostname, the cloudflared version and the last error.

Setting up a named tunnel

  1. In Cloudflare Zero Trust: Networks → Tunnels → Create a tunnel → Cloudflared, name it, and copy the token from the install command (the long string after --token).
  2. On the tunnel's Public Hostname tab add your hostname, for example rooms.example.com, with service HTTP → nginx:8081 (the portal server block).
  3. Paste the token and https://rooms.example.com into Settings → Guest Portal, select Named tunnel, save. The status card confirms the connection.
  4. Recommended: Access → Applications, a self-hosted application on that hostname with a one-time-PIN policy for the domains you invite. Guests then confirm their email with Cloudflare before the portal even loads. IRIS-NG does not yet verify the Access token itself; that binding is planned.

Switching modes changes the hostname, so existing invitation links stop working; use New link on each room afterwards.

What to know before relying on it

  • Invitation secrets and passwords are stored hashed; both are shown exactly once.
  • The tunnel exposes the portal to the internet. Use a named tunnel with Cloudflare Access for anything beyond a test, keep the shared agent key out of version control (.env is ignored), and treat the quick tunnel as a demo.
  • A guest's messages reach every room member through the normal notification events; a guest receives no notifications, only the invitation and password emails.
  • Granular per-guest permissions and a per-case "share with guests" switch are not part of this version; AI features are never offered to guests.
  • The tunnel does not watch the rooms: with no room, every room closed or every room deleted it stays up and the hostname keeps answering (room addresses 404 or 410, the sign-in paths stay reachable, rate limited). Stop the tunnel container, or switch to a named tunnel behind Cloudflare Access, when nothing is open. An explicit off mode is under consideration.

Troubleshooting

The invitation link uses the instance's own address. No public URL is set and no connected tunnel report exists. Either start the tunnel agent (its hostname is used automatically in quick mode) or set the public URL on Settings → Guest Portal.

The status card says the agent is silent. The tunnel container is not running, cannot reach the app, or has a different PORTAL_TUNNEL_AGENT_KEY than the app. Check docker logs iriswebapp_tunnel; the app reads the key from .env at container start, so a changed key needs app recreated as well.

Named tunnel: not connected, "Provided Tunnel token is not valid." The token was copied incompletely or belongs to a deleted tunnel. Paste it again; the agent retries every fifteen seconds.

A guest gets 503 on the invitation link. The join and sign-in paths are rate-limited (ten per minute per client). Wait a minute.

A guest sees "Sign-in failed" with the right password. Ten failures lock the guest for fifteen minutes; the Members tab shows locked. A password reset clears it immediately.

A guest can see a case they should not. The case is attached to the room: attaching shares a case with every guest in that room. Detach it, or move the guests to a room that only carries the cases they may see.

Clone this wiki locally