Skip to content

Commit 14af5be

Browse files
committed
fix: metrics endpoint
1 parent 97858e1 commit 14af5be

File tree

6 files changed

+56
-6
lines changed

6 files changed

+56
-6
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ See the [Releases section](https://github.com/freifunkMUC/wg-access-server/relea
135135
- `wg_access_server_devices_bytes_received_total`: sum of received bytes across devices
136136
- `wg_access_server_devices_bytes_transmitted_total`: sum of transmitted bytes across devices
137137

138-
If both `EnableMetadata` and `EnableDeviceMetrics` are enabled, device-specific metrics are included in the output.
138+
If both `EnableMetadata` and `EnableDeviceMetrics` are enabled, device-specific metrics are included in the output. Set `metrics.basicAuth.username` and `metrics.basicAuth.passwordHash` (bcrypt) to protect the `/metrics` endpoint with HTTP Basic Auth.
139139

140140
The software consists of a Golang server and a React app.
141141

cmd/serve/main.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func Register(app *kingpin.Application) *servecmd {
4545
cli.Flag("external-host", "The external origin of the server (e.g. https://mydomain.com)").Envar("WG_EXTERNAL_HOST").StringVar(&cmd.AppConfig.ExternalHost)
4646
cli.Flag("storage", "The storage backend connection string").Envar("WG_STORAGE").Default("memory://").StringVar(&cmd.AppConfig.Storage)
4747
cli.Flag("enable-metadata", "Enable metadata collection (i.e. metrics)").Envar("WG_ENABLE_METADATA").Default("false").BoolVar(&cmd.AppConfig.EnableMetadata)
48-
cli.Flag("enable-device-metrics", "Expose device-level metrics on /metrics (requires enable-metadata)").Envar("WG_ENABLE_DEVICE_METRICS").Default("true").BoolVar(&cmd.AppConfig.EnableDeviceMetrics)
48+
cli.Flag("metrics-basic-auth-username", "Require basic auth for /metrics (username)").Envar("WG_METRICS_BASIC_AUTH_USERNAME").StringVar(&cmd.AppConfig.Metrics.BasicAuth.Username)
49+
cli.Flag("metrics-basic-auth-password-hash", "Require basic auth for /metrics (bcrypt hash)").Envar("WG_METRICS_BASIC_AUTH_PASSWORD_HASH").StringVar(&cmd.AppConfig.Metrics.BasicAuth.PasswordHash)
4950
cli.Flag("enable-inactive-device-deletion", "Enable inactive device deletion").Envar("WG_ENABLE_INACTIVE_DEVICE_DELETION").Default("false").BoolVar(&cmd.AppConfig.EnableInactiveDeviceDeletion)
5051
cli.Flag("inactive-device-grace-period", "Duration after inactive device are deleted").Envar("WG_INACTIVE_DEVICE_GRACE_PERIOD").Default((1 * config.Year).String()).DurationVar(&cmd.AppConfig.InactiveDeviceGracePeriod)
5152
cli.Flag("filename", "The configuration filename (e.g. WireGuard-Home)").Envar("WG_FILENAME").StringVar(&cmd.AppConfig.Filename)
@@ -249,8 +250,8 @@ func (cmd *servecmd) Run() {
249250
// Health check endpoint
250251
router.PathPrefix("/health").Handler(services.HealthEndpoint(deviceManager))
251252

252-
// Prometheus metrics endpoint (public, no auth)
253-
router.Path("/metrics").Handler(services.MetricsHandler(&services.MetricsDeps{
253+
// Prometheus metrics endpoint (optionally basic-auth protected)
254+
router.Path("/metrics").Handler(services.MetricsEndpoint(&services.MetricsDeps{
254255
Config: conf,
255256
DeviceManager: deviceManager,
256257
}))
@@ -378,6 +379,13 @@ func (cmd *servecmd) ReadConfig() *config.AppConfig {
378379
} else if !cmd.AppConfig.EnableDeviceMetrics {
379380
logrus.Info("Device-level Prometheus metrics are disabled; metadata remains available for the UI")
380381
}
382+
if cmd.AppConfig.Metrics.BasicAuth.Username != "" {
383+
if cmd.AppConfig.Metrics.BasicAuth.PasswordHash == "" {
384+
logrus.Warn("Metrics basic auth username is set but password hash is missing")
385+
} else {
386+
logrus.Info("Basic auth is enabled for /metrics")
387+
}
388+
}
381389

382390
if !cmd.AppConfig.Auth.IsEnabled() {
383391
if cmd.AppConfig.AdminPassword == "" {

docs/2-configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Here's what you can configure:
4040
| `WG_STORAGE` | `--storage` | `storage` | | `sqlite3:///data/db.sqlite3` | A storage backend connection string. See [storage docs](./3-storage.md) |
4141
| `WG_ENABLE_METADATA` | `--enable-metadata` | `enableMetadata` | | `false` | Turn on collection of device metadata logging. Includes last handshake time and RX/TX bytes only. |
4242
| `WG_ENABLE_DEVICE_METRICS` | `--enable-device-metrics` | `enableDeviceMetrics` | | `true` | Expose device-level Prometheus metrics on `/metrics`. Requires `enableMetadata` to provide data. |
43+
| `WG_METRICS_BASIC_AUTH_USERNAME` | `--metrics-basic-auth-username` | `metrics.basicAuth.username` | | | Username required when accessing `/metrics`. Leave empty to keep the endpoint unauthenticated. |
44+
| `WG_METRICS_BASIC_AUTH_PASSWORD_HASH` | `--metrics-basic-auth-password-hash` | `metrics.basicAuth.passwordHash` | | | Bcrypt hash of the password required for `/metrics`. Use together with the username to protect the endpoint. |
4345
| `WG_ENABLE_INACTIVE_DEVICE_DELETION` | `--enable-inactive-device-deletion` | `enableInactiveDeviceDeletion` | | `false` | Enable/Disable the automatic deletion of inactive devices. |
4446
| `WG_INACTIVE_DEVICE_GRACE_PERIOD` | `--inactive-device-grace-period` | `inactiveDeviceGracePeriod` | | `8760h` (1 Year) | The duration after which inactive devices are automatically deleted, if automatic deletion is enabled. A device is inactive if it has not been connected to the server for longer than the inactive device grace period. The duration format is the go duration string format |
4547
| `WG_FILENAME ` | `--filename` | `filename` | | `WireGuard` | Change the name of the configuration file the user can download (Do not include the '.conf' extension ) |

internal/config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,15 @@ type AppConfig struct {
160160
// Defaults to 0 (disabled)
161161
PersistentKeepalive int `yaml:"PersistentKeepalive"`
162162
} `yaml:"clientConfig"`
163+
// Metrics configures access to the /metrics endpoint.
164+
Metrics struct {
165+
BasicAuth struct {
166+
// Username required when accessing /metrics. Empty disables auth.
167+
Username string `yaml:"username"`
168+
// Bcrypt hashed password required when accessing /metrics.
169+
PasswordHash string `yaml:"passwordHash"`
170+
} `yaml:"basicAuth"`
171+
} `yaml:"metrics"`
163172
// Auth configures optional authentication backends
164173
// to control access to the web ui.
165174
// Devices will be managed on a per-user basis if any

internal/services/basic_auth.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package services
2+
3+
import (
4+
"crypto/subtle"
5+
"net/http"
6+
7+
"golang.org/x/crypto/bcrypt"
8+
)
9+
10+
// basicAuthHandler wraps h with HTTP Basic Auth using a bcrypt hashed password.
11+
func basicAuthHandler(h http.Handler, realm, username, passwordHash string) http.Handler {
12+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
u, p, ok := r.BasicAuth()
14+
if !ok || subtle.ConstantTimeCompare([]byte(u), []byte(username)) != 1 || bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(p)) != nil {
15+
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
16+
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
17+
return
18+
}
19+
h.ServeHTTP(w, r)
20+
})
21+
}

internal/services/metrics.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ type MetricsDeps struct {
1818
}
1919

2020
// MetricsHandler returns an http.Handler that exposes Prometheus metrics.
21-
// It honors EnableMetadata/EnableDeviceMetrics by including device-specific metrics only when both are enabled,
22-
// but still exposes basic process/go/build metrics.
21+
// Device-level gauges are only registered when both metadata collection and
22+
// device metrics are enabled, but process/build metrics are always exposed.
2323
func MetricsHandler(deps *MetricsDeps) http.Handler {
2424
reg := prometheus.NewRegistry()
2525

@@ -130,3 +130,13 @@ func MetricsHandler(deps *MetricsDeps) http.Handler {
130130

131131
return promhttp.HandlerFor(reg, promhttp.HandlerOpts{EnableOpenMetrics: true})
132132
}
133+
134+
// MetricsEndpoint wraps MetricsHandler with optional basic auth protection.
135+
func MetricsEndpoint(deps *MetricsDeps) http.Handler {
136+
h := MetricsHandler(deps)
137+
creds := deps.Config.Metrics.BasicAuth
138+
if creds.Username == "" || creds.PasswordHash == "" {
139+
return h
140+
}
141+
return basicAuthHandler(h, "metrics", creds.Username, creds.PasswordHash)
142+
}

0 commit comments

Comments
 (0)