feat: enforce cluster secret requirement for non-bootstrap nodes

- Added documentation for joining additional nodes, specifying the need for the same IPFS Cluster secret as the bootstrap host.
- Updated the production command to require the `--cluster-secret` flag for non-bootstrap nodes, ensuring consistent cluster PSKs during deployment.
- Enhanced error handling to validate the cluster secret format and provide user feedback if the secret is missing or invalid.
- Modified the configuration setup to accommodate the cluster secret, improving security and deployment integrity.
This commit is contained in:
anonpenguin23 2025-11-14 07:12:03 +02:00
parent 358de8a8ad
commit 747be5863b
8 changed files with 178 additions and 54 deletions

View File

@ -13,6 +13,20 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
### Deprecated ### Deprecated
### Fixed ### Fixed
## [0.69.12] - 2025-11-14
### Added
- The `prod install` command now requires the `--cluster-secret` flag for all non-bootstrap nodes to ensure correct IPFS Cluster configuration.
### Changed
- Updated IPFS configuration to bind API and Gateway addresses to `0.0.0.0` instead of `127.0.0.1` for better network accessibility.
### Deprecated
### Removed
### Fixed
\n
## [0.69.11] - 2025-11-13 ## [0.69.11] - 2025-11-13
### Added ### Added

View File

@ -19,7 +19,7 @@ test-e2e:
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill .PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill
VERSION := 0.69.11 VERSION := 0.69.12
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'

View File

@ -58,6 +58,23 @@ All files will be under `/home/debros/.debros`:
└── secrets/ # Keys and certificates └── secrets/ # Keys and certificates
``` ```
### Joining Additional Nodes
Every non-bootstrap node must use the exact same IPFS Cluster secret as the bootstrap host. When you provision a follower node:
1. Copy the secret from the bootstrap machine:
```bash
scp debros@<bootstrap-ip>:/home/debros/.debros/secrets/cluster-secret ./cluster-secret
```
2. Run the installer with the `--cluster-secret` flag:
```bash
sudo dbn prod install --vps-ip <public_ip> \
--peers /ip4/<bootstrap-ip>/tcp/4001/p2p/<peer-id> \
--cluster-secret $(cat ./cluster-secret)
```
The installer now enforces `--cluster-secret` for all non-bootstrap nodes, which prevents mismatched cluster PSKs during deployment.
## Service Management ## Service Management
### Check Service Status ### Check Service Status

View File

