-
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 - Hashing uses PHP's
password_hash()withPASSWORD_DEFAULT; authentication usespassword_verify()to re-hash the input and compare, so passwords are one-way β they cannot be reversed from the stored hash -
Per-hash salt: every call to
password_hash()generates a unique random salt, so the same password produces a different hash each time - Cost factor: bcrypt runs configurable work rounds (currently 2^12 = 4,096 iterations), keeping brute-force attacks slow
- Password expiry policy: a configurable maximum password age (30β365 days), with a forced password change on next login once expired
- Account lockout: a configurable maximum number of failed login attempts before a temporary account lock, with a configurable lockout duration
- Minimum password length: 8 characters (enforced on change and reset)
- Default admin:
admin/freeitsmβ change this immediately on first login. The SQL script ships a pre-computed hash for this account (once the password is changed, a new unique hash is generated), and thedb_verify.phpendpoint also seeds it at runtime β with a fresh hash β if no analysts exist
- 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 - Disabling MFA requires the account password β preventing unauthorised deactivation from an unattended session
- Trusted devices: users can opt in to skip the OTP prompt on a trusted browser for a configurable number of days β cookie-based, with SHA-256-hashed tokens stored server-side
MFA is optional and per-analyst β analysts with MFA disabled log in with just username and password as normal. When an analyst has enabled MFA, the login flow adds a second verification step:
- Analyst enters username and password on
login.php - Password verified β MFA pending state stored in the session (
mfa_pending_analyst_idetc.) βanalyst_idis NOT set yet - Login page renders the OTP form (shield icon, 6-digit input, auto-submit)
- Analyst enters the code from their authenticator app β JS calls
api/myaccount/verify_login_otp.php - Server decrypts the stored TOTP secret and verifies the code (Β±1 time window)
- On success:
$_SESSION['analyst_id']is set, the pending state is cleared, and the analyst is redirected toindex.php
The "Cancel and return to login" link clears the pending state so a different analyst (who may not have MFA) can log in on the same browser.
- "Forgot password" sends a single-use token via email (configurable mail provider)
- Token expires after a short window
- The expiry window is one hour
- 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)
- Optional directory sign-in alongside OIDC β passwords are checked against your existing directory (Active Directory, OpenLDAP, FreeIPA, 389) by bind, people can be auto-created on first login, and access is gated by directory group
- Guards against the RFC 4513 unauthenticated-bind trap: an empty (or NUL-containing) password is rejected before any network call
- The read-only service account's password is encrypted at rest and never returned to the browser
- Full details: LDAP & Active Directory
- Repeated failed login attempts from the same IP are throttled
- Both username and IP-level lockouts to defend against credential stuffing
- Escalating bans for IPs attempting logins against non-existent or locked accounts, with configurable thresholds and 24-hour bans
- 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). - The analyst's team memberships are stored in the session at login, and API endpoints filter by team-accessible departments.
- An analyst with no team memberships sees everything β the unrestricted (admin) case.
- 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 - Key: a 256-bit random key
- Nonce: a fresh 96-bit random IV per encryption, so the same value encrypted twice produces different ciphertext
- Auth tag: 128-bit β detects any tampering with encrypted data
-
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 -
imap_username,imap_password,smtp_server(basic IMAP/SMTP mailboxes)
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.
The masked set covers vcenter_password, knowledge_ai_api_key, knowledge_openai_api_key and intune_client_secret, plus every per-feature AI key (see AI Providers) β i.e. the values that are secrets, as opposed to encrypted-but-displayable configuration such as server names. The "leave unchanged" convention is applied by api/settings/save_system_settings.php.
- 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
- SQL injection: PDO prepared statements throughout β parameters are never concatenated into SQL
-
XSS: server-side output encoding with
htmlspecialchars(); client-side rendering escapes untrusted values via the DOMtextContentβinnerHTMLpattern
- Every ticket change is written to the ticket's audit trail
- Login attempts (success and failure) are logged with IP and user agent β see IP-based brute-force protection above
- OAuth 2.0 for Microsoft 365 and Google Workspace mailboxes β no plaintext mailbox passwords
-
Mailbox whitelist: per-mailbox domain and email-address whitelisting (
mailbox_email_whitelisttable); non-whitelisted senders are rejected and logged. A mailbox with no whitelist entries accepts all senders β the whitelist only restricts once at least one entry exists -
Mailbox activity log: every email imported or rejected during mailbox processing is recorded in
mailbox_activity_log(action, sender, subject, reason, resulting ticket) - 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)