Documentation
¶
Overview ¶
Package secret provides a simple, provider-agnostic interface for retrieving secrets within GitLab services.
The package is built around a Provider interface with a single method, allowing the underlying secret backend to be swapped without changing application code. A Client wraps any Provider and is the primary entry point for callers.
Providers ¶
Two built-in providers are included:
[NewEnvProvider] reads secrets from environment variables [NewFileProvider] reads secrets from files in a directory, matching the layout produced by Kubernetes secret volume mounts
Additional backends (e.g. HashiCorp Vault) can be implemented by satisfying the Provider interface and would live in a dedicated subpackage to contain their dependencies.
Live secret rotation ¶
For secrets that change in place (e.g. Kubernetes secret volume mounts updated by the kubelet), RotatingFileProvider wraps FileProvider with a background poll loop. It implements RotatingProvider, a superset of Provider that adds [RotatingProvider.Start], [RotatingProvider.Shutdown], and [RotatingProvider.OnRotate]:
p := secret.NewRotatingFileProvider("/var/run/secrets/my-service", "db-password", time.Minute)
if err := p.Start(ctx); err != nil {
// handle
}
defer p.Shutdown(ctx)
go func() {
for event := range p.OnRotate() {
if event.Err != nil {
// event.Err may contain the secret key name (e.g. "secret not
// found: db-password") but never the secret value, which is
// always held in a [Secret] and redacted by its String method.
log.Printf("rotation poll error: %v", event.Err)
continue
}
pool.Reconnect(event.NewValue.Value())
}
}()
// Point-in-time reads still work via Get.
val, err := p.Get(ctx, "db-password")
Basic usage ¶
// Default: reads from environment variables.
client := secret.New()
// Custom provider: reads from a Kubernetes secret volume mount.
client = secret.NewWithConfig(&secret.Config{
Provider: secret.NewFileProvider("/var/run/secrets/my-service"),
})
val, err := client.Get(ctx, "DB_PASSWORD")
if errors.Is(err, secret.ErrNotFound) {
// secret not configured
}
Testing ¶
Use the MockProvider from the secrettest package to supply known values in tests without touching the environment or the filesystem:
import "gitlab.com/gitlab-org/labkit/v2/testing/secrettest"
client := secret.NewWithConfig(&secret.Config{
Provider: secrettest.NewMockProvider(map[string]string{
"DB_PASSWORD": "hunter2",
}),
})
Example ¶
Example shows the default client backed by environment variables.
package main
import (
"context"
"os"
"gitlab.com/gitlab-org/labkit/v2/secret"
)
func main() {
os.Setenv("DB_PASSWORD", "hunter2")
defer os.Unsetenv("DB_PASSWORD")
client := secret.New()
s, err := client.Get(context.Background(), "DB_PASSWORD")
if err != nil {
panic(err)
}
// Pass the value explicitly only when needed; never log or print it.
_ = s.Value()
}
Output:
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrAlreadyStarted = errors.New("rotating provider already started")
ErrAlreadyStarted is returned by [RotatingProvider.Start] if Start has already been called on this instance.
var ErrInvalidKey = errors.New("invalid secret key")
ErrInvalidKey is returned when the key itself is malformed or unsafe, for example when a FileProvider key contains path traversal sequences.
var ErrNotFound = errors.New("secret not found")
ErrNotFound is returned by a Provider when the requested secret key does not exist in the underlying backend. Callers should use errors.Is to distinguish a missing secret from other failure modes.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client wraps a Provider and is the primary entry point for retrieving secrets. Construct one with New or NewWithConfig and inject it wherever secrets are needed.
func NewWithConfig ¶
NewWithConfig returns a Client configured with cfg. A nil cfg is treated identically to an empty Config (EnvProvider default).
func (*Client) Get ¶
Get retrieves the secret identified by key from the underlying Provider. If the key does not exist, Get returns an error wrapping ErrNotFound. Errors are wrapped with the provider's concrete type to aid debugging when multiple backends are in use.
Example ¶
ExampleClient_Get shows error handling for a missing key.
package main
import (
"context"
"errors"
"fmt"
"gitlab.com/gitlab-org/labkit/v2/secret"
)
func main() {
client := secret.New() // backed by environment variables
_, err := client.Get(context.Background(), "MISSING_KEY")
if errors.Is(err, secret.ErrNotFound) {
fmt.Println("secret not found")
}
}
Output: secret not found
type Config ¶
type Config struct {
// Provider is the secret backend. When nil, an EnvProvider is used.
Provider Provider
}
Config holds optional configuration for NewWithConfig. Zero values produce sensible defaults (EnvProvider).
type EnvProvider ¶
type EnvProvider struct{}
EnvProvider retrieves secrets from environment variables. An unset variable returns ErrNotFound; a variable explicitly set to an empty string is returned as-is.
type FileProvider ¶
type FileProvider struct {
// contains filtered or unexported fields
}
FileProvider retrieves secrets from files within a base directory. Each file's name is the secret key and its contents are the secret value. This layout matches Kubernetes secret volume mounts.
func NewFileProvider ¶
func NewFileProvider(dir string) *FileProvider
NewFileProvider returns a FileProvider that reads secrets from dir.
Example ¶
ExampleNewFileProvider shows a client reading secrets from a directory, following the Kubernetes secret volume mount convention (one file per key).
package main
import (
"gitlab.com/gitlab-org/labkit/v2/secret"
)
func main() {
p := secret.NewFileProvider("/var/run/secrets/my-service")
client := secret.NewWithConfig(&secret.Config{Provider: p})
// client.Get(ctx, "db-password") reads /var/run/secrets/my-service/db-password
_ = client
}
Output:
func (*FileProvider) Get ¶
Get reads the file at <dir>/<key> and returns its contents, trimmed of leading and trailing whitespace. If the file does not exist, Get returns an error wrapping ErrNotFound. If the key escapes the base directory via path traversal sequences (e.g. "../../etc/passwd"), Get returns an error wrapping ErrInvalidKey. Other I/O errors are returned as-is.
type Provider ¶
Provider is the interface that all secret backends must implement. A Provider retrieves a secret value by key from its underlying source.
Implementations that perform network I/O (for example, Vault or cloud KMS) should honour context cancellation and deadlines. The built-in EnvProvider and FileProvider do not use the context because their operations are inherently fast and local.
type RotatingFileProvider ¶
type RotatingFileProvider struct {
// contains filtered or unexported fields
}
RotatingFileProvider polls a single file within a base directory on a configurable interval. When the file's contents change, it sends a RotationEvent on the channel returned by [OnRotate].
Construct with NewRotatingFileProvider, call [Start] to begin polling, and [Shutdown] to stop. [Get] delegates to the embedded FileProvider and reads the file fresh on every call, independent of the poll loop.
After [Shutdown] returns, [Get] continues to read from disk normally. The notification channel returned by [OnRotate] is closed after Shutdown.
Start and Shutdown must not be called concurrently. The expected usage is to call Start once during application startup and Shutdown once during graceful shutdown, consistent with the [app.Component] lifecycle.
func NewRotatingFileProvider ¶
func NewRotatingFileProvider(dir, key string, interval time.Duration) *RotatingFileProvider
NewRotatingFileProvider returns a RotatingFileProvider that watches the file at <dir>/<key> and polls it every interval.
One provider watches one file. To watch multiple secrets, create multiple RotatingFileProvider instances.
interval must be positive. Passing zero or a negative duration panics, because time.NewTicker would panic at the first poll tick.
func (*RotatingFileProvider) Get ¶
Get reads the file identified by key directly from disk. It delegates to the embedded FileProvider and is independent of the background poll loop.
func (*RotatingFileProvider) OnRotate ¶
func (p *RotatingFileProvider) OnRotate() <-chan RotationEvent
OnRotate returns a receive-only channel that delivers a RotationEvent whenever the watched file's contents change. It is safe to call before [Start]; events will arrive once Start is called.
The channel has a buffer of one. If the consumer is slow and the buffer is full, the incoming event is dropped. The consumer can call [Get] at any time to read the current value from disk.
func (*RotatingFileProvider) Shutdown ¶
func (p *RotatingFileProvider) Shutdown(ctx context.Context) error
Shutdown stops the background goroutine and closes the notification channel. It blocks until the goroutine has exited or ctx is cancelled, whichever comes first. If Start was never called, Shutdown is a no-op. Calling Shutdown more than once is safe.
func (*RotatingFileProvider) Start ¶
func (p *RotatingFileProvider) Start(ctx context.Context) error
Start launches the background polling goroutine. It returns ErrAlreadyStarted if called more than once on the same provider.
type RotatingProvider ¶
type RotatingProvider interface {
Provider
Start(ctx context.Context) error
Shutdown(ctx context.Context) error
OnRotate() <-chan RotationEvent
}
RotatingProvider extends Provider with a background polling loop and a change-notification channel.
Start kicks off the background goroutine; Shutdown stops it and closes the notification channel. OnRotate returns a receive-only channel that fires whenever the secret value changes.
The built-in EnvProvider and FileProvider implement only Provider. RotatingFileProvider is the concrete implementation of RotatingProvider for Kubernetes secret volume mounts.
type RotationEvent ¶
RotationEvent is delivered on the channel returned by OnRotate whenever the secret value at the watched path changes. Err is non-nil if the poll read failed; in that case NewValue is the zero Secret{} and the last known-good value remains available via Get.
Key is included because future backends (Vault, OpenBao) may watch multiple secret paths from a single provider instance, and consumers need to disambiguate which secret changed without maintaining separate channels per path.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret holds a secret value. Its String method always returns [REDACTED] to prevent the value from appearing in logs, error messages, or fmt calls. Use Value to obtain the underlying string when you need it explicitly.
func NewSecret ¶
NewSecret wraps value in a Secret. It is intended for use by custom Provider implementations and test helpers.
func (Secret) GoString ¶
GoString implements fmt.GoStringer and always returns [REDACTED], preventing value leakage when using debug formatting like fmt.Printf("%#v", secret).
func (Secret) LogValue ¶
LogValue implements slog.LogValuer. It returns [REDACTED] so that secrets passed as structured log attributes are never recorded.
func (Secret) MarshalJSON ¶
MarshalJSON implements json.Marshaler. It always returns "[REDACTED]" to prevent secret values from leaking through JSON serialization.
func (Secret) MarshalText ¶
MarshalText implements encoding.TextMarshaler. It always returns [REDACTED] to prevent secret values from leaking through text serialization paths such as XML or CSV encoding.
func (Secret) String ¶
String implements fmt.Stringer and always returns [REDACTED], making it safe to include a Secret in log lines, error messages, or fmt calls without accidentally leaking the value.
Example ¶
ExampleSecret_String shows that a Secret is always redacted when printed, making it safe to include in log lines or error messages.
package main
import (
"fmt"
"gitlab.com/gitlab-org/labkit/v2/secret"
)
func main() {
s := secret.NewSecret("supersecret")
fmt.Println(s)
}
Output: [REDACTED]