Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Kimmer's avatar

cURL error 60: SSL certificate problem - widely suggested solution does not solve the issue

I'm sorry for posting a new question about the infamous cURL error 60. Believe me I read all posts and know what should fix this error but it doesn't.

I am trying to get Laravel Websockets to work on a clean Laravel install. The install is running locally on my Mac and I'm using MAMP to run the local server.

The Laravel Websockets dashboard works nicely and it is able to connect when "php artisan websockets:serve" is running.

The error occurs when trying to run a simple broadcast testing event.

As described all over the interwebzzz I have downloaded the latest cacert.pem file and placed it in /Applications/MAMP/Library/OpenSSL/certs/

I used this path in the php8.0.8.ini as a value for curl.cainfo and openssl.cafile. I updated the file via MAMP->file->Open Template->PHP (php.ini)

I restarted the server and "php artisan websockets:serve" a million times.

I checked phpinfo() and it contains the correct path for curl.cainfo and openssl.cafile.

Sadly I still get the "cURL error 60: SSL certificate problem: unable to get local issuer certificate".

I also tried the suggested solution that required changing 'verify' the Client.php file in vendor/guzzlehttp. This solves the error but since it requires a vendor file to be edited this is not a good solution.

I've been on this for many hours and found myself at a dead end. Anyone hav any suggestion on what to try next?

Thanks!

0 likes
8 replies
Kimmer's avatar

Yeah well, often creating a post seems to be a trigger to finding a solutions

Apparently, you can set Guzzle client options in app/config/broadcasting.php. Like this

 'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => true,
                'encrypted' => true,
                'host' => '127.0.0.1',
                'port' => 6001,
                'scheme' => 'https',
                // for self signed ssl cert
                'curl_options' => [
                    CURLOPT_SSL_VERIFYHOST => 0,
                    CURLOPT_SSL_VERIFYPEER => 0
                ],
            ],
            'client_options' => [
                // for self signed ssl cert
                'verify' => false, // <- Added this
                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
            ],
        ],
...

Not the cleanest solution but one that will do for development.

2 likes
Kimmer's avatar

I tried transporting this solution to another Laravell install. The one I'm actually working on end I ended up with the same "cURL error 60: SSL certificate problem: unable to get local issuer certificate". Now my own solution (mentioned above) also does not work.

I updated the Guzzle install on my project but again I find myself in a dead end. :-(

Kimmer's avatar

I updated a bunch of vendor dependencies in my project. Most importantly "laravel/framework", which resulted in a bunch of other changes to dependencies.

Anyhow, this seemed to have solved my issues on the production project. I hope I didn't create any new ones.

psrz's avatar

Who issued the ssl certificate ?

I'm far from an expert on the matter but it If you made your own, like self signed certificate for your websocket server then you'll have to register the public key of said certificate on the clients you want to connect from.

That's why whatever it is you download from the web doesn't solve your problem. That bundle obviously doesn't have the home made certificate you have running on your websocket server

The websocket dashboard works on browser right ? Did it show a big warning about the certificate and you "told" it is all good, right ? That's the browser being helpful. But no such like with cUrl. This is not a php/laravel issue.

Unless I totally missunderstood your setup

Kimmer's avatar

Thanks for your reply @psrz !

Yeah, It gets rather complicated and I tried to find a balance between being detailed and create a readable post. Maybe I could have been more specific. Also I'm mainly doing this to learn so much of what I know now I din't know when I started the thread.

I'm using MAMP to run a local server on my Mac. I think it's basically Xamp but for Mac. If you want it creates unsigned certifcats for your local projects so you can use SSL while developing. I could have turned SSL off in MAMP and just use http but I really wanted to get Laravel Websockets working locally with SSL hoping to catch issues before my project goes to production. And boy did I ;-)

Yes, the Websockets dashboard worked in a browser without errors. The issue popped up when trying to broadcast an event. So in the basic chat app - I was building following a tutorial - the error occurred when sending a message.

