mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-11 08:18:49 +00:00
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:
parent
358de8a8ad
commit
747be5863b
14
CHANGELOG.md
14
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
|
||||
|
||||
2
Makefile
2
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)'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user