monorepo/cloud/maplefile-backend/pkg/security/secureconfig/secureconfig.go

187 lines
5 KiB
Go

// Package secureconfig provides secure access to configuration secrets.
// It wraps sensitive configuration values in memguard-protected buffers
// to prevent secret leakage through memory dumps.
package secureconfig
import (
"sync"
"github.com/awnumar/memguard"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
)
// SecureConfigProvider provides secure access to configuration secrets.
// Secrets are stored in memguard LockedBuffers and wiped when no longer needed.
type SecureConfigProvider struct {
mu sync.RWMutex
// Cached secure buffers - created on first access
jwtSecret *memguard.LockedBuffer
dbPassword *memguard.LockedBuffer
cachePassword *memguard.LockedBuffer
s3AccessKey *memguard.LockedBuffer
s3SecretKey *memguard.LockedBuffer
mailgunAPIKey *memguard.LockedBuffer
// Original config for initial loading
cfg *config.Config
}
// NewSecureConfigProvider creates a new secure config provider from the given config.
// The original config secrets are copied to secure buffers and should be cleared
// from the original config after this call.
func NewSecureConfigProvider(cfg *config.Config) *SecureConfigProvider {
provider := &SecureConfigProvider{
cfg: cfg,
}
// Pre-load secrets into secure buffers
provider.loadSecrets()
return provider
}
// loadSecrets copies secrets from config into memguard buffers.
// SECURITY: Original config strings remain in memory but secure buffers provide
// additional protection for long-lived secret access.
func (p *SecureConfigProvider) loadSecrets() {
p.mu.Lock()
defer p.mu.Unlock()
// JWT Secret
if p.cfg.JWT.Secret != "" {
p.jwtSecret = memguard.NewBufferFromBytes([]byte(p.cfg.JWT.Secret))
}
// Database Password
if p.cfg.Database.Password != "" {
p.dbPassword = memguard.NewBufferFromBytes([]byte(p.cfg.Database.Password))
}
// Cache Password
if p.cfg.Cache.Password != "" {
p.cachePassword = memguard.NewBufferFromBytes([]byte(p.cfg.Cache.Password))
}
// S3 Access Key
if p.cfg.S3.AccessKey != "" {
p.s3AccessKey = memguard.NewBufferFromBytes([]byte(p.cfg.S3.AccessKey))
}
// S3 Secret Key
if p.cfg.S3.SecretKey != "" {
p.s3SecretKey = memguard.NewBufferFromBytes([]byte(p.cfg.S3.SecretKey))
}
// Mailgun API Key
if p.cfg.Mailgun.APIKey != "" {
p.mailgunAPIKey = memguard.NewBufferFromBytes([]byte(p.cfg.Mailgun.APIKey))
}
}
// JWTSecret returns the JWT secret as a secure byte slice.
// The returned bytes should not be stored - use immediately and let GC collect.
func (p *SecureConfigProvider) JWTSecret() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.jwtSecret == nil || !p.jwtSecret.IsAlive() {
return nil
}
return p.jwtSecret.Bytes()
}
// DatabasePassword returns the database password as a secure byte slice.
func (p *SecureConfigProvider) DatabasePassword() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.dbPassword == nil || !p.dbPassword.IsAlive() {
return nil
}
return p.dbPassword.Bytes()
}
// CachePassword returns the cache password as a secure byte slice.
func (p *SecureConfigProvider) CachePassword() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.cachePassword == nil || !p.cachePassword.IsAlive() {
return nil
}
return p.cachePassword.Bytes()
}
// S3AccessKey returns the S3 access key as a secure byte slice.
func (p *SecureConfigProvider) S3AccessKey() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.s3AccessKey == nil || !p.s3AccessKey.IsAlive() {
return nil
}
return p.s3AccessKey.Bytes()
}
// S3SecretKey returns the S3 secret key as a secure byte slice.
func (p *SecureConfigProvider) S3SecretKey() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.s3SecretKey == nil || !p.s3SecretKey.IsAlive() {
return nil
}
return p.s3SecretKey.Bytes()
}
// MailgunAPIKey returns the Mailgun API key as a secure byte slice.
func (p *SecureConfigProvider) MailgunAPIKey() []byte {
p.mu.RLock()
defer p.mu.RUnlock()
if p.mailgunAPIKey == nil || !p.mailgunAPIKey.IsAlive() {
return nil
}
return p.mailgunAPIKey.Bytes()
}
// Destroy securely wipes all cached secrets from memory.
// Should be called during application shutdown.
func (p *SecureConfigProvider) Destroy() {
p.mu.Lock()
defer p.mu.Unlock()
if p.jwtSecret != nil && p.jwtSecret.IsAlive() {
p.jwtSecret.Destroy()
}
if p.dbPassword != nil && p.dbPassword.IsAlive() {
p.dbPassword.Destroy()
}
if p.cachePassword != nil && p.cachePassword.IsAlive() {
p.cachePassword.Destroy()
}
if p.s3AccessKey != nil && p.s3AccessKey.IsAlive() {
p.s3AccessKey.Destroy()
}
if p.s3SecretKey != nil && p.s3SecretKey.IsAlive() {
p.s3SecretKey.Destroy()
}
if p.mailgunAPIKey != nil && p.mailgunAPIKey.IsAlive() {
p.mailgunAPIKey.Destroy()
}
p.jwtSecret = nil
p.dbPassword = nil
p.cachePassword = nil
p.s3AccessKey = nil
p.s3SecretKey = nil
p.mailgunAPIKey = nil
}
// Config returns the underlying config for non-secret access.
// Prefer using the specific secret accessor methods for sensitive data.
func (p *SecureConfigProvider) Config() *config.Config {
return p.cfg
}