Skip to content
Merged
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
58 changes: 9 additions & 49 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,22 @@ DB_DRIVER=postgres
DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS=25
DB_MAX_IDLE_CONNECTIONS=5
DB_CONN_MAX_LIFETIME=5
DB_CONN_MAX_LIFETIME_MINUTES=5

# Server configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_SHUTDOWN_TIMEOUT_SECONDS=10

# Logging
LOG_LEVEL=info

# Metrics configuration
METRICS_ENABLED=true
METRICS_NAMESPACE=secrets
METRICS_PORT=8081

# Master keys (Envelope Encryption)
# All deployments REQUIRE KMS mode - master keys must be encrypted at rest using external Key Management Service
# Generate a new KMS master key using: ./bin/app create-master-key --kms-provider=<provider> --kms-key-uri=<uri>
# Rotate master keys using: ./bin/app rotate-master-key --id=<new-key-id>
#
# 🔒 SECURITY WARNING: KMS_KEY_URI is HIGHLY SENSITIVE
# - Controls access to ALL encrypted data in this deployment
# - NEVER commit KMS_KEY_URI to source control (even private repos)
# - Store in secrets manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, HashiCorp Vault)
# - Use .env files excluded from git (.env is in .gitignore)
# - Inject via CI/CD secrets for automated deployments
# - NEVER use base64key:// provider in staging or production (local development only)
# - Rotate KMS keys quarterly or per organizational policy
# - See docs/configuration.md#kms_key_uri for incident response procedures
#
# KMS Providers:
# - localsecrets: Local testing ONLY (base64key://<32-byte-base64-key>) ❌ DO NOT USE IN PRODUCTION
# - gcpkms: Google Cloud KMS (gcpkms://projects/<project>/locations/<location>/keyRings/<ring>/cryptoKeys/<key>)
# - awskms: AWS KMS (awskms:///<key-id> or awskms:///<alias>)
# - azurekeyvault: Azure Key Vault (azurekeyvault://<vault-name>.vault.azure.net/keys/<key-name>)
# - hashivault: HashiCorp Vault (hashivault:///<path>)
#
# === LOCAL DEVELOPMENT (localsecrets provider - INSECURE, DEVELOPMENT ONLY) ===
# For local development, use the localsecrets provider with a base64-encoded 32-byte key
KMS_PROVIDER=localsecrets
KMS_KEY_URI=base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=
MASTER_KEYS=default:ARiEeAASDiXKAxzOQCw2NxQfrHAc33CPP/7SsvuVjVvq1olzRBudplPoXRkquRWUXQ+CnEXi15LACqXuPGszLS+anJUrdn04
ACTIVE_MASTER_KEY_ID=default
#
# === PRODUCTION EXAMPLES (commented - uncomment and configure for your environment) ===
#
# GCP KMS Configuration:
# KMS_PROVIDER=gcpkms
# KMS_KEY_URI=gcpkms://projects/my-prod-project/locations/us-central1/keyRings/secrets-keyring/cryptoKeys/master-key
# MASTER_KEYS=default:ARiEeAASDiXKAxzOQCw2NxQfrHAc33CPP/7SsvuVjVvq1olzRBudplPoXRkquRWUXQ+CnEXi15LACqXuPGszLS+anJUrdn04
# ACTIVE_MASTER_KEY_ID=default
#
# AWS KMS Configuration:
# KMS_PROVIDER=awskms
# KMS_KEY_URI=awskms:///alias/secrets-master-key
# MASTER_KEYS=default:ARiEeAASDiXKAxzOQCw2NxQfrHAc33CPP/7SsvuVjVvq1olzRBudplPoXRkquRWUXQ+CnEXi15LACqXuPGszLS+anJUrdn04
# ACTIVE_MASTER_KEY_ID=default
#
# Azure Key Vault Configuration:
# KMS_PROVIDER=azurekeyvault
# KMS_KEY_URI=azurekeyvault://my-prod-vault.vault.azure.net/keys/secrets-master-key
# MASTER_KEYS=default:ARiEeAASDiXKAxzOQCw2NxQfrHAc33CPP/7SsvuVjVvq1olzRBudplPoXRkquRWUXQ+CnEXi15LACqXuPGszLS+anJUrdn04
# ACTIVE_MASTER_KEY_ID=default
# ...

# Authentication configuration
# Token expiration in seconds (default: 14400 = 4 hours)
Expand All @@ -86,6 +41,11 @@ RATE_LIMIT_TOKEN_ENABLED=true
RATE_LIMIT_TOKEN_REQUESTS_PER_SEC=5.0
RATE_LIMIT_TOKEN_BURST=10

