Implemented a comprehensive secure authentication mechanism for inter-node cluster communication with the following features: 1. Global Cluster Secret (GCS) - Auto-generated cryptographically secure random secret (256-bit) - Configurable via YAML config file - Shared across all cluster nodes for authentication 2. Cluster Authentication Middleware - Validates X-Cluster-Secret and X-Node-ID headers - Applied to all cluster endpoints (/members/*, /merkle_tree/*, /kv_range) - Comprehensive logging of authentication attempts 3. Authenticated HTTP Client - Custom HTTP client with cluster auth headers - TLS support with configurable certificate verification - Protocol-aware (http/https based on TLS settings) 4. Secure Bootstrap Endpoint - New /auth/cluster-bootstrap endpoint - Protected by JWT authentication with admin scope - Allows new nodes to securely obtain cluster secret 5. Updated Cluster Communication - All gossip protocol requests include auth headers - All Merkle tree sync requests include auth headers - All data replication requests include auth headers 6. Configuration - cluster_secret: Shared secret (auto-generated if not provided) - cluster_tls_enabled: Enable TLS for inter-node communication - cluster_tls_cert_file: Path to TLS certificate - cluster_tls_key_file: Path to TLS private key - cluster_tls_skip_verify: Skip TLS verification (testing only) This implementation addresses the security vulnerability of unprotected cluster endpoints and provides a flexible, secure approach to protecting internal cluster communication while allowing for automated node bootstrapping. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
"kvs/types"
|
|
)
|
|
|
|
// Default configuration
|
|
func Default() *types.Config {
|
|
hostname, _ := os.Hostname()
|
|
return &types.Config{
|
|
NodeID: hostname,
|
|
BindAddress: "127.0.0.1",
|
|
Port: 8080,
|
|
DataDir: "./data",
|
|
SeedNodes: []string{},
|
|
ReadOnly: false,
|
|
LogLevel: "info",
|
|
GossipIntervalMin: 60, // 1 minute
|
|
GossipIntervalMax: 120, // 2 minutes
|
|
SyncInterval: 300, // 5 minutes
|
|
CatchupInterval: 120, // 2 minutes
|
|
BootstrapMaxAgeHours: 720, // 30 days
|
|
ThrottleDelayMs: 100,
|
|
FetchDelayMs: 50,
|
|
|
|
// Default compression settings
|
|
CompressionEnabled: true,
|
|
CompressionLevel: 3, // Balance between performance and compression ratio
|
|
|
|
// Default TTL and size limit settings
|
|
DefaultTTL: "0", // No default TTL
|
|
MaxJSONSize: 1048576, // 1MB default max JSON size
|
|
|
|
// Default rate limiting settings
|
|
RateLimitRequests: 100, // 100 requests per window
|
|
RateLimitWindow: "1m", // 1 minute window
|
|
|
|
// Default tamper-evident logging settings
|
|
TamperLogActions: []string{"data_write", "user_create", "auth_failure"},
|
|
|
|
// Default backup system settings
|
|
BackupEnabled: true,
|
|
BackupSchedule: "0 0 * * *", // Daily at midnight
|
|
BackupPath: "./backups",
|
|
BackupRetention: 7, // Keep backups for 7 days
|
|
|
|
// Default feature toggle settings (all enabled by default)
|
|
AuthEnabled: true,
|
|
TamperLoggingEnabled: true,
|
|
ClusteringEnabled: true,
|
|
RateLimitingEnabled: true,
|
|
RevisionHistoryEnabled: true,
|
|
|
|
// Default anonymous access settings (both disabled by default for security)
|
|
AllowAnonymousRead: false,
|
|
AllowAnonymousWrite: false,
|
|
|
|
// Default cluster authentication settings (Issue #13)
|
|
ClusterSecret: generateClusterSecret(),
|
|
ClusterTLSEnabled: false,
|
|
ClusterTLSCertFile: "",
|
|
ClusterTLSKeyFile: "",
|
|
ClusterTLSSkipVerify: false,
|
|
}
|
|
}
|
|
|
|
// generateClusterSecret generates a cryptographically secure random cluster secret
|
|
func generateClusterSecret() string {
|
|
// Generate 32 bytes (256 bits) of random data
|
|
randomBytes := make([]byte, 32)
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
// Fallback to a warning - this should never happen in practice
|
|
fmt.Fprintf(os.Stderr, "Warning: Failed to generate secure cluster secret: %v\n", err)
|
|
return ""
|
|
}
|
|
// Encode as base64 for easy configuration file storage
|
|
return base64.StdEncoding.EncodeToString(randomBytes)
|
|
}
|
|
|
|
// Load configuration from file or create default
|
|
func Load(configPath string) (*types.Config, error) {
|
|
config := Default()
|
|
|
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
|
// Create default config file
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
|
}
|
|
|
|
data, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal default config: %v", err)
|
|
}
|
|
|
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
|
return nil, fmt.Errorf("failed to write default config: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Created default configuration at %s\n", configPath)
|
|
return config, nil
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config file: %v", err)
|
|
}
|
|
|
|
if err := yaml.Unmarshal(data, config); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config file: %v", err)
|
|
}
|
|
|
|
// Generate cluster secret if not provided and clustering is enabled (Issue #13)
|
|
if config.ClusteringEnabled && config.ClusterSecret == "" {
|
|
config.ClusterSecret = generateClusterSecret()
|
|
fmt.Printf("Warning: No cluster_secret configured. Generated a random secret.\n")
|
|
fmt.Printf(" To share this secret with other nodes, add it to your config:\n")
|
|
fmt.Printf(" cluster_secret: %s\n", config.ClusterSecret)
|
|
}
|
|
|
|
return config, nil
|
|
}
|