{"id":13,"date":"2026-07-16T00:00:00","date_gmt":"2026-07-16T00:00:00","guid":{"rendered":"http:\/\/\/var\/www\/html\/?p=13"},"modified":"2026-07-21T01:02:30","modified_gmt":"2026-07-21T01:02:30","slug":"caddy-reverse-proxy-tls-handshake-failed-verification-error-on-ubuntu-2004-lts","status":"publish","type":"post","link":"https:\/\/butitworkedlocal.com\/caddy-reverse-proxy-tls-handshake-failed-verification-error-on-ubuntu-2004-lts\/","title":{"rendered":"Troubleshooting Caddy Reverse Proxy TLS Handshake Failed Verification on Ubuntu 20.04 LTS"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Resolve Caddy reverse proxy TLS handshake failures with upstream servers on Ubuntu 20.04 LTS. Diagnose certificate validation, SNI, and CA issues for seamless reverse proxy operations.<\/strong><\/p>\n\n\n<h2>Introduction<\/h2>\n<p>Caddy has established itself as a powerful, easy-to-use web server and reverse proxy, renowned for its automatic HTTPS capabilities. However, when operating Caddy as a reverse proxy, you might encounter situations where it fails to establish a secure (TLS) connection with its upstream backend server. A &quot;TLS handshake failed verification&quot; error signifies that Caddy initiated a TLS connection but was unable to validate the certificate presented by the upstream server. This guide provides a highly technical, step-by-step approach to diagnose and resolve this issue specifically on Ubuntu 20.04 LTS environments.<\/p>\n<h2>### Symptom &amp; Error Signature<\/h2>\n<p>When Caddy experiences a TLS handshake failure with an upstream, client requests attempting to reach the proxied service will typically receive a <code>502 Bad Gateway<\/code> error. In the Caddy server logs, you&#039;ll find entries indicating certificate validation problems.<\/p>\n<p>To view Caddy&#039;s logs, use <code>journalctl<\/code>:<\/p>\n<pre><code class=\"language-bash\">sudo journalctl -u caddy -f --no-hostname\n<\/code><\/pre>\n<p>Typical error signatures you might observe include:<\/p>\n<pre><code>{\n  &quot;level&quot;: &quot;error&quot;,\n  &quot;ts&quot;: 1678886400.000,\n  &quot;logger&quot;: &quot;http.log.error&quot;,\n  &quot;msg&quot;: &quot;x509: certificate signed by unknown authority&quot;,\n  &quot;trace&quot;: &quot;...&quot;,\n  &quot;request&quot;: {\n    &quot;method&quot;: &quot;GET&quot;,\n    &quot;uri&quot;: &quot;\/api\/data&quot;,\n    &quot;proto&quot;: &quot;HTTP\/2.0&quot;,\n    &quot;remote_addr&quot;: &quot;192.168.1.100:54321&quot;,\n    &quot;host&quot;: &quot;your-caddy-domain.com&quot;,\n    &quot;headers&quot;: {\n      &quot;User-Agent&quot;: [&quot;Mozilla\/5.0 ...&quot;],\n      &quot;Accept&quot;: [&quot;*\/*&quot;]\n    }\n  },\n  &quot;error&quot;: &quot;reverse proxy: selected upstream is unhealthy: connection to upstream timed out after 10s&quot;\n}\n<\/code><\/pre>\n<p>Or a more generic handshake failure:<\/p>\n<pre><code>{\n  &quot;level&quot;: &quot;error&quot;,\n  &quot;ts&quot;: 1678886405.000,\n  &quot;logger&quot;: &quot;http.handlers.reverse_proxy&quot;,\n  &quot;msg&quot;: &quot;aborting with incomplete response&quot;,\n  &quot;error&quot;: &quot;remote error: tls: handshake failure&quot;,\n  &quot;duration&quot;: 0.0001\n}\n<\/code><\/pre>\n<p>Or an SNI related error:<\/p>\n<pre><code>{\n  &quot;level&quot;: &quot;error&quot;,\n  &quot;ts&quot;: 1678886410.000,\n  &quot;logger&quot;: &quot;http.log.error&quot;,\n  &quot;msg&quot;: &quot;tls: failed to verify certificate: x509: certificate is valid for example.com, not your-upstream-hostname.internal&quot;,\n  &quot;trace&quot;: &quot;...&quot;,\n  &quot;error&quot;: &quot;reverse proxy: selected upstream is unhealthy: connection to upstream timed out after 10s&quot;\n}\n<\/code><\/pre>\n<h2>### Root Cause Analysis<\/h2>\n<p>The &quot;TLS handshake failed verification&quot; error fundamentally means that Caddy, acting as a TLS client to the upstream server, could not trust the certificate presented by that server. The most common underlying reasons for this include:<\/p>\n<ol>\n<li><strong>Untrusted Certificate Authority (CA)<\/strong>: The upstream server&#039;s certificate is signed by a CA that is not present or trusted in Caddy&#039;s (or the underlying system&#039;s) trust store. This is common with self-signed certificates, private enterprise CAs, or newly issued CA roots not yet widely distributed.<\/li>\n<li><strong>Certificate Name Mismatch (SNI\/SAN)<\/strong>:<ul>\n<li><strong>Common Name (CN) \/ Subject Alternative Name (SAN) Mismatch<\/strong>: The hostname Caddy is attempting to connect to (or the <code>Host<\/code> header it&#039;s sending) does not match the CN or SANs listed within the upstream server&#039;s certificate.<\/li>\n<li><strong>Incorrect Server Name Indication (SNI)<\/strong>: Caddy might not be sending the correct SNI hostname during the TLS handshake, causing the upstream server (especially if it hosts multiple virtual hosts with different certificates) to present the wrong certificate or refuse the connection.<\/li>\n<\/ul>\n<\/li>\n<li><strong>Expired or Invalid Certificate<\/strong>: The upstream server&#039;s certificate has expired, is not yet valid, or has been revoked.<\/li>\n<li><strong>Network\/Firewall Issues<\/strong>: While less direct for &quot;verification failure,&quot; network problems or restrictive firewalls between Caddy and the upstream could interrupt the TLS handshake process, leading to a perceived failure.<\/li>\n<li><strong>System Time Skew<\/strong>: Significant clock drift on either the Caddy server or the upstream server can cause certificate validity checks (e.g., <code>notBefore<\/code>, <code>notAfter<\/code> dates) to fail.<\/li>\n<\/ol>\n<h2>### Step-by-Step Resolution<\/h2>\n<p>Follow these steps to diagnose and resolve the TLS handshake verification error.<\/p>\n<h4>1. Verify Upstream Server&#039;s Certificate Directly<\/h4>\n<p>Before troubleshooting Caddy, confirm the upstream server&#039;s certificate is valid and correctly configured from the perspective of the Caddy server&#039;s host.<\/p>\n<pre><code class=\"language-bash\"># Replace &#039;your_upstream_hostname&#039; with the actual hostname Caddy connects to\n# Replace &#039;upstream_ip_address&#039; with the upstream server&#039;s IP\n# Replace &#039;upstream_port&#039; with the port the upstream listens on (e.g., 443, 8443)\nopenssl s_client -connect your_upstream_ip_address:upstream_port -servername your_upstream_hostname &lt;\/dev\/null\n<\/code><\/pre>\n<p>Inspect the output carefully:<\/p>\n<ul>\n<li><strong><code>Verify return code: 0 (ok)<\/code><\/strong>: Indicates the certificate chain is trusted by the system&#039;s CA store. If you see anything else (e.g., <code>20 (unable to get local issuer certificate)<\/code>, <code>21 (unable to verify the first certificate)<\/code>), it points to an untrusted CA.<\/li>\n<li><strong><code>Not Before<\/code> \/ <code>Not After<\/code><\/strong>: Check if the certificate is within its valid date range.<\/li>\n<li><strong><code>Subject Common Name (CN)<\/code> \/ <code>Subject Alternative Name (SAN)<\/code><\/strong>: Ensure <code>your_upstream_hostname<\/code> is listed here. If not, you have a hostname mismatch.<\/li>\n<li><strong><code>depth<\/code> and <code>issuer<\/code><\/strong>: Review the certificate chain to identify the issuing CA.<\/li>\n<\/ul>\n<h4>2. Update System CA Trust Store (for Untrusted CAs)<\/h4>\n<p>If <code>openssl s_client<\/code> returned an error indicating an untrusted issuer (e.g., <code>Verify return code: 20<\/code>), you need to add the upstream&#039;s CA certificate to Caddy&#039;s system trust store. This is common for self-signed certificates or private enterprise CAs.<\/p>\n<ol>\n<li><p><strong>Obtain the CA Certificate<\/strong>:\nIf you have the upstream CA&#039;s <code>.crt<\/code> file, copy it. If not, you can often extract it using <code>openssl<\/code>:<\/p>\n<pre><code class=\"language-bash\"># Connect to the upstream and extract the full certificate chain\nopenssl s_client -showcerts -connect your_upstream_ip_address:upstream_port -servername your_upstream_hostname &lt;\/dev\/null &gt; upstream_chain.pem\n\n# Open &#039;upstream_chain.pem&#039; and identify the root CA certificate.\n# It&#039;s usually the last certificate in the chain and self-signed (Issuer and Subject are the same).\n# Copy its content (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n# into a new file, e.g., &#039;upstream_ca.crt&#039;.\n<\/code><\/pre>\n<\/li>\n<li><p><strong>Add the CA Certificate to System Trust<\/strong>:<\/p>\n<pre><code class=\"language-bash\">sudo cp \/path\/to\/upstream_ca.crt \/usr\/local\/share\/ca-certificates\/\nsudo update-ca-certificates\n<\/code><\/pre>\n<p>You should see output similar to: <code>Updating certificates in \/etc\/ssl\/certs... 1 added, 0 removed; done.<\/code><\/p>\n<\/li>\n<li><p><strong>Restart Caddy<\/strong>:<\/p>\n<pre><code class=\"language-bash\">sudo systemctl restart caddy\nsudo journalctl -u caddy -f --no-hostname # Check logs for success\n<\/code><\/pre>\n<\/li>\n<\/ol>\n<blockquote class=\"important\"><p>Ensure the CA certificate has a <code>.crt<\/code> extension when copied to <code>\/usr\/local\/share\/ca-certificates\/<\/code>. The <code>update-ca-certificates<\/code> utility only processes files with this extension in that directory by default.<\/p>\n<\/blockquote>\n<h4>3. Configure Caddy for Specific Upstream TLS Trust (Caddyfile Directive)<\/h4>\n<p>For more granular control, especially if you only want Caddy to trust a specific custom CA for a particular upstream and not globally, you can configure this directly in your <code>Caddyfile<\/code>.<\/p>\n<ol>\n<li><p><strong>Place CA Certificate<\/strong>: Store the upstream CA certificate file (e.g., <code>upstream_ca.crt<\/code>) in a secure, Caddy-readable location, typically <code>\/etc\/caddy\/certs\/<\/code>.<\/p>\n<\/li>\n<li><p><strong>Modify Caddyfile<\/strong>:\nAdd or modify the <code>transport<\/code> block within your <code>reverse_proxy<\/code> directive:<\/p>\n<pre><code class=\"language-caddyfile\">your-caddy-domain.com {\n    reverse_proxy your_upstream_ip_address:upstream_port {\n        transport http {\n            # Trusts the specified CA certificate for this upstream only\n            tls_trusted_ca \/etc\/caddy\/certs\/upstream_ca.crt\n            # Optional: specify the server name if different from the Caddy domain\n            # This ensures the correct certificate is requested via SNI\n            tls_server_name your_upstream_hostname\n        }\n    }\n}\n<\/code><\/pre>\n<\/li>\n<li><p><strong>Validate and Apply Caddyfile Changes<\/strong>:<\/p>\n<pre><code class=\"language-bash\">sudo caddy validate --config \/etc\/caddy\/Caddyfile\nsudo systemctl reload caddy\nsudo journalctl -u caddy -f --no-hostname # Monitor logs\n<\/code><\/pre>\n<\/li>\n<\/ol>\n<h4>4. Address Hostname\/SNI Mismatch<\/h4>\n<p>If <code>openssl s_client<\/code> revealed a hostname mismatch (CN\/SAN mismatch) or if your upstream requires a specific SNI hostname to present the correct certificate:<\/p>\n<ol>\n<li><p><strong>Caddyfile <code>reverse_proxy<\/code> Configuration<\/strong>:\nEnsure Caddy is sending the correct <code>Host<\/code> header and <code>tls_server_name<\/code> (SNI) for the upstream.<\/p>\n<pre><code class=\"language-caddyfile\">your-caddy-domain.com {\n    reverse_proxy your_upstream_ip_address:upstream_port {\n        # This sets the Host header sent to the upstream\n        # Crucial if upstream expects a specific hostname\n        header_up Host your_upstream_hostname\n\n        transport http {\n            # This sets the SNI hostname for the TLS handshake\n            # Required if the upstream server serves multiple certificates\n            tls_server_name your_upstream_hostname\n        }\n    }\n}\n<\/code><\/pre>\n<p>Replace <code>your_upstream_hostname<\/code> with the actual hostname that the upstream&#039;s certificate is valid for. This is often the internal FQDN of the backend server.<\/p>\n<\/li>\n<li><p><strong>Validate and Apply Caddyfile Changes<\/strong>:<\/p>\n<pre><code class=\"language-bash\">sudo caddy validate --config \/etc\/caddy\/Caddyfile\nsudo systemctl reload caddy\nsudo journalctl -u caddy -f --no-hostname\n<\/code><\/pre>\n<\/li>\n<\/ol>\n<h4>5. Verify System Time Synchronization<\/h4>\n<p>Significant time differences can cause certificates to appear invalid. Ensure both Caddy and the upstream server are synchronized with an NTP server.<\/p>\n<pre><code class=\"language-bash\">timedatectl\n<\/code><\/pre>\n<p>If time is unsynchronized, ensure <code>systemd-timesyncd<\/code> is enabled or install <code>ntp<\/code>:<\/p>\n<pre><code class=\"language-bash\">sudo systemctl enable --now systemd-timesyncd\n# Or for legacy NTP daemon:\n# sudo apt update &amp;&amp; sudo apt install ntp\n# sudo systemctl enable --now ntp\n<\/code><\/pre>\n<h4>6. Bypass TLS Verification (Development\/Testing ONLY)<\/h4>\n<blockquote class=\"warning\"><p>This step <strong>disables all TLS certificate verification<\/strong>. It should <strong>NEVER<\/strong> be used in production environments as it makes your system vulnerable to Man-in-the-Middle attacks. Use this <em>only<\/em> for debugging in isolated development environments or if you fully understand and accept the security implications.<\/p>\n<\/blockquote>\n<p>If all else fails and you need to quickly confirm if TLS verification is the root cause, you can temporarily disable it:<\/p>\n<pre><code class=\"language-caddyfile\">your-caddy-domain.com {\n    reverse_proxy your_upstream_ip_address:upstream_port {\n        transport http {\n            tls_insecure_skip_verify\n            # Optionally, still specify server name for SNI\n            # tls_server_name your_upstream_hostname\n        }\n    }\n}\n<\/code><\/pre>\n<p>Validate and apply, then immediately remove this directive once you&#039;ve confirmed the issue.<\/p>\n<h4>7. Check Upstream Server&#039;s Certificate Expiration\/Validity<\/h4>\n<p>If <code>openssl s_client<\/code> revealed an expired or not-yet-valid certificate on the upstream, the solution lies with the upstream server&#039;s administrator. They need to renew or correctly install a valid certificate. Caddy cannot fix an invalid certificate presented by its backend.<\/p>\n<p>This comprehensive guide should help you systematically troubleshoot and resolve the &quot;Caddy reverse proxy TLS handshake failed verification&quot; error on your Ubuntu 20.04 LTS system. Remember to always apply the least permissive fix possible for security.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Resolve Caddy reverse proxy TLS handshake failures with upstream servers on Ubuntu 20.04 LTS. Diagnose certificate validation, SNI, and CA issues for seamless reverse proxy operations.<\/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,41,40,37,27,28,13],"class_list":["post-13","post","type-post","status-publish","format-standard","hentry","category-web-server","tag-caddy","tag-certificate-validation","tag-devops","tag-reverse-proxy","tag-ssl","tag-tls","tag-ubuntu"],"_links":{"self":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/13","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=13"}],"version-history":[{"count":2,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/13\/revisions"}],"predecessor-version":[{"id":261,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/posts\/13\/revisions\/261"}],"wp:attachment":[{"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/media?parent=13"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/categories?post=13"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/butitworkedlocal.com\/wp-json\/wp\/v2\/tags?post=13"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}