|
| 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