@ -96,6 +96,7 @@ func showProdHelp() {
fmt.Printf(" --bootstrap - Install as bootstrap node\n") fmt.Printf(" --bootstrap - Install as bootstrap node\n")
fmt.Printf(" --vps-ip IP - VPS public IP address (required for non-bootstrap)\n") fmt.Printf(" --vps-ip IP - VPS public IP address (required for non-bootstrap)\n")
fmt.Printf(" --peers ADDRS - Comma-separated bootstrap peer multiaddrs (required for non-bootstrap)\n") fmt.Printf(" --peers ADDRS - Comma-separated bootstrap peer multiaddrs (required for non-bootstrap)\n")
fmt.Printf(" --cluster-secret HEX - 64-hex cluster secret (required for non-bootstrap)\n")
fmt.Printf(" --bootstrap-join ADDR - Bootstrap raft join address (for secondary bootstrap)\n") fmt.Printf(" --bootstrap-join ADDR - Bootstrap raft join address (for secondary bootstrap)\n")
fmt.Printf(" --domain DOMAIN - Domain for HTTPS (optional)\n") fmt.Printf(" --domain DOMAIN - Domain for HTTPS (optional)\n")
fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly, default: main)\n") fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly, default: main)\n")
@ -151,6 +152,7 @@ func handleProdInstall(args []string) {
peersStr := fs.String("peers", "", "Comma-separated bootstrap peer multiaddrs (required for non-bootstrap)") peersStr := fs.String("peers", "", "Comma-separated bootstrap peer multiaddrs (required for non-bootstrap)")
bootstrapJoin := fs.String("bootstrap-join", "", "Bootstrap raft join address (for secondary bootstrap)") bootstrapJoin := fs.String("bootstrap-join", "", "Bootstrap raft join address (for secondary bootstrap)")
branch := fs.String("branch", "main", "Git branch to use (main or nightly)") branch := fs.String("branch", "main", "Git branch to use (main or nightly)")
clusterSecret := fs.String("cluster-secret", "", "Hex-encoded 32-byte cluster secret (required for non-bootstrap nodes)")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp { if err == flag.ErrHelp {
@ -210,11 +212,23 @@ func handleProdInstall(args []string) {
fmt.Fprintf(os.Stderr, " Example: --peers /ip4/10.0.0.1/tcp/4001/p2p/Qm...\n") fmt.Fprintf(os.Stderr, " Example: --peers /ip4/10.0.0.1/tcp/4001/p2p/Qm...\n")
os.Exit(1) os.Exit(1)
} }
if *clusterSecret == "" {
fmt.Fprintf(os.Stderr, "❌ --cluster-secret is required for non-bootstrap nodes\n")
fmt.Fprintf(os.Stderr, " Provide the 64-hex secret from the bootstrap node (cat ~/.debros/secrets/cluster-secret)\n")
os.Exit(1)
}
}
if *clusterSecret != "" {
if err := production.ValidateClusterSecret(*clusterSecret); err != nil {
fmt.Fprintf(os.Stderr, "❌ Invalid --cluster-secret: %v\n", err)
os.Exit(1)
}
} }
debrosHome := "/home/debros" debrosHome := "/home/debros"
debrosDir := debrosHome + "/.debros" debrosDir := debrosHome + "/.debros"
setup := production.NewProductionSetup(debrosHome, os.Stdout, *force, *branch, false, *skipResourceChecks) setup := production.NewProductionSetup(debrosHome, os.Stdout, *force, *branch, false, *skipResourceChecks, *clusterSecret)
// Check port availability before proceeding // Check port availability before proceeding
if err := ensurePortsAvailable("prod install", defaultPorts()); err != nil { if err := ensurePortsAvailable("prod install", defaultPorts()); err != nil {
@ -349,7 +363,7 @@ func handleProdUpgrade(args []string) {
fmt.Printf(" This will preserve existing configurations and data\n") fmt.Printf(" This will preserve existing configurations and data\n")
fmt.Printf(" Configurations will be updated to latest format\n\n") fmt.Printf(" Configurations will be updated to latest format\n\n")
setup := production.NewProductionSetup(debrosHome, os.Stdout, *force, *branch, *noPull, false) setup := production.NewProductionSetup(debrosHome, os.Stdout, *force, *branch, *noPull, false, "")
// Log if --no-pull is enabled // Log if --no-pull is enabled
if *noPull { if *noPull {

View File

@ -7,7 +7,9 @@ import (
"net" "net"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/DeBrosOfficial/network/pkg/environments/templates" "github.com/DeBrosOfficial/network/pkg/environments/templates"
@ -224,16 +226,33 @@ func (cg *ConfigGenerator) GenerateOlricConfig(bindAddr string, httpPort, member
// SecretGenerator manages generation of shared secrets and keys // SecretGenerator manages generation of shared secrets and keys
type SecretGenerator struct { type SecretGenerator struct {
debrosDir string debrosDir string
clusterSecretOverride string
} }
// NewSecretGenerator creates a new secret generator // NewSecretGenerator creates a new secret generator
func NewSecretGenerator(debrosDir string) *SecretGenerator { func NewSecretGenerator(debrosDir string, clusterSecretOverride string) *SecretGenerator {
return &SecretGenerator{ return &SecretGenerator{
debrosDir: debrosDir, debrosDir: debrosDir,
clusterSecretOverride: clusterSecretOverride,
} }
} }
// ValidateClusterSecret ensures a cluster secret is 32 bytes of hex
func ValidateClusterSecret(secret string) error {
secret = strings.TrimSpace(secret)
if secret == "" {
return fmt.Errorf("cluster secret cannot be empty")
}
if len(secret) != 64 {
return fmt.Errorf("cluster secret must be 64 hex characters (32 bytes)")
}
if _, err := hex.DecodeString(secret); err != nil {
return fmt.Errorf("cluster secret must be valid hex: %w", err)
}
return nil
}
// EnsureClusterSecret gets or generates the IPFS Cluster secret // EnsureClusterSecret gets or generates the IPFS Cluster secret
func (sg *SecretGenerator) EnsureClusterSecret() (string, error) { func (sg *SecretGenerator) EnsureClusterSecret() (string, error) {
secretPath := filepath.Join(sg.debrosDir, "secrets", "cluster-secret") secretPath := filepath.Join(sg.debrosDir, "secrets", "cluster-secret")
@ -244,10 +263,38 @@ func (sg *SecretGenerator) EnsureClusterSecret() (string, error) {
return "", fmt.Errorf("failed to create secrets directory: %w", err) return "", fmt.Errorf("failed to create secrets directory: %w", err)
} }
// Use override if provided
if sg.clusterSecretOverride != "" {
secret := strings.TrimSpace(sg.clusterSecretOverride)
if err := ValidateClusterSecret(secret); err != nil {
return "", err
}
needsWrite := true
if data, err := os.ReadFile(secretPath); err == nil {
if strings.TrimSpace(string(data)) == secret {
needsWrite = false
}
}
if needsWrite {
if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil {
return "", fmt.Errorf("failed to save cluster secret override: %w", err)
}
}
if err := ensureSecretFilePermissions(secretPath); err != nil {
return "", err
}
return secret, nil
}
// Try to read existing secret // Try to read existing secret
if data, err := os.ReadFile(secretPath); err == nil { if data, err := os.ReadFile(secretPath); err == nil {
secret := strings.TrimSpace(string(data)) secret := strings.TrimSpace(string(data))
if len(secret) == 64 { if len(secret) == 64 {
if err := ensureSecretFilePermissions(secretPath); err != nil {
return "", err
}
return secret, nil return secret, nil
} }
} }
@ -263,10 +310,35 @@ func (sg *SecretGenerator) EnsureClusterSecret() (string, error) {
if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil { if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil {
return "", fmt.Errorf("failed to save cluster secret: %w", err) return "", fmt.Errorf("failed to save cluster secret: %w", err)
} }
if err := ensureSecretFilePermissions(secretPath); err != nil {
return "", err
}
return secret, nil return secret, nil
} }
func ensureSecretFilePermissions(secretPath string) error {
if err := os.Chmod(secretPath, 0600); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", secretPath, err)
}
if usr, err := user.Lookup("debros"); err == nil {
uid, err := strconv.Atoi(usr.Uid)
if err != nil {
return fmt.Errorf("failed to parse debros UID: %w", err)
}
gid, err := strconv.Atoi(usr.Gid)
if err != nil {
return fmt.Errorf("failed to parse debros GID: %w", err)
}
if err := os.Chown(secretPath, uid, gid); err != nil {
return fmt.Errorf("failed to change ownership of %s: %w", secretPath, err)
}
}
return nil
}
// EnsureSwarmKey gets or generates the IPFS private swarm key // EnsureSwarmKey gets or generates the IPFS private swarm key
func (sg *SecretGenerator) EnsureSwarmKey() ([]byte, error) { func (sg *SecretGenerator) EnsureSwarmKey() ([]byte, error) {
swarmKeyPath := filepath.Join(sg.debrosDir, "secrets", "swarm.key") swarmKeyPath := filepath.Join(sg.debrosDir, "secrets", "swarm.key")

View File

@ -508,8 +508,12 @@ func (bi *BinaryInstaller) configureIPFSAddresses(ipfsRepoPath string, apiPort,
// Set Addresses // Set Addresses
config["Addresses"] = map[string]interface{}{ config["Addresses"] = map[string]interface{}{
"API": []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", apiPort)}, "API": []string{
"Gateway": []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", gatewayPort)}, fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", apiPort),
},
"Gateway": []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", gatewayPort),
},
"Swarm": []string{ "Swarm": []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort), fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort),
fmt.Sprintf("/ip6/::/tcp/%d", swarmPort), fmt.Sprintf("/ip6/::/tcp/%d", swarmPort),

View File

@ -12,29 +12,30 @@ import (
// ProductionSetup orchestrates the entire production deployment // ProductionSetup orchestrates the entire production deployment
type ProductionSetup struct { type ProductionSetup struct {
osInfo *OSInfo osInfo *OSInfo
arch string arch string
debrosHome string debrosHome string
debrosDir string debrosDir string
logWriter io.Writer logWriter io.Writer
forceReconfigure bool forceReconfigure bool
skipOptionalDeps bool skipOptionalDeps bool
skipResourceChecks bool skipResourceChecks bool
privChecker *PrivilegeChecker clusterSecretOverride string
osDetector *OSDetector privChecker *PrivilegeChecker
archDetector *ArchitectureDetector osDetector *OSDetector
resourceChecker *ResourceChecker archDetector *ArchitectureDetector
fsProvisioner *FilesystemProvisioner resourceChecker *ResourceChecker
userProvisioner *UserProvisioner fsProvisioner *FilesystemProvisioner
stateDetector *StateDetector userProvisioner *UserProvisioner
configGenerator *ConfigGenerator stateDetector *StateDetector
secretGenerator *SecretGenerator configGenerator *ConfigGenerator
serviceGenerator *SystemdServiceGenerator secretGenerator *SecretGenerator
serviceController *SystemdController serviceGenerator *SystemdServiceGenerator
binaryInstaller *BinaryInstaller serviceController *SystemdController
branch string binaryInstaller *BinaryInstaller
skipRepoUpdate bool branch string
NodePeerID string // Captured during Phase3 for later display skipRepoUpdate bool
NodePeerID string // Captured during Phase3 for later display
} }
// ReadBranchPreference reads the stored branch preference from disk // ReadBranchPreference reads the stored branch preference from disk
@ -65,9 +66,10 @@ func SaveBranchPreference(debrosDir, branch string) error {
} }
// NewProductionSetup creates a new production setup orchestrator // NewProductionSetup creates a new production setup orchestrator
func NewProductionSetup(debrosHome string, logWriter io.Writer, forceReconfigure bool, branch string, skipRepoUpdate bool, skipResourceChecks bool) *ProductionSetup { func NewProductionSetup(debrosHome string, logWriter io.Writer, forceReconfigure bool, branch string, skipRepoUpdate bool, skipResourceChecks bool, clusterSecretOverride string) *ProductionSetup {
debrosDir := debrosHome + "/.debros" debrosDir := debrosHome + "/.debros"
arch, _ := (&ArchitectureDetector{}).Detect() arch, _ := (&ArchitectureDetector{}).Detect()
normalizedSecret := strings.TrimSpace(strings.ToLower(clusterSecretOverride))
// If branch is empty, try to read from stored preference, otherwise default to main // If branch is empty, try to read from stored preference, otherwise default to main
if branch == "" { if branch == "" {
@ -75,26 +77,27 @@ func NewProductionSetup(debrosHome string, logWriter io.Writer, forceReconfigure
} }
return &ProductionSetup{ return &ProductionSetup{
debrosHome: debrosHome, debrosHome: debrosHome,
debrosDir: debrosDir, debrosDir: debrosDir,
logWriter: logWriter, logWriter: logWriter,
forceReconfigure: forceReconfigure, forceReconfigure: forceReconfigure,
arch: arch, arch: arch,
branch: branch, branch: branch,
skipRepoUpdate: skipRepoUpdate, skipRepoUpdate: skipRepoUpdate,
skipResourceChecks: skipResourceChecks, skipResourceChecks: skipResourceChecks,
privChecker: &PrivilegeChecker{}, clusterSecretOverride: normalizedSecret,
osDetector: &OSDetector{}, privChecker: &PrivilegeChecker{},
archDetector: &ArchitectureDetector{}, osDetector: &OSDetector{},
resourceChecker: NewResourceChecker(), archDetector: &ArchitectureDetector{},
fsProvisioner: NewFilesystemProvisioner(debrosHome), resourceChecker: NewResourceChecker(),
userProvisioner: NewUserProvisioner("debros", debrosHome, "/bin/bash"), fsProvisioner: NewFilesystemProvisioner(debrosHome),
stateDetector: NewStateDetector(debrosDir), userProvisioner: NewUserProvisioner("debros", debrosHome, "/bin/bash"),
configGenerator: NewConfigGenerator(debrosDir), stateDetector: NewStateDetector(debrosDir),
secretGenerator: NewSecretGenerator(debrosDir), configGenerator: NewConfigGenerator(debrosDir),
serviceGenerator: NewSystemdServiceGenerator(debrosHome, debrosDir), secretGenerator: NewSecretGenerator(debrosDir, normalizedSecret),
serviceController: NewSystemdController(), serviceGenerator: NewSystemdServiceGenerator(debrosHome, debrosDir),
binaryInstaller: NewBinaryInstaller(arch, logWriter), serviceController: NewSystemdController(),
binaryInstaller: NewBinaryInstaller(arch, logWriter),
} }
} }

View File

@ -1069,7 +1069,7 @@ func (cm *ClusterConfigManager) FixIPFSConfigAddresses() error {
} }
// Always ensure API address is correct (don't just check, always set it) // Always ensure API address is correct (don't just check, always set it)
correctAPIAddr := fmt.Sprintf(`["/ip4/127.0.0.1/tcp/%d"]`, ipfsPort) correctAPIAddr := fmt.Sprintf(`["/ip4/0.0.0.0/tcp/%d"]`, ipfsPort)
cm.logger.Info("Ensuring IPFS API address is correct", cm.logger.Info("Ensuring IPFS API address is correct",
zap.String("repo", ipfsRepoPath), zap.String("repo", ipfsRepoPath),
zap.Int("port", ipfsPort), zap.Int("port", ipfsPort),
@ -1083,7 +1083,7 @@ func (cm *ClusterConfigManager) FixIPFSConfigAddresses() error {
} }
// Always ensure Gateway address is correct // Always ensure Gateway address is correct
correctGatewayAddr := fmt.Sprintf(`["/ip4/127.0.0.1/tcp/%d"]`, gatewayPort) correctGatewayAddr := fmt.Sprintf(`["/ip4/0.0.0.0/tcp/%d"]`, gatewayPort)
cm.logger.Info("Ensuring IPFS Gateway address is correct", cm.logger.Info("Ensuring IPFS Gateway address is correct",
zap.String("repo", ipfsRepoPath), zap.String("repo", ipfsRepoPath),
zap.Int("port", gatewayPort), zap.Int("port", gatewayPort),