- Add Routes model to services UI (paths, headers, IP whitelisting per route) - Show TLS cert info per service with inline provision/renew actions - Remove TLS Certificates section from dashboard (now on services page) - Make gateway router port list dynamic from config + VPN state - Add TODO for header validation in HAProxy config generation
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package secrets
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
|
|
"github.com/wild-cloud/wild-central/internal/storage"
|
|
"github.com/wild-cloud/wild-central/internal/tools"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
// DefaultSecretLength is 32 characters
|
|
DefaultSecretLength = 32
|
|
// Alphanumeric characters for secret generation
|
|
alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
)
|
|
|
|
// Manager handles secret storage backed by a single YAML file.
|
|
type Manager struct {
|
|
secretsPath string
|
|
yq *tools.YQ
|
|
}
|
|
|
|
// NewManager creates a new secrets manager bound to the given file path.
|
|
func NewManager(secretsPath string) *Manager {
|
|
return &Manager{
|
|
secretsPath: secretsPath,
|
|
yq: tools.NewYQ(),
|
|
}
|
|
}
|
|
|
|
// GenerateSecret generates a cryptographically secure random alphanumeric string.
|
|
func GenerateSecret(length int) (string, error) {
|
|
if length <= 0 {
|
|
length = DefaultSecretLength
|
|
}
|
|
|
|
result := make([]byte, length)
|
|
for i := range result {
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumeric))))
|
|
if err != nil {
|
|
return "", fmt.Errorf("generating random number: %w", err)
|
|
}
|
|
result[i] = alphanumeric[num.Int64()]
|
|
}
|
|
|
|
return string(result), nil
|
|
}
|
|
|
|
// GetSecret retrieves a secret value by dot-notation key.
|
|
func (m *Manager) GetSecret(key string) (string, error) {
|
|
if !storage.FileExists(m.secretsPath) {
|
|
return "", fmt.Errorf("secrets file not found: %s", m.secretsPath)
|
|
}
|
|
|
|
value, err := m.yq.Get(m.secretsPath, fmt.Sprintf(".%s", key))
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting secret %s: %w", key, err)
|
|
}
|
|
|
|
if value == "" || value == "null" {
|
|
return "", fmt.Errorf("secret not found: %s", key)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// SetSecret sets a secret value by dot-notation key.
|
|
func (m *Manager) SetSecret(key, value string) error {
|
|
if !storage.FileExists(m.secretsPath) {
|
|
return fmt.Errorf("secrets file not found: %s", m.secretsPath)
|
|
}
|
|
|
|
lockPath := m.secretsPath + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
if err := m.yq.Set(m.secretsPath, fmt.Sprintf(".%s", key), value); err != nil {
|
|
return err
|
|
}
|
|
return storage.EnsureFilePermissions(m.secretsPath, 0600)
|
|
})
|
|
}
|
|
|
|
// DeleteSecret removes a secret by dot-notation key.
|
|
func (m *Manager) DeleteSecret(key string) error {
|
|
if !storage.FileExists(m.secretsPath) {
|
|
return fmt.Errorf("secrets file not found: %s", m.secretsPath)
|
|
}
|
|
|
|
lockPath := m.secretsPath + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
if err := m.yq.Delete(m.secretsPath, fmt.Sprintf(".%s", key)); err != nil {
|
|
return err
|
|
}
|
|
return storage.EnsureFilePermissions(m.secretsPath, 0600)
|
|
})
|
|
}
|
|
|
|
// GetAll reads and returns the full secrets map.
|
|
func (m *Manager) GetAll() (map[string]any, error) {
|
|
data, err := os.ReadFile(m.secretsPath)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return map[string]any{}, nil
|
|
}
|
|
return nil, fmt.Errorf("reading secrets: %w", err)
|
|
}
|
|
|
|
var secretsMap map[string]any
|
|
if err := yaml.Unmarshal(data, &secretsMap); err != nil {
|
|
return nil, fmt.Errorf("parsing secrets: %w", err)
|
|
}
|
|
if secretsMap == nil {
|
|
secretsMap = map[string]any{}
|
|
}
|
|
|
|
return secretsMap, nil
|
|
}
|
|
|
|
// MergeUpdate merges the provided key-value pairs into the existing secrets file.
|
|
func (m *Manager) MergeUpdate(updates map[string]any) error {
|
|
lockPath := m.secretsPath + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
existing, err := m.GetAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for key, value := range updates {
|
|
existing[key] = value
|
|
}
|
|
|
|
yamlContent, err := yaml.Marshal(existing)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling secrets: %w", err)
|
|
}
|
|
|
|
return storage.WriteFile(m.secretsPath, yamlContent, 0600)
|
|
})
|
|
}
|