Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions secrets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Secret Management

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.

## Concepts

The package is built around a few core concepts:

* **`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.
* **`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.
* **`Manager`**: A component that discovers all `SecretField` instances within a configuration struct, manages their lifecycle, and handles periodic refreshing of secrets.

## How to Use

Using the `secrets` package involves three main steps: defining your configuration struct, initializing the secret manager, and accessing the secret values.

### 1. Define Your Configuration Struct

In your configuration struct, use the `secrets.SecretField` type for any fields that should contain secrets.

```go
package main

import "github.com/prometheus/common/secrets"

type MyConfig struct {
APIKey secrets.SecretField `yaml:"api_key"`
Password secrets.SecretField `yaml:"password"`
// ... other config fields
}
```

### 2. Configure Secrets in YAML

Users can then provide secrets in their YAML configuration file.

For simple secrets, an inline string can be used:

```yaml
api_key: "my_super_secret_api_key"
```

To load a secret from a file, use the `file` provider:

```yaml
password:
file: /path/to/password.txt
```

### 3. Initialize the Secret Manager

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.

```go
import (
"context"
"log"

"github.com/prometheus/common/secrets"
"gopkg.in/yaml.v2"
)

func main() {
// Load config from file
configData := []byte(`
api_key: "my_super_secret_api_key"
password:
file: /path/to/password.txt
`)
var cfg MyConfig
if err := yaml.Unmarshal(configData, &cfg); err != nil {
log.Fatalf("Error unmarshaling config: %v", err)
}

// Create a secret manager. This discovers and manages all SecretFields in cfg.
// The manager will handle refreshing secrets in the background.
manager, err := secrets.NewManager(context.Background(), &cfg)
if err != nil {
log.Fatalf("Error creating secret manager: %v", err)
}
defer manager.Stop()

// ... your application logic ...

// Wait for the secrets in cfg to be ready.
for {
if ready, err := manager.SecretsReady(&cfg); err != nil {
log.Fatalf("Error checking secret readiness: %v", err)
} else if ready {
break
}
}

// Access the secret value when needed.
apiKey := cfg.APIKey.Get()
password := cfg.Password.Get()

log.Printf("API Key: %s", apiKey)
log.Printf("Password: %s", password)
}
```

### 4. Accessing Secrets

To get the string value of a secret, simply call the `Get()` method on the `SecretField`.

```go
secretValue := myConfig.APIKey.Get()
```

The manager handles caching and refreshing, so `Get()` will always return the current valid secret.

### Secret Validation

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`, or if no validation has passed.

```go
cfg.Password.SetSecretValidation(myValidator) // myValidator must implement secrets.SecretValidator
```
125 changes: 125 additions & 0 deletions secrets/field.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package secrets

import (
"encoding/json"
"fmt"

"gopkg.in/yaml.v2"
)

// SecretField is a field containing a secret.
type SecretField struct {
provider Provider
manager *Manager
// TODO: Add global secret options here
}

func (s SecretField) String() string {
return fmt.Sprintf("SecretField{Provider: %s}", s.provider.Name())
}

// MarshalYAML implements the yaml.Marshaler interface for SecretField.
func (s SecretField) MarshalYAML() (interface{}, error) {
if s.provider.Name() == "inline" && s.manager != nil && (*s.manager).MarshalInlineSecrets {
return s.Get(), nil
}
out := make(map[string]interface{})
out[s.provider.Name()] = s.provider
return out, nil
}

// MarshalJSON implements the json.Marshaler interface for SecretField.
func (s SecretField) MarshalJSON() ([]byte, error) {
data, err := s.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(data)
}

// providerBase is used to extract the type of the provider.
type providerBase = map[string]interface{}

func (s *SecretField) UnmarshalYAML(unmarshal func(interface{}) error) error {
var plainSecret string
if err := unmarshal(&plainSecret); err == nil {
s.provider = &InlineProvider{
secret: plainSecret,
}
return nil
}

var base providerBase
if err := unmarshal(&base); err != nil {
return err
}

if len(base) != 1 {
return fmt.Errorf("secret must contain exactly one provider type, but found %d", len(base))
}

var name string
var providerConfig interface{}
for providerType, data := range base {
name = providerType
providerConfig = data
break
}

concreteProvider, err := Providers.Get(name)
if err != nil {
return err
}
configBytes, err := yaml.Marshal(providerConfig)

Check failure on line 86 in secrets/field.go

View workflow job for this annotation

GitHub Actions / lint

undefined: yaml (typecheck)
if err != nil {
return fmt.Errorf("failed to re-marshal config for %s provider: %w", name, err)
}

if err := yaml.Unmarshal(configBytes, concreteProvider); err != nil {

Check failure on line 91 in secrets/field.go

View workflow job for this annotation

GitHub Actions / lint

undefined: yaml (typecheck)
return fmt.Errorf("failed to unmarshal into %s provider: %w", name, err)
}

s.provider = concreteProvider
return nil
}

// SetSecretValidation registers an optional validation function for the secret.
//
// When the secret manager fetches a new version of the secret, it will not
// be used immediately if there is a validator. Instead, the manager will
// hold the new secret in a pending state and call the provided Validate
// with it until it returns true, there is an explicit refresh request,
// there is a time out, or the old secret was never valid.
func (s *SecretField) SetSecretValidation(validator SecretValidator) {
if s.manager == nil {
panic("secret field has not been discovered by a manager; was NewManager(&cfg) called?")
}
(*s.manager).setSecretValidation(s, validator)
}

func (s *SecretField) Get() string {
if s.manager == nil {
panic("secret field has not been discovered by a manager; was NewManager(&cfg) called?")
}
return s.manager.get(s)
}

func (s *SecretField) TriggerRefresh() {
if s.manager == nil {
panic("secret field has not been discovered by a manager; was NewManager(&cfg) called?")
}
s.manager.triggerRefresh(s)
}
Loading
Loading