security

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 7, 2025 License: MIT Imports: 3 Imported by: 0

README

Security Package

Secure credential management for the Event Sourcing Framework

Security Badge

This package provides enterprise-grade credential management using Go Cloud Development Kit, enabling secure storage and retrieval of sensitive data across multiple cloud providers and local environments.

Table of Contents

Quick Start

Installation

The credentials package is part of the eventsourcing framework. Import the providers you need:

import (
    "github.com/plaenen/eventstore/pkg/security/credentials"

    // Cloud provider imports (choose what you need):
    _ "gocloud.dev/secrets/awskms"        // AWS Secrets Manager
    _ "gocloud.dev/secrets/gcpkms"        // GCP Secret Manager
    _ "gocloud.dev/secrets/azurekeyvault" // Azure Key Vault
    _ "gocloud.dev/secrets/hashivault"    // HashiCorp Vault
    _ "gocloud.dev/secrets/localsecrets"  // Local development
)
Basic Usage
ctx := context.Background()

// Development: Environment variables
provider := credentials.NewEnvTokenProvider("NATS_TOKEN", 5*time.Minute)
defer provider.Close()

// Get credentials
creds, err := provider.GetCredentials(ctx)
if err != nil {
    log.Fatal(err)
}

// Use with transport
transport, err := natscqrs.NewTransport(&natscqrs.TransportConfig{
    URL:                "nats://localhost:4222",
    CredentialProvider: provider, // ✅ Secure
})

Provider Types

Overview
Provider Use Case Security Level Rotation Support
SecretProvider Production (Cloud) ⭐⭐⭐⭐⭐ High ✅ Yes (automatic)
EnvProvider CI/CD, Containers ⭐⭐⭐⭐ Good ✅ Yes (manual)
ChainProvider Fallback scenarios ⭐⭐⭐⭐ Good ✅ Yes
StaticProvider Development only ⭐ Low ❌ No

Uses Go Cloud Development Kit for vendor-agnostic secret management.

Supported Backends:

  • AWS Secrets Manager
  • GCP Secret Manager
  • Azure Key Vault
  • HashiCorp Vault
  • Local file encryption (development)

Features:

  • Automatic credential rotation
  • Built-in caching with configurable TTL
  • Background refresh
  • Thread-safe

Example:

// AWS Secrets Manager
provider, err := credentials.NewSecretProvider(ctx,
    "awskms://arn:aws:secretsmanager:us-east-1:123456789:secret:nats-credentials")

// GCP Secret Manager
provider, err := credentials.NewSecretProvider(ctx,
    "gcpkms://projects/my-project/secrets/nats-creds/versions/latest")

// Azure Key Vault
provider, err := credentials.NewSecretProvider(ctx,
    "azurekeyvault://my-vault.vault.azure.net/secrets/nats-creds")

// Local (development)
provider, err := credentials.NewSecretProvider(ctx,
    "base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=")

Reads credentials from environment variables at runtime.

Best for:

  • Kubernetes secrets
  • Docker containers
  • CI/CD pipelines
  • 12-factor apps

Example:

// Token from environment
provider := credentials.NewEnvTokenProvider("NATS_TOKEN", 5*time.Minute)

// Username/password from environment
provider := credentials.NewEnvUserPasswordProvider("NATS_USER", "NATS_PASS")

Tries multiple providers in order, falling back on failure.

Example:

provider := credentials.NewChainProvider(
    // Try cloud secret manager first
    credentials.NewSecretProvider(ctx, "awskms://..."),
    // Fall back to environment variables
    credentials.NewEnvTokenProvider("NATS_TOKEN", 5*time.Minute),
    // Last resort: static (dev only)
    credentials.NewStaticTokenProvider("dev-token", 24*time.Hour),
)
defer provider.Close()
4. StaticProvider (Development Only)

⚠️ WARNING: Never use in production!

Provides credentials from static values. Useful only for local development.

Example:

