# Laravel SSL Handshake Failed: How to Find the Cause and Fix It

> Fix "SSL handshake failed" errors in Laravel's HTTP client, mailer, MySQL and Redis connections: reproduce the failure with openssl s_client, then fix CA bundles, TLS versions, ports and SNI without turning off certificate verification.

**Severity:** medium | **Category:** Infrastructure Security

---

## The Issue

An SSL handshake failure means your Laravel application and the server it is connecting to could not agree on how to set up an encrypted connection, so the connection was dropped before any data was sent. In Laravel it usually shows up as cURL error 35 from the HTTP client, a "wrong version number" or STARTTLS error from the mailer, or an SSL error from a MySQL or Redis connection.

## Steps to Fix

### 1. Reproduce the handshake outside Laravel

Run the handshake from the same server or container that runs Laravel, so you test the same OpenSSL build and CA bundle:

openssl s_client -connect api.example.com:443 -servername api.example.com

Force a protocol version to see which ones the remote side accepts:

openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_2
openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3

Check which OpenSSL and cURL builds PHP itself is using, since the CLI and PHP-FPM can differ:

php -r 'echo OPENSSL_VERSION_TEXT, PHP_EOL, curl_version()["ssl_version"], PHP_EOL;'

If openssl s_client also breaks, the problem is the remote server, the network or protocol support, not your Laravel code. If it succeeds but Laravel does not, compare the hostname, port and CA bundle in your Laravel config.

### 2. Update the CA bundle PHP verifies against

If the error mentions "certificate verify failed" or "unable to get local issuer certificate", PHP cannot find a trusted CA that signed the remote certificate. Update the system bundle:

sudo apt install --reinstall ca-certificates

Slim Docker images often ship without it, so install it in the image itself:

# Dockerfile
RUN apt-get update && apt-get install -y ca-certificates

Check whether php.ini points PHP at a different, older bundle:

php -i | grep -E "openssl.cafile|curl.cainfo"

If either is set, point both at the system bundle (/etc/ssl/certs/ca-certificates.crt on Debian and Ubuntu) or remove them. If openssl s_client shows "verify error:num=20" against a public API, the remote server may be missing its intermediate certificate. That is the provider's bug to fix. Report it rather than disabling verification.

For a private CA, such as an internal service, trust that one CA for that one client:

Http::withOptions(['verify' => storage_path('certs/internal-ca.pem')])->get($url);

### 3. Fix protocol, cipher and SNI mismatches

"sslv3 alert handshake failure" means the two sides share no protocol version or cipher suite. Check what a working client negotiates:

curl -sv https://api.example.com -o /dev/null 2>&1 | grep -E "SSL connection|TLSv"

OpenSSL 3, the version on Ubuntu 22.04 and later, rejects TLS 1.0, TLS 1.1 and legacy renegotiation by default. If the remote server only speaks those, the fix belongs on that server. Lowering your OpenSSL security level re-enables broken protocol versions for every connection your app makes.

Connect by hostname, not IP address. A request to an IP sends no SNI, and servers behind Cloudflare or a shared load balancer then cannot pick a certificate and drop the handshake. The same applies to your database and Redis host settings.

If the failure only happens on one network, look for a corporate proxy or firewall doing TLS inspection. It replaces the remote certificate with its own, which your server does not trust.

### 4. Match the mail port to the right scheme

"wrong version number" from the mailer almost always means TLS on a plaintext port or the reverse. Port 465 expects TLS from the first byte (smtps). Port 587 starts in plaintext and upgrades with STARTTLS (smtp):

MAIL_PORT=465
MAIL_SCHEME=smtps

MAIL_PORT=587
MAIL_SCHEME=smtp

In Laravel 11 and later, an empty scheme setting makes Laravel pick smtps for port 465 and smtp for every other port, so the usual mistake is an explicit smtps scheme left over on port 587. Laravel 10 and earlier configure this through the encryption setting instead. Symfony Mailer upgrades port 587 connections with STARTTLS whenever the server offers it, so no extra setting is needed for that.

### 5. Configure TLS for MySQL and Redis

Managed databases from AWS, DigitalOcean, Aiven and others usually require TLS and sign their certificates with their own CA. Download the provider's CA file and point MySQL at it. The default config/database.php already reads this variable:

DB_HOST=db.example.com
MYSQL_ATTR_SSL_CA=/etc/ssl/certs/provider-ca.pem

For managed Redis, switch to a rediss:// URL. Laravel maps the rediss scheme to TLS:

REDIS_URL=rediss://default:your-password@redis.example.com:6380

Use the hostname from your provider's dashboard, not a private IP, so the certificate matches.

## Verification

Re-run the failing call from the same server, as the same user that runs PHP-FPM or your queue workers:

php artisan tinker

Http::get('https://api.example.com')->status();

For mail, send a test message from tinker and watch the log for the TLS error:

Mail::raw('TLS test', fn ($message) => $message->to('you@example.com'));

Then check that nobody "fixed" the error by disabling verification:

grep -rn "withoutVerifying\|'verify' => false\|verify_peer" app config

## Prevention

Keep the base image and the ca-certificates package updated, connect to every service by hostname, and set the mail port and MAIL_SCHEME together. Keep Http::withoutVerifying() out of production code and flag it in code review. StackShield monitors your own certificate and TLS configuration, so handshake failures caused by your server show up before your users report them.

---

## Frequently Asked Questions

### What does "SSL handshake failed" mean in Laravel?

It means Laravel and the server it was connecting to could not agree on how to set up an encrypted connection, so the connection was closed before any data was sent. The cause is usually an outdated CA bundle, a protocol or cipher mismatch, a mail port configured with the wrong scheme, or a connection made by IP address without SNI.

### Is it safe to use Http::withoutVerifying() to fix SSL errors?

No. It keeps the connection encrypted but stops Laravel checking the server certificate, so anyone who can intercept the traffic can impersonate the API and read the credentials you send it. Fix the CA bundle, or trust a specific private CA with the verify option, instead.

### Why does the URL work in my browser but fail in Laravel?

Your browser uses its own certificate store and can fetch missing intermediate certificates on its own. Laravel uses the OpenSSL library and CA bundle on the server running PHP, which may be older, missing in a slim Docker image, or behind a proxy that inspects TLS. Test from that server with openssl s_client to see what PHP sees.

### What is cURL error 35 in Laravel?

cURL error 35 is a generic SSL connect error raised by the HTTP client when the TLS handshake fails. The OpenSSL message after it, such as SSL_ERROR_SYSCALL or "sslv3 alert handshake failure", tells you whether the connection was dropped or the two sides had no protocol or cipher in common.

