Skip to content

Commit ef24c3d

Browse files
committed
feat(secrets): Add new package for managing secrets
Signed-off-by: Henrique Spanoudis Matulis <[email protected]>
1 parent 0df7b91 commit ef24c3d

13 files changed

+2182
-1
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/julienschmidt/httprouter v1.3.0
1111
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
1212
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
13+
github.com/prometheus/client_golang v1.20.4
1314
github.com/prometheus/client_model v0.6.2
1415
github.com/stretchr/testify v1.11.1
1516
go.yaml.in/yaml/v2 v2.4.2
@@ -25,7 +26,6 @@ require (
2526
github.com/davecgh/go-spew v1.1.1 // indirect
2627
github.com/jpillora/backoff v1.0.0 // indirect
2728
github.com/pmezard/go-difflib v1.0.0 // indirect
28-
github.com/prometheus/client_golang v1.20.4 // indirect
2929
github.com/prometheus/procfs v0.15.1 // indirect
3030
github.com/rogpeppe/go-internal v1.10.0 // indirect
3131
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect

secrets/README.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Secret Management
2+
3+
The `secrets` package provides a unified way to handle secrets within configuration files for Prometheus and its ecosystem components. It allows secrets to be specified inline, loaded from files, or fetched from other sources through a pluggable provider mechanism.
4+
5+
## Concepts
6+
7+
The package is built around a few core concepts:
8+
9+
* `SecretField`: A type used in configuration structs to represent a field that holds a secret. It handles the logic for unmarshaling from different secret sources, and the API for accessing secrets.
10+
* `Provider`: An interface for fetching secrets from a specific source (e.g., inline string, file on disk). The package comes with built-in providers, and new ones can be registered.
11+
* `Manager`: A component that discovers all `SecretField` instances within a configuration struct, manages their lifecycle, and handles periodic refreshing of secrets.
12+
13+
## How to Use
14+
15+
Using the `secrets` package involves three main steps: defining your configuration struct, initializing the secret manager, and accessing the secret values.
16+
17+
### 1. Define Your Configuration Struct
18+
19+
In your configuration struct, use the `secrets.SecretField` type for any fields that should contain secrets.
20+
21+
```go
22+
package main
23+
24+
import "github.com/prometheus/common/secrets"
25+
26+
type MyConfig struct {
27+
APIKey secrets.SecretField `yaml:"api_key"`
28+
Password secrets.SecretField `yaml:"password"`
29+
// ... other config fields
30+
}
31+
```
32+
33+
### 2. Configure Secrets in YAML
34+
35+
Users can then provide secrets in their YAML configuration file.
36+
37+
For simple secrets, an inline string can be used:
38+
39+
```yaml
40+
api_key: "my_super_secret_api_key"
41+
```
42+
43+
To load a secret from a file, use the `file` provider:
44+
45+
```yaml
46+
password:
47+
file: /path/to/password.txt
48+
```
49+
50+
### 3. Initialize the Secret Manager
51+
52+
After unmarshaling your configuration file into your struct, you must create a `secrets.Manager` to manage the lifecycle of the secrets. The manager is initialized with a pointer to your configuration struct.
53+
54+
```go
55+
import (
56+
"context"
57+
"log"
58+
59+
"github.com/prometheus/client_golang/prometheus"
60+
"github.com/prometheus/common/secrets"
61+
"go.yaml.in/yaml/v2"
62+
)
63+
64+
func main() {
65+
// A Prometheus registry is needed to register the secret manager's metrics.
66+
promRegisterer := prometheus.NewRegistry()
67+
68+
// Load config from file
69+
configData := []byte(`
70+
api_key: "my_super_secret_api_key"
71+
password:
72+
file: /path/to/password.txt
73+
`)
74+
var cfg MyConfig
75+
if err := yaml.Unmarshal(configData, &cfg); err != nil {
76+
log.Fatalf("Error unmarshaling config: %v", err)
77+
}
78+
79+
// Create a secret manager. This discovers and manages all SecretFields in cfg.
80+
// The manager will handle refreshing secrets in the background.
81+
manager, err := secrets.NewManager(promRegisterer, &cfg)
82+
if err != nil {
83+
log.Fatalf("Error creating secret manager: %v", err)
84+
}
85+
// Start the manager's background refresh loop.
86+
manager.Start(context.Background())
87+
defer manager.Stop()
88+
89+
90+
// ... your application logic ...
91+
92+
// Wait for the secrets in cfg to be ready.
93+
for {
94+
if ready, err := manager.SecretsReady(&cfg); err != nil {
95+
log.Fatalf("Error checking secret readiness: %v", err)
96+
} else if ready {
97+
break
98+
}
99+
}
100+
101+
// Access the secret value when needed.
102+
apiKey := cfg.APIKey.Get()
103+
password := cfg.Password.Get()
104+
105+
log.Printf("API Key: %s", apiKey)
106+
log.Printf("Password: %s", password)
107+
}
108+
```
109+
110+
### 4. Accessing Secrets
111+
112+
To get the string value of a secret, simply call the `Get()` method on the `SecretField`.
113+
114+
```go
115+
secretValue := myConfig.APIKey.Get()
116+
```
117+
118+
The manager handles caching and refreshing, so `Get()` will always return the current valid secret.
119+
120+
## Built-in Providers
121+
122+
The `secrets` package comes with two built-in providers:
123+
124+
* `inline`: For secrets that are specified directly as a string in the configuration file. This is the default if a plain string is provided.
125+
```yaml
126+
api_key: "my_inline_secret"
127+
```
128+
* `file`: For secrets that are loaded from a file on disk.
129+
```yaml
130+
password:
131+
file:
132+
path: /etc/prometheus/secrets/password
133+
```
134+
135+
## Custom Providers
136+
137+
You can extend the functionality by creating your own custom secret providers. A custom provider must implement the `Provider` interface:
138+
139+
```go
140+
type Provider interface {
141+
// FetchSecret retrieves the secret value.
142+
FetchSecret(ctx context.Context) (string, error)
143+
144+
// Name returns the provider's name (e.g., "inline").
145+
Name() string
146+
}
147+
```
148+
149+
Once you have implemented the interface, you need to register a factory function for your provider with the global `ProviderRegistry`. This is typically done in an `init()` function.
150+
151+
```go
152+
package myprovider
153+
154+
import (
155+
"context"
156+
"github.com/prometheus/common/secrets"
157+
)
158+
159+
type MyCustomProvider struct {
160+
// ... fields for your provider
161+
}
162+
163+
func (p *MyCustomProvider) FetchSecret(ctx context.Context) (string, error) {
164+
// ... logic to fetch your secret
165+
}
166+
167+
func (p *MyCustomProvider) Name() string {
168+
return "my_custom_provider"
169+
}
170+
171+
func init() {
172+
secrets.Providers.Register(func() secrets.Provider {
173+
return &MyCustomProvider{}
174+
})
175+
}
176+
```
177+
178+
## Secret Validation
179+
180+
For secrets that can be rotated (e.g., loaded from a file that gets updated), you can provide an optional validation function. This prevents a broken or partially written secret from being loaded into your application after a rotation. The manager will use the new secret only after your validation function returns `true`.
181+
182+
A common use case is to verify that a new authentication token can successfully access a protected endpoint before it is put into active use. This avoids causing monitoring gaps if, for example, a new bearer token is invalid.
183+
184+
To use this feature, implement the `SecretValidator` interface and attach it to a `SecretField` instance.
185+
186+
Here is an example of a validator that checks if an HTTP endpoint can be reached using the new secret as a bearer token. It performs an `HEAD` request and considers the secret valid if the server responds with any status code other than `401 Unauthorized` or `403 Forbidden`.
187+
188+
```go
189+
import (
190+
"context"
191+
"fmt"
192+
"net/http"
193+
194+
"github.com/prometheus/common/secrets"
195+
)
196+
197+
// HTTPBearerTokenValidator checks if a secret is a valid bearer token for a given URL.
198+
type HTTPBearerTokenValidator struct {
199+
EndpointURL string
200+
client *http.Client
201+
}
202+
203+
func NewHTTPBearerTokenValidator(url string) *HTTPBearerTokenValidator {
204+
return &HTTPBearerTokenValidator{
205+
EndpointURL: url,
206+
client: &http.Client{},
207+
}
208+
}
209+
210+
func (v *HTTPBearerTokenValidator) Validate(ctx context.Context, secret string) bool {
211+
req, err := http.NewRequestWithContext(ctx, "HEAD", v.EndpointURL, nil)
212+
if err != nil {
213+
// Could not create the request, so we cannot validate.
214+
return false
215+
}
216+
217+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", secret))
218+
219+
resp, err := v.client.Do(req)
220+
if err != nil {
221+
// The request failed, so we cannot consider this valid.
222+
return false
223+
}
224+
defer resp.Body.Close()
225+
226+
// If the status is Unauthorized or Forbidden, the token is invalid.
227+
// Any other status code (e.g., 200 OK, 404 Not Found) means the token
228+
// was accepted for authentication, so we consider it valid for rotation.
229+
return resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden
230+
}
231+
232+
func (v *HTTPBearerTokenValidator) Settings() secrets.ValidationSettings {
233+
// Return custom settings or use the default.
234+
return secrets.DefaultValidationSettings()
235+
}
236+
237+
// In your application code, after unmarshaling the config:
238+
validator := NewHTTPBearerTokenValidator("https://my-protected-api.com/v1/status")
239+
cfg.APIKey.SetSecretValidation(validator)
240+
```
241+
242+
The `ValidationSettings` allow you to configure timeouts, backoff, and retry attempts for the validation logic, making the process resilient to temporary network issues.
243+
244+
## Prometheus Metrics
245+
246+
The `Manager` exposes several Prometheus metrics to monitor the state of the secrets it manages. These metrics are registered with the `prometheus.Registerer` that is passed to `NewManager`.
247+
248+
The following metrics are available, all labeled with `provider` and `secret_id`:
249+
250+
* `prometheus_remote_secret_last_successful_fetch_seconds`: (Gauge) The Unix timestamp of the last successful secret fetch.
251+
* `prometheus_remote_secret_state`: (Gauge) Describes the current state of a remotely fetched secret (0=success, 1=stale, 2=error, 3=initializing).
252+
* `prometheus_remote_secret_fetch_success_total`: (Counter) Total number of successful secret fetches.
253+
* `prometheus_remote_secret_fetch_failures_total`: (Counter) Total number of failed secret fetches.
254+
* `prometheus_remote_secret_fetch_duration_seconds`: (Histogram) Duration of secret fetch attempts.
255+
* `prometheus_remote_secret_validation_failures_total`: (Counter) Total number of failed secret validations.
256+
257+
## Error Handling and Panics
258+
259+
The `secrets` package is designed to be robust, but there is one critical error condition that will cause a panic: using a `SecretField` before the `Manager` has been initialized.
260+
261+
If you call `Get()` or `TriggerRefresh()` on a `SecretField` that has not been discovered by a `Manager`, your program will panic with the message:
262+
263+
```
264+
secret field has not been discovered by a manager; was NewManager(&cfg) called?
265+
```
266+
267+
This is a safeguard to prevent the use of unmanaged and potentially empty secrets. To avoid this panic, ensure that you always create a `Manager` by passing a pointer to your configuration struct to `secrets.NewManager` immediately after you unmarshal your configuration.

0 commit comments

Comments
 (0)