{"id":99,"date":"2026-07-19T00:00:00","date_gmt":"2026-07-19T00:00:00","guid":{"rendered":"http:\/\/\/var\/www\/html\/?p=99"},"modified":"2026-07-19T16:42:01","modified_gmt":"2026-07-19T16:42:01","slug":"openssl-self-signed-certificate-in-certificate-chain-validation-on-macos-local-environment","status":"publish","type":"post","link":"https:\/\/staging.butitworkedlocal.com\/?p=99","title":{"rendered":"Fixing OpenSSL Self-Signed Certificate Chain Validation Errors on macOS Local Environments"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Resolve OpenSSL validation issues with self-signed certificates on macOS. Learn to trust local development certificates in Keychain Access and for various tools.<\/strong><\/p>\n\n\n<h2>Introduction<\/h2>\n\n<p>Developing locally on macOS often involves interacting with services that use self-signed SSL certificates. Whether it&#039;s a backend API, a local database, or a custom microservice running in Docker, these certificates are not inherently trusted by your operating system or development tools. This leads to frustrating &quot;certificate validation failed&quot; errors when your applications or <code>curl<\/code> commands try to establish secure connections. This guide will walk you through the precise steps to properly trust these certificates on macOS, ensuring your local development environment runs smoothly.<\/p>\n\n<h3>Symptom &amp; Error Signature<\/h3>\n\n<p>You will typically encounter errors when attempting to connect to your local HTTPS service via a web browser, <code>curl<\/code>, Node.js, Python, or other client applications. The exact error message can vary but usually points to an untrusted certificate or certificate chain.<\/p>\n\n<p><strong>Common Error Outputs:<\/strong><\/p>\n\n<ul><li>  <strong><code>curl<\/code>:<\/strong><\/li><li>  <code><\/code>`bash<\/li><li>  curl: (60) SSL certificate problem: self signed certificate in certificate chain<\/li><li>  More details here: https:\/\/curl.haxx.se\/docs\/sslcerts.html<\/li><li>  curl failed to verify the legitimacy of the server and therefore could not<\/li><li>  establish a secure connection to it. To learn more about this situation and<\/li><li>  how to fix it, please visit the web page mentioned above.<\/li><li>  <code><\/code>`<\/li><\/ul>\n\n<ul><li>  <strong>Node.js (e.g., <code>fetch<\/code> or <code>https<\/code> module):<\/strong><\/li><li>  <code><\/code>`<\/li><li>  FetchError: request to https:\/\/localhost:8443\/api failed, reason: self-signed certificate in certificate chain<\/li><li>      at ClientRequest.&lt;anonymous&gt; (\/path\/to\/node_modules\/node-fetch\/lib\/index.js:1505:11)<\/li><li>      at ClientRequest.emit (node:events:514:28)<\/li><li>      at TLSSocket.socketErrorListener (node:<em>http<\/em>client:481:9)<\/li><li>      at TLSSocket.emit (node:events:514:28)<\/li><li>      at emitErrorNT (node:internal\/streams\/destroy:151:8)<\/li><li>      at emitErrorCloseNT (node:internal\/streams\/destroy:120:3)<\/li><li>      at process.processTicksAndRejections (node:internal\/process\/task_queues:82:21) {<\/li><li>    type: &#039;system&#039;,<\/li><li>    errno: &#039;DEPTH<em>ZERO<\/em>SELF<em>SIGNED<\/em>CERT&#039;,<\/li><li>    code: &#039;DEPTH<em>ZERO<\/em>SELF<em>SIGNED<\/em>CERT&#039;<\/li><li>  }<\/li><li>  <code><\/code>`<\/li><\/ul>\n\n<ul><li>  <strong>Python (<code>requests<\/code> library):<\/strong><\/li><li>  <code><\/code>`python<\/li><li>  requests.exceptions.SSLError: HTTPSConnectionPool(host=&#039;localhost&#039;, port=8443): Max retries exceeded with url: \/api (Caused by SSLError(1, &#039;[SSL: CERTIFICATE<em>VERIFY<\/em>FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1007)&#039;))<\/li><li>  <code><\/code>`<\/li><\/ul>\n\n<ul><li>  <strong>Browser (e.g., Chrome\/Firefox):<\/strong><\/li><li>  You will see a &quot;Your connection is not private&quot; or &quot;Warning: Potential Security Risk Ahead&quot; page with an error code like <code>NET::ERR<em>CERT<\/em>AUTHORITY<em>INVALID<\/code> or <code>SEC<\/em>ERROR<em>UNKNOWN<\/em>ISSUER<\/code>.<\/li><\/ul>\n\n<h3>Root Cause Analysis<\/h3>\n\n<p>The core of the problem lies in the <em>trust chain<\/em> of your SSL certificate. When a client (browser, <code>curl<\/code>, Node.js app) connects to an HTTPS server, it receives the server&#039;s SSL certificate. The client then attempts to validate this certificate by tracing its signing authority back to a trusted Root Certificate Authority (CA).<\/p>\n\n<ol><li> <strong>Self-Signed Certificates:<\/strong> In a local development environment, you often create certificates that are &quot;self-signed,&quot; meaning the certificate is signed by its own private key, or signed by a custom Certificate Authority (CA) that <em>you<\/em> created.<\/li><li> <strong>Untrusted Authority:<\/strong> Neither your self-signed certificate nor your custom CA&#039;s certificate is recognized or pre-installed in the default trust stores of macOS or the various applications you use. These trust stores contain public certificates from globally recognized CAs (like Let&#039;s Encrypt, DigiCert, etc.).<\/li><li> <strong>macOS Keychain vs. Application Trust:<\/strong> macOS has its own system-wide trust store (Keychain Access). While many applications, especially GUI ones and those using Apple&#039;s Secure Transport framework, will leverage this, others (like <code>curl<\/code> linked to Homebrew&#039;s OpenSSL, Node.js, Python&#039;s <code>requests<\/code> library) might rely on their own bundled CA bundles or specific OpenSSL configurations. This divergence means trusting a certificate in Keychain Access might not automatically resolve issues for all tools.<\/li><li> <strong><code>certificate chain<\/code><\/strong>: The error &quot;self signed certificate in certificate chain&quot; indicates that the certificate presented by the server is part of a chain where one or more intermediate certificates, or the root certificate itself, is self-signed and not trusted. Typically, for local development, you create a <em>Root CA<\/em> and then sign your <em>server certificate<\/em> with that Root CA. The client then needs to trust <em>your Root CA<\/em>.<\/li><\/ol>\n\n<h3>Step-by-Step Resolution<\/h3>\n\n<p>The resolution involves two primary phases: ensuring you have a properly generated self-signed certificate (or CA) and then explicitly adding that certificate to the relevant trust stores on your macOS system.<\/p>\n\n<h4>1. Generate a Self-Signed Root CA and Server Certificate (If you don&#039;t have one)<\/h4>\n\n<p>If you are just using a basic, self-signed server certificate, your client may complain about a &quot;depth 0&quot; error. For a more robust local setup that mimics production, it&#039;s better to create your own Root Certificate Authority (CA) and then use it to sign individual server certificates. This allows you to trust your single CA on your development machine, and all certificates signed by it will automatically be trusted.<\/p>\n\n<blockquote><p>[!IMPORTANT]\nIf you are already using a self-signed certificate or have a custom CA certificate (e.g., from a Docker container or another local setup), you can skip this step and proceed to Step 2 with your existing CA certificate. Ensure you have the <code>.crt<\/code> (or <code>.pem<\/code>) file of your <strong>Root CA<\/strong>.<\/p><\/blockquote>\n\n<p>We&#039;ll use <code>openssl<\/code> via Homebrew on macOS.<\/p>\n\n<p>First, install or ensure OpenSSL is updated via Homebrew:\n<code><\/code>`bash\nbrew update\nbrew install openssl@3\n# Link openssl@3 if not already linked (this might prompt for sudo access or specific commands)\nbrew link openssl@3 &#8211;force\n<code><\/code>`<\/p>\n\n<p>Now, let&#039;s create a directory for our certificates and generate the CA:\n<code><\/code>`bash\nmkdir -p ~\/certs\/local-ca\ncd ~\/certs\/local-ca<\/p>\n\n<h1>1. Generate CA Private Key\nopenssl genrsa -aes256 -out ca.key 4096<\/h1>\n\n<h1>2. Create CA Certificate Request\nopenssl req -new -x509 -sha256 -days 3650 -key ca.key -out ca.crt \n    -subj &quot;\/C=US\/ST=CA\/O=Local Development\/CN=Local Development CA&quot;<\/h1>\n\n<h1>Now generate a server certificate signed by this CA:\n# We&#039;ll use example.com and localhost for the server certificate\nSERVER_NAME=&quot;localhost&quot; # Or your local dev domain, e.g., myapp.local<\/h1>\n\n<h1>1. Generate Server Private Key\nopenssl genrsa -out $SERVER_NAME.key 2048<\/h1>\n\n<h1>2. Create Server Certificate Signing Request (CSR)\nopenssl req -new -key $SERVER<em>NAME.key -out $SERVER<\/em>NAME.csr \n    -subj &quot;\/C=US\/ST=CA\/O=Local Development\/CN=$SERVER_NAME&quot;<\/h1>\n\n<h1>3. Create a V3 Ext File for SAN (Subject Alternative Name)\n# This is crucial for modern browsers and clients which require SAN.\ncat &lt;&lt;EOF &gt; $SERVER_NAME.ext\nauthorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName = @alt_names<\/h1>\n\n<p>[alt_names]\nDNS.1 = $SERVER_NAME\nDNS.2 = localhost\nIP.1 = 127.0.0.1\nEOF<\/p>\n\n<h1>4. Sign the Server Certificate with your CA\nopenssl x509 -req -in $SERVER_NAME.csr -CA ca.crt -CAkey ca.key -CAcreateserial \n    -out $SERVER<em>NAME.crt -days 365 -sha256 -extfile $SERVER<\/em>NAME.ext<\/h1>\n\n<p>echo &quot;Certificates generated in ~\/certs\/local-ca:&quot;\nls -l ~\/certs\/local-ca\n<code><\/code>`\nYou now have <code>ca.crt<\/code> (your Root CA certificate) and <code>$SERVER_NAME.crt<\/code> (your server certificate) along with their respective keys. The <code>ca.crt<\/code> is what needs to be trusted.<\/p>\n\n<h4>2. Trust the Root CA Certificate in macOS Keychain Access<\/h4>\n\n<p>This step adds your custom Root CA to the macOS system trust store, which many applications (especially browsers and those using Apple&#039;s Secure Transport) will automatically use.<\/p>\n\n<pre><code class=\"language-bash\"># Assuming your CA certificate is named ca.crt and is in ~\/certs\/local-ca\nsudo security add-trusted-cert -d -r trustRoot -k \/Library\/Keychains\/System.keychain ~\/certs\/local-ca\/ca.crt\n```<\/code><\/pre>\n\n<p>After running the command:\n1.  Open <strong>Keychain Access.app<\/strong> (Applications &gt; Utilities &gt; Keychain Access).\n2.  Select &quot;System&quot; under &quot;Keychains&quot; on the left sidebar.\n3.  Select &quot;Certificates&quot; under &quot;Category&quot; on the left sidebar.\n4.  Search for &quot;Local Development CA&quot; (or whatever CN you used).\n5.  Double-click your CA certificate.\n6.  Expand the &quot;Trust&quot; section.\n7.  For &quot;When using this certificate:&quot;, select &quot;Always Trust&quot; from the dropdown.\n8.  Close the window, and you&#039;ll be prompted to enter your password again to save changes.<\/p>\n\n<blockquote><p>[!IMPORTANT]\nSome applications, especially those from Homebrew, might use a different OpenSSL configuration that doesn&#039;t directly leverage the macOS Keychain. This requires additional steps.<\/p><\/blockquote>\n\n<h4>3. Configure Applications and Development Runtimes to Use the Trusted CA<\/h4>\n\n<p>Even after adding to Keychain, some applications might still fail. This is because they might be looking for certificates in specific locations or using their own bundled CAs.<\/p>\n\n<h5>a. For <code>curl<\/code> and Homebrew-installed OpenSSL<\/h5>\n\n<p>Homebrew-installed OpenSSL typically looks for trusted certificates in <code>\/usr\/local\/etc\/openssl@3\/certs\/<\/code>. You need to symlink your CA certificate there and then rehash the directory.<\/p>\n\n<pre><code class=\"language-bash\"># Assuming your CA certificate is ~\/certs\/local-ca\/ca.crt\nCA_CERT_PATH=~\/certs\/local-ca\/ca.crt<\/code><\/pre>\n\n<h1>Create a symlink to your CA cert in OpenSSL&#039;s certs directory\nln -s &quot;$CA<em>CERT<\/em>PATH&quot; &quot;$OPENSSL<em>CERTS<\/em>DIR\/$(openssl x509 -hash -noout -in &quot;$CA<em>CERT<\/em>PATH&quot;).0&quot;<\/h1>\n\n<h1>Update OpenSSL&#039;s certificate trust store\n# This command re-scans the directory and creates necessary links\n\/usr\/local\/opt\/openssl@3\/bin\/c<em>rehash &quot;$OPENSSL<\/em>CERTS_DIR&quot;\n<code><\/code>`<\/h1>\n\n<p>Now, test <code>curl<\/code>:\n<code><\/code>`bash\ncurl https:\/\/localhost:8443\/ # Replace with your local service URL\n<code><\/code>`\nIt should now connect successfully without <code>-k<\/code> or <code>--insecure<\/code>.<\/p>\n\n<h5>b. For Node.js Applications<\/h5>\n\n<p>Node.js can be configured using the <code>NODE<em>EXTRA<\/em>CA_CERTS<\/code> environment variable.<\/p>\n\n<pre><code class=\"language-bash\"># Add this to your shell profile (.bashrc, .zshrc, .profile)<\/code><\/pre>\n\n<h1>Or set it when running a specific command\nNODE<em>EXTRA<\/em>CA<em>CERTS=~\/certs\/local-ca\/ca.crt node your<\/em>app.js\n<code><\/code>`<\/h1>\n\n<blockquote><p>[!NOTE]\nFor Electron apps or webviews, ensuring the certificate is trusted in Keychain Access (Step 2) is usually sufficient, as they often leverage the system&#039;s trust store.<\/p><\/blockquote>\n\n<h5>c. For Python Applications (e.g., <code>requests<\/code> library)<\/h5>\n\n<p>Python&#039;s <code>requests<\/code> library often uses <code>certifi<\/code> which has its own bundle. You can specify an additional CA bundle.<\/p>\n\n<pre><code class=\"language-bash\"># Add this to your shell profile (.bashrc, .zshrc, .profile)<\/code><\/pre>\n\n<h1>Or set it when running a specific script\nREQUESTS<em>CA<\/em>BUNDLE=~\/certs\/local-ca\/ca.crt python your_script.py\n<code><\/code>`<\/h1>\n\n<p>Alternatively, you can modify the <code>certifi<\/code> bundle directly (less recommended for maintainability) or pass the <code>verify<\/code> parameter to <code>requests<\/code> (not recommended for general use, but useful for debugging).<\/p>\n\n<h5>d. For PHP Applications (cURL extension)<\/h5>\n\n<p>PHP&#039;s cURL extension, when compiled against OpenSSL, might need the <code>CURLOPT_CAINFO<\/code> option or a global <code>curl.cainfo<\/code> setting.<\/p>\n\n<pre><code class=\"language-php\">&lt;?php\n$ch = curl_init();\ncurl_setopt($ch, CURLOPT_URL, &quot;https:\/\/localhost:8443\/&quot;);\ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n\/\/ Specify the path to your CA certificate\ncurl_setopt($ch, CURLOPT_CAINFO, &quot;\/Users\/youruser\/certs\/local-ca\/ca.crt&quot;);\n$output = curl_exec($ch);\nif (curl_errno($ch)) {\n    echo &#039;Curl error: &#039; . curl_error($ch);\n}\ncurl_close($ch);\necho $output;\n?&gt;<\/code><\/pre>\n\n<p>For a more global approach, you can set the <code>SSL<em>CERT<\/em>FILE<\/code> environment variable for the PHP process or ensure the OpenSSL <code>ca-certificates<\/code> package (if applicable to your PHP setup, e.g., via Homebrew or Docker) is updated with your CA.<\/p>\n\n<h5>e. For Ruby Applications<\/h5>\n\n<p>Ruby&#039;s <code>Net::HTTP<\/code> and other libraries that use OpenSSL typically respect the <code>SSL<em>CERT<\/em>FILE<\/code> or <code>SSL<em>CERT<\/em>DIR<\/code> environment variables.<\/p>\n\n<pre><code class=\"language-bash\"># Add this to your shell profile (.bashrc, .zshrc, .profile)<\/code><\/pre>\n\n<h1>Or set it when running a specific script\nSSL<em>CERT<\/em>FILE=~\/certs\/local-ca\/ca.crt ruby your_script.rb\n<code><\/code>`<\/h1>\n\n<h5>f. For Git<\/h5>\n\n<p>Git can be configured to trust your CA certificate.<\/p>\n\n<pre><code class=\"language-bash\">git config --global http.sslCAInfo ~\/certs\/local-ca\/ca.crt<\/code><\/pre>\n\n<blockquote><p>[!WARNING]\nWhile <code>git config --global http.sslVerify false<\/code> can temporarily fix issues, it&#039;s a <strong>security risk<\/strong> as it disables all SSL verification. Only use it for temporary debugging and never in production or for sensitive operations. Always prefer to properly trust the certificate.<\/p><\/blockquote>\n\n<h4>4. Restart Services and Applications<\/h4>\n\n<p>After making changes to environment variables or adding certificates, it&#039;s crucial to restart any applications, terminals, or services that rely on these configurations. A full system reboot might sometimes be necessary to ensure all processes pick up the new trust settings, especially for GUI applications.<\/p>\n\n<p>For Docker containers, if your container is the one initiating the connection to an external (or host-local) HTTPS service and needs to trust your CA, you&#039;ll need to inject your <code>ca.crt<\/code> into the container&#039;s trust store.<\/p>\n\n<p><strong>Example for a Debian\/Ubuntu-based Docker container:<\/strong><\/p>\n\n<ol><li> Copy your CA certificate into the container&#039;s build context.<\/li><li> Modify your <code>Dockerfile<\/code>:<\/li><li>    <code><\/code>`dockerfile<\/li><li>    # In your Dockerfile<\/li><li>    COPY ~\/certs\/local-ca\/ca.crt \/usr\/local\/share\/ca-certificates\/local-dev-ca.crt<\/li><li>    RUN chmod 644 \/usr\/local\/share\/ca-certificates\/local-dev-ca.crt &amp;&amp; update-ca-certificates<\/li><li>    <code><\/code>`<\/li><li>    This adds your CA to the container&#039;s trust store, making applications inside the container trust it.<\/li><\/ol>\n\n<p>By following these detailed steps, you should successfully overcome the &quot;self signed certificate in certificate chain validation&quot; errors on your macOS local development environment, leading to a much smoother and more secure development workflow.<\/p>","protected":false},"excerpt":{"rendered":"<p>Resolve OpenSSL validation issues with self-signed certificates on macOS. Learn to trust local development certificates in Keychain Access and for various tools.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[41,48,47,240,268,27],"class_list":["post-99","post","type-post","status-publish","format-standard","hentry","category-ssl-certs","tag-certificate-validation","tag-local-development","tag-macos","tag-openssl","tag-self-signed","tag-ssl"],"_links":{"self":[{"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/posts\/99","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=99"}],"version-history":[{"count":1,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/posts\/99\/revisions"}],"predecessor-version":[{"id":218,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=\/wp\/v2\/posts\/99\/revisions\/218"}],"wp:attachment":[{"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=99"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=99"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/staging.butitworkedlocal.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=99"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}