# Account Lockout
LOCKOUT_MAX_ATTEMPTS=10
LOCKOUT_DURATION_MINUTES=30


# CORS configuration
# ⚠️ SECURITY WARNING: CORS is disabled by default for server-to-server API
# Enable only if browser-based access is required
Expand Down
15 changes: 10 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ⚙️ Environment Variables

> Last updated: 2026-02-28
> Last updated: 2026-03-02

Secrets is configured through environment variables.

Expand All @@ -12,11 +12,12 @@ DB_DRIVER=postgres
DB_CONNECTION_STRING=postgres://user:password@localhost:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS=25
DB_MAX_IDLE_CONNECTIONS=5
DB_CONN_MAX_LIFETIME=5
DB_CONN_MAX_LIFETIME_MINUTES=5

# Server configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_SHUTDOWN_TIMEOUT_SECONDS=10
LOG_LEVEL=info

# Master key configuration (KMS mode required as of v0.19.0)
Expand Down Expand Up @@ -98,7 +99,7 @@ Maximum number of open database connections (default: `25`).

Maximum number of idle database connections (default: `5`).

### DB_CONN_MAX_LIFETIME
### DB_CONN_MAX_LIFETIME_MINUTES

Maximum lifetime of a connection in minutes (default: `5`).

Expand All @@ -110,11 +111,15 @@ Host address to bind the HTTP server (default: `0.0.0.0`).

### SERVER_PORT

Port to bind the HTTP server (default: `8080`).
Port to bind the HTTP server (default: `8080`). Must be between 1 and 65535 and different from `METRICS_PORT`.

### SERVER_SHUTDOWN_TIMEOUT_SECONDS

Maximum time to wait for the server to gracefully shutdown in seconds (default: `10`).

### LOG_LEVEL

Logging level. Supported values: `debug`, `info`, `warn`, `error` (default: `info`).
Logging level. Supported values: `debug`, `info`, `warn`, `error`, `fatal`, `panic` (default: `info`).

## Master key configuration

Expand Down
6 changes: 4 additions & 2 deletions docs/getting-started/docker.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🐳 Run with Docker (Recommended)

> Last updated: 2026-02-28
> Last updated: 2026-03-02

This is the default way to run Secrets.

Expand Down Expand Up @@ -181,10 +181,11 @@ DB_DRIVER=postgres
DB_CONNECTION_STRING=postgres://user:password@secrets-postgres:5432/mydb?sslmode=disable
DB_MAX_OPEN_CONNECTIONS=25
DB_MAX_IDLE_CONNECTIONS=5
DB_CONN_MAX_LIFETIME=5
DB_CONN_MAX_LIFETIME_MINUTES=5

SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_SHUTDOWN_TIMEOUT_SECONDS=10
LOG_LEVEL=info

# KMS Configuration (required in v0.19.0+)
Expand All @@ -206,6 +207,7 @@ RATE_LIMIT_TOKEN_BURST=10

METRICS_ENABLED=true
METRICS_NAMESPACE=secrets
METRICS_PORT=8081
EOF

```
Expand Down
5 changes: 4 additions & 1 deletion docs/operations/deployment/database-scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ As your Secrets deployment grows, you may encounter:
| **Database CPU** | > 70% | Vertical scaling (larger instance) or read replicas |
| **Disk IOPS** | > 80% of provisioned | Increase IOPS or use faster storage |

> Last updated: 2026-03-02
...

## Connection Pooling

### Built-in Connection Pool
Expand All @@ -50,7 +53,7 @@ Secrets uses `database/sql` connection pooling (Go standard library):
# Environment variables for connection pooling
DB_MAX_OPEN_CONNECTIONS=25 # Max connections to database (default: 25)
DB_MAX_IDLE_CONNECTIONS=5 # Max idle connections in pool (default: 5)
DB_CONN_MAX_LIFETIME=5 # Max connection lifetime in minutes (default: 5 min)
DB_CONN_MAX_LIFETIME_MINUTES=5 # Max connection lifetime in minutes (default: 5 min)
```

### Tuning Guidelines
Expand Down
137 changes: 111 additions & 26 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,44 @@
package config

import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/allisson/go-env"
validation "github.com/jellydator/validation"
"github.com/joho/godotenv"
)

