-
Notifications
You must be signed in to change notification settings - Fork 15
SSL Verification Global Setting
How outbound HTTPS certificate verification went from off by default and scattered to on by default, working out of the box, and governed by a single switch. Shipped as #919, resolving GitHub issue #49 (raised by @chris18890).
This is the developer/architecture companion to the operator guide HTTPS Certificates & CA Bundles and the in-app help at Workflows β Help β HTTPS certificates (workflow/help-ssl.php).
FreeITSM makes a great many outbound HTTPS calls β Microsoft Graph and other mailboxes, AI providers, SSO/OIDC discovery, Intune and vCenter syncs, webhooks, knowledge/change share emails, password-reset mail. On every one of those, verifying the far end's certificate is what stops the app being tricked into handing ticket data, customer email addresses or a webhook signing secret to an impostor (a "man-in-the-middle").
The shipped default was the opposite of secure:
// config.php β as shipped, before #919
define('SSL_VERIFY_PEER', false);That constant was threaded into 66 cURL call sites across 33 files. So out of the box, nothing verified. Worse, the comment directly above it called the setting "INSECUREβ¦ ONLY for testing" β the shipped default contradicted its own warning.
Why was it false? Because on a stock Windows/WAMP install, PHP has no list of trusted certificate authorities (no CA bundle), so flipping it to true would make every outbound call fail with unable to get local issuer certificate. The insecure default was a workaround for a setup problem.
On top of that, verification was configured inconsistently in three different places:
| Where | How it behaved | Verified on a stock install? |
|---|---|---|
Webhooks (webhook_delivery.php) |
hard-coded true
|
Yes β always |
| AI providers (shared settings panel) |
SSL_VERIFY_PEER AND a per-module tick-box |
No β the false global forced it off |
| Everything else | the SSL_VERIFY_PEER constant |
No |
| Intune / RFP Builder | their own per-module tick-box, default on, independent of the global | Yes |
The per-module "Verify SSL" tick-boxes were the visible symptom. On the AI panel the value shown was global AND row, so on a normal install the box rendered unchecked and appeared not to save β because the global false was dragging it down. Intune and RFP, meanwhile, verified independently. Same label, three behaviours.
Before writing a line, the CA-bundle mechanism was probed empirically on the real environment (PHP 8.4.0 / libcurl 8.10.1 on Windows), because the whole design hinges on how cURL finds a bundle:
| Mechanism | Result | Verdict |
|---|---|---|
curl.cainfo via ini_set() at runtime |
no effect | it is PHP_INI_SYSTEM β only settable in php.ini
|
putenv('CURL_CA_BUNDLE=β¦') before curl_init
|
ignored | this libcurl build does not honour the env var |
CURLOPT_CAINFO per handle |
works | the only reliable programmatic lever |
The consequence is the load-bearing fact of the whole change: there is no global, zero-code way to point cURL at a bundle. Making verify-on work without hand-editing php.ini requires setting the bundle on each handle with CURLOPT_CAINFO. That is why this became a shared helper applied to all 66 sites, rather than a one-line config tweak.
A second discovery: because the AI panel's effective value was global AND toggle, the shipped false meant every AI call β Anthropic, OpenAI, OpenRouter β was already skipping verification, and the tick-box could never rescue it. Flipping the global is what makes AI verification actually happen. So issue #49's request was a bigger security win than it first appeared.
One switch, one helper, one bundled fallback.
// config.php β after #919
define('SSL_VERIFY_PEER', true); // the single global switch
require_once(__DIR__ . '/includes/ssl.php');
if (!defined('SSL_CA_BUNDLE')) {
define('SSL_CA_BUNDLE', sslResolveCaBundle());
}// includes/ssl.php β every outbound handle calls this after curl_init()
sslApplyCurl($ch); // verify per SSL_VERIFY_PEER, attach CA bundle
sslApplyCurl($ch, true); // ALWAYS verify (webhooks) β no off switchsslResolveCaBundle() picks, in order:
- a bundle an admin/OS already configured in
php.ini(curl.cainfo/openssl.cafile) β honour it; - on Windows only, the
includes/cacert.pemnow shipped with the app (PHP-on-Windows has none); - otherwise
''β on Linux with nothing configured, leave cURL on its OS trust store rather than override it with a possibly-staler copy.
So a fresh Windows/WAMP install now verifies out of the box with no php.ini surgery. This was proven end-to-end under the Apache php.ini (which has no curl.cainfo) β the exact case that failed before now returns TLS OK (verified).
The per-module tick-boxes were removed entirely. aiSettingsLoad() still returns a verify_ssl key (now simply mirroring the global) so the many legacy callers don't break, but it no longer drives any handle.
β οΈ vCenter / ESXi with a self-signed certificate now FAILS. Those calls verified nothing before (they rode thefalseglobal); they are now checked. Add the host's certificate tocacert.pem, or the sync errors. The same applies to any internal appliance presenting a private certificate. This is the one behaviour change that can break a working setup.
π Never hand-write
CURLOPT_SSL_VERIFYPEERagain. Any new outbound call must go throughsslApplyCurl($ch). A raw site silently opts out of the global policy and the bundled CA β reintroducing exactly the scattered inconsistency this change removed.
-
Two
php.inifiles on WAMP. Apache's serves the browser; the CLI's serves background workers (mailbox poll, webhook delivery). Historically only one got the bundle configured β a "half-fix". The shippedcacert.pemnow covers both regardless, because it is applied in code, notphp.ini. -
Don't override Linux's OS trust store. The Windows-only fallback in
sslResolveCaBundle()is deliberate: on Linux, cURL's system bundle is fresher and correct, so we return''and leave it alone. -
Webhooks keep an always-verify path (
sslApplyCurl($ch, true)). They carry record data to third parties over the public internet β the worst place to stop checking identities β so there is deliberately no off switch, even if an admin sets the global tofalse. -
The bundle goes stale slowly.
includes/cacert.pemis the Mozilla/curl list (public certs, safe to commit). Re-download fromhttps://curl.se/ca/cacert.pemroughly yearly. - Corporate TLS-inspection proxies: the fix is to append your internal root to the bundle, never to disable verification.
Colour key: ποΈ config Β· π§© new Β· βοΈ engine Β· π API Β· π₯οΈ UI Β· π¨ CSS/JS Β· π docs
| File | Before | After | |
|---|---|---|---|
| ποΈ | config.php |
SSL_VERIFY_PEER = false (verification off everywhere); no CA bundle concept |
SSL_VERIFY_PEER = true; loads ssl.php and defines SSL_CA_BUNDLE
|
| ποΈ | docker/config.php |
separate Docker bootstrap, also false, and did not load ssl.php β so after the flip it would have fatally called an undefined sslApplyCurl()
|
true, loads ssl.php, defines SSL_CA_BUNDLE (its Debian base image supplies the OS CA store, so no shipped cert needed) |
| π§© | includes/ssl.php |
did not exist | the single helper: sslApplyCurl() + sslResolveCaBundle()
|
| π§© | includes/cacert.pem |
did not exist | Mozilla CA bundle shipped with the app so Windows verifies out of the box |
| βοΈ | includes/webhook_delivery.php |
hard-coded CURLOPT_SSL_VERIFYPEER => true, no CA bundle β failed on stock Windows |
sslApplyCurl($ch, true) β still always verifies, now with the bundle attached |
| File | Before | After | |
|---|---|---|---|
| π₯οΈ | includes/ai_settings_panel.php |
rendered a "Verify SSL certificate" checkbox on every AI settings page | checkbox gone |
| π¨ | assets/js/ai-settings.js |
loaded/saved the checkbox state | references removed |
| βοΈ | includes/ai_settings.php |
stored/read a per-namespace *_verify_ssl row, ANDed with the global |
no row; verify_ssl in the return now just mirrors the global (kept so callers don't break) |
| π |
api/system/ai/get_settings.php, save_settings.php
|
returned / persisted verify_ssl
|
field dropped |
| π₯οΈ | asset-management/settings/index.php |
Intune Verify SSL checkbox + warning + JS | removed; Intune follows the global |
| π₯οΈ | contracts/settings/index.php |
RFP Builder Verify SSL toggle + warning + JS (RFP UI lives here) | removed; RFP follows the global |
| π | workflow/help-ssl.php |
"AI providers have a Verify SSL toggle" | rewritten: one global switch, app ships a bundle |
All of these previously read SSL_VERIFY_PEER (or a per-module variable) directly and set no CA bundle β so on a stock install they either skipped verification or, once forced on, failed. After: each calls sslApplyCurl($ch), verifying per the global switch with the CA bundle attached.
| Area | Files |
|---|---|
| βοΈ Mail / Graph |
includes/gmail.php, includes/mailbox_graph.php, includes/template_email.php, api/tickets/check_mailbox_email.php, api/tickets/send_email.php, api/tickets/verify_mailbox_folder.php
|
| βοΈ AI |
includes/ai_provider.php, includes/rfp_ai.php, includes/knowledge/kb_ai.php, includes/services/knowledge.php, api/knowledge/ai_chat.php, api/knowledge/generate_embedding.php, api/workflow/_ai_helpers.php, api/workflow/test_ai_key.php, api/forms/test_ai_key.php, api/cmdb/test_ai_key.php, api/rfp-builder/test_ai_connection.php, api/tickets/test_reply_cleanup_key.php
|
| βοΈ SSO / OIDC |
includes/oidc.php, auth/oauth_callback.php, auth/google_oauth_callback.php, api/system/test_oidc_discovery.php, api/system/debug-tools/D003_selfservice_sso.php
|
| βοΈ Assets |
includes/intune.php, api/assets/get_vcenter.php, api/assets/debug_vcenter.php
|
| βοΈ Messaging / mail-out |
includes/messaging/MessagingProvider.php, api/messaging/test_channel.php, api/knowledge/send_share_email.php, api/change-management/send_share_email.php, api/auth/request_password_reset.php
|
| File | Before | After | |
|---|---|---|---|
| π | CHANGELOG.local.md |
β | entry #919 |
| π | this wiki + HTTPS Certificates & CA Bundles | described the per-module toggles | describe the single global switch + shipped bundle |
The reusable check β load the app's real config.php, then make a verified call the way every site now does:
require 'config.php';
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
sslApplyCurl($ch);
echo curl_exec($ch) !== false ? "TLS OK (verified)\n" : "FAIL: " . curl_error($ch) . "\n";Run it under both the Apache and CLI php.ini (see HTTPS Certificates & CA Bundles for why both matter). With the shipped bundle it should print TLS OK even when curl.cainfo is empty.
Confirm no raw sites remain: a repo-wide search for CURLOPT_SSL_VERIFYPEER should return only includes/ssl.php (the helper) and workflow/help-ssl.php (documentation).
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)