{"id":14,"date":"2026-07-16T00:00:00","date_gmt":"2026-07-16T00:00:00","guid":{"rendered":"http:\/\/\/var\/www\/html\/?p=14"},"modified":"2026-07-21T01:02:30","modified_gmt":"2026-07-21T01:02:30","slug":"caddy-reverse-proxy-tls-handshake-failed-verification-error-on-windows-wsl2-ubuntu","status":"publish","type":"post","link":"https:\/\/butitworkedlocal.com\/caddy-reverse-proxy-tls-handshake-failed-verification-error-on-windows-wsl2-ubuntu\/","title":{"rendered":"Caddy Reverse Proxy: Resolving TLS Handshake Failed Verification (x509) on Windows WSL2 Ubuntu"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Troubleshoot &#039;TLS handshake failed verification&#039; when Caddy in WSL2 acts as a reverse proxy. Learn to manage certificate trust for backend services.<\/strong><\/p>\n\n\n<h2>Caddy Reverse Proxy: Resolving TLS Handshake Failed Verification (x509) on Windows WSL2 Ubuntu<\/h2>\n<p>When operating Caddy as a reverse proxy within a Windows Subsystem for Linux 2 (WSL2) Ubuntu environment, you might encounter errors related to TLS handshake verification. This typically happens when Caddy attempts to establish a secure connection to your backend service (e.g., a web application, API, or another internal server) but fails to validate the backend&#039;s TLS certificate. The result is an inability for Caddy to proxy requests, leading to unresponsive services or error pages for your users.<\/p>\n<p>This guide will walk you through diagnosing and resolving the common causes of &quot;TLS handshake failed verification&quot; errors, particularly focusing on certificate trust issues within the WSL2 environment.<\/p>\n<h3>Symptom &amp; Error Signature<\/h3>\n<p>Users attempting to access your service through Caddy will likely see generic browser errors such as <code>NET::ERR_CERT_INVALID<\/code>, <code>ERR_SSL_PROTOCOL_ERROR<\/code>, or <code>502 Bad Gateway<\/code> if Caddy cannot successfully connect to the backend.<\/p>\n<p>The most telling sign will be in your Caddy logs. You&#039;ll observe entries similar to these, indicating Caddy&#039;s struggle to trust the backend certificate:<\/p>\n<pre><code class=\"language-log\">ERROR   http.log.error  reverse proxy: selected backend is unhealthy: TLS handshake failed: remote error: tls: handshake failure\nERROR   http.log.error  reverse proxy: x509: certificate signed by unknown authority\nERROR   http.log.error  reverse proxy: tls: failed to verify certificate: x509: certificate signed by unknown authority\nERROR   http.log.error  reverse proxy: dial tcp 172.17.0.2:443: connect: connection refused\n<\/code><\/pre>\n<p>The specific messages <code>x509: certificate signed by unknown authority<\/code> or <code>tls: failed to verify certificate<\/code> are key indicators of a certificate trust problem.<\/p>\n<h3>Root Cause Analysis<\/h3>\n<p>The &quot;TLS handshake failed verification&quot; error, especially with <code>x509: certificate signed by unknown authority<\/code>, means that Caddy, acting as a client to your backend server, received a certificate but couldn&#039;t validate its authenticity using its list of trusted Certificate Authorities (CAs). This usually stems from one or more of the following:<\/p>\n<ol>\n<li><strong>Self-Signed Certificates:<\/strong> The backend server is using a self-signed TLS certificate. These certificates are not issued by a publicly trusted CA and therefore are inherently untrusted by default.<\/li>\n<li><strong>Internal CA Certificates:<\/strong> The backend server&#039;s certificate is issued by an internal or private Certificate Authority (CA) specific to your organization or development setup. WSL2&#039;s Ubuntu instance, by default, only trusts widely recognized public CAs.<\/li>\n<li><strong>Missing Intermediate Certificates:<\/strong> The backend server might be serving its certificate without providing the full chain of intermediate certificates, preventing Caddy from building a trusted path to a known root CA.<\/li>\n<li><strong>Incorrect Caddy Configuration:<\/strong> The <code>reverse_proxy<\/code> directive might be pointing to the wrong IP address or port, or is missing necessary TLS configuration options.<\/li>\n<li><strong>Network\/Firewall Issues:<\/strong> Though less likely to cause a &quot;TLS handshake failed verification&quot; specifically (which implies a connection was made and a certificate received), underlying network connectivity issues or firewall blocks could result in similar high-level errors like <code>502 Bad Gateway<\/code> and precede this specific TLS error, showing as <code>connection refused<\/code> initially.<\/li>\n<li><strong>Expired or Invalid Certificates:<\/strong> The backend&#039;s certificate might be expired, revoked, or have a hostname mismatch, leading to validation failure.<\/li>\n<\/ol>\n<p>The most prevalent causes for WSL2 scenarios involving internal services are self-signed or internal CA certificates, as WSL2&#039;s Ubuntu environment needs explicit instruction to trust them.<\/p>\n<h3>Step-by-Step Resolution<\/h3>\n<p>Follow these steps to diagnose and resolve the TLS handshake verification error.<\/p>\n<h4>1. Verify Caddy Configuration<\/h4>\n<p>First, ensure your <code>Caddyfile<\/code>&#039;s <code>reverse_proxy<\/code> directive is correctly configured to point to your backend service&#039;s address and port.<\/p>\n<pre><code class=\"language-caddyfile\">yourdomain.com {\n    reverse_proxy https:\/\/your_backend_ip_or_hostname:port {\n        # Other directives if needed\n    }\n}\n<\/code><\/pre>\n<p>Make sure you&#039;re using <code>https:\/\/<\/code> if your backend is serving over TLS, which is the assumption for this error.<\/p>\n<h4>2. Inspect Caddy Logs for Detailed Error Messages<\/h4>\n<p>The Caddy logs are your primary diagnostic tool. If Caddy is running as a <code>systemd<\/code> service, you can retrieve logs using <code>journalctl<\/code>.<\/p>\n<pre><code class=\"language-bash\">sudo journalctl -u caddy -f --since &quot;5 minutes ago&quot;\n<\/code><\/pre>\n<p>Look for the exact error message string. <code>x509: certificate signed by unknown authority<\/code> is a strong indicator that Caddy does not trust the CA that signed your backend&#039;s certificate.<\/p>\n<h4>3. Determine Your Backend Certificate&#039;s Origin<\/h4>\n<p>You need to know if your backend&#039;s TLS certificate is:<\/p>\n<ul>\n<li>Issued by a publicly trusted CA (e.g., Let&#039;s Encrypt, DigiCert).<\/li>\n<li>Self-signed.<\/li>\n<li>Issued by an internal\/private CA.<\/li>\n<\/ul>\n<p>You can inspect the certificate directly from WSL2 using <code>openssl<\/code>. Replace <code>your_backend_ip_or_hostname:port<\/code> with your actual backend&#039;s address.<\/p>\n<pre><code class=\"language-bash\">openssl s_client -connect your_backend_ip_or_hostname:port -showcerts &lt;\/dev\/null 2&gt;\/dev\/null | openssl x509 -text -noout | grep &quot;Issuer:|Subject:|Not Before:|Not After:&quot;\n<\/code><\/pre>\n<p>Pay close attention to the <code>Issuer<\/code> field. If it&#039;s your own company, a custom name, or the same as <code>Subject<\/code>, it&#039;s likely a self-signed or internal CA certificate.<\/p>\n<h4>4. Add the CA Certificate to WSL2&#039;s Trust Store (Recommended)<\/h4>\n<p>This is the most robust solution for internal or self-signed certificates. You need to obtain the root or intermediate CA certificate that signed your backend&#039;s certificate and add it to your WSL2 Ubuntu instance&#039;s system-wide trust store.<\/p>\n<h5>a. Obtain the CA Certificate File<\/h5>\n<p>You might receive this file from your internal IT department, or you can extract it if you have access to the backend server. If you only have the backend server&#039;s <em>leaf<\/em> certificate, you&#039;ll need the CA certificate that signed it.<\/p>\n<blockquote class=\"important\"><p>The certificate file <strong>must<\/strong> be in PEM format (base64-encoded ASCII with <code>-----BEGIN CERTIFICATE-----<\/code> and <code>-----END CERTIFICATE-----<\/code> headers) and typically have a <code>.crt<\/code> extension.<\/p>\n<\/blockquote>\n<p>If you don&#039;t have the <code>.crt<\/code> file directly, you can often extract it from the backend server&#039;s certificate chain. For example, if you can <code>curl<\/code> your backend (even if it fails validation, you might get the cert):<\/p>\n<pre><code class=\"language-bash\"># This command tries to fetch the certificate chain from your backend\n# and save it as backend_chain.pem. You&#039;ll then need to identify\n# and extract the CA cert from this chain.\nopenssl s_client -showcerts -verify 5 -connect your_backend_ip_or_hostname:port &lt; \/dev\/null | awk &#039;\/BEGIN CERTIFICATE\/,\/END CERTIFICATE\/{ print $0 }&#039; &gt; backend_chain.pem\n<\/code><\/pre>\n<p>Examine <code>backend_chain.pem<\/code>. You&#039;ll typically find multiple certificates. The root or intermediate CA certificate is usually one that <em>doesn&#039;t<\/em> have <code>Subject<\/code> matching your backend&#039;s domain but <em>does<\/em> have <code>Issuer<\/code> matching its own <code>Subject<\/code> (for a root) or an upstream CA (for an intermediate). Extract this specific CA certificate into its own <code>.crt<\/code> file (e.g., <code>my-internal-ca.crt<\/code>).<\/p>\n<h5>b. Install the CA Certificate in WSL2 Ubuntu<\/h5>\n<ol>\n<li><p>Copy your <code>.crt<\/code> file (e.g., <code>my-internal-ca.crt<\/code>) into the <code>\/usr\/local\/share\/ca-certificates\/<\/code> directory within your WSL2 Ubuntu instance.<\/p>\n<pre><code class=\"language-bash\">sudo cp \/path\/to\/my-internal-ca.crt \/usr\/local\/share\/ca-certificates\/\n<\/code><\/pre>\n<\/li>\n<li><p>Update the system&#039;s CA certificate store.<\/p>\n<pre><code class=\"language-bash\">sudo update-ca-certificates\n<\/code><\/pre>\n<p>You should see output similar to <code>Adding new cert into \/etc\/ssl\/certs\/<\/code> and <code>1 added, 0 removed; done.<\/code>.<\/p>\n<\/li>\n<li><p>Restart Caddy to pick up the new trust store.<\/p>\n<pre><code class=\"language-bash\">sudo systemctl restart caddy\n<\/code><\/pre>\n<p>Check Caddy logs again (<code>sudo journalctl -u caddy -f<\/code>) to ensure the error is resolved.<\/p>\n<\/li>\n<\/ol>\n<h4>5. Configure Caddy with <code>tls_trusted_ca_certs<\/code> (Alternative\/Specific)<\/h4>\n<p>If you prefer not to add the CA certificate system-wide, or if you need more granular control, you can specify the trusted CA certificate directly in your <code>Caddyfile<\/code> for a specific <code>reverse_proxy<\/code> block.<\/p>\n<pre><code class=\"language-caddyfile\">yourdomain.com {\n    reverse_proxy https:\/\/your_backend_ip_or_hostname:port {\n        tls_trusted_ca_certs \/path\/to\/my-internal-ca.crt\n    }\n}\n<\/code><\/pre>\n<blockquote><p>Ensure the <code>\/path\/to\/my-internal-ca.crt<\/code> is accessible by the Caddy user and specifies the path <em>within your WSL2 filesystem<\/em>.<\/p>\n<\/blockquote>\n<p>After modifying your <code>Caddyfile<\/code>, reload Caddy:<\/p>\n<pre><code class=\"language-bash\">sudo systemctl reload caddy\n# Or if reload fails to pick up all changes:\nsudo systemctl restart caddy\n<\/code><\/pre>\n<h4>6. Temporarily Skip TLS Verification (Development\/Last Resort)<\/h4>\n<blockquote class=\"warning\"><p>Using <code>tls_insecure_skip_verify<\/code> <strong>is highly discouraged in production environments<\/strong>. It completely disables TLS certificate validation, making your connection vulnerable to Man-in-the-Middle (MITM) attacks and compromising the security of your data. Only use this for development or debugging where security implications are fully understood and accepted.<\/p>\n<\/blockquote>\n<p>If you&#039;re in a development environment and absolutely need to proceed without certificate validation, you can instruct Caddy to skip verification:<\/p>\n<pre><code class=\"language-caddyfile\">yourdomain.com {\n    reverse_proxy https:\/\/your_backend_ip_or_hostname:port {\n        tls_insecure_skip_verify\n    }\n}\n<\/code><\/pre>\n<p>Reload or restart Caddy after this change. Remove this directive as soon as you implement a proper certificate trust solution.<\/p>\n<h4>7. Verify Network Connectivity and DNS Resolution<\/h4>\n<p>If you&#039;re still facing issues, or if Caddy logs initially showed <code>connection refused<\/code> before <code>TLS handshake failed<\/code>, verify network connectivity from your WSL2 instance to the backend.<\/p>\n<ol>\n<li><p><strong>Ping the backend:<\/strong><\/p>\n<pre><code class=\"language-bash\">ping your_backend_ip_or_hostname\n<\/code><\/pre>\n<\/li>\n<li><p><strong>Test direct HTTPS connection with <code>curl<\/code> from WSL2:<\/strong><\/p>\n<pre><code class=\"language-bash\">curl -v https:\/\/your_backend_ip_or_hostname:port\/your_health_check_path\n<\/code><\/pre>\n<p>This <code>curl<\/code> command will show detailed TLS handshake information and certificate validation steps, similar to how Caddy operates. If <code>curl<\/code> fails with <code>SSL certificate problem: unable to get local issuer certificate<\/code>, it confirms the trust store issue. If it fails with <code>Connection refused<\/code>, it indicates a network or firewall problem.<\/p>\n<\/li>\n<li><p><strong>Check DNS Resolution:<\/strong> If using a hostname, ensure WSL2 can resolve it. Add an entry to <code>\/etc\/hosts<\/code> if it&#039;s an internal hostname not resolved by your DNS server.<\/p>\n<pre><code class=\"language-bash\"># Example \/etc\/hosts entry\n192.168.1.100   your_backend_hostname\n<\/code><\/pre>\n<\/li>\n<\/ol>\n<h4>8. Check Backend Certificate Expiry and Hostname<\/h4>\n<p>Ensure the backend server&#039;s certificate is not expired and that its <code>Subject Alternative Name<\/code> (SAN) or <code>Common Name<\/code> (CN) matches the hostname or IP address Caddy is using to connect.<\/p>\n<pre><code class=\"language-bash\">openssl s_client -connect your_backend_ip_or_hostname:port -showcerts &lt;\/dev\/null 2&gt;\/dev\/null | openssl x509 -text -noout | grep -E &quot;Subject Alternative Name|Not Before:|Not After:&quot;\n<\/code><\/pre>\n<p>If the certificate is expired or the hostname doesn&#039;t match, you&#039;ll need to renew or re-issue the backend&#039;s certificate.<\/p>\n<p>By systematically following these steps, you should be able to diagnose and resolve the &quot;TLS handshake failed verification&quot; error with Caddy acting as a reverse proxy in your Windows WSL2 Ubuntu environment. Prioritize adding trusted CA certificates to the system store for a secure and robust solution.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Troubleshoot &#8216;TLS handshake failed verification&#8217; when Caddy in WSL2 acts as a reverse proxy. Learn to manage certificate trust for backend services.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[36,43,40,37,28,13,12,42],"class_list":["post-14","post","type-post","status-publish","format-standard","hentry","category-web-server","tag-caddy","tag-certificate-trust","tag-devops","tag-reverse-proxy","tag-tls","tag-ubuntu","tag-wsl2","tag-x509"],"_links":{"self":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/14","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/comments?post=14"}],"version-history":[{"count":2,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/14\/revisions"}],"predecessor-version":[{"id":262,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/14\/revisions\/262"}],"wp:attachment":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/media?parent=14"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/categories?post=14"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/tags?post=14"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}