Skip to content

HTTPS Certificates and CA Bundles

Ed Mozley edited this page Aug 13, 2026 · 4 revisions

HTTPS Certificates & CA Bundles

If a webhook just failed with unable to get local issuer certificate, start here. Your webhook is probably fine. The server it runs on hasn't been told which certificate authorities to trust, so it can't confirm it's really talking to Slack or Discord. It's a one-time server fix, and it takes about five minutes.

This is the operations-side companion to the in-app guide at Workflows β†’ Help β†’ HTTPS certificates (workflow/help-ssl.php).

⚠️ Wrong page if a client can't trust FreeITSM. This page is about outbound calls β€” FreeITSM reaching Slack, Discord or a webhook endpoint and not being able to verify them. If your symptom is the inventory agent refusing to post with "Could not establish trust relationship for the SSL/TLS secure channel", that's the inbound direction and nothing here will fix it. See The inventory agent instead.

Since #919, FreeITSM ships its own CA bundle (includes/cacert.pem) and points cURL at it automatically, so a fresh install now verifies out of the box β€” the manual php.ini steps below are no longer required on a stock setup. They remain the reference for configuring a system-wide bundle, for the CLI worker, and for the edge cases in Troubleshooting. Verification is now governed by a single global switch; see SSL Verification β€” the Global Setting.


The error

Transport error: SSL certificate problem: unable to get local issuer certificate

Variants that mean the same thing:

  • certificate verify failed
  • server certificate verification failed. CAfile: none (Linux/GnuTLS wording)
  • self-signed certificate in certificate chain
  • unable to get issuer certificate

Read the rest of the panel before changing anything. If FreeITSM showed you the JSON it built under Sent (sample data), your workflow, your variables and your payload format all worked. Only the final network hop failed.


What certificate verification actually is

When FreeITSM posts to https://discord.com/…, two separate things must happen:

  • Encryption β€” scramble the traffic so nobody in between can read it.
  • Verification β€” confirm the server on the other end really is Discord, and not someone impersonating it.

Encryption without verification is close to worthless: you'd have a beautifully encrypted conversation with an impostor.

Verification works like checking a passport. Discord presents a certificate saying "I am discord.com", signed by a certificate authority (CA) β€” an organisation whose job is vouching for identities. Your server checks that signature against its own list of trusted authorities.

The error above is not "this certificate is fake". It's something more basic: "I have no list of authorities, so I can't check anybody's passport."


What a CA bundle is, in plain English

A CA bundle is that list. It's a plain text file β€” conventionally cacert.pem β€” containing the public certificates of the ~120–150 organisations the world has agreed to trust. Open it in a text editor and you'll see block after block of -----BEGIN CERTIFICATE-----.

The one almost everyone uses is the Mozilla CA bundle: the trust list that ships inside Firefox, extracted and republished by the curl project. Using it means your server trusts exactly who a mainstream browser trusts.

What it is not:

  • It contains no secrets. These are public certificates. Safe to read, copy, back up, commit to a private repo.
  • It is not FreeITSM-specific, or webhook-specific, or Discord-specific. It's the same list every other program uses.
  • It is not a licence or an account. It's free; you download it.

It goes stale slowly as authorities are added or withdrawn, so re-downloading once a year is good hygiene. Nothing breaks immediately if you don't.


Why Windows hits this and Linux doesn't

On Linux, the OS maintains a CA bundle and PHP finds it automatically. Most people never learn any of this exists.

On Windows, PHP itself ships with no CA bundle and no pointer to one. The two settings that would tell it where to look β€” curl.cainfo and openssl.cafile β€” are commented out in the default php.ini, and WAMP supplies no cacert.pem. So raw PHP on a stock Windows/WAMP box cannot make a verified HTTPS request to anything, out of the box.

FreeITSM works around this for you. Since #919 it ships its own includes/cacert.pem and points cURL at it automatically, so FreeITSM specifically verifies out of the box even when php.ini is empty. This section explains the underlying PHP situation β€” useful background, and it matters if you ever see this error, but you usually won't have to touch php.ini. See Fixing it for what to actually do.


Fixing it

Since FreeITSM ships its own bundle, you should only be here if the /setup check went red ("on, but cannot verify") β€” meaning the shipped includes/cacert.pem is missing or unreadable β€” or you'd rather manage the trust list yourself. Two ways, easiest first.

Option A β€” put a cacert.pem in the app's includes/ folder (simplest)

FreeITSM automatically uses includes/cacert.pem whenever php.ini has no bundle configured. So if the shipped one is missing, or you want to refresh a stale one:

  1. Download the Mozilla bundle from its canonical home β€” https://curl.se/ca/cacert.pem (~200 KB, full of -----BEGIN CERTIFICATE----- blocks; if it's a few hundred bytes of HTML, your browser saved an error page).
  2. Save it as includes/cacert.pem in the application folder.
  3. Reload the page. No php.ini editing, no Apache restart.

This is the fix the /setup page points you to, and it covers everything in one step β€” the browser app and the background worker β€” because FreeITSM attaches the bundle in code (sslApplyCurl), not through php.ini. So the "two php.ini files" trap below can't bite you.

Option B β€” configure a system-wide bundle in php.ini (optional)

Do this only if you'd rather every PHP application on the server share one bundle, or you're on a Linux image with no system certificates. It's the traditional route and still works; it just isn't required for FreeITSM on its own.

1. Download the Mozilla CA bundle

From https://curl.se/ca/cacert.pem. Save it outside your versioned PHP folder, so upgrading PHP doesn't delete it β€” e.g. C:\wamp64\cacert.pem.

2. Point PHP at it β€” in BOTH php.ini files

