-
Notifications
You must be signed in to change notification settings - Fork 15
Security
FreeITSM ships with several layers of defence covering authentication, sensitive-data storage, and brute-force resistance. This page is the security reference for evaluators and administrators.
- Username + password stored as bcrypt hashes in
analysts.password_hash - Minimum password length: 8 characters (enforced on change and reset)
- Default admin:
admin/freeitsmβ change this immediately on first login
- Pure-PHP implementation of RFC 6238 (TOTP) and RFC 4226 (HOTP)
- No external dependencies β uses PHP's
hash_hmac()andrandom_bytes() - 6-digit codes, 30-second time step, Β±1 window tolerance (90s)
- Compatible with any standard authenticator: Google Authenticator, Microsoft Authenticator, Authy, 1Password, Bitwarden
- Setup: account menu β MFA β scan QR (or paste secret) β verify a code β enabled
- Secrets encrypted at rest with AES-256-GCM in
analysts.totp_secret
- "Forgot password" sends a single-use token via email (configurable mail provider)
- Token expires after a short window
- Token comparison uses
hash_equals()(timing-safe)
- Optional OpenID Connect SSO alongside local login β works with Keycloak, Microsoft Entra ID, Okta, Google Workspace, Authentik, etc.
- Authorization Code + PKCE (S256), with
stateandnoncevalidation; ID-token signature verified against the provider's JWKS (vendoredfirebase/php-jwt) - Client secrets encrypted at rest; local login retained as a break-glass fallback so a down IdP can't lock anyone out
- Full details: Single Sign-On (SSO / OIDC)
- Repeated failed login attempts from the same IP are throttled
- Both username and IP-level lockouts to defend against credential stuffing
- Login attempts (success + failure) logged with IP and user agent in the Reporting module
Authorisation is three layers, each answering a different question, each enforced server-side and failing closed. Hiding a button is never the control; it is at most a courtesy on top of one.
| Layer | Question | Gate | Default |
|---|---|---|---|
| 1 β Module access | Can they enter this module? |
requireModuleAccess() / requireModuleAccessJson()
|
Absence = everything (an analyst with no restriction gets all modules) |
| 2 β Capabilities (Roles) | Can they administer this part of it? |
requireCapability(Cap::X) / requireCapabilityJson(Cap::X)
|
Deny by default (no role, no settings) |
| 3 β Administrator | Can they run the System module? |
requireAdminJson() / the page gate |
Deny by default (analysts.is_admin) |
is_admin is a superset: an administrator implicitly holds every capability, which is what makes Layer 2's deny-by-default safe to switch on β the tightening can never lock the instance owner out.
- Per-analyst and per-team module grants, managed at System β Analysts / Teams; unioned at one choke-point (
getAnalystAllowedModules()). - Enforced on the launcher cards, the waffle menu, and on the pages and APIs themselves β a restricted analyst cannot simply type the URL.
- Checks are authoritative (re-read from the database per request), so a just-revoked analyst is stopped even on a stale session.
- See Module Access Control.
- Permission goes down to the individual settings tab, so someone can maintain the ticket statuses without reaching the mailbox OAuth credentials two tabs along.
- A settings tab you lack is not rendered into the page at all β absent, not hidden. There is no DOM to un-hide.
- Every settings write endpoint carries its own guard. Operational reads are deliberately not guarded β they are what the everyday module needs to function. Except a read that returns credentials: ask what the response contains, not just what the endpoint does.
- Settings that share the generic
save_system_settings.phpendpoint are authorised per setting key (includes/settings_keys.php), because one guard cannot cover five callers with five audiences. A key no module claims cannot be written through it. - See Roles & Permissions and the Developer Guide.
-
analysts.is_admingates the whole System module β page and API (includes/admin_api_guard.php). Before this, every analyst was effectively an administrator and could self-escalate. - The last administrator is protected from removal.
- See Admin Access Control.
Type-safe permissions catch a misspelled check. Nothing catches a check somebody simply forgot to write β no language feature saves you from a line that isn't there, and that omission has repeatedly turned out to be the real hole.
System β Debug tools β D005 (Endpoint permission coverage) reads every endpoint under api/ and reports what actually guards each one β a capability, administrator-only, module access, an API key, a webhook signature, a URL token, "logged in and nothing more", or nothing at all β ranked by how much damage the gap allows. It also flags capabilities offered on System β Roles that no endpoint enforces (a tick-box granting nothing), and capabilities passed as a bare string instead of a Cap:: constant.
Run it after adding endpoints and before a release. Endpoints that are public by design (login, self-service, the REST API, inbound webhooks) and those acting only on the caller's own account (your own password, your own MFA) are declared as exclusions with reasons, so the findings stay short enough to act on.
- Company (tenant) access is scoped per analyst and per team, unioned the same way, and applied at the query level.
- The Tickets module filters department visibility by team membership (
get_my_departments.phpvsget_departments.php). - See Multi-Tenancy.
AES-256-GCM authenticated encryption for sensitive database values.
-
Key file: stored outside web root at
C:\wamp64\encryption_keys\sdtickets.key -
Format:
ENC:+ base64(IV + auth tag + ciphertext) β auth tag prevents tampering -
Migration-safe: values without
ENC:prefix pass through unchanged for gradual rollout -
Key generation: one-click from System β Encryption (or
php -r "echo bin2hex(random_bytes(32));") - No regenerate button β preventing accidental key destruction is more important than convenience
system_settings:
-
vcenter_server,vcenter_user,vcenter_password -
knowledge_ai_api_key,knowledge_openai_api_key -
intune_tenant_id,intune_client_id,intune_client_secret - (Per-feature AI keys β
<ns>_api_keyforknowledge_ai,cmdb_ai,workflow_ai,forms_ai,tickets_reply_cleanup, plus RFP Builder β each encrypted and masked separately; see AI Providers)
target_mailboxes:
-
azure_tenant_id,azure_client_id,azure_client_secret -
oauth_redirect_uri,imap_server,target_mailbox
analysts:
totp_secret
A subset of true secrets listed in MASKED_SETTING_KEYS are returned as ****<last4> rather than plaintext by api/settings/get_system_settings.php. The corresponding save endpoints treat blank or asterisk-prefixed submissions as "leave unchanged" so the user can re-save the settings form without re-typing the secret each time.
- Standard PHP sessions (
session_start()) - Session key:
$_SESSION['analyst_id']for analyst auth,$_SESSION['ss_user_id']for self-service portal users (separate auth domains) - Password-expiry guard in
includes/waffle-menu.phpβ if$_SESSION['password_expired']is set, every page redirects toforce_password_change.phpuntil resolved
- OAuth 2.0 for Microsoft 365 and Google Workspace mailboxes β no plaintext mailbox passwords
- Mailbox whitelist: per-mailbox domain and email-address whitelisting; non-whitelisted senders are rejected and logged
- Rejection actions: configurable per-mailbox actions for rejected emails (delete, move to Deleted Items, mark as read)
- Import actions: configurable post-import actions (delete, move to folder) with folder existence verified via Graph API before saving
- Module-specific upload folders (e.g.
tickets/attachments/,lms/content/,forms/uploads/) β each is gitignored - Generated filenames; original names stored in DB for display
- Served via PHP endpoints that check session auth before serving the file
For the browser extension and external integrations (e.g. PowerShell asset inventory):
- API keys generated at Software β Settings β API Keys
- Rate limited: 60 requests per minute per key (configurable in System Settings)
- Bearer-token style header
- Revocable per-key with last-used tracking
When moving from evaluation to production:
- Change the default
adminpassword -
Delete the
/setupfolder β it contains diagnostic info useful only during install - Generate a unique encryption key if you used a sample one
- Back up the encryption key file β losing it makes encrypted columns unrecoverable
- Configure OAuth for any mailboxes β avoid IMAP/password where possible
- Enable MFA on the admin account
- Restrict module access per analyst at System β Modules
- Configure the password reset email so analysts can recover access without admin intervention
- Review whitelist entries for each mailbox to limit who can create tickets via email
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)