Aparently the solutions suggested on a large thread on StackOverflow and in this thread https://laracasts.com/discuss/channels/general-discussion/curl-error-60-ssl-certificate-problem-unable-to-get-local-issuer-certificate didn't work for me. Turning off verification in the Client.php file in the guzzlehttp package did work (also mentioned in SO and Laracasts threads). But since that requires changing a vendor file I didn't want to do that.

I figured out you can set 'verification' for the guzzlehttp Client options off in the broadcasting.php Config file. This is how I made the chat app work fully on a clean and recent Laravel install.

However, when trying to apply what I learned on a personal project I have been working on for the last couple of years the fix did not work. I ended up with the same cURL error 60 while the code was identical to the code on my test Laravel install.

My project was running on Laravel 8 and the test project Laravel 9 so I updated the Laravel version. This seemed to have fixed it. I think the option to set the 'verify' guzzlehttp option in the broadcasting.php Config file was added from Laravel 9.

At this moment it seems to me this is mainly a Mac/Mamp related issue.

Anyhow, I will post the code that worked for me below. Maybe it can be a help to someone running into the same issue.

app/config/broadcasting.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Broadcaster
    |--------------------------------------------------------------------------
    |
    | This option controls the default broadcaster that will be used by the
    | framework when an event needs to be broadcast. You may set this to
    | any of the connections defined in the "connections" array below.
    |
    | Supported: "pusher", "ably", "redis", "log", "null"
    |
    */

    'default' => env('BROADCAST_DRIVER', 'null'),

    /*
    |--------------------------------------------------------------------------
    | Broadcast Connections
    |--------------------------------------------------------------------------
    |
    | Here you may define all of the broadcast connections that will be used
    | to broadcast events to other systems or over websockets. Samples of
    | each available type of connection are provided inside this array.
    |
    */

    'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => true,
                'encrypted' => true,
                'host' => '127.0.0.1',
                'port' => 6001,
                'scheme' => 'https',
                // for self signed ssl cert
                'curl_options' => [
                    CURLOPT_SSL_VERIFYHOST => 0,
                    CURLOPT_SSL_VERIFYPEER => 0
                ],
            ],
            'client_options' => [
                // for self signed ssl cert
                'verify' => false, // <- Added this
                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
            ],
        ],

        'ably' => [
            'driver' => 'ably',
            'key' => env('ABLY_KEY'),
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => 'default',
        ],

        'log' => [
            'driver' => 'log',
        ],

        'null' => [
            'driver' => 'null',
        ],

    ],

];

app/config/websockets.php

<?php

use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize;