WAMP keeps two, and using only one is the classic way to half-fix this:

Which Typical path Why it matters
Apache's C:\wamp64\bin\apache\apache<version>\bin\php.ini Everything you do in the browser β€” including the Send test button.
The CLI's C:\wamp64\bin\php\php<version>\php.ini Scheduled tasks β€” including the background webhook delivery worker. Miss this and Send test passes while real deliveries keep failing.

In each, find these two lines (commented out with a leading ;), remove the semicolon, set the path:

curl.cainfo = "C:/wamp64/cacert.pem"
openssl.cafile = "C:/wamp64/cacert.pem"

Forward slashes; keep the quotes. Both are needed β€” curl.cainfo covers cURL, openssl.cafile covers PHP's other stream-based HTTPS calls.

3. Restart Apache

php.ini is read once at startup. WAMP tray icon β†’ Restart All Services. (The CLI re-reads its ini on every run, so it needs no restart.)


Verify it worked

Easiest: press Send test on the webhook action again. You want Delivered, an HTTP 204 (Discord) or 200, and the message actually appearing in your channel.

To check the setting rather than the symptom, drop this in the web root as catest.php, load it, then delete it:

<?php
header('Content-Type: text/plain');
echo "curl.cainfo    = " . (ini_get('curl.cainfo')    ?: '(EMPTY - not set)') . "\n";
echo "openssl.cafile = " . (ini_get('openssl.cafile') ?: '(EMPTY - not set)') . "\n";

$ch = curl_init("https://curl.se/");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
echo curl_exec($ch) === false
    ? "TLS: FAILED - " . curl_error($ch) . "\n"
    : "TLS: OK - certificate verified\n";

Both paths printed, TLS OK. If the paths still read EMPTY, Apache is on the old config β€” wrong php.ini, or not restarted.

Don't forget the worker (Option B only). A passing Send test only proves Apache's PHP is fixed. Real webhooks are delivered by the background worker under the CLI PHP. Tests passing while live deliveries fail = you edited only one of the two php.ini files. Option A sidesteps this entirely β€” the shipped includes/cacert.pem is attached in code, so it covers both the browser app and the worker at once.


Turning verification off β€” and why you shouldn't

Searching this error online will suggest "just disable SSL verification". It makes the error go away. Here's the cost.

⚠️ Disabling verification means FreeITSM stops checking who it is talking to. It still encrypts β€” but it will hand your ticket data, your customers' email addresses and your webhook signing secret to anyone who intercepts the connection and claims to be Discord. That is precisely the man-in-the-middle attack verification exists to prevent. You'd be turning off the lock because you couldn't find the key.

Webhooks: no off switch, deliberately

The webhook transport always verifies (CURLOPT_SSL_VERIFYPEER => true, includes/webhook_delivery.php) and offers no setting to change that. Webhooks carry record data to third parties over the public internet β€” the worst possible place to stop checking identities. Install the CA bundle instead.

Everything else: one global switch

Since #919 there are no per-module "Verify SSL" toggles. Certificate verification for every outbound call β€” AI providers, mailboxes, SSO, Intune, vCenter, share and reset email β€” is governed by a single setting, SSL_VERIFY_PEER in config.php, applied through the shared sslApplyCurl() helper. It ships on. Turning it off disables verification everywhere at once, so it is not a casual switch. See SSL Verification β€” the Global Setting for the full design.

The only legitimate case for touching it is a corporate network that intercepts outbound TLS with an inspection proxy presenting a certificate signed by an internal authority your server has never heard of. Even then, the correct fix is to append your company's internal root certificate to cacert.pem, so your server trusts the proxy legitimately and keeps verifying everything else β€” never to disable verification. Whoever runs your network will have that root certificate ready, because every other application on the network needs it too.

⚠️ vCenter/ESXi with a self-signed certificate now verifies where it did not before (#919). Add that host's certificate to cacert.pem, or the asset sync will fail.

Rule of thumb. Turning verification off is only ever defensible for a service inside your own network that you fully control, and even then it's a stopgap. For anything on the public internet β€” Slack, Discord, Teams, OpenAI, Anthropic β€” it is never the right answer. If it "fixed" your problem, what it actually did was hide it.


Troubleshooting

What you see What it usually means
Check page still prints (EMPTY - not set) Apache is on the old config. Restart it β€” and make sure you edited Apache's php.ini, not just the CLI one. WAMP tray β†’ PHP β†’ php.ini opens the right file.
Send test passes, live deliveries fail The classic half-fix: Apache's ini done, CLI's not. The worker runs under the CLI PHP. Do both.
self-signed certificate in certificate chain Something is intercepting your TLS β€” a corporate inspection proxy, or antivirus that scans HTTPS. Append that product's root certificate to cacert.pem. Don't disable verification.
Still unable to get local issuer certificate Check the php.ini path actually points at the file (typos and backslashes bite), and that cacert.pem is a real bundle rather than a saved HTML error page.
Could not resolve host Not a certificate problem β€” DNS. Check the URL for a typo and that the server can reach the internet.

How FreeITSM surfaces this

webhookDiagnoseError() (includes/webhook_delivery.php) recognises the TLS-trust family β€” plus DNS, connection-refused and timeout β€” and returns a plain-English explanation with a link to the in-app version of this page. It renders in both places a webhook failure appears:

  • the workflow editor's Send test panel, and
  • the System β†’ Webhooks delivery log.

Diagnosis happens at render time from the stored error, so historic failed deliveries get the explanation retroactively β€” no schema change, nothing to backfill.

See also: Workflow & Webhook Pitfalls for why this error was worth an entire page.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally