Documentation
¶
Overview ¶
Package secretstore provides interfaces and types for secret storage systems in dsops.
This package defines the modern secret store abstraction that replaces the older provider interface with a cleaner, more focused API specifically designed for secret storage operations. It represents the storage layer in dsops's architecture, handling where secrets are stored and how they are retrieved.
Architecture Overview ¶
The secretstore package sits at the foundation of dsops's layered architecture, providing the storage abstraction upon which all other operations are built:
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (CLI, Config, Templates) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Resolution Engine │
│ (internal/resolve/) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Secret Store Interface │
│ (pkg/secretstore/) ◄────────────┤
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Secret Store Implementations │
│ (internal/secretstores/) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ AWS │ │ Vault │ │ 1Password │ ... │
│ │ Secrets │ │ Store │ │ Store │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Design Philosophy ¶
## Separation of Concerns
The secretstore package embodies dsops's core architectural principle of separating secret storage from secret consumption:
**Secret Stores**: Systems that store and retrieve secret values
AWS Secrets Manager, HashiCorp Vault, 1Password, Azure Key Vault
Focus: Storage, versioning, access control, retrieval
This package's domain
**Services**: Systems that consume secrets for operations
PostgreSQL, Stripe API, GitHub, Kubernetes clusters
Focus: Service configuration, rotation, lifecycle management
Handled by pkg/service and pkg/rotation
This separation enables:
- Clear architectural boundaries
- Independent evolution of storage and consumption layers
- Flexible mixing and matching of stores and services
- Simplified testing and maintenance
## Modern Reference System
The package introduces a modern URI-based reference system that replaces the legacy provider+key approach:
Legacy: provider: "aws", key: "database/password" Modern: store://aws-prod/database/password#password?version=current
Benefits of the new system:
- **Hierarchical addressing**: Natural path-based organization
- **Field extraction**: Direct access to specific JSON fields
- **Version selection**: Native support for versioned secrets
- **Extensible options**: Store-specific parameters via query strings
- **URL-like familiarity**: Intuitive for developers
## Capability-Driven Design
Rather than assuming all secret stores have identical capabilities, the package uses capability negotiation to adapt behavior:
- **Feature detection**: Stores expose what they can do
- **Graceful degradation**: Operations adapt to store limitations
- **Future-proof**: New capabilities can be added without breaking changes
- **User transparency**: Clear feedback about what operations are possible
Core Interface: SecretStore ¶
The SecretStore interface defines five essential operations:
- **Name()**: Unique identifier for the store instance
- **Resolve()**: Retrieve secret values with metadata
- **Describe()**: Get secret metadata without retrieving values
- **Capabilities()**: Expose supported features and limitations
- **Validate()**: Verify configuration and connectivity
This minimal but complete interface enables:
- Consistent behavior across all secret stores
- Efficient metadata-only operations
- Capability-aware application logic
- Robust error handling and validation
Reference System Deep Dive ¶
## SecretRef Structure
The SecretRef type provides structured access to URI components:
type SecretRef struct {
Store string // Store instance name
Path string // Path within the store
Field string // Optional field extraction
Version string // Optional version selection
Options map[string]string // Store-specific parameters
}
## URI Format
The complete URI format supports rich addressing:
store://store-name/path/to/secret#field?version=v&option=value
Examples across different stores:
# AWS Secrets Manager store://aws-prod/database/credentials#password?version=AWSCURRENT # HashiCorp Vault KV v2 store://vault/secret/data/app#api_key?version=2 # 1Password store://onepassword/Production/Database#password?vault=Private # Azure Key Vault store://azure/app-secrets#connection-string?version=latest
## Parsing and Validation
The package provides robust parsing with comprehensive validation:
- URI format validation
- Required component checking
- Query parameter parsing
- Error reporting with actionable messages
Metadata and Capabilities ¶
## SecretMetadata
The SecretMetadata type enables metadata-only operations:
- Existence checking without value retrieval
- Size and version information
- Permission and tag data
- Performance optimization for validation
## SecretStoreCapabilities
Comprehensive capability reporting enables adaptive behavior:
- **Feature flags**: Versioning, metadata, binary support, watching
- **Authentication**: Required methods and supported mechanisms
- **Rotation support**: Version management and constraints
- **Performance characteristics**: Batch operation support, caching
This information drives:
- UI feature enablement/disabling
- Configuration validation
- Operation routing and optimization
- User experience adaptation
Error Handling Strategy ¶
The package defines a comprehensive error taxonomy:
## NotFoundError
Indicates missing secrets with precise location information:
- Distinguishes from authentication failures
- Enables retry logic and fallback strategies
- Provides actionable error messages
## AuthError
Covers authentication and authorization failures:
- Invalid credentials
- Expired tokens
- Insufficient permissions
- Network authentication issues
## ValidationError
Handles malformed requests and configuration issues:
- Invalid URI format
- Missing required parameters
- Constraint violations
- Configuration errors
This structured approach enables:
- Appropriate error handling strategies
- Clear user feedback
- Automated retry and recovery logic
- Comprehensive logging and monitoring
Implementation Guidelines ¶
## Threading and Concurrency
All SecretStore implementations must be thread-safe:
- Multiple goroutines may call methods concurrently
- Internal state must be properly synchronized
- Context cancellation must be respected
- Resource cleanup must be thread-safe
## Security Requirements
Secret store implementations must follow security best practices:
- Never log secret values (use logging.Secret wrapper)
- Validate all inputs to prevent injection attacks
- Use secure transport (TLS) for network operations
- Handle credentials securely in memory
- Support proper context cancellation
## Performance Considerations
Implementations should optimize for common patterns:
- Describe() operations should be faster than Resolve()
- Connection pooling and reuse where appropriate
- Efficient batch operations when supported
- Appropriate caching with security considerations
- Respect context timeouts and cancellation
## Store-Specific Adaptations
Each store type has unique characteristics that implementations should handle:
### AWS Secrets Manager
- JSON field extraction for structured secrets
- Version labels (AWSCURRENT, AWSPENDING)
- Cross-region replication
- IAM-based access control
### HashiCorp Vault
- Path-based organization with engines
- Version numbers for KV v2
- Token-based authentication
- Namespace support in Enterprise
### 1Password
- Vault/Item/Field hierarchy
- SCIM integration for team management
- CLI-based authentication flow
- Item template variations
### Azure Key Vault
- Managed identity integration
- Certificate vs secret vs key distinction
- Soft delete and purge protection
- Network access restrictions
Integration Patterns ¶
## Configuration Integration
Secret stores integrate with dsops configuration through:
- Store definitions in dsops.yaml secretStores section
- Factory functions registered in the registry
- Validation during configuration loading
- Runtime capability checking
## Resolution Integration
The resolution engine leverages secret stores through:
- URI parsing and SecretRef creation
- Store selection based on reference
- Parallel resolution for batch operations
- Error aggregation and reporting
## Rotation Integration
Rotation operations interact with secret stores for:
- Storing newly generated secret values
- Version management during rotation
- Cleanup of deprecated versions
- Audit trail storage
Testing Strategy ¶
The package supports comprehensive testing through:
## Interface Contracts
- Standard test suites for all implementations
- Capability verification tests
- Error condition testing
- Thread safety validation
## Mock Implementations
- In-memory stores for unit testing
- Configurable behavior for edge cases
- Performance and concurrency testing
- Error injection for resilience testing
## Integration Testing
- Real store connectivity tests
- Authentication validation
- Cross-store compatibility tests
- Performance benchmarking
Future Evolution ¶
The secretstore package is designed for extensibility:
## New Store Types
- Plugin architecture for external stores
- Community-contributed implementations
- Experimental and preview stores
- Legacy system integrations
## Enhanced Capabilities
- Real-time change notifications
- Advanced caching strategies
- Batch operation optimization
- Cross-store replication
## Protocol Evolution
- New authentication methods
- Enhanced metadata schemas
- Performance optimizations
- Security enhancements
This package provides the stable foundation for dsops's secret management capabilities, enabling secure, scalable, and reliable secret storage operations across diverse infrastructure environments.
Package secretstore provides interfaces and types for secret storage systems in dsops.
This package defines the modern secret store abstraction that replaces the older provider interface. Secret stores are systems that store and retrieve secret values, as distinct from services that consume secrets for operational purposes.
Secret Store vs Service Distinction ¶
dsops separates two key concepts:
- **Secret Stores**: Where secrets are stored (AWS Secrets Manager, Vault, 1Password, etc.)
- **Services**: What uses secrets (PostgreSQL, Stripe API, GitHub, etc.)
This package focuses exclusively on secret stores - the storage layer.
Modern Reference Format ¶
This package uses the new URI-based reference format:
store://store-name/path#field?version=v&option=value
Examples:
- store://aws-prod/database/password
- store://vault/secret/app#api_key?version=2
- store://onepassword/Production/Database#password
Key Features ¶
- **URI-based references**: Consistent addressing across all stores
- **Capability negotiation**: Stores expose their supported features
- **Version support**: Built-in support for secret versioning
- **Metadata access**: Describe secrets without retrieving values
- **Standardized errors**: Consistent error types across stores
- **Rotation integration**: Native support for secret rotation
Implementing a Secret Store ¶
To implement a custom secret store:
- Implement the SecretStore interface
- Handle URI parsing for your store's format
- Provide appropriate capabilities
- Register with the secret store registry
Example:
type MySecretStore struct {
client MyStoreClient
name string
}
func (s *MySecretStore) Name() string {
return s.name
}
func (s *MySecretStore) Resolve(ctx context.Context, ref SecretRef) (SecretValue, error) {
// Validate reference format
if !ref.IsValid() {
return SecretValue{}, ValidationError{
Store: s.name,
Message: "invalid secret reference",
}
}
// Retrieve secret from your storage system
value, err := s.client.GetSecret(ref.Path)
if err != nil {
if isNotFound(err) {
return SecretValue{}, NotFoundError{
Store: s.name,
Path: ref.Path,
}
}
return SecretValue{}, err
}
// Extract specific field if requested
if ref.Field != "" {
fieldValue, err := extractField(value, ref.Field)
if err != nil {
return SecretValue{}, err
}
value = fieldValue
}
return SecretValue{
Value: value,
Version: "1",
UpdatedAt: time.Now(),
}, nil
}
// ... implement other methods
Error Handling ¶
Use the standardized error types:
- NotFoundError: Secret doesn't exist
- AuthError: Authentication failed
- ValidationError: Invalid request or configuration
Security Considerations ¶
Secret store implementations must:
- Never log secret values (use logging.Secret wrapper)
- Validate all inputs to prevent injection attacks
- Use secure transport (TLS) for network operations
- Support context cancellation for timeouts
- Handle concurrent access safely
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AuthError ¶
type AuthError struct {
// Store is the name of the secret store that failed authentication.
Store string
// Message provides details about the authentication failure.
Message string
}
AuthError indicates that authentication to the secret store failed.
This error should be returned when:
- Credentials are invalid or expired
- Authentication method is not supported
- Network authentication fails
- Permission is denied for the requested operation
Example:
if !isAuthenticated() {
return SecretValue{}, AuthError{
Store: s.Name(),
Message: "API key is invalid or expired",
}
}
type NotFoundError ¶
type NotFoundError struct {
// Store is the name of the secret store where the secret was not found.
Store string
// Path is the secret path that could not be found.
Path string
}
NotFoundError indicates that a requested secret does not exist in the store.
Secret stores should return this error when a SecretRef points to a non-existent secret. This is distinct from authentication failures or permission errors.
Example:
if !secretExists(ref.Path) {
return SecretValue{}, NotFoundError{
Store: s.Name(),
Path: ref.Path,
}
}
func (NotFoundError) Error ¶
func (e NotFoundError) Error() string
Error implements the error interface.
type RotationCapabilities ¶
type RotationCapabilities struct {
// SupportsRotation indicates if the store can create new versions
// of existing secrets for rotation purposes.
SupportsRotation bool
// SupportsVersioning indicates if the store maintains multiple versions
// simultaneously, allowing for zero-downtime rotation strategies.
SupportsVersioning bool
// MaxVersions specifies the maximum number of versions the store will keep.
// 0 means unlimited versions are supported.
MaxVersions int
// MinRotationTime specifies the minimum time that must pass between
// rotation attempts for the same secret.
MinRotationTime time.Duration
// Constraints contains store-specific rotation constraints and requirements.
// Common keys include:
// - "max_length": Maximum secret value length
// - "min_length": Minimum secret value length
// - "charset": Allowed character set
// - "complexity": Password complexity requirements
// - "format": Required format (e.g., "json", "pem")
Constraints map[string]string
}
RotationCapabilities describes rotation features supported by the store.
This structure defines what rotation operations a secret store can perform, including version management, timing constraints, and format requirements. Used by the rotation engine to plan and validate rotation operations.
Example:
rotationCaps := &RotationCapabilities{
SupportsRotation: true,
SupportsVersioning: true,
MaxVersions: 5, // Keep 5 versions
MinRotationTime: time.Hour, // Minimum 1 hour between rotations
Constraints: map[string]string{
"max_length": "4096",
"min_length": "8",
"charset": "alphanumeric",
"complexity": "high",
},
}
type SecretMetadata ¶
type SecretMetadata struct {
// Exists indicates whether the secret exists in the store.
// If false, other fields may be empty or meaningless.
Exists bool
// Version identifies the current version of the secret.
// Empty if versioning not supported or secret doesn't exist.
Version string
// UpdatedAt indicates when the secret was last modified.
// Zero time if not supported or secret doesn't exist.
UpdatedAt time.Time
// Size is the approximate size of the secret value in bytes.
// May be 0 if not supported, not available, or secret doesn't exist.
Size int
// Type describes the kind of secret (password, certificate, api_key, etc.).
// Store-specific classification. May be empty.
Type string
// Permissions lists the operations the current credentials can perform
// on this secret. Common values: "read", "write", "delete", "list".
// Empty slice if not supported.
Permissions []string
// Tags contains store-specific metadata and labels.
// Common keys include environment, owner, purpose, etc.
// Empty map if not supported.
Tags map[string]string
}
SecretMetadata describes a secret without exposing its value.
Used by the Describe method to provide information about a secret's existence, properties, and attributes without retrieving the actual secret value. Useful for validation, auditing, and planning operations.
Example:
meta := SecretMetadata{
Exists: true,
Version: "AWSCURRENT",
UpdatedAt: time.Now(),
Size: 256, // bytes
Type: "password",
Permissions: []string{"read", "list"},
Tags: map[string]string{
"environment": "production",
"team": "platform",
"criticality": "high",
},
}
type SecretRef ¶
type SecretRef struct {
// Store is the name of the secret store that contains this secret.
// Must match a configured store name in dsops.yaml.
Store string
// Path identifies the secret within the store's namespace.
// Format varies by store type:
// - AWS: Secret name or ARN
// - Vault: Full path like "secret/data/app"
// - 1Password: "Vault/Item" format
// - Azure: Secret name
Path string
// Field specifies a particular field within a structured secret.
// Used when secrets contain multiple values (JSON objects, key-value pairs).
// Optional - if empty, the entire secret value is returned.
Field string
// Version specifies a particular version of the secret.
// Format is store-specific:
// - AWS: "AWSCURRENT", "AWSPENDING", or version UUID
// - Vault: Integer version number as string
// - Others: Store-defined versioning scheme
// Optional - if empty, returns the current/latest version.
Version string
// Options contains additional store-specific parameters.
// Common options include:
// - "vault": Vault name for 1Password
// - "namespace": Namespace for Vault Enterprise
// - "region": AWS region override
Options map[string]string
}
SecretRef identifies a secret within a secret store using the modern URI-based format.
This structure represents a parsed store:// URI and provides a standardized way to reference secrets across different storage systems. It replaces the older Provider+Key format with a more flexible and extensible approach.
Different stores use different addressing patterns:
- AWS Secrets Manager: Path is the secret name, Field for JSON extraction
- HashiCorp Vault: Path is the full secret path, Field is the key name
- 1Password: Path is Vault/Item format, Field is the field name
- Azure Key Vault: Path is the secret name, Version for specific versions
Examples:
// AWS Secrets Manager with JSON field extraction
SecretRef{
Store: "aws-prod",
Path: "database/credentials",
Field: "password",
Version: "AWSCURRENT",
}
// HashiCorp Vault KV store
SecretRef{
Store: "vault",
Path: "secret/data/app",
Field: "api_key",
Version: "2",
}
// 1Password item
SecretRef{
Store: "onepassword",
Path: "Production/Database",
Field: "password",
Options: map[string]string{"vault": "Private"},
}
func ParseSecretRef ¶
ParseSecretRef parses a store:// URI into a SecretRef.
This function converts URI-based secret references into structured SecretRef objects. It supports the full dsops store:// URI format with optional field extraction, version selection, and custom options.
URI Format:
store://store-name/path#field?version=v&option=value
Components:
- scheme: Must be "store://"
- store-name: Name of the secret store (required)
- path: Path to the secret within the store (required)
- field: Optional field name for extraction (after #)
- version: Optional version specifier (in query params)
- options: Additional store-specific parameters (in query params)
Examples:
// Basic secret reference
ref, err := ParseSecretRef("store://aws-prod/database/password")
// With field extraction
ref, err := ParseSecretRef("store://vault/secret/app#api_key")
// With version and options
ref, err := ParseSecretRef("store://aws-prod/cert?version=AWSCURRENT®ion=us-west-2")
// Complex example
ref, err := ParseSecretRef("store://onepassword/Production/Database#password?vault=Private")
Returns ValidationError for malformed URIs or missing required components.
Example ¶
Example demonstrates URI parsing and reference creation
package main
import (
"fmt"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
// Parse various URI formats
examples := []string{
"store://aws-prod/database/password",
"store://vault/secret/app#api_key?version=2",
"store://onepassword/Production/Database#password?vault=Private",
"store://azure/app-secrets?version=latest®ion=eastus",
}
for _, uri := range examples {
ref, err := secretstore.ParseSecretRef(uri)
if err != nil {
fmt.Printf("Failed to parse %s: %v\n", uri, err)
continue
}
fmt.Printf("URI: %s\n", uri)
fmt.Printf(" Store: %s\n", ref.Store)
fmt.Printf(" Path: %s\n", ref.Path)
if ref.Field != "" {
fmt.Printf(" Field: %s\n", ref.Field)
}
if ref.Version != "" {
fmt.Printf(" Version: %s\n", ref.Version)
}
if len(ref.Options) > 0 {
fmt.Printf(" Options: %v\n", ref.Options)
}
fmt.Printf(" Valid: %t\n", ref.IsValid())
fmt.Println()
}
}
Output: URI: store://aws-prod/database/password Store: aws-prod Path: database/password Valid: true URI: store://vault/secret/app#api_key?version=2 Store: vault Path: secret/app Field: api_key Version: 2 Valid: true URI: store://onepassword/Production/Database#password?vault=Private Store: onepassword Path: Production/Database Field: password Options: map[vault:Private] Valid: true URI: store://azure/app-secrets?version=latest®ion=eastus Store: azure Path: app-secrets Version: latest Options: map[region:eastus] Valid: true
func (SecretRef) IsValid ¶
IsValid checks if a SecretRef has all required fields.
A valid SecretRef must have both Store and Path fields populated. Field, Version, and Options are optional and don't affect validity.
This method is used for validation before performing secret operations to ensure the reference can be processed correctly.
Example:
ref := SecretRef{
Store: "aws-prod",
Path: "database/password",
// Field, Version, Options are optional
}
if !ref.IsValid() {
return ValidationError{
Message: "invalid secret reference",
}
}
Returns true if both Store and Path are non-empty, false otherwise.
func (SecretRef) String ¶
String converts a SecretRef back to URI format.
This method reconstructs a store:// URI from a SecretRef structure, including all optional components like field extraction, version selection, and custom options.
The output format matches what ParseSecretRef expects:
store://store-name/path#field?version=v&option=value
Examples:
ref := SecretRef{
Store: "aws-prod",
Path: "database/password",
Field: "password",
Version: "AWSCURRENT",
Options: map[string]string{"region": "us-west-2"},
}
uri := ref.String()
// Result: "store://aws-prod/database/password#password?version=AWSCURRENT®ion=us-west-2"
Returns empty string if the SecretRef is invalid (missing Store or Path).
Example ¶
Example demonstrates SecretRef round-trip conversion
package main
import (
"fmt"
"log"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
// Create a complex SecretRef
ref := secretstore.SecretRef{
Store: "production-vault",
Path: "services/api/credentials",
Field: "api_key",
Version: "latest",
Options: map[string]string{
"namespace": "production",
"region": "us-east-1",
},
}
// Convert to URI string
uri := ref.String()
fmt.Printf("Original ref: %s\n", ref.String())
fmt.Printf("URI: %s\n", uri)
// Parse back from URI
parsed, err := secretstore.ParseSecretRef(uri)
if err != nil {
log.Fatalf("Failed to parse URI: %v", err)
}
fmt.Printf("Parsed ref: %s\n", parsed.String())
fmt.Printf("Round-trip successful: %t\n",
ref.Store == parsed.Store &&
ref.Path == parsed.Path &&
ref.Field == parsed.Field &&
ref.Version == parsed.Version)
}
Output: Original ref: store://production-vault/services/api/credentials#api_key?namespace=production®ion=us-east-1&version=latest URI: store://production-vault/services/api/credentials#api_key?namespace=production®ion=us-east-1&version=latest Parsed ref: store://production-vault/services/api/credentials#api_key?namespace=production®ion=us-east-1&version=latest Round-trip successful: true
type SecretStore ¶
type SecretStore interface {
// Name returns the secret store's unique identifier.
//
// This should be a stable identifier that matches the store name used in
// configuration and URI references. Examples: "aws-prod", "vault-dev", "onepassword".
//
// The name is used for:
// - Error messages and logging
// - Store registration and lookup
// - Configuration validation
Name() string
// Resolve retrieves a secret value from the store.
//
// This is the primary method for accessing secret values. It takes a SecretRef
// that specifies the exact secret to retrieve, including optional field
// extraction and version selection.
//
// The method should:
// - Support context cancellation and timeouts
// - Return NotFoundError for missing secrets
// - Return AuthError for authentication failures
// - Extract specific fields if ref.Field is specified
// - Handle version selection if ref.Version is specified
// - Never log the secret value
//
// Example:
//
// ref := SecretRef{
// Store: "aws-secrets",
// Path: "prod/database/credentials",
// Field: "password",
// Version: "AWSCURRENT",
// }
//
// secret, err := store.Resolve(ctx, ref)
// if err != nil {
// var notFound NotFoundError
// if errors.As(err, ¬Found) {
// // Handle missing secret
// }
// return err
// }
Resolve(ctx context.Context, ref SecretRef) (SecretValue, error)
// Describe returns metadata about a secret without retrieving its value.
//
// This method provides information about a secret's existence, properties,
// and attributes without exposing the actual secret value. It's useful for:
// - Validation and planning operations
// - Checking secret existence before retrieval
// - Gathering metadata for audit and reporting
//
// Unlike Resolve, this method should:
// - Be faster since no secret value is retrieved
// - Return metadata with Exists=false for missing secrets
// - Not return NotFoundError (use Exists field instead)
// - Include available version and size information
//
// Example:
//
// meta, err := store.Describe(ctx, ref)
// if err != nil {
// return err
// }
// if !meta.Exists {
// fmt.Println("Secret does not exist")
// } else {
// fmt.Printf("Secret size: %d bytes, version: %s\n", meta.Size, meta.Version)
// }
Describe(ctx context.Context, ref SecretRef) (SecretMetadata, error)
// Capabilities returns the secret store's supported features and limitations.
//
// This method exposes what functionality the secret store supports, allowing
// dsops to adapt its behavior accordingly. The capabilities are used for:
// - Feature availability checks
// - Configuration validation
// - UI/CLI feature enablement
// - Rotation planning
//
// Example:
//
// caps := store.Capabilities()
// if !caps.SupportsVersioning {
// fmt.Println("Warning: Store doesn't support versioning")
// }
// if caps.Rotation != nil && caps.Rotation.SupportsRotation {
// fmt.Println("Store supports secret rotation")
// }
Capabilities() SecretStoreCapabilities
// Validate checks if the secret store is properly configured and authenticated.
//
// This method verifies that the store can successfully connect to its
// backend system and has appropriate permissions. It should be called
// before performing any secret operations.
//
// The method should:
// - Test connectivity to the backend system
// - Verify authentication credentials
// - Check minimum required permissions
// - Support context cancellation
// - Return AuthError for authentication failures
// - Return descriptive errors for configuration issues
//
// Example:
//
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
//
// if err := store.Validate(ctx); err != nil {
// var authErr AuthError
// if errors.As(err, &authErr) {
// fmt.Printf("Authentication failed: %v\n", authErr)
// } else {
// fmt.Printf("Validation failed: %v\n", err)
// }
// return err
// }
Validate(ctx context.Context) error
}
SecretStore defines the interface for systems that store and retrieve secrets.
This interface replaces the storage functionality from the original Provider interface, providing a cleaner separation between secret stores (storage) and services (consumers).
All secret store implementations must be thread-safe as multiple goroutines may call these methods concurrently.
Example usage:
store := &MySecretStore{}
if err := store.Validate(ctx); err != nil {
return fmt.Errorf("store validation failed: %w", err)
}
ref := SecretRef{
Store: "my-store",
Path: "app/database/password",
}
secret, err := store.Resolve(ctx, ref)
if err != nil {
return fmt.Errorf("failed to resolve secret: %w", err)
}
Example (Basic) ¶
Example demonstrates basic secret store usage with URI references
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
// Create a mock secret store for demonstration
fixedTime := time.Date(2025, 11, 15, 0, 0, 0, 0, time.UTC)
store := &MockSecretStore{
name: "example-store",
secrets: map[string]secretstore.SecretValue{
"database/credentials": {
Value: `{"username":"dbuser","password":"secret123"}`,
Version: "v1.0",
UpdatedAt: fixedTime,
Metadata: map[string]string{
"environment": "production",
"created_by": "platform-team",
},
},
},
}
ctx := context.Background()
// Validate store is properly configured
if err := store.Validate(ctx); err != nil {
log.Fatalf("Store validation failed: %v", err)
}
// Create a secret reference using the new format
ref := secretstore.SecretRef{
Store: "example-store",
Path: "database/credentials",
Field: "password", // Extract specific field from JSON
}
// Resolve the secret
secret, err := store.Resolve(ctx, ref)
if err != nil {
log.Fatalf("Failed to resolve secret: %v", err)
}
fmt.Printf("Store name: %s\n", store.Name())
fmt.Printf("Secret version: %s\n", secret.Version)
fmt.Printf("Environment: %s\n", secret.Metadata["environment"])
fmt.Printf("Field extracted: %s\n", secret.Value) // Would be "secret123" after JSON extraction
}
// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
name string
secrets map[string]secretstore.SecretValue
metadata map[string]secretstore.SecretMetadata
capabilities secretstore.SecretStoreCapabilities
validateErr error
}
func (m *MockSecretStore) Name() string {
return m.name
}
func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
if !ref.IsValid() {
return secretstore.SecretValue{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
secret, exists := m.secrets[ref.Path]
if !exists {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path,
}
}
if ref.Version != "" && ref.Version != secret.Version {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path + " (version " + ref.Version + ")",
}
}
_ = ref.Field
return secret, nil
}
func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
if !ref.IsValid() {
return secretstore.SecretMetadata{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
if m.metadata != nil {
if meta, exists := m.metadata[ref.Path]; exists {
return meta, nil
}
}
_, exists := m.secrets[ref.Path]
return secretstore.SecretMetadata{Exists: exists}, nil
}
func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
return m.capabilities
}
func (m *MockSecretStore) Validate(ctx context.Context) error {
return m.validateErr
}
Output: Store name: example-store Secret version: v1.0 Environment: production Field extracted: {"username":"dbuser","password":"secret123"}
Example (Capabilities) ¶
Example demonstrates capability-driven feature detection
package main
import (
"context"
"fmt"
"time"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
store := &MockSecretStore{
name: "advanced-store",
capabilities: secretstore.SecretStoreCapabilities{
SupportsVersioning: true,
SupportsMetadata: true,
SupportsWatching: false,
SupportsBinary: true,
RequiresAuth: true,
AuthMethods: []string{"iam", "api_key"},
Rotation: &secretstore.RotationCapabilities{
SupportsRotation: true,
SupportsVersioning: true,
MaxVersions: 10,
MinRotationTime: time.Hour,
Constraints: map[string]string{
"max_length": "4096",
"complexity": "high",
},
},
},
}
caps := store.Capabilities()
fmt.Printf("Store: %s\n", store.Name())
fmt.Printf("Supports versioning: %t\n", caps.SupportsVersioning)
fmt.Printf("Supports metadata: %t\n", caps.SupportsMetadata)
fmt.Printf("Supports binary: %t\n", caps.SupportsBinary)
fmt.Printf("Requires auth: %t\n", caps.RequiresAuth)
fmt.Printf("Auth methods: %v\n", caps.AuthMethods)
// Check rotation capabilities
if caps.Rotation != nil {
fmt.Printf("Supports rotation: %t\n", caps.Rotation.SupportsRotation)
fmt.Printf("Max versions: %d\n", caps.Rotation.MaxVersions)
fmt.Printf("Min rotation time: %s\n", caps.Rotation.MinRotationTime)
if constraint, ok := caps.Rotation.Constraints["max_length"]; ok {
fmt.Printf("Max secret length: %s\n", constraint)
}
}
// Use capabilities to adapt application behavior
if caps.SupportsVersioning {
fmt.Println("Version-specific operations available")
}
if !caps.SupportsWatching {
fmt.Println("Real-time notifications not supported - using polling")
}
}
// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
name string
secrets map[string]secretstore.SecretValue
metadata map[string]secretstore.SecretMetadata
capabilities secretstore.SecretStoreCapabilities
validateErr error
}
func (m *MockSecretStore) Name() string {
return m.name
}
func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
if !ref.IsValid() {
return secretstore.SecretValue{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
secret, exists := m.secrets[ref.Path]
if !exists {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path,
}
}
if ref.Version != "" && ref.Version != secret.Version {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path + " (version " + ref.Version + ")",
}
}
_ = ref.Field
return secret, nil
}
func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
if !ref.IsValid() {
return secretstore.SecretMetadata{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
if m.metadata != nil {
if meta, exists := m.metadata[ref.Path]; exists {
return meta, nil
}
}
_, exists := m.secrets[ref.Path]
return secretstore.SecretMetadata{Exists: exists}, nil
}
func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
return m.capabilities
}
func (m *MockSecretStore) Validate(ctx context.Context) error {
return m.validateErr
}
Output: Store: advanced-store Supports versioning: true Supports metadata: true Supports binary: true Requires auth: true Auth methods: [iam api_key] Supports rotation: true Max versions: 10 Min rotation time: 1h0m0s Max secret length: 4096 Version-specific operations available Real-time notifications not supported - using polling
Example (Describe) ¶
Example demonstrates metadata-only operations for efficiency
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
store := &MockSecretStore{
name: "metadata-store",
metadata: map[string]secretstore.SecretMetadata{
"app/certificate": {
Exists: true,
Version: "v3.2",
UpdatedAt: time.Date(2024, 3, 15, 14, 30, 0, 0, time.UTC),
Size: 2048,
Type: "certificate",
Permissions: []string{"read", "rotate"},
Tags: map[string]string{
"environment": "production",
"team": "security",
"expires": "2024-12-31",
},
},
},
}
ctx := context.Background()
ref := secretstore.SecretRef{
Store: "metadata-store",
Path: "app/certificate",
}
// Get metadata without retrieving the secret value
meta, err := store.Describe(ctx, ref)
if err != nil {
log.Fatalf("Failed to describe secret: %v", err)
}
if meta.Exists {
fmt.Printf("Secret: %s\n", ref.Path)
fmt.Printf("Version: %s\n", meta.Version)
fmt.Printf("Size: %d bytes\n", meta.Size)
fmt.Printf("Type: %s\n", meta.Type)
fmt.Printf("Updated: %s\n", meta.UpdatedAt.Format("2006-01-02 15:04"))
fmt.Printf("Permissions: %v\n", meta.Permissions)
fmt.Printf("Team: %s\n", meta.Tags["team"])
fmt.Printf("Expires: %s\n", meta.Tags["expires"])
// Use metadata to make decisions without retrieving the secret
if meta.Type == "certificate" {
fmt.Println("Certificate detected - checking expiration...")
}
if contains(meta.Permissions, "rotate") {
fmt.Println("Secret supports rotation")
}
} else {
fmt.Printf("Secret %s does not exist\n", ref.Path)
}
}
// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
name string
secrets map[string]secretstore.SecretValue
metadata map[string]secretstore.SecretMetadata
capabilities secretstore.SecretStoreCapabilities
validateErr error
}
func (m *MockSecretStore) Name() string {
return m.name
}
func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
if !ref.IsValid() {
return secretstore.SecretValue{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
secret, exists := m.secrets[ref.Path]
if !exists {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path,
}
}
if ref.Version != "" && ref.Version != secret.Version {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path + " (version " + ref.Version + ")",
}
}
_ = ref.Field
return secret, nil
}
func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
if !ref.IsValid() {
return secretstore.SecretMetadata{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
if m.metadata != nil {
if meta, exists := m.metadata[ref.Path]; exists {
return meta, nil
}
}
_, exists := m.secrets[ref.Path]
return secretstore.SecretMetadata{Exists: exists}, nil
}
func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
return m.capabilities
}
func (m *MockSecretStore) Validate(ctx context.Context) error {
return m.validateErr
}
// Helper function used in examples
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
Output: Secret: app/certificate Version: v3.2 Size: 2048 bytes Type: certificate Updated: 2024-03-15 14:30 Permissions: [read rotate] Team: security Expires: 2024-12-31 Certificate detected - checking expiration... Secret supports rotation
Example (ErrorHandling) ¶
Example demonstrates error handling with structured error types
package main
import (
"context"
"errors"
"fmt"
"github.com/systmms/dsops/pkg/secretstore"
)
func main() {
store := &MockSecretStore{
name: "example-store",
secrets: make(map[string]secretstore.SecretValue),
}
ctx := context.Background()
ref := secretstore.SecretRef{
Store: "example-store",
Path: "nonexistent/secret",
}
_, err := store.Resolve(ctx, ref)
if err != nil {
// Handle different error types appropriately
var notFoundErr secretstore.NotFoundError
var authErr secretstore.AuthError
var validationErr secretstore.ValidationError
switch {
case errors.As(err, ¬FoundErr):
fmt.Printf("Secret not found: %s in store %s\n",
notFoundErr.Path, notFoundErr.Store)
// Could implement fallback logic here
case errors.As(err, &authErr):
fmt.Printf("Authentication failed for store %s: %s\n",
authErr.Store, authErr.Message)
// Could trigger re-authentication here
case errors.As(err, &validationErr):
fmt.Printf("Validation error: %s\n", validationErr.Message)
// Could provide user guidance here
default:
fmt.Printf("Unexpected error: %v\n", err)
}
}
}
// MockSecretStore implements SecretStore interface for examples and testing
type MockSecretStore struct {
name string
secrets map[string]secretstore.SecretValue
metadata map[string]secretstore.SecretMetadata
capabilities secretstore.SecretStoreCapabilities
validateErr error
}
func (m *MockSecretStore) Name() string {
return m.name
}
func (m *MockSecretStore) Resolve(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretValue, error) {
if !ref.IsValid() {
return secretstore.SecretValue{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
secret, exists := m.secrets[ref.Path]
if !exists {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path,
}
}
if ref.Version != "" && ref.Version != secret.Version {
return secretstore.SecretValue{}, secretstore.NotFoundError{
Store: m.name,
Path: ref.Path + " (version " + ref.Version + ")",
}
}
_ = ref.Field
return secret, nil
}
func (m *MockSecretStore) Describe(ctx context.Context, ref secretstore.SecretRef) (secretstore.SecretMetadata, error) {
if !ref.IsValid() {
return secretstore.SecretMetadata{}, secretstore.ValidationError{
Store: m.name,
Message: "invalid secret reference",
}
}
if m.metadata != nil {
if meta, exists := m.metadata[ref.Path]; exists {
return meta, nil
}
}
_, exists := m.secrets[ref.Path]
return secretstore.SecretMetadata{Exists: exists}, nil
}
func (m *MockSecretStore) Capabilities() secretstore.SecretStoreCapabilities {
return m.capabilities
}
func (m *MockSecretStore) Validate(ctx context.Context) error {
return m.validateErr
}
Output: Secret not found: nonexistent/secret in store example-store
type SecretStoreCapabilities ¶
type SecretStoreCapabilities struct {
// SupportsVersioning indicates if the store maintains multiple versions
// of secrets and can retrieve specific versions.
SupportsVersioning bool
// SupportsMetadata indicates if the store supports additional metadata
// like tags, descriptions, and custom attributes beyond the secret value.
SupportsMetadata bool
// SupportsWatching indicates if the store can notify about secret changes
// in real-time or support long-polling for updates.
SupportsWatching bool
// SupportsBinary indicates if the store can store and retrieve binary data
// (certificates, keys, images) or only text-based secrets.
SupportsBinary bool
// RequiresAuth indicates if the store requires authentication to access secrets.
// If false, the store may work without credentials (e.g., literal store).
RequiresAuth bool
// AuthMethods lists the authentication methods supported by this store.
// Common values include:
// - "api_key": API key/token authentication
// - "basic": Username/password authentication
// - "oauth2": OAuth2 flow
// - "iam": Cloud IAM roles
// - "certificate": Client certificate authentication
// - "cli": CLI-based authentication (like aws configure)
AuthMethods []string
// Rotation contains rotation-related capabilities for stores that
// support creating and managing multiple versions for rotation.
// Nil if the store doesn't support rotation operations.
Rotation *RotationCapabilities
}
SecretStoreCapabilities describes what features and operations a secret store supports.
This structure allows dsops to adapt its behavior based on store capabilities, enable/disable features appropriately, and provide accurate user feedback about what operations are possible.
Example:
caps := SecretStoreCapabilities{
SupportsVersioning: true,
SupportsMetadata: true,
SupportsWatching: false,
SupportsBinary: true,
RequiresAuth: true,
AuthMethods: []string{"iam", "api_key"},
Rotation: &RotationCapabilities{
SupportsRotation: true,
SupportsVersioning: true,
MaxVersions: 10,
MinRotationTime: time.Hour,
},
}
type SecretValue ¶
type SecretValue struct {
// Value is the actual secret data as a string.
// For binary data, this should be base64 encoded.
// Implementations must never log this field.
Value string
// Version identifies the specific version of this secret.
// Format is store-specific. May be empty if versioning not supported.
Version string
// UpdatedAt indicates when this secret was last modified.
// May be zero time if the store doesn't support timestamps.
UpdatedAt time.Time
// Metadata contains store-specific information about the secret.
// Common keys include:
// - "created_by": Who created the secret
// - "environment": Environment tag (prod, staging, dev)
// - "owner": Team or individual responsible
// - "rotation_id": Rotation tracking identifier
// - "content_type": MIME type for binary data
Metadata map[string]string
}
SecretValue represents a retrieved secret with its metadata.
This structure contains the actual secret value along with version information and timestamps. The Value field contains the raw secret data as a string. For binary data, this should be base64 encoded by the store implementation.
Example:
secret := SecretValue{
Value: "super-secret-password",
Version: "v1.2.3",
UpdatedAt: time.Now(),
Metadata: map[string]string{
"created_by": "rotation-service",
"environment": "production",
"owner": "platform-team",
},
}
type ValidationError ¶
type ValidationError struct {
// Store is the name of the secret store where validation failed.
// May be empty for general validation errors.
Store string
// Message provides details about what validation failed.
Message string
}
ValidationError indicates that a request or configuration is invalid.
This error should be returned when:
- SecretRef format is invalid
- Required parameters are missing
- Configuration values are out of range
- URI parsing fails
Example:
if !ref.IsValid() {
return SecretValue{}, ValidationError{
Store: s.Name(),
Message: "invalid secret reference format",
}
}
func (ValidationError) Error ¶
func (e ValidationError) Error() string
Error implements the error interface.