Extract the Central networking functionality from wild-cloud/api into a standalone service. Wild Central manages DNS (dnsmasq), gateway (HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot), security (CrowdSec), and DDNS — all the network appliance concerns. All Cloud-specific code (instances, clusters, nodes, apps, backups, operations, kubectl/talosctl tooling) has been removed. The API struct and route registration contain only Central endpoints. Tests updated to match the new API signature. Builds and all tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
238 lines
6.6 KiB
Go
238 lines
6.6 KiB
Go
package secrets
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math/big"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/wild-cloud/wild-central/internal/storage"
|
|
)
|
|
|
|
// YQ provides a wrapper around the yq command-line tool
|
|
type YQ struct {
|
|
yqPath string
|
|
}
|
|
|
|
// NewYQ creates a new YQ wrapper
|
|
func NewYQ() *YQ {
|
|
path, err := exec.LookPath("yq")
|
|
if err != nil {
|
|
path = "yq"
|
|
}
|
|
return &YQ{yqPath: path}
|
|
}
|
|
|
|
// Get retrieves a value from a YAML file using a yq expression
|
|
func (y *YQ) Get(filePath, expression string) (string, error) {
|
|
cmd := exec.Command(y.yqPath, expression, filePath)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("yq get failed: %w, stderr: %s", err, stderr.String())
|
|
}
|
|
return strings.TrimSpace(stdout.String()), nil
|
|
}
|
|
|
|
// Set sets a value in a YAML file using a yq expression
|
|
func (y *YQ) Set(filePath, expression, value string) error {
|
|
if !strings.HasPrefix(expression, ".") {
|
|
expression = "." + expression
|
|
}
|
|
quotedValue := fmt.Sprintf(`"%s"`, strings.ReplaceAll(value, `"`, `\"`))
|
|
setExpr := fmt.Sprintf("%s = %s", expression, quotedValue)
|
|
cmd := exec.Command(y.yqPath, "-i", setExpr, filePath)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("yq set failed: %w, stderr: %s", err, stderr.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete removes a key from a YAML file
|
|
func (y *YQ) Delete(filePath, expression string) error {
|
|
delExpr := fmt.Sprintf("del(%s)", expression)
|
|
cmd := exec.Command(y.yqPath, "-i", delExpr, filePath)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("yq delete failed: %w, stderr: %s", err, stderr.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if a YAML file is valid
|
|
func (y *YQ) Validate(filePath string) error {
|
|
cmd := exec.Command(y.yqPath, "eval", ".", filePath)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("yaml validation failed: %w, stderr: %s", err, stderr.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
// DefaultSecretLength is 32 characters
|
|
DefaultSecretLength = 32
|
|
// Alphanumeric characters for secret generation
|
|
alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
)
|
|
|
|
// Manager handles secret generation and storage
|
|
type Manager struct {
|
|
yq *YQ
|
|
}
|
|
|
|
// NewManager creates a new secrets manager
|
|
func NewManager() *Manager {
|
|
return &Manager{
|
|
yq: 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
|
|
}
|
|
|
|
// EnsureSecretsFile ensures a secrets file exists with proper structure and permissions
|
|
func (m *Manager) EnsureSecretsFile(dataDir, instanceName string) error {
|
|
secretsPath := filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml")
|
|
|
|
// Check if secrets file already exists
|
|
if storage.FileExists(secretsPath) {
|
|
// Ensure proper permissions
|
|
if err := storage.EnsureFilePermissions(secretsPath, 0600); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create minimal secrets structure
|
|
initialSecrets := `# Wild Cloud Instance Secrets
|
|
# WARNING: This file contains sensitive data. Keep secure!
|
|
cluster:
|
|
talosSecrets: ""
|
|
kubeconfig: ""
|
|
cloudflare:
|
|
token: ""
|
|
`
|
|
|
|
// Ensure config directory exists
|
|
if err := storage.EnsureDir(filepath.Join(dataDir, "instances", instanceName, "config"), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write secrets file with restrictive permissions (0600)
|
|
if err := storage.WriteFile(secretsPath, []byte(initialSecrets), 0600); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetSecret retrieves a secret value from a secrets file
|
|
func (m *Manager) GetSecret(secretsPath, key string) (string, error) {
|
|
if !storage.FileExists(secretsPath) {
|
|
return "", fmt.Errorf("secrets file not found: %s", secretsPath)
|
|
}
|
|
|
|
value, err := m.yq.Get(secretsPath, fmt.Sprintf(".%s", key))
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting secret %s: %w", key, err)
|
|
}
|
|
|
|
// yq returns "null" for non-existent keys
|
|
if value == "" || value == "null" {
|
|
return "", fmt.Errorf("secret not found: %s", key)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// SetSecret sets a secret value in a secrets file
|
|
func (m *Manager) SetSecret(secretsPath, key, value string) error {
|
|
if !storage.FileExists(secretsPath) {
|
|
return fmt.Errorf("secrets file not found: %s", secretsPath)
|
|
}
|
|
|
|
// Acquire lock before modifying
|
|
lockPath := secretsPath + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
// Don't wrap value in quotes - yq handles YAML quoting automatically
|
|
if err := m.yq.Set(secretsPath, fmt.Sprintf(".%s", key), value); err != nil {
|
|
return err
|
|
}
|
|
// Ensure permissions remain secure after modification
|
|
return storage.EnsureFilePermissions(secretsPath, 0600)
|
|
})
|
|
}
|
|
|
|
// EnsureSecret generates and sets a secret only if it doesn't exist (idempotent)
|
|
func (m *Manager) EnsureSecret(secretsPath, key string, length int) (string, error) {
|
|
if !storage.FileExists(secretsPath) {
|
|
return "", fmt.Errorf("secrets file not found: %s", secretsPath)
|
|
}
|
|
|
|
// Check if secret already exists
|
|
existingSecret, err := m.GetSecret(secretsPath, key)
|
|
if err == nil && existingSecret != "" && existingSecret != "null" {
|
|
// Secret already exists, return it
|
|
return existingSecret, nil
|
|
}
|
|
|
|
// Generate new secret
|
|
secret, err := GenerateSecret(length)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Set the secret
|
|
if err := m.SetSecret(secretsPath, key, secret); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
// GenerateAndStoreSecret is a convenience function that generates a secret and stores it
|
|
func (m *Manager) GenerateAndStoreSecret(secretsPath, key string) (string, error) {
|
|
return m.EnsureSecret(secretsPath, key, DefaultSecretLength)
|
|
}
|
|
|
|
// DeleteSecret removes a secret from a secrets file
|
|
func (m *Manager) DeleteSecret(secretsPath, key string) error {
|
|
if !storage.FileExists(secretsPath) {
|
|
return fmt.Errorf("secrets file not found: %s", secretsPath)
|
|
}
|
|
|
|
// Acquire lock before modifying
|
|
lockPath := secretsPath + ".lock"
|
|
return storage.WithLock(lockPath, func() error {
|
|
if err := m.yq.Delete(secretsPath, fmt.Sprintf(".%s", key)); err != nil {
|
|
return err
|
|
}
|
|
// Ensure permissions remain secure after modification
|
|
return storage.EnsureFilePermissions(secretsPath, 0600)
|
|
})
|
|
}
|