Security hardening: input validation, secrets protection, NATS auth

Config injection prevention:
- Add FQDN validation for domain names (RFC 1123) in Register/Update —
  rejects newlines, spaces, shell metacharacters that could inject into
  HAProxy/dnsmasq configs
- Add backend address validation (valid host:port format, valid IP or
  hostname, port 1-65535). DNS-only backends allow bare IPs.
- Add header key/value validation — keys must be HTTP token chars only,
  values must not contain newlines or NULs
- Add WireGuard peer name validation (alphanumeric + hyphens + underscores)
- Add defense-in-depth domain validation in certbot Provision()

Secrets protection:
- Remove ?raw=true bypass on GET /api/v1/secrets — secrets are now always
  redacted in API responses regardless of query parameters
- Update test to verify redaction cannot be bypassed

NATS authentication:
- Generate random auth token on first startup, store in secrets.yaml
- Pass token to embedded NATS server via Authorization option
- Internal client connects with the same token
- External NATS clients (Wild Cloud) must now authenticate

Security headers:
- Add X-Content-Type-Options: nosniff
- Add X-Frame-Options: DENY
- Add Cache-Control: no-store
This commit is contained in:
2026-07-14 12:38:17 +00:00
parent 4500a1a45e
commit 60f3ca4a3a
8 changed files with 149 additions and 16 deletions

View File

@@ -37,8 +37,9 @@ type Server struct {
// Config for the embedded NATS server.
type Config struct {
Port int // listen port (default 4222)
DataDir string // JetStream storage directory
Port int // listen port (default 4222)
DataDir string // JetStream storage directory
AuthToken string // require this token for client connections (empty = no auth)
}
// Start launches the embedded NATS server with JetStream and creates
@@ -52,9 +53,11 @@ func Start(cfg Config) (*Server, error) {
Port: cfg.Port,
JetStream: true,
StoreDir: cfg.DataDir,
// Quiet logging — NATS logs at its own level
NoLog: true,
NoSigs: true,
NoLog: true,
NoSigs: true,
}
if cfg.AuthToken != "" {
opts.Authorization = cfg.AuthToken
}
ns, err := natsserver.NewServer(opts)
@@ -87,7 +90,11 @@ func Start(cfg Config) (*Server, error) {
slog.Info("NATS JetStream server started", "port", cfg.Port, "dataDir", cfg.DataDir)
// Connect as internal client
nc, err := nats.Connect(ns.ClientURL())
connectOpts := []nats.Option{}
if cfg.AuthToken != "" {
connectOpts = append(connectOpts, nats.Token(cfg.AuthToken))
}
nc, err := nats.Connect(ns.ClientURL(), connectOpts...)
if err != nil {
ns.Shutdown()
return nil, fmt.Errorf("connecting to embedded NATS: %w", err)