// ⚠️ DEVELOPMENT ONLY
provider := credentials.NewStaticTokenProvider("dev-token-12345", 24*time.Hour)
provider := credentials.NewStaticUserPasswordProvider("user", "pass")

Usage Examples

Example 1: NATS Transport with Secure Credentials
package main

import (
    "context"
    "log"
    "time"

    natscqrs "github.com/plaenen/eventstore/pkg/cqrs/nats"
    "github.com/plaenen/eventstore/pkg/security/credentials"
    _ "gocloud.dev/secrets/awskms"
)

func main() {
    ctx := context.Background()

    // Load credentials from AWS Secrets Manager
    provider, err := credentials.NewSecretProvider(ctx,
        "awskms://arn:aws:secretsmanager:us-east-1:123456:secret:nats-prod-creds")
    if err != nil {
        log.Fatalf("Failed to create provider: %v", err)
    }
    defer provider.Close()

    // Create transport with secure credentials
    transport, err := natscqrs.NewTransport(&natscqrs.TransportConfig{
        URL:                "nats://production.example.com:4222",
        CredentialProvider: provider,
        Name:               "my-service",
    })
    if err != nil {
        log.Fatalf("Failed to create transport: %v", err)
    }
    defer transport.Close()

    log.Printf("Connected to NATS: %s", transport.ConnectedURL())
}
Example 2: Multiple Credential Types
// Token authentication
tokenCreds := &credentials.Credentials{
    Type:  credentials.CredentialTypeToken,
    Token: "your-secret-token",
    ExpiresAt: &expiryTime,
}

// Username/Password authentication
userPassCreds := &credentials.Credentials{
    Type:     credentials.CredentialTypeUserPassword,
    User:     "admin",
    Password: "secure-password",
}

// NKey authentication (NATS)
nkeyCreds := &credentials.Credentials{
    Type:      credentials.CredentialTypeNKey,
    PublicKey: "UABC...",
    Seed:      "SUABC...",
}

// JWT authentication
jwtCreds := &credentials.Credentials{
    Type:     credentials.CredentialTypeJWT,
    JWTToken: "eyJhbGciOiJIUzI1NiIs...",
}

// mTLS authentication
mtlsCreds := &credentials.Credentials{
    Type:    credentials.CredentialTypeMTLS,
    CertPEM: "-----BEGIN CERTIFICATE-----\n...",
    KeyPEM:  "-----BEGIN PRIVATE KEY-----\n...",
}
Example 3: Credential Rotation
provider, err := credentials.NewSecretProvider(ctx, secretURL)
if err != nil {
    log.Fatal(err)
}
defer provider.Close()

// Manual rotation
if err := provider.Rotate(ctx); err != nil {
    log.Printf("Rotation failed: %v", err)
}

// Automatic rotation is enabled by default with:
config := credentials.DefaultConfig()
// config.AutoRefresh = true
// config.RefreshInterval = 2.5 * time.Minute
provider, err := credentials.NewSecretProviderWithConfig(ctx, secretURL, config)
Example 4: Custom Configuration
config := credentials.ProviderConfig{
    URL:             "awskms://...",
    CacheTTL:        10 * time.Minute,      // Cache for 10 minutes
    AutoRefresh:     true,                   // Enable auto-refresh
    RefreshInterval: 5 * time.Minute,        // Refresh every 5 minutes
}

provider, err := credentials.NewSecretProviderWithConfig(ctx, config.URL, config)
Example 5: Storing Credentials
// Create credentials
creds := &credentials.Credentials{
    Type:  credentials.CredentialTypeToken,
    Token: "production-secret-token",
    Metadata: map[string]string{
        "environment": "production",
        "service":     "nats",
    },
}

// Validate
if err := creds.Validate(); err != nil {
    log.Fatalf("Invalid credentials: %v", err)
}

// Store in secret backend
err := credentials.StoreCredentials(ctx,
    "awskms://arn:aws:secretsmanager:us-east-1:123:secret:my-secret",
    creds)
if err != nil {
    log.Fatalf("Failed to store: %v", err)
}

Production Deployment

