Fixed issue with shelly self signed certificate.
Build Docker image on push / docker (push) Successful in 20s
Build and push Docker image on tag / docker (push) Successful in 9s

This commit is contained in:
2026-08-18 01:00:56 +02:00
parent d9d989d63f
commit 9ec7a92fa1
5 changed files with 40 additions and 5 deletions
+1
View File
@@ -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.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+13 -1
View File
@@ -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
+22
View File
@@ -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)