# Security How the help desk dashboard protects MCCS ServiceDesk Plus data: who can reach it, who can sign in, what each person can see, and what the server keeps. This describes version 2.1 (October 2026). ## Summary - The site only answers MCCS network addresses. - Every person signs in with their own Zoho account, and only accounts at MCCS email domains are let in. - Every ServiceDesk Plus (SDP) query runs as the signed-in person, with read-only access. The dashboard can never show anyone more than SDP already lets them see. - Zoho tokens never reach the web page and are never stored on the server. - The server keeps no ticket data. The only things it stores are each person's dashboard preferences and a list of site, group and technician names. ## Layers of protection A request has to pass every layer below, in order, before any MCCS data is involved. ### 1. Network: MCCS addresses only - The dashboard runs on a VPS behind Nginx Proxy Manager (NPM). An NPM access list allows MCCS' public IP blocks (campuses and VPN) and refuses everything else with 403 Forbidden. This was tested live on 2026-10-15: an MCCS address got in, and a phone on mobile data was refused. - The application's container **publishes no ports**. Docker's port publishing bypasses the host firewall, so a published port could skip NPM's access list. Instead, NPM reaches the container over a private Docker network. - HTTPS is enforced by NPM ("Force SSL") with a valid certificate. ### 2. Sign-in: personal Zoho accounts at MCCS domains - There is no shared account or service token. Every person signs in through Zoho's standard web sign-in (OAuth 2.0 authorization code flow). Their Zoho password and MFA are handled entirely by Zoho; the dashboard never sees them. - **Domain restriction.** Any Zoho account holder in the world can complete Zoho's consent screen, so the dashboard checks the email address Zoho reports. A session is only created for emails at the domains in `ALLOWED_EMAIL_DOMAINS` (exact match, so lookalikes such as `evilmainecc.edu` or `mainecc.edu.example.com` are refused). Refused sign-ins have their Zoho token revoked immediately. The check is repeated on **every** request, not just at sign-in, so removing a domain cuts off existing sessions too. The server refuses to start if the setting is missing. - **Sign-in forgery protection.** Each sign-in carries a one-time random `state` value tied to the browser that started it, compared in constant time. A sign-in started elsewhere, or older than 10 minutes, is rejected. - **Sign-out** deletes the session and revokes the Zoho refresh token at Zoho, so it can't be reused. ### 3. Sessions: encrypted, browser-held, time-limited - The session (the person's Zoho tokens, name and email) lives only in a browser cookie, encrypted and authenticated with AES-256-GCM using a server-side secret (`SESSION_SECRET`). A cookie that has been altered, truncated or made with a different secret is rejected. - The cookie is `HttpOnly` (page scripts, including any injected ones, can't read it), `Secure` (sent over HTTPS only) and `SameSite=Lax` (not sent with cross-site form posts or background requests). - Each sign-in expires after `SESSION_DAYS` (7 by default) no matter how active it is. Zoho access tokens last about an hour and are renewed automatically. If Zoho or SDP stops accepting a token (for example, the account was disabled or the access revoked), the person is signed out on their next request. - The server keeps no session list, so there is no session database to steal. ### 4. Data access: read-only, as the signed-in person - The only SDP permission the dashboard asks for is `SDPOnDemand.requests.READ`, plus Zoho's basic profile read (for the name and email). It can't create, change or delete anything in SDP. - Every SDP query uses the signed-in person's own token. **SDP's own permissions decide what comes back.** The region and technician filters in the dashboard only narrow that down; they can never widen it. A person who changes their settings to another site sees only what SDP would already show them in ServiceDesk Plus itself. - The site, group and technician name suggestions in Settings are shared between users. Before returning them, the server makes the caller's own token read from SDP. If SDP refuses that person, they get no names. - API responses are marked `Cache-Control: no-store`, so browsers and proxies don't keep copies of ticket data. ### 5. The web page - The page never receives Zoho tokens. It only receives ticket summaries (number, technician, status, site, group and times) and counts. - **No ticket subjects or descriptions.** Subjects can name students or describe their situation, which may be covered by FERPA. The dashboard never asks SDP for the subject or any other free-text field, and if SDP sends one anyway, the server drops it before replying, so subjects never reach the page or the logs. (Removed on 2026-10-16; v2.1.0 and earlier showed subjects in the closed feed.) - Everything shown on the page that came from SDP or from saved settings is HTML-escaped, so a technician, site or group name can't inject script into the page. - Response headers block other sites from framing the dashboard (`X-Frame-Options: DENY`, against clickjacking), stop browsers guessing file types (`X-Content-Type-Options: nosniff`), and keep the dashboard's address out of requests to other sites (`Referrer-Policy: same-origin`). - Changing settings only accepts JSON requests, which a malicious site can't send cross-site with the person's cookie. ### 6. What the server stores | Stored | Where | Contents | | --- | --- | --- | | Saved settings | `settings.json` in the data volume | For each person who saved settings: their email, the sites, groups and technicians they chose, refresh times, weather location, hidden panels. No tokens, no ticket data. | | Name suggestions | `names.json` in the data volume | Site, group and technician names seen on recent tickets. No ticket data. | | Server log | Docker's container log | Sign-ins and sign-outs (email and IP address), refused sign-ins, and SDP error messages. No tokens, no ticket contents. | Ticket data passes through the server's memory while a request is answered and is then discarded. It is never written to disk. Every value a person saves is checked by the server: names must be plain text of limited length, lists have size limits, refresh intervals have minimums (so a misconfigured dashboard can't hammer SDP), and weather locations must come from a fixed list of campuses. ### 7. Secrets - The Zoho client secret, `SESSION_SECRET` and the OpenWeatherMap key live only in the `.env` file on the server. `.env` is excluded from the Git repository (`.gitignore`) and from the Docker image (`.dockerignore`), so the published image contains no secrets. - Docker Hub credentials for the build pipeline are stored as Gitea repository secrets. ### 8. Container and hosting - The container runs as an unprivileged user (`node`), not root, on a minimal Alpine Linux base image with production-only dependencies. - The server runs with `NODE_ENV=production`, so error pages don't reveal stack traces. - Images are built by a Gitea Actions workflow, only from version tags, and deployed to the VPS by hand. ### 9. Third parties | Service | What it receives | | --- | --- | | Zoho (accounts.zoho.com, US) | Sign-in and token renewal. This is MCCS' existing identity provider for SDP. | | ServiceDesk Plus (ithelp.mainecc.edu) | Read-only queries, made with the signed-in person's token. | | OpenWeatherMap | A campus zip code, from the server. No MCCS data. Viewers' browsers load the weather icons directly. | | Google Fonts | Viewers' browsers load the page fonts. No MCCS data. | ## Limits and assumptions These are the boundaries of the protections above: - **The domain check isn't a role check.** Anyone with a Zoho account at an allowed domain can sign in. What they then see is limited by their own SDP permissions. A non-technician would only see what SDP shows them, but they would see the default region's site, group and technician names. Checking that the person is an SDP technician at sign-in is the planned next step. - **A signed-in screen is a signed-in screen.** A wallboard or unlocked computer shows whatever the signed-in person can see until the session expires or they sign out. - **The protections assume** NPM's access list stays attached to the proxy host, `.env` stays private on the server, and staff Zoho accounts are themselves protected (strong passwords and MFA, enforced by Zoho). ## Open items to investigate These haven't been checked yet. They are listed roughly by importance, and each should be marked done here, with the date and outcome, once it has been. 1. **Policy approval for where data is hosted.** Ticket data passes through a VPS at RackNerd, administered by Skylar Grant rather than MCCS IT. Get written approval from MCCS IT security for the hosting arrangement and for who has administrative access to the server. *2026-10-16: ticket subjects, the main FERPA concern, were removed (see section 5). What still passes through is ticket numbers, staff names, status, site, group and times, plus staff email and IP addresses in the logs. Approval is still needed, but the request is now for staff and operational data only.* 2. **What a non-technician sees.** Until the technician check is built, test with a non-IT staff account at an allowed domain and record exactly what the dashboard shows them. 3. **Who shares the allowed IP addresses.** Ask the network team what traffic leaves through the allowed address blocks. If campus or guest Wi-Fi does, the network layer protects less than it appears to, and sign-in becomes the main barrier. 4. **Paths around Nginx Proxy Manager.** Check that: - the VPS's bare IP address and NPM's default site don't reach the dashboard - no other proxy host forwards to `sdp-dashboard:3000` - no untrusted containers share NPM's Docker network, from which the app can be called without the access list (sign-in would still be required) 5. **The build and deploy pipeline.** A compromised build could put a malicious image on the server, where it would run with the real secrets. Review: - access to the self-hosted Gitea runner, which holds the Docker Hub credentials - two-factor authentication on the Docker Hub account - whether the image repository is public - dependency version ranges in `package.json` Consider deploying exact version tags instead of `latest`, and running `npm audit` before each release. 6. **Content-Security-Policy and HSTS.** HTML escaping is currently the only defense against injected script. A Content-Security-Policy that limits scripts to the dashboard itself, and allows only Google Fonts and OpenWeatherMap images from outside, would contain any mistake. NPM can also send an HSTS header. 7. **SDP API rate limits.** Find out whether Zoho limits SDP API calls per user or per organization. If the limit is per organization, many open dashboards, or one misbehaving one, could use up the allowance that other MCCS integrations depend on. 8. **Wallboard sessions.** A wallboard signed in as a person keeps that person's access for up to 7 days. Consider a dedicated account with limited SDP permissions for wallboards, or a shorter `SESSION_DAYS`. 9. **Hardening the VPS.** Review: - SSH: key-only sign-in and no root login - operating system patching - `.env` file permissions (`chmod 600`) - whether backups or snapshots copy `.env` or the data volume elsewhere - how long Docker keeps container logs, which contain staff email and IP addresses 10. **Ownership of the Zoho client and secrets.** Record which Zoho account owns the API client, and what happens if that person leaves. Write down how to rotate `CLIENT_SECRET` and `SESSION_SECRET`. Changing `SESSION_SECRET` immediately signs everyone out, which serves as an emergency switch. 11. **Email verification in Zoho.** The domain check trusts the email address Zoho reports. Confirm that Zoho won't accept an unverified address as an account's primary email, or that MCCS has verified its domains in Zoho, which settles the question. 12. **Independent review and automated tests.** So far, testing has been one live run plus simulated tests written by the developers. A short review by MCCS IT, and automated tests run on every release, would give independent assurance. ## How this was checked - Live, on 2026-10-15: sign-in, sign-out, live data, and the IP access list (an MCCS address allowed; mobile data refused with 403). - With SDP and Zoho simulated: forged and missing sign-in state, refused and failed sign-ins, tampered cookies, expired sessions, revoked tokens, SDP rejecting a token, tokens never appearing in page responses, settings validation, outside and lookalike email domains, older cookies from outside accounts, and name suggestions refused when SDP refuses the caller. - On 2026-10-16, with SDP simulated to return a subject it wasn't asked for: neither query requested `subject`, and nothing the server returned contained it. ## Reporting a problem Contact Skylar Grant. Please don't post security problems in public issue trackers or chat channels.