return [

    /*
     * Set a custom dashboard configuration
     */
    'dashboard' => [
        'port' => env('LARAVEL_WEBSOCKETS_PORT', 6001),
    ],

    /*
     * This package comes with multi tenancy out of the box. Here you can
     * configure the different apps that can use the webSockets server.
     *
     * Optionally you specify capacity so you can limit the maximum
     * concurrent connections for a specific app.
     *
     * Optionally you can disable client events so clients cannot send
     * messages to each other via the webSockets.
     */
    'apps' => [
        [
            'id' => env('PUSHER_APP_ID'),
            'name' => env('APP_NAME'),
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'path' => env('PUSHER_APP_PATH'),
            'capacity' => null,
            'enable_client_messages' => true,
            'enable_statistics' => true,
        ],
    ],

    /*
     * This class is responsible for finding the apps. The default provider
     * will use the apps defined in this config file.
     *
     * You can create a custom provider by implementing the
     * `AppProvider` interface.
     */
    'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,

    /*
     * This array contains the hosts of which you want to allow incoming requests.
     * Leave this empty if you want to accept requests from all hosts.
     */
    'allowed_origins' => [
        //
    ],

    /*
     * The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
     */
    'max_request_size_in_kb' => 250,

    /*
     * This path will be used to register the necessary routes for the package.
     */
    'path' => 'laravel-websockets',

    /*
     * Dashboard Routes Middleware
     *
     * These middleware will be assigned to every dashboard route, giving you
     * the chance to add your own middleware to this list or change any of
     * the existing middleware. Or, you can simply stick with this list.
     */
    'middleware' => [
        'web',
        Authorize::class,
    ],

    'statistics' => [
        /*
         * This model will be used to store the statistics of the WebSocketsServer.
         * The only requirement is that the model should extend
         * `WebSocketsStatisticsEntry` provided by this package.
         */
        'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,

        /**
         * The Statistics Logger will, by default, handle the incoming statistics, store them
         * and then release them into the database on each interval defined below.
         */
        'logger' => BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger::class,

        /*
         * Here you can specify the interval in seconds at which statistics should be logged.
         */
        'interval_in_seconds' => 60,

        /*
         * When the clean-command is executed, all recorded statistics older than
         * the number of days specified here will be deleted.
         */
        'delete_statistics_older_than_days' => 60,

        /*
         * Use an DNS resolver to make the requests to the statistics logger
         * default is to resolve everything to 127.0.0.1.
         */
        'perform_dns_lookup' => false,
    ],

    /*
     * Define the optional SSL context for your WebSocket connections.
     * You can see all available options at: http://php.net/manual/en/context.ssl.php
     */
    'ssl' => [
        /*
         * Path to local certificate file on filesystem. It must be a PEM encoded file which
         * contains your certificate and private key. It can optionally contain the
         * certificate chain of issuers. The private key also may be contained
         * in a separate file specified by local_pk.
         */
        'local_cert' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT', null),

        /*
         * Path to local private key file on filesystem in case of separate files for
         * certificate (local_cert) and private key.
         */
        'local_pk' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_PK', null),

        /*
         * Passphrase for your local_cert file.
         */
        'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null),

        // for self signed ssl cert
        'verify_peer' => false,
        'verify_peer_name' => false,
    ],

    /*
     * Channel Manager
     * This class handles how channel persistence is handled.
     * By default, persistence is stored in an array by the running webserver.
     * The only requirement is that the class should implement
     * `ChannelManager` interface provided by this package.
     */
    'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
];

app/config/app.php

App\Providers\BroadcastServiceProvider::class, // <- uncommented

.env

...
BROADCAST_DRIVER=pusher
...
PUSHER_APP_ID=local
PUSHER_APP_KEY=local
PUSHER_APP_SECRET=local
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT="/Applications/MAMP/Library/OpenSSL/certs/certificate.crt" // <- Dummy file name
LARAVEL_WEBSOCKETS_SSL_LOCAL_PK="/Applications/MAMP/Library/OpenSSL/certs/certificate_key.key" // <- Dummy file name
LARAVEL_WEBSOCKETS_SSL_PASSPHRASE=""
psrz's avatar

The problem is using a unsigned certificate

When cUrl tries to interact with that server it can't because it doesn't recognize the issuer (a local developing machine)

SSL is not only about encrypting, but also authenticating the site's identity to the client that is trying to connect to said site. "Someone" has to vouch for that. Take a look at Laracasts certificate for example: Cloudfare, Inc.

But your local developing machine is "nobody" so cUrl won't trust it just like that, not without installing the public key of the certificate on the machines that connect to that websocket server and make it part of the ssl certificates bundle that curl uses when making requests.

Easier said that done. I did only once a really long time ago and it took a bunch of stackoverflow reading.

If you're planning on releasing this project to production then you will have to either purchase a ssl certificate from certificate authority (CA) or use a free one , like Letsencrypt.

I use Letsencrypt on some of my projects and it's really easy to set up if you have shell access to the server. Using a ssl from an actual CA will solve the issue of curl that you're experiencing.

So I guess my advice would be to turn off ssl for development to save yourself unnecessary aggravation

1 like
Kimmer's avatar

Thanks, I am aware of that. In my code I have added some lines below" // for self signed ssl cert" . removing these lines should make evertyhing work with a real certificate on production. "Should", we'll see ;-)

Arzlo's avatar

For anyone who stumble here, my solution was to include the self-signed certificate contents into the bundled certificate already provided by Xampp for cURL.

It is located at xampp/apache/bin/curl-ca-bundle.crt which is also the default value on curl.cainfo at php.ini

Please or to participate in this conversation.