Fixed issue with shelly self signed certificate.
This commit is contained in:
@@ -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`.
|
- Correct Prometheus energy counters for both Gen1 `meters` and EM/3EM `emeters`.
|
||||||
- Concurrent collection from all configured devices.
|
- Concurrent collection from all configured devices.
|
||||||
- Per-device availability, request duration, and error metrics.
|
- 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.
|
- Strict YAML validation, graceful shutdown, and a health endpoint.
|
||||||
- Generic `gen1` product type for compatible devices not explicitly listed.
|
- Generic `gen1` product type for compatible devices not explicitly listed.
|
||||||
|
|
||||||
|
|||||||
@@ -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` |
|
| `http://192.168.0.30/` | `http://192.168.0.30/status` |
|
||||||
| `https://shelly.example/device` | `https://shelly.example/device/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
|
## Product identifiers
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -157,10 +157,10 @@ groups:
|
|||||||
## Security
|
## Security
|
||||||
|
|
||||||
- Store `config.yaml` outside source control and restrict it to the exporter account, for example with mode `0600` on Linux.
|
- 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.
|
- 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.
|
- 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
|
## Troubleshooting
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ Confirm that the configured username and password match the device. The exporter
|
|||||||
|
|
||||||
### TLS certificate error
|
### 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
|
### Device is up but an expected metric is absent
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package shelly
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -40,10 +41,21 @@ func NewClient(rawURL, username, password string, timeout time.Duration) (*Clien
|
|||||||
password: password,
|
password: password,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
|
Transport: insecureTLSTransport(),
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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.
|
// GetStatus decodes the /status response into destination.
|
||||||
func (c *Client) GetStatus(ctx context.Context, destination any) error {
|
func (c *Client) GetStatus(ctx context.Context, destination any) error {
|
||||||
statusURL := *c.baseURL
|
statusURL := *c.baseURL
|
||||||
|
|||||||
@@ -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) {
|
func TestClientGetStatusReportsHTTPError(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
||||||
http.Error(response, "not authorized", http.StatusUnauthorized)
|
http.Error(response, "not authorized", http.StatusUnauthorized)
|
||||||
|
|||||||
Reference in New Issue
Block a user