// Default configuration values.
const (
DefaultServerHost = "0.0.0.0"
DefaultServerPort = 8080
DefaultServerShutdownTimeout = 10 // seconds
DefaultDBDriver = "postgres"
DefaultDBConnectionString = "postgres://user:password@localhost:5432/mydb?sslmode=disable" //nolint:gosec
DefaultDBMaxOpenConnections = 25

DefaultDBMaxIdleConnections = 5
DefaultDBConnMaxLifetime = 5 // minutes
DefaultLogLevel = "info"
DefaultAuthTokenExpiration = 14400 // seconds
DefaultRateLimitEnabled = true
DefaultRateLimitRequests = 10.0
DefaultRateLimitBurst = 20
DefaultRateLimitTokenEnabled = true
DefaultRateLimitTokenRequests = 5.0
DefaultRateLimitTokenBurst = 10
DefaultCORSEnabled = false
DefaultCORSAllowOrigins = ""
DefaultMetricsEnabled = true
DefaultMetricsNamespace = "secrets"
DefaultMetricsPort = 8081
DefaultLockoutMaxAttempts = 10
DefaultLockoutDuration = 30 // minutes
)

// Config holds all application configuration.
type Config struct {
// ServerHost is the host address the server will bind to.
Expand All @@ -30,7 +60,7 @@ type Config struct {
// DBConnMaxLifetime is the maximum amount of time a connection may be reused.
DBConnMaxLifetime time.Duration

// LogLevel is the logging level (e.g., "debug", "info", "warn", "error").
// LogLevel is the logging level (e.g., "debug", "info", "warn", "error", "fatal", "panic").
LogLevel string

// AuthTokenExpiration is the duration after which an authentication token expires.
Expand Down Expand Up @@ -62,7 +92,7 @@ type Config struct {
// MetricsPort is the port number for the metrics server.
MetricsPort int

// KMSProvider is the KMS provider to use (e.g., "google", "aws", "azure").
// KMSProvider is the KMS provider to use (e.g., "google", "aws", "azure", "hashivault", "localsecrets").
KMSProvider string
// KMSKeyURI is the URI for the master key in the KMS.
KMSKeyURI string
Expand All @@ -73,60 +103,115 @@ type Config struct {
LockoutDuration time.Duration
}

// Validate checks if the configuration is valid.
func (c *Config) Validate() error {
return validation.ValidateStruct(
c,
validation.Field(&c.DBDriver, validation.Required, validation.In("postgres", "mysql")),
validation.Field(&c.DBConnectionString, validation.Required),
validation.Field(&c.ServerPort, validation.Required, validation.Min(1), validation.Max(65535)),
validation.Field(
&c.MetricsPort,
validation.Required,
validation.Min(1),
validation.Max(65535),
validation.NotIn(c.ServerPort),
),
validation.Field(
&c.LogLevel,
validation.Required,
validation.In("debug", "info", "warn", "error", "fatal", "panic"),
),
validation.Field(&c.KMSProvider, validation.When(c.KMSKeyURI != "", validation.Required)),
validation.Field(&c.KMSKeyURI, validation.When(c.KMSProvider != "", validation.Required)),
validation.Field(
&c.RateLimitRequestsPerSec,
validation.When(c.RateLimitEnabled, validation.Required, validation.Min(0.1)),
),
validation.Field(
&c.RateLimitTokenRequestsPerSec,
validation.When(c.RateLimitTokenEnabled, validation.Required, validation.Min(0.1)),
),
)
}

// Load loads configuration from environment variables and .env file.
func Load() *Config {
// Try to load .env file recursively
loadDotEnv()

return &Config{
cfg := &Config{
// Server configuration
ServerHost: env.GetString("SERVER_HOST", "0.0.0.0"),
ServerPort: env.GetInt("SERVER_PORT", 8080),
ServerShutdownTimeout: env.GetDuration("SERVER_SHUTDOWN_TIMEOUT", 10, time.Second),
ServerHost: env.GetString("SERVER_HOST", DefaultServerHost),
ServerPort: env.GetInt("SERVER_PORT", DefaultServerPort),
ServerShutdownTimeout: env.GetDuration(
"SERVER_SHUTDOWN_TIMEOUT_SECONDS",
DefaultServerShutdownTimeout,
time.Second,
),

// Database configuration
DBDriver: env.GetString("DB_DRIVER", "postgres"),
DBDriver: env.GetString("DB_DRIVER", DefaultDBDriver),
DBConnectionString: env.GetString(
"DB_CONNECTION_STRING",
"postgres://user:password@localhost:5432/mydb?sslmode=disable",
DefaultDBConnectionString,
),
DBMaxOpenConnections: env.GetInt("DB_MAX_OPEN_CONNECTIONS", DefaultDBMaxOpenConnections),
DBMaxIdleConnections: env.GetInt("DB_MAX_IDLE_CONNECTIONS", DefaultDBMaxIdleConnections),
DBConnMaxLifetime: env.GetDuration(
"DB_CONN_MAX_LIFETIME_MINUTES",
DefaultDBConnMaxLifetime,
time.Minute,
),
DBMaxOpenConnections: env.GetInt("DB_MAX_OPEN_CONNECTIONS", 25),
DBMaxIdleConnections: env.GetInt("DB_MAX_IDLE_CONNECTIONS", 5),
DBConnMaxLifetime: env.GetDuration("DB_CONN_MAX_LIFETIME", 5, time.Minute),

// Logging
LogLevel: env.GetString("LOG_LEVEL", "info"),
LogLevel: env.GetString("LOG_LEVEL", DefaultLogLevel),

// Auth
AuthTokenExpiration: env.GetDuration("AUTH_TOKEN_EXPIRATION_SECONDS", 14400, time.Second),
AuthTokenExpiration: env.GetDuration(
"AUTH_TOKEN_EXPIRATION_SECONDS",
DefaultAuthTokenExpiration,
time.Second,
),

// Rate Limiting (authenticated endpoints)
RateLimitEnabled: env.GetBool("RATE_LIMIT_ENABLED", true),
RateLimitRequestsPerSec: env.GetFloat64("RATE_LIMIT_REQUESTS_PER_SEC", 10.0),
RateLimitBurst: env.GetInt("RATE_LIMIT_BURST", 20),
RateLimitEnabled: env.GetBool("RATE_LIMIT_ENABLED", DefaultRateLimitEnabled),
RateLimitRequestsPerSec: env.GetFloat64("RATE_LIMIT_REQUESTS_PER_SEC", DefaultRateLimitRequests),
RateLimitBurst: env.GetInt("RATE_LIMIT_BURST", DefaultRateLimitBurst),

// Rate Limiting for Token Endpoint (IP-based, unauthenticated)
RateLimitTokenEnabled: env.GetBool("RATE_LIMIT_TOKEN_ENABLED", true),
RateLimitTokenRequestsPerSec: env.GetFloat64("RATE_LIMIT_TOKEN_REQUESTS_PER_SEC", 5.0),
RateLimitTokenBurst: env.GetInt("RATE_LIMIT_TOKEN_BURST", 10),
RateLimitTokenEnabled: env.GetBool("RATE_LIMIT_TOKEN_ENABLED", DefaultRateLimitTokenEnabled),
RateLimitTokenRequestsPerSec: env.GetFloat64(
"RATE_LIMIT_TOKEN_REQUESTS_PER_SEC",
DefaultRateLimitTokenRequests,
),
RateLimitTokenBurst: env.GetInt("RATE_LIMIT_TOKEN_BURST", DefaultRateLimitTokenBurst),

// CORS
CORSEnabled: env.GetBool("CORS_ENABLED", false),
CORSAllowOrigins: env.GetString("CORS_ALLOW_ORIGINS", ""),
CORSEnabled: env.GetBool("CORS_ENABLED", DefaultCORSEnabled),
CORSAllowOrigins: env.GetString("CORS_ALLOW_ORIGINS", DefaultCORSAllowOrigins),

// Metrics
MetricsEnabled: env.GetBool("METRICS_ENABLED", true),
MetricsNamespace: env.GetString("METRICS_NAMESPACE", "secrets"),
MetricsPort: env.GetInt("METRICS_PORT", 8081),
MetricsEnabled: env.GetBool("METRICS_ENABLED", DefaultMetricsEnabled),
MetricsNamespace: env.GetString("METRICS_NAMESPACE", DefaultMetricsNamespace),
MetricsPort: env.GetInt("METRICS_PORT", DefaultMetricsPort),

// KMS configuration
KMSProvider: env.GetString("KMS_PROVIDER", ""),
KMSKeyURI: env.GetString("KMS_KEY_URI", ""),

// Account Lockout
LockoutMaxAttempts: env.GetInt("LOCKOUT_MAX_ATTEMPTS", 10),
LockoutDuration: env.GetDuration("LOCKOUT_DURATION_MINUTES", 30, time.Minute),
LockoutMaxAttempts: env.GetInt("LOCKOUT_MAX_ATTEMPTS", DefaultLockoutMaxAttempts),
LockoutDuration: env.GetDuration("LOCKOUT_DURATION_MINUTES", DefaultLockoutDuration, time.Minute),
}

// Validate configuration
if err := cfg.Validate(); err != nil {
fmt.Printf("configuration validation failed: %v\n", err)
os.Exit(1)
}

return cfg
}

// GetGinMode returns the appropriate Gin mode based on log level.
Expand Down
Loading
Loading