AWS Secrets Manager
package main

import (
    "context"
    "log"

    "github.com/plaenen/eventstore/pkg/security/credentials"
    _ "gocloud.dev/secrets/awskms"
)

func main() {
    ctx := context.Background()

    // ARN format: arn:aws:secretsmanager:REGION:ACCOUNT:secret:NAME
    provider, err := credentials.NewSecretProvider(ctx,
        "awskms://arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/nats-credentials-AbCdEf")
    if err != nil {
        log.Fatal(err)
    }
    defer provider.Close()

    creds, err := provider.GetCredentials(ctx)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Loaded credentials: type=%s", creds.Type)
}

Setup:

  1. Create secret in AWS:
aws secretsmanager create-secret \
    --name prod/nats-credentials \
    --description "NATS production credentials" \
    --secret-string file://secret.json
  1. Grant IAM permissions:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:prod/nats-credentials-*"
    }
  ]
}
  1. Secret format (secret.json):
{
  "credentials": {
    "type": "token",
    "token": "your-secret-token",
    "metadata": {
      "environment": "production"
    }
  },
  "version": 1,
  "created_at": "2024-01-15T10:00:00Z"
}
GCP Secret Manager
provider, err := credentials.NewSecretProvider(ctx,
    "gcpkms://projects/my-project-123/secrets/nats-credentials/versions/latest")

Setup:

# Create secret
gcloud secrets create nats-credentials \
    --data-file=secret.json \
    --replication-policy=automatic

# Grant access
gcloud secrets add-iam-policy-binding nats-credentials \
    --member="serviceAccount:my-service@my-project.iam.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"
Azure Key Vault
provider, err := credentials.NewSecretProvider(ctx,
    "azurekeyvault://my-vault-name.vault.azure.net/secrets/nats-credentials")

Setup:

# Create secret
az keyvault secret set \
    --vault-name my-vault-name \
    --name nats-credentials \
    --file secret.json

# Grant access
az keyvault set-policy \
    --name my-vault-name \
    --object-id <service-principal-id> \
    --secret-permissions get list
Kubernetes

Use environment variables with Kubernetes secrets:

apiVersion: v1
kind: Secret
metadata:
  name: nats-credentials
type: Opaque
stringData:
  NATS_TOKEN: "your-secret-token"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  template:
    spec:
      containers:
      - name: app
        env:
        - name: NATS_TOKEN
          valueFrom:
            secretKeyRef:
              name: nats-credentials
              key: NATS_TOKEN

Application code:

provider := credentials.NewEnvTokenProvider("NATS_TOKEN", 5*time.Minute)

Security Best Practices

✅ DO
  1. Use cloud secret managers in production

    provider, err := credentials.NewSecretProvider(ctx, "awskms://...")
    
  2. Use environment variables for CI/CD

    provider := credentials.NewEnvTokenProvider("SECRET_TOKEN", ttl)
    
  3. Enable automatic rotation

    config := credentials.DefaultConfig()
    config.AutoRefresh = true
    provider, _ := credentials.NewSecretProviderWithConfig(ctx, url, config)
    
  4. Set appropriate cache TTLs

    config.CacheTTL = 5 * time.Minute // Balance security vs performance
    
  5. Close providers when done

    defer provider.Close() // Always clean up
    
  6. Validate credentials before use

    if err := creds.Validate(); err != nil {
        log.Fatal(err)
    }
    
❌ DON'T
  1. Never hardcode credentials

    // ❌ BAD
    Token: "my-secret-token"
    
    // ✅ GOOD
    CredentialProvider: provider
    
  2. Never commit secrets to version control

    # Add to .gitignore
    .env
    *.pem
    *.key
    secrets/
    
  3. Never use static providers in production

    // ❌ BAD - Development only!
    provider := credentials.NewStaticTokenProvider(token, ttl)
    
  4. Never log sensitive credentials

    // ✅ GOOD - Credentials.MarshalJSON() redacts sensitive fields
    log.Printf("Credentials: %+v", creds) // Safe - passwords redacted
    
  5. Never share credentials between environments

    • Dev, staging, and production must use separate credentials
    • Rotate immediately if credentials are exposed
  6. Never set long cache TTLs

    // ❌ BAD
    CacheTTL: 24 * time.Hour
    
    // ✅ GOOD
    CacheTTL: 5 * time.Minute
    

