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
### 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

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
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)'

View File

@ -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@<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
### Check Service Status

View File

@ -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 {

View File

@ -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")

View File

@ -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),

View File

@ -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),
}
}

View File

@ -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),