From 9d69b6177902b363326d1193fa74123384f314b8 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 4 Jul 2026 16:41:58 +0200 Subject: [PATCH] fix(health): don't error when the advertised HTTPS URL is unreachable from the service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `service_cert_chain` check ("HTTPS endpoint TLS configuration") dials the service's own configured HTTPS URL. When that dial fails before any certificate is presented (connection refused, timeout, handshake reset), it reported a hard red error. But from inside the service we can't distinguish "the endpoint is down" from "the advertised HTTPS URL simply isn't reachable from here" — and the latter is a normal, healthy deployment: TLS terminated by a reverse proxy in front of AfterTouch, or a Docker-published port / LAN-only hostname that the container itself can't dial. In those setups the red error is a false alarm (issue #355: reporter runs HTTP 8080 / HTTPS 8443 and noted "in my configuration that is expected"). Downgrade that specific case (no cert presented) to a warning, reword it to name the expected reverse-proxy / unreachable-advertised-URL case, and add an `openssl s_client` command to verify the endpoint from a client that actually reaches the advertised URL. Cert-classification outcomes (own-CA info, foreign-chain warning) are unchanged. Reproduced locally on a clean data dir before/after: custom ports and localhost/127.0.0.1 already returned INFO; only the unreachable-URL case produced the error, which now returns a warning. refs #355 Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/service/health/checks_cert_chain.go | 27 +++++++++++++++++--- pkg/service/health/checks_cert_chain_test.go | 24 ++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/pkg/service/health/checks_cert_chain.go b/pkg/service/health/checks_cert_chain.go index d27ddc4..6568a69 100644 --- a/pkg/service/health/checks_cert_chain.go +++ b/pkg/service/health/checks_cert_chain.go @@ -29,6 +29,10 @@ const CheckIDCertChain = "service_cert_chain" // something else → warning with an `openssl s_client` // investigation prompt (foreign chain / reverse proxy / // ingress cert). +// - endpoint not reachable from inside the service (no cert +// presented) → warning, not error: the advertised HTTPS URL +// is often intentionally unreachable from the service itself +// (reverse proxy, Docker-published port, LAN-only hostname). // - HTTPS URL not configured → skip silently. // // caCertFn returns AfterTouch's own CA leaf certificate (nil if @@ -82,10 +86,27 @@ func runCertChainCheck(httpsURL string, caCertFn func() *x509.Certificate) []Fin // reachability problem. leaf := leafFromVerifyError(err) if leaf == nil { + // The dial failed before any certificate was presented + // (connection refused, timeout, handshake reset). From + // inside the service we can't tell "the endpoint is down" + // apart from "the advertised HTTPS URL simply isn't + // reachable from here" — the latter is a normal, healthy + // setup (TLS terminated by a reverse proxy, or a + // Docker-published port / external hostname that only + // resolves on the LAN). Reporting a hard error there is a + // false alarm (issue #355), so this is a warning with the + // context to tell the two apart. return []Finding{{ - Severity: SeverityError, - Message: fmt.Sprintf("Could not connect to %s: %v", addr, err), - Details: "AfterTouch's HTTPS endpoint isn't reachable from inside the service, or the peer dropped the handshake before presenting a certificate. Check that the listener is bound and the URL host:port resolves correctly.", + Severity: SeverityWarning, + Message: fmt.Sprintf("Configured HTTPS endpoint %s isn't reachable from inside the service.", addr), + Details: fmt.Sprintf("AfterTouch dialed its own configured HTTPS URL (%s) and the connection failed before any certificate was presented: %v. "+ + "This is expected — and not a problem — if TLS is terminated by a reverse proxy in front of AfterTouch, or the advertised HTTPS URL isn't reachable from inside the container (for example a Docker-published port, or a hostname that only resolves elsewhere on your network). In that case, verify the endpoint from a client instead (see below). "+ + "If AfterTouch is meant to serve HTTPS directly, check that the HTTPS listener is bound and that the URL host:port is correct.", httpsURL, err), + ManualCommands: []ManualCommand{{ + Label: "Verify the endpoint from a machine on your network:", + Command: fmt.Sprintf("openssl s_client -connect %s -servername %s