Migration Guide

From Plaintext to Secure Credentials
Before (Insecure)
transport, err := natscqrs.NewTransport(&natscqrs.TransportConfig{
    URL:   "nats://production:4222",
    Token: "my-secret-token", // ❌ Plaintext in code!
})
After (Secure)
// Option 1: Environment variables
provider := credentials.NewEnvTokenProvider("NATS_TOKEN", 5*time.Minute)

transport, err := natscqrs.NewTransport(&natscqrs.TransportConfig{
    URL:                "nats://production:4222",
    CredentialProvider: provider, // ✅ Secure!
})
// Option 2: Cloud secret manager
provider, _ := credentials.NewSecretProvider(ctx, "awskms://...")

transport, err := natscqrs.NewTransport(&natscqrs.TransportConfig{
    URL:                "nats://production:4222",
    CredentialProvider: provider, // ✅ Even better!
})
Migration Steps
  1. Identify hardcoded credentials

    grep -r "Token:" . --include="*.go"
    grep -r "Password:" . --include="*.go"
    
  2. Store credentials securely

    # AWS example
    aws secretsmanager create-secret \
        --name my-service/credentials \
        --secret-string file://secret.json
    
  3. Update application code

    • Replace Token with CredentialProvider
    • Add provider initialization
  4. Test in development

    provider := credentials.NewEnvTokenProvider("DEV_TOKEN", 5*time.Minute)
    
  5. Deploy to production

    provider, _ := credentials.NewSecretProvider(ctx, "awskms://...")
    
  6. Verify and rotate

    • Test connectivity
    • Rotate old credentials
    • Remove old plaintext configs

API Reference

Provider Interface
type Provider interface {
    // GetCredentials retrieves the current credentials
    GetCredentials(ctx context.Context) (*Credentials, error)

    // Rotate triggers credential rotation (if supported)
    Rotate(ctx context.Context) error

    // Type returns the credential type this provider manages
    Type() CredentialType

    // Close releases any resources held by the provider
    Close() error
}
Credentials Types
const (
    CredentialTypeToken        CredentialType = "token"
    CredentialTypeNKey         CredentialType = "nkey"
    CredentialTypeJWT          CredentialType = "jwt"
    CredentialTypeUserPassword CredentialType = "user_password"
    CredentialTypeMTLS         CredentialType = "mtls"
)
Credentials Struct
type Credentials struct {
    Type      CredentialType
    Token     string
    User      string
    Password  string
    PublicKey string
    Seed      string
    JWTToken  string
    CertPEM   string
    KeyPEM    string
    ExpiresAt *time.Time
    Metadata  map[string]string
}

Methods:

  • IsExpired() bool - Check if credentials have expired
  • Validate() error - Validate credentials for their type
  • MarshalJSON() ([]byte, error) - Safely marshal (redacts sensitive fields)
Configuration
type ProviderConfig struct {
    URL             string
    CacheTTL        time.Duration
    AutoRefresh     bool
    RefreshInterval time.Duration
}

Defaults:

  • CacheTTL: 5 minutes
  • AutoRefresh: true
  • RefreshInterval: 2.5 minutes (50% of CacheTTL)
Errors
var (
    ErrCredentialsExpired = errors.New("credentials expired")
    ErrInvalidCredentials = errors.New("invalid credentials")
    ErrProviderClosed     = errors.New("provider is closed")
)

Troubleshooting

Common Issues
1. "Provider is closed"

Problem: Attempting to use a provider after calling Close().

Solution:

// Ensure Close() is only called once using defer
defer provider.Close()

