From 747be5863b428ccde977e23d3f3bb803f877cecf Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Fri, 14 Nov 2025 07:12:03 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 14 ++++ Makefile | 2 +- PRODUCTION_INSTALL.md | 17 ++++ pkg/cli/prod_commands.go | 18 +++- pkg/environments/production/config.go | 78 +++++++++++++++++- pkg/environments/production/installers.go | 8 +- pkg/environments/production/orchestrator.go | 91 +++++++++++---------- pkg/ipfs/cluster.go | 4 +- 8 files changed, 178 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd24ef..e13dd74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,20 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Deprecated ### 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 ### Added diff --git a/Makefile b/Makefile index cc82e39..5de5d6c 100644 --- a/Makefile +++ b/Makefile @@ -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 -VERSION := 0.69.11 +VERSION := 0.69.12 COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' diff --git a/PRODUCTION_INSTALL.md b/PRODUCTION_INSTALL.md index e6ad959..de0ac93 100644 --- a/PRODUCTION_INSTALL.md +++ b/PRODUCTION_INSTALL.md @@ -58,6 +58,23 @@ All files will be under `/home/debros/.debros`: └── 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@:/home/debros/.debros/secrets/cluster-secret ./cluster-secret + ``` +2. Run the installer with the `--cluster-secret` flag: + ```bash + sudo dbn prod install --vps-ip \ + --peers /ip4//tcp/4001/p2p/ \ + --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 ### Check Service Status diff --git a/pkg/cli/prod_commands.go b/pkg/cli/prod_commands.go index 9e25f91..1ebe089 100644 --- a/pkg/cli/prod_commands.go +++ b/pkg/cli/prod_commands.go @@ -96,6 +96,7 @@ func showProdHelp() { fmt.Printf(" --bootstrap - Install as bootstrap node\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(" --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(" --domain DOMAIN - Domain for HTTPS (optional)\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)") bootstrapJoin := fs.String("bootstrap-join", "", "Bootstrap raft join address (for secondary bootstrap)") 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 == 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") 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" 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 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(" 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 if *noPull { diff --git a/pkg/environments/production/config.go b/pkg/environments/production/config.go index e040f1d..e04071c 100644 --- a/pkg/environments/production/config.go +++ b/pkg/environments/production/config.go @@ -7,7 +7,9 @@ import ( "net" "os" "os/exec" + "os/user" "path/filepath" + "strconv" "strings" "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 type SecretGenerator struct { - debrosDir string + debrosDir string + clusterSecretOverride string } // NewSecretGenerator creates a new secret generator -func NewSecretGenerator(debrosDir string) *SecretGenerator { +func NewSecretGenerator(debrosDir string, clusterSecretOverride string) *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 func (sg *SecretGenerator) EnsureClusterSecret() (string, error) { 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) } + // 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 if data, err := os.ReadFile(secretPath); err == nil { secret := strings.TrimSpace(string(data)) if len(secret) == 64 { + if err := ensureSecretFilePermissions(secretPath); err != nil { + return "", err + } return secret, nil } } @@ -263,10 +310,35 @@ func (sg *SecretGenerator) EnsureClusterSecret() (string, error) { if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil { return "", fmt.Errorf("failed to save cluster secret: %w", err) } + if err := ensureSecretFilePermissions(secretPath); err != nil { + return "", err + } 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 func (sg *SecretGenerator) EnsureSwarmKey() ([]byte, error) { swarmKeyPath := filepath.Join(sg.debrosDir, "secrets", "swarm.key") diff --git a/pkg/environments/production/installers.go b/pkg/environments/production/installers.go index 72a2a93..63c7236 100644 --- a/pkg/environments/production/installers.go +++ b/pkg/environments/production/installers.go @@ -508,8 +508,12 @@ func (bi *BinaryInstaller) configureIPFSAddresses(ipfsRepoPath string, apiPort, // Set Addresses config["Addresses"] = map[string]interface{}{ - "API": []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", apiPort)}, - "Gateway": []string{fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", gatewayPort)}, + "API": []string{ + fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", apiPort), + }, + "Gateway": []string{ + fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", gatewayPort), + }, "Swarm": []string{ fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort), fmt.Sprintf("/ip6/::/tcp/%d", swarmPort), diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index f05fd76..487d646 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -12,29 +12,30 @@ import ( // ProductionSetup orchestrates the entire production deployment type ProductionSetup struct { - osInfo *OSInfo - arch string - debrosHome string - debrosDir string - logWriter io.Writer - forceReconfigure bool - skipOptionalDeps bool - skipResourceChecks bool - privChecker *PrivilegeChecker - osDetector *OSDetector - archDetector *ArchitectureDetector - resourceChecker *ResourceChecker - fsProvisioner *FilesystemProvisioner - userProvisioner *UserProvisioner - stateDetector *StateDetector - configGenerator *ConfigGenerator - secretGenerator *SecretGenerator - serviceGenerator *SystemdServiceGenerator - serviceController *SystemdController - binaryInstaller *BinaryInstaller - branch string - skipRepoUpdate bool - NodePeerID string // Captured during Phase3 for later display + osInfo *OSInfo + arch string + debrosHome string + debrosDir string + logWriter io.Writer + forceReconfigure bool + skipOptionalDeps bool + skipResourceChecks bool + clusterSecretOverride string + privChecker *PrivilegeChecker + osDetector *OSDetector + archDetector *ArchitectureDetector + resourceChecker *ResourceChecker + fsProvisioner *FilesystemProvisioner + userProvisioner *UserProvisioner + stateDetector *StateDetector + configGenerator *ConfigGenerator + secretGenerator *SecretGenerator + serviceGenerator *SystemdServiceGenerator + serviceController *SystemdController + binaryInstaller *BinaryInstaller + branch string + skipRepoUpdate bool + NodePeerID string // Captured during Phase3 for later display } // 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 -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" 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 == "" { @@ -75,26 +77,27 @@ func NewProductionSetup(debrosHome string, logWriter io.Writer, forceReconfigure } return &ProductionSetup{ - debrosHome: debrosHome, - debrosDir: debrosDir, - logWriter: logWriter, - forceReconfigure: forceReconfigure, - arch: arch, - branch: branch, - skipRepoUpdate: skipRepoUpdate, - skipResourceChecks: skipResourceChecks, - privChecker: &PrivilegeChecker{}, - osDetector: &OSDetector{}, - archDetector: &ArchitectureDetector{}, - resourceChecker: NewResourceChecker(), - fsProvisioner: NewFilesystemProvisioner(debrosHome), - userProvisioner: NewUserProvisioner("debros", debrosHome, "/bin/bash"), - stateDetector: NewStateDetector(debrosDir), - configGenerator: NewConfigGenerator(debrosDir), - secretGenerator: NewSecretGenerator(debrosDir), - serviceGenerator: NewSystemdServiceGenerator(debrosHome, debrosDir), - serviceController: NewSystemdController(), - binaryInstaller: NewBinaryInstaller(arch, logWriter), + debrosHome: debrosHome, + debrosDir: debrosDir, + logWriter: logWriter, + forceReconfigure: forceReconfigure, + arch: arch, + branch: branch, + skipRepoUpdate: skipRepoUpdate, + skipResourceChecks: skipResourceChecks, + clusterSecretOverride: normalizedSecret, + privChecker: &PrivilegeChecker{}, + osDetector: &OSDetector{}, + archDetector: &ArchitectureDetector{}, + resourceChecker: NewResourceChecker(), + fsProvisioner: NewFilesystemProvisioner(debrosHome), + userProvisioner: NewUserProvisioner("debros", debrosHome, "/bin/bash"), + stateDetector: NewStateDetector(debrosDir), + configGenerator: NewConfigGenerator(debrosDir), + secretGenerator: NewSecretGenerator(debrosDir, normalizedSecret), + serviceGenerator: NewSystemdServiceGenerator(debrosHome, debrosDir), + serviceController: NewSystemdController(), + binaryInstaller: NewBinaryInstaller(arch, logWriter), } } diff --git a/pkg/ipfs/cluster.go b/pkg/ipfs/cluster.go index ca7cd93..8c0fd69 100644 --- a/pkg/ipfs/cluster.go +++ b/pkg/ipfs/cluster.go @@ -1069,7 +1069,7 @@ func (cm *ClusterConfigManager) FixIPFSConfigAddresses() error { } // 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", zap.String("repo", ipfsRepoPath), zap.Int("port", ipfsPort), @@ -1083,7 +1083,7 @@ func (cm *ClusterConfigManager) FixIPFSConfigAddresses() error { } // 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", zap.String("repo", ipfsRepoPath), zap.Int("port", gatewayPort),