From 9ec7a92fa16fda163ee643d8e81a2b5fa8662d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Lo=C5=A1=C5=A5=C3=A1k?= Date: Tue, 18 Aug 2026 01:00:56 +0200 Subject: [PATCH] Fixed issue with shelly self signed certificate. --- README.md | 1 + docs/configuration.md | 2 +- docs/operations.md | 6 +++--- shelly/client.go | 14 +++++++++++++- shelly/client_test.go | 22 ++++++++++++++++++++++ 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 318cf5f..579947f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Prometheus exporter for Shelly Gen1 Wi-Fi devices that expose monitoring data th - Correct Prometheus energy counters for both Gen1 `meters` and EM/3EM `emeters`. - Concurrent collection from all configured devices. - Per-device availability, request duration, and error metrics. +- HTTPS support for devices with self-signed or otherwise invalid certificates. - Strict YAML validation, graceful shutdown, and a health endpoint. - Generic `gen1` product type for compatible devices not explicitly listed. diff --git a/docs/configuration.md b/docs/configuration.md index 41f63bf..6cf5919 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -91,7 +91,7 @@ The exporter sends an `Authorization: Basic` header when either `username` or `p | `http://192.168.0.30/` | `http://192.168.0.30/status` | | `https://shelly.example/device` | `https://shelly.example/device/status` | -HTTPS uses the operating system trust store and normal certificate verification. There is no option to disable TLS verification. +For HTTPS device URLs, the exporter deliberately skips certificate verification. Self-signed, expired, and hostname-mismatched certificates are accepted without additional configuration. ## Product identifiers diff --git a/docs/operations.md b/docs/operations.md index d6b0b9d..2081fa5 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -157,10 +157,10 @@ groups: ## Security - Store `config.yaml` outside source control and restrict it to the exporter account, for example with mode `0600` on Linux. -- Prefer a trusted management network or HTTPS because Basic Authentication does not encrypt credentials over plain HTTP. +- Prefer a trusted management network. HTTPS encrypts credentials in transit, but the exporter does not verify device certificates and therefore does not authenticate the remote endpoint. - The exporter endpoints themselves do not require authentication. Restrict port 9090 with firewall, reverse proxy, or network policy when necessary. - Device passwords are not metric labels, but request failures can include the target URL in logs. Do not put credentials in URL user-info; use `username` and `password` fields. -- HTTPS certificate verification is enabled and cannot be bypassed by configuration. +- HTTPS device certificates are not verified. Restrict device traffic to a trusted network to reduce man-in-the-middle risk. ## Troubleshooting @@ -193,7 +193,7 @@ Confirm that the configured username and password match the device. The exporter ### TLS certificate error -The device certificate must be trusted by the exporter operating system or container. Use a certificate issued by a trusted internal CA and add that CA to the runtime trust store. +The exporter deliberately ignores certificate validity for HTTPS device connections, including trust, expiry, and hostname checks. A certificate validation error therefore indicates that an older exporter build may still be running; update and restart it. Other TLS errors, such as unsupported protocol versions or cipher suites, are not certificate validation errors and can still fail the scrape. ### Device is up but an expected metric is absent diff --git a/shelly/client.go b/shelly/client.go index b7e51ea..9a4f7d4 100644 --- a/shelly/client.go +++ b/shelly/client.go @@ -3,6 +3,7 @@ package shelly import ( "context" + "crypto/tls" "encoding/json" "fmt" "io" @@ -39,11 +40,22 @@ func NewClient(rawURL, username, password string, timeout time.Duration) (*Clien username: username, password: password, httpClient: &http.Client{ - Timeout: timeout, + Timeout: timeout, + Transport: insecureTLSTransport(), }, }, nil } +// insecureTLSTransport retains the standard HTTP transport behavior but accepts +// certificates that are expired, self-signed, or issued for another hostname. +func insecureTLSTransport() *http.Transport { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, // #nosec G402 -- Shelly device certificates are intentionally not verified. + } + return transport +} + // GetStatus decodes the /status response into destination. func (c *Client) GetStatus(ctx context.Context, destination any) error { statusURL := *c.baseURL diff --git a/shelly/client_test.go b/shelly/client_test.go index 3ff5d86..7654a12 100644 --- a/shelly/client_test.go +++ b/shelly/client_test.go @@ -38,6 +38,28 @@ func TestClientGetStatusUsesStatusEndpointAndBasicAuth(t *testing.T) { } } +func TestClientGetStatusAcceptsUntrustedTLSCertificate(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + response.Header().Set("Content-Type", "application/json") + _, _ = response.Write([]byte(`{"serial":42}`)) + })) + defer server.Close() + + client, err := NewClient(server.URL, "", "", time.Second) + if err != nil { + t.Fatal(err) + } + var status struct { + Serial int `json:"serial"` + } + if err := client.GetStatus(context.Background(), &status); err != nil { + t.Fatalf("GetStatus() returned an unexpected TLS certificate error: %v", err) + } + if status.Serial != 42 { + t.Fatalf("Serial = %d, want 42", status.Serial) + } +} + func TestClientGetStatusReportsHTTPError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { http.Error(response, "not authorized", http.StatusUnauthorized)