// Or check before use
creds, err := provider.GetCredentials(ctx)
if errors.Is(err, credentials.ErrProviderClosed) {
    // Recreate provider
}
2. "Credentials expired"

Problem: Credentials have passed their expiration time.

Solution:

creds, err := provider.GetCredentials(ctx)
if errors.Is(err, credentials.ErrCredentialsExpired) {
    // Trigger rotation
    if err := provider.Rotate(ctx); err != nil {
        log.Fatal(err)
    }
    creds, err = provider.GetCredentials(ctx)
}
3. "Failed to open secret keeper"

Problem: Invalid secret URL or missing cloud provider import.

Solution:

// Ensure correct import
import _ "gocloud.dev/secrets/awskms"

// Verify URL format
url := "awskms://arn:aws:secretsmanager:us-east-1:123:secret:name"
//       ^^^^^^^ Scheme must match imported provider
4. "Failed to decrypt secret"

Problem: Insufficient permissions or incorrect secret format.

Solution:

# AWS: Check IAM permissions
aws secretsmanager get-secret-value --secret-id my-secret

# Verify secret format
{
  "credentials": {
    "type": "token",
    "token": "..."
  },
  "version": 1,
  "created_at": "2024-01-15T10:00:00Z"
}
5. Environment variable not set

Problem: EnvProvider can't find required environment variable.

Solution:

# Set environment variable
export NATS_TOKEN="your-token-here"

# Or in code, check before creating provider
if os.Getenv("NATS_TOKEN") == "" {
    log.Fatal("NATS_TOKEN environment variable not set")
}
Debug Logging

Enable debug logging to troubleshoot issues:

// Add logging around provider operations
log.Printf("Creating provider with URL: %s", url)
provider, err := credentials.NewSecretProvider(ctx, url)
if err != nil {
    log.Printf("Provider creation failed: %v", err)
}

log.Printf("Getting credentials...")
creds, err := provider.GetCredentials(ctx)
if err != nil {
    log.Printf("Failed to get credentials: %v", err)
}

log.Printf("Credentials loaded: type=%s", creds.Type)
Testing
Unit Tests
func TestMyService(t *testing.T) {
    // Use static provider for tests
    provider := credentials.NewStaticTokenProvider("test-token", 1*time.Hour)
    defer provider.Close()

    service := NewService(provider)
    // Test service...
}
Integration Tests
func TestIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test")
    }

    // Use real provider
    ctx := context.Background()
    provider, err := credentials.NewSecretProvider(ctx,
        os.Getenv("TEST_SECRET_URL"))
    if err != nil {
        t.Fatalf("Failed to create provider: %v", err)
    }
    defer provider.Close()

    // Run integration test...
}

Additional Resources

Support

For issues or questions:

  1. Check Troubleshooting section
  2. Review examples
  3. Open an issue on GitHub

License

See the main project LICENSE file.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetHTTPStatus

func GetHTTPStatus(code ErrorCode) int

GetHTTPStatus returns the HTTP status code equivalent for an error code.

func IsClientError

func IsClientError(code ErrorCode) bool

IsClientError returns true if the error code represents a client error (4xx equivalent).

func IsServerError

func IsServerError(code ErrorCode) bool

IsServerError returns true if the error code represents a server error (5xx equivalent).

Types

type ErrorCode

type ErrorCode string

ErrorCode represents a safe error code that can be returned to clients.

const (
	// Client errors (4xx equivalent)
	ErrorCodeNotFound            ErrorCode = "NOT_FOUND"
	ErrorCodeAlreadyExists       ErrorCode = "ALREADY_EXISTS"
	ErrorCodeInvalidInput        ErrorCode = "INVALID_INPUT"
	ErrorCodeConcurrencyConflict ErrorCode = "CONCURRENCY_CONFLICT"
	ErrorCodePermissionDenied    ErrorCode = "PERMISSION_DENIED"
	ErrorCodeUnauthenticated     ErrorCode = "UNAUTHENTICATED"
	ErrorCodeDuplicateCommand    ErrorCode = "DUPLICATE_COMMAND"

	// Server errors (5xx equivalent)
	ErrorCodeInternal     ErrorCode = "INTERNAL_ERROR"
	ErrorCodeUnavailable  ErrorCode = "SERVICE_UNAVAILABLE"
	ErrorCodeStorageError ErrorCode = "STORAGE_ERROR"
	ErrorCodeTimeout      ErrorCode = "TIMEOUT"
)

type ErrorMode

type ErrorMode string

ErrorMode determines how errors are sanitized.

const (
	// ErrorModeProduction sanitizes all errors to prevent information disclosure
	ErrorModeProduction ErrorMode = "production"

	// ErrorModeDevelopment returns detailed errors for debugging
	ErrorModeDevelopment ErrorMode = "development"
)

type ErrorSanitizer

type ErrorSanitizer struct {
	// contains filtered or unexported fields
}

ErrorSanitizer sanitizes errors based on the current mode.

func NewErrorSanitizer

func NewErrorSanitizer(mode ErrorMode) *ErrorSanitizer

NewErrorSanitizer creates a new error sanitizer with the specified mode.

func (*ErrorSanitizer) SanitizeDatabaseError

func (s *ErrorSanitizer) SanitizeDatabaseError(operation string, err error) error

SanitizeDatabaseError sanitizes database errors to prevent information disclosure.

SEC-004: Database errors can expose: - File paths - SQL queries - Database schema - Internal state

func (*ErrorSanitizer) SanitizeError

func (s *ErrorSanitizer) SanitizeError(err error) error

SanitizeError converts an error into a safe error appropriate for the current mode.

In production mode, it returns sanitized error messages with error codes. In development mode, it returns detailed error information for debugging.

Example:

sanitizer := NewErrorSanitizer(ErrorModeProduction)
safeErr := sanitizer.SanitizeError(err)
// Returns: "An internal error occurred" with code INTERNAL_ERROR

func (*ErrorSanitizer) SanitizePanicError

func (s *ErrorSanitizer) SanitizePanicError(panicValue interface{}) error

SanitizePanicError sanitizes panic errors to prevent information disclosure.

SEC-004: Panic values can contain sensitive information from the call stack.

func (*ErrorSanitizer) SanitizeUniqueConstraintError

func (s *ErrorSanitizer) SanitizeUniqueConstraintError(indexName, value, ownerID string) error

SanitizeUniqueConstraintError creates a safe error from a UniqueConstraintError.

SEC-004: UniqueConstraintError exposes sensitive information (index names, values, owner IDs). This function sanitizes it to prevent information disclosure.

type SafeError

type SafeError struct {
	Code            ErrorCode
	Message         string
	InternalError   error
	InternalDetails map[string]interface{}
}

SafeError represents an error with a safe client-facing message and error code.

func NewSafeError

func NewSafeError(code ErrorCode, clientMessage string, internalError error, details map[string]interface{}) *SafeError

NewSafeError creates a new SafeError with the specified code and messages.

The clientMessage is returned to clients. The internalMessage and error are logged server-side only.

func (*SafeError) Error

func (e *SafeError) Error() string

func (*SafeError) GetCode

func (e *SafeError) GetCode() ErrorCode

GetCode returns the error code for client responses.

func (*SafeError) GetInternalDetails

func (e *SafeError) GetInternalDetails() map[string]interface{}

GetInternalDetails returns additional context for server-side logging.

func (*SafeError) GetInternalError

func (e *SafeError) GetInternalError() error

GetInternalError returns the original error for server-side logging.

func (*SafeError) Unwrap

func (e *SafeError) Unwrap() error

Directories

Path Synopsis
Package credentials provides secure credential management using Go Cloud Development Kit.
Package credentials provides secure credential management using Go Cloud Development Kit.
Package encryption provides data encryption at rest for the EventSourcing framework.
Package encryption provides data encryption at rest for the EventSourcing framework.
Package tls provides TLS configuration and certificate management for secure transport.
Package tls provides TLS configuration and certificate management for secure transport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL