From 7163aad85041ea3a9f905dd0d0f92f8617c3a327 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Mon, 16 Feb 2026 10:01:35 +0200 Subject: [PATCH] Refactor installation scripts and improve security measures - Updated `clean-testnet.sh` to stop and disable legacy services. - Added `upload-source.sh` for streamlined source archive uploads. - Enhanced password input handling in `ssh.go` for better security. - Adjusted directory permissions in `validator.go` for improved security. - Simplified node configuration logic in `config.go`. - Removed unnecessary commands from `gateway.go` to streamline installation. --- pkg/cli/production/install/orchestrator.go | 57 +--------- pkg/cli/production/install/remote.go | 60 ++-------- pkg/cli/production/install/ssh.go | 15 ++- pkg/cli/production/install/validator.go | 4 +- pkg/environments/production/config.go | 16 ++- .../production/installers/gateway.go | 34 +----- scripts/clean-testnet.sh | 4 + scripts/upload-source.sh | 103 ++++++++++++++++++ 8 files changed, 143 insertions(+), 150 deletions(-) create mode 100755 scripts/upload-source.sh diff --git a/pkg/cli/production/install/orchestrator.go b/pkg/cli/production/install/orchestrator.go index 17e1b92..1ee2bbb 100644 --- a/pkg/cli/production/install/orchestrator.go +++ b/pkg/cli/production/install/orchestrator.go @@ -437,59 +437,12 @@ func (o *Orchestrator) verifyWGTunnel(peers []joinhandlers.WGPeerInfo) error { return fmt.Errorf("could not reach %s via WireGuard after 30s", targetIP) } -func (o *Orchestrator) buildIPFSPeerInfo() *production.IPFSPeerInfo { - if o.flags.IPFSPeerID != "" { - var addrs []string - if o.flags.IPFSAddrs != "" { - addrs = strings.Split(o.flags.IPFSAddrs, ",") - } - return &production.IPFSPeerInfo{ - PeerID: o.flags.IPFSPeerID, - Addrs: addrs, - } - } - return nil -} - -func (o *Orchestrator) buildIPFSClusterPeerInfo() *production.IPFSClusterPeerInfo { - if o.flags.IPFSClusterPeerID != "" { - var addrs []string - if o.flags.IPFSClusterAddrs != "" { - addrs = strings.Split(o.flags.IPFSClusterAddrs, ",") - } - return &production.IPFSClusterPeerInfo{ - PeerID: o.flags.IPFSClusterPeerID, - Addrs: addrs, - } - } - return nil -} - func (o *Orchestrator) printFirstNodeSecrets() { - fmt.Printf("📋 Save these for joining future nodes:\n\n") - - // Print cluster secret - clusterSecretPath := filepath.Join(o.oramaDir, "secrets", "cluster-secret") - if clusterSecretData, err := os.ReadFile(clusterSecretPath); err == nil { - fmt.Printf(" Cluster Secret (--cluster-secret):\n") - fmt.Printf(" %s\n\n", string(clusterSecretData)) - } - - // Print swarm key - swarmKeyPath := filepath.Join(o.oramaDir, "secrets", "swarm.key") - if swarmKeyData, err := os.ReadFile(swarmKeyPath); err == nil { - swarmKeyContent := strings.TrimSpace(string(swarmKeyData)) - lines := strings.Split(swarmKeyContent, "\n") - if len(lines) >= 3 { - // Extract just the hex part (last line) - fmt.Printf(" IPFS Swarm Key (--swarm-key, last line only):\n") - fmt.Printf(" %s\n\n", lines[len(lines)-1]) - } - } - - // Print peer ID - fmt.Printf(" Node Peer ID:\n") - fmt.Printf(" %s\n\n", o.setup.NodePeerID) + fmt.Printf("📋 To add more nodes to this cluster:\n\n") + fmt.Printf(" 1. Generate an invite token:\n") + fmt.Printf(" orama invite\n\n") + fmt.Printf(" 2. Run the printed command on the new VPS.\n\n") + fmt.Printf(" Node Peer ID: %s\n\n", o.setup.NodePeerID) } // promptForBaseDomain interactively prompts the user to select a network environment diff --git a/pkg/cli/production/install/remote.go b/pkg/cli/production/install/remote.go index 09bc5c4..44b3aaf 100644 --- a/pkg/cli/production/install/remote.go +++ b/pkg/cli/production/install/remote.go @@ -2,7 +2,6 @@ package install import ( "fmt" - "os" "strconv" "strings" @@ -13,9 +12,8 @@ import ( // It uploads the source archive, extracts it on the VPS, and runs // the actual install command remotely. type RemoteOrchestrator struct { - flags *Flags - node inspector.Node - archive string + flags *Flags + node inspector.Node } // NewRemoteOrchestrator creates a new remote orchestrator. @@ -25,11 +23,6 @@ func NewRemoteOrchestrator(flags *Flags) (*RemoteOrchestrator, error) { return nil, fmt.Errorf("--vps-ip is required\nExample: orama install --vps-ip 1.2.3.4 --nameserver --domain orama-testnet.network") } - // Check source archive exists - if _, err := os.Stat(sourceArchivePath); os.IsNotExist(err) { - return nil, fmt.Errorf("source archive not found at %s\nRun: make build-linux && ./scripts/generate-source-archive.sh", sourceArchivePath) - } - // Resolve SSH credentials node, err := resolveSSHCredentials(flags.VpsIP) if err != nil { @@ -37,31 +30,17 @@ func NewRemoteOrchestrator(flags *Flags) (*RemoteOrchestrator, error) { } return &RemoteOrchestrator{ - flags: flags, - node: node, - archive: sourceArchivePath, + flags: flags, + node: node, }, nil } -// Execute runs the full remote install process. +// Execute runs the remote install process. +// Source must already be uploaded via: ./scripts/upload-source.sh func (r *RemoteOrchestrator) Execute() error { fmt.Printf("Installing on %s via SSH (%s@%s)...\n\n", r.flags.VpsIP, r.node.User, r.node.Host) - // Step 1: Upload archive - fmt.Printf("Uploading source archive...\n") - if err := r.uploadArchive(); err != nil { - return fmt.Errorf("upload failed: %w", err) - } - fmt.Printf(" Done.\n\n") - - // Step 2: Extract on VPS - fmt.Printf("Extracting on VPS...\n") - if err := r.extractOnVPS(); err != nil { - return fmt.Errorf("extract failed: %w", err) - } - fmt.Printf(" Done.\n\n") - - // Step 3: Run remote install + // Run remote install fmt.Printf("Running install on VPS...\n\n") if err := r.runRemoteInstall(); err != nil { return err @@ -70,31 +49,6 @@ func (r *RemoteOrchestrator) Execute() error { return nil } -// uploadArchive copies the source archive to the VPS. -func (r *RemoteOrchestrator) uploadArchive() error { - return uploadFile(r.node, r.archive, "/tmp/network-source.tar.gz") -} - -// extractOnVPS runs extract-deploy.sh on the VPS. -func (r *RemoteOrchestrator) extractOnVPS() error { - // Extract source archive and install only the CLI binary. - // All other binaries are built from source on the VPS during install. - extractCmd := r.sudoPrefix() + "bash -c '" + - `ARCHIVE="/tmp/network-source.tar.gz" && ` + - `SRC_DIR="/opt/orama/src" && ` + - `BIN_DIR="/opt/orama/bin" && ` + - `rm -rf "$SRC_DIR" && mkdir -p "$SRC_DIR" "$BIN_DIR" && ` + - `tar xzf "$ARCHIVE" -C "$SRC_DIR" && ` + - // Install pre-built CLI binary (only binary cross-compiled locally) - `if [ -f "$SRC_DIR/bin-linux/orama" ]; then ` + - `cp "$SRC_DIR/bin-linux/orama" /usr/local/bin/orama && ` + - `chmod +x /usr/local/bin/orama; fi && ` + - `echo "Extract complete."` + - "'" - - return runSSHStreaming(r.node, extractCmd) -} - // runRemoteInstall executes `orama install` on the VPS. func (r *RemoteOrchestrator) runRemoteInstall() error { cmd := r.buildRemoteCommand() diff --git a/pkg/cli/production/install/ssh.go b/pkg/cli/production/install/ssh.go index 5fd8b71..5ba1034 100644 --- a/pkg/cli/production/install/ssh.go +++ b/pkg/cli/production/install/ssh.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/DeBrosOfficial/network/pkg/inspector" + "golang.org/x/term" ) const sourceArchivePath = "/tmp/network-source.tar.gz" @@ -65,8 +66,18 @@ func promptSSHCredentials(vpsIP string) inspector.Node { } fmt.Print(" SSH password: ") - password, _ := reader.ReadString('\n') - password = strings.TrimSpace(password) + passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() // newline after hidden input + if err != nil { + // Fall back to plain read if terminal is not available + password, _ := reader.ReadString('\n') + return inspector.Node{ + User: user, + Host: vpsIP, + Password: strings.TrimSpace(password), + } + } + password := string(passwordBytes) return inspector.Node{ User: user, diff --git a/pkg/cli/production/install/validator.go b/pkg/cli/production/install/validator.go index a5178d8..ebfe6b2 100644 --- a/pkg/cli/production/install/validator.go +++ b/pkg/cli/production/install/validator.go @@ -84,7 +84,7 @@ func (v *Validator) SaveSecrets() error { // If cluster secret was provided, save it to secrets directory before setup if v.flags.ClusterSecret != "" { secretsDir := filepath.Join(v.oramaDir, "secrets") - if err := os.MkdirAll(secretsDir, 0755); err != nil { + if err := os.MkdirAll(secretsDir, 0700); err != nil { return fmt.Errorf("failed to create secrets directory: %w", err) } secretPath := filepath.Join(secretsDir, "cluster-secret") @@ -97,7 +97,7 @@ func (v *Validator) SaveSecrets() error { // If swarm key was provided, save it to secrets directory in full format if v.flags.SwarmKey != "" { secretsDir := filepath.Join(v.oramaDir, "secrets") - if err := os.MkdirAll(secretsDir, 0755); err != nil { + if err := os.MkdirAll(secretsDir, 0700); err != nil { return fmt.Errorf("failed to create secrets directory: %w", err) } // Extract hex only (strips headers if user passed full file content) diff --git a/pkg/environments/production/config.go b/pkg/environments/production/config.go index 786ab2b..3bf6cd1 100644 --- a/pkg/environments/production/config.go +++ b/pkg/environments/production/config.go @@ -178,15 +178,13 @@ func (cg *ConfigGenerator) GenerateNodeConfig(peerAddresses []string, vpsIP stri WGIP: vpsIP, } - // Set MinClusterSize based on whether this is a genesis or joining node. - // Genesis nodes (no join address) bootstrap alone, so MinClusterSize=1. - // Joining nodes should wait for at least 2 remote peers before writing peers.json - // to prevent accidental solo bootstrap during mass restarts. - if rqliteJoinAddr != "" { - data.MinClusterSize = 3 - } else { - data.MinClusterSize = 1 - } + // MinClusterSize=1 for all nodes. Joining nodes use the -join flag to + // connect to the existing cluster; gating on peer discovery caused a + // deadlock where the WG sync loop (needs RQLite) couldn't add new peers + // and RQLite (needs WG peers discovered) couldn't start. + // Solo-bootstrap protection is already handled by performPreStartClusterDiscovery + // which refuses to write a single-node peers.json. + data.MinClusterSize = 1 // RQLite node-to-node TLS encryption is disabled by default // This simplifies certificate management - RQLite uses plain TCP for internal Raft diff --git a/pkg/environments/production/installers/gateway.go b/pkg/environments/production/installers/gateway.go index a98fd36..a8e0f03 100644 --- a/pkg/environments/production/installers/gateway.go +++ b/pkg/environments/production/installers/gateway.go @@ -117,20 +117,6 @@ func (gi *GatewayInstaller) InstallDeBrosBinaries(oramaHome string) error { if err := exec.Command("chmod", "-R", "755", binDir).Run(); err != nil { fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chmod bin directory: %v\n", err) } - if err := exec.Command("chown", "-R", "orama:orama", binDir).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chown bin directory: %v\n", err) - } - - // Grant CAP_NET_BIND_SERVICE to orama-node to allow binding to ports 80/443 without root - nodeBinary := filepath.Join(binDir, "orama-node") - if _, err := os.Stat(nodeBinary); err == nil { - if err := exec.Command("setcap", "cap_net_bind_service=+ep", nodeBinary).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to setcap on orama-node: %v\n", err) - fmt.Fprintf(gi.logWriter, " ⚠️ Gateway may not be able to bind to port 80/443\n") - } else { - fmt.Fprintf(gi.logWriter, " ✓ Set CAP_NET_BIND_SERVICE on orama-node\n") - } - } fmt.Fprintf(gi.logWriter, " ✓ Orama binaries installed\n") return nil @@ -230,22 +216,10 @@ func (gi *GatewayInstaller) InstallAnyoneClient() error { fmt.Fprintf(gi.logWriter, " ⚠️ Failed to create %s: %v\n", dir, err) continue } - // Fix ownership to orama user (sequential to avoid race conditions) - if err := exec.Command("chown", "orama:orama", dir).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chown %s: %v\n", dir, err) - } - if err := exec.Command("chmod", "700", dir).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chmod %s: %v\n", dir, err) - } } - // Recursively fix ownership of entire .npm directory to ensure all nested files are owned by orama - if err := exec.Command("chown", "-R", "orama:orama", filepath.Join(oramaHome, ".npm")).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chown .npm directory: %v\n", err) - } - - // Run npm cache verify as orama user with proper environment - cacheInitCmd := exec.Command("sudo", "-u", "orama", "npm", "cache", "verify", "--silent") + // Run npm cache verify + cacheInitCmd := exec.Command("npm", "cache", "verify", "--silent") cacheInitCmd.Env = append(os.Environ(), "HOME="+oramaHome) if err := cacheInitCmd.Run(); err != nil { fmt.Fprintf(gi.logWriter, " ⚠️ NPM cache verify warning: %v (continuing anyway)\n", err) @@ -261,10 +235,6 @@ func (gi *GatewayInstaller) InstallAnyoneClient() error { termsFile := filepath.Join(oramaHome, "terms-agreement") if err := os.WriteFile(termsFile, []byte("agreed"), 0644); err != nil { fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to create terms-agreement: %v\n", err) - } else { - if err := exec.Command("chown", "orama:orama", termsFile).Run(); err != nil { - fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chown terms-agreement: %v\n", err) - } } // Verify installation - try npx with the correct CLI name (anyone-client, not full scoped package name) diff --git a/scripts/clean-testnet.sh b/scripts/clean-testnet.sh index d0ea5d0..b340bb1 100755 --- a/scripts/clean-testnet.sh +++ b/scripts/clean-testnet.sh @@ -31,9 +31,13 @@ export DEBIAN_FRONTEND=noninteractive echo " Stopping services..." systemctl stop orama-node orama-gateway orama-ipfs orama-ipfs-cluster orama-olric orama-anyone-relay orama-anyone-client coredns caddy 2>/dev/null || true systemctl disable orama-node orama-gateway orama-ipfs orama-ipfs-cluster orama-olric orama-anyone-relay orama-anyone-client coredns caddy 2>/dev/null || true +# Legacy debros-* services (pre-rename) +systemctl stop debros-anyone-relay debros-anyone-client 2>/dev/null || true +systemctl disable debros-anyone-relay debros-anyone-client 2>/dev/null || true echo " Removing systemd service files..." rm -f /etc/systemd/system/orama-*.service +rm -f /etc/systemd/system/debros-*.service rm -f /etc/systemd/system/coredns.service rm -f /etc/systemd/system/caddy.service rm -f /etc/systemd/system/orama-deploy-*.service diff --git a/scripts/upload-source.sh b/scripts/upload-source.sh new file mode 100755 index 0000000..b529871 --- /dev/null +++ b/scripts/upload-source.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Upload and extract the source archive to one or more VPS nodes. +# +# Prerequisites: +# make build-linux +# ./scripts/generate-source-archive.sh +# +# Usage: +# ./scripts/upload-source.sh [ ...] +# ./scripts/upload-source.sh --env testnet # upload to all testnet nodes +# +# After uploading, run install: +# ./bin/orama install --vps-ip --nameserver --domain ... + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ARCHIVE="/tmp/network-source.tar.gz" +CONF="$SCRIPT_DIR/remote-nodes.conf" + +if [ ! -f "$ARCHIVE" ]; then + echo "Error: $ARCHIVE not found" + echo "Run: make build-linux && ./scripts/generate-source-archive.sh" + exit 1 +fi + +# Resolve VPS list from --env flag or direct IPs +resolve_nodes() { + if [ "$1" = "--env" ] && [ -n "$2" ] && [ -f "$CONF" ]; then + grep "^$2|" "$CONF" | while IFS='|' read -r env userhost pass role; do + local user="${userhost%%@*}" + local host="${userhost##*@}" + echo "$user|$host|$pass" + done + return + fi + + # Direct IPs — look up credentials from conf + for ip in "$@"; do + if [ -f "$CONF" ]; then + local match + match=$(grep "|[^|]*@${ip}|" "$CONF" | head -1) + if [ -n "$match" ]; then + local userhost pass + userhost=$(echo "$match" | cut -d'|' -f2) + pass=$(echo "$match" | cut -d'|' -f3) + local user="${userhost%%@*}" + echo "$user|$ip|$pass" + continue + fi + fi + # Fallback: prompt for credentials + echo "ubuntu|$ip|" + done +} + +upload_to_node() { + local user="$1" host="$2" pass="$3" + + echo "→ Uploading to $user@$host..." + + # Upload archive + if [ -n "$pass" ]; then + sshpass -p "$pass" scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + -o PreferredAuthentications=password -o PubkeyAuthentication=no \ + "$ARCHIVE" "$user@$host:/tmp/network-source.tar.gz" + else + scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + "$ARCHIVE" "$user@$host:/tmp/network-source.tar.gz" + fi + + # Extract on VPS + local sudo_prefix="" + [ "$user" != "root" ] && sudo_prefix="sudo " + + local extract_cmd="${sudo_prefix}bash -c 'rm -rf /opt/orama/src && mkdir -p /opt/orama/src /opt/orama/bin && tar xzf /tmp/network-source.tar.gz -C /opt/orama/src 2>/dev/null && if [ -f /opt/orama/src/bin-linux/orama ]; then cp /opt/orama/src/bin-linux/orama /usr/local/bin/orama && chmod +x /usr/local/bin/orama; fi && echo \" ✓ Extracted (\$(ls /opt/orama/src/ | wc -l) files)\"'" + + if [ -n "$pass" ]; then + sshpass -p "$pass" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + -o PreferredAuthentications=password -o PubkeyAuthentication=no \ + "$user@$host" "$extract_cmd" + else + ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \ + "$user@$host" "$extract_cmd" + fi +} + +# Main +if [ $# -eq 0 ]; then + echo "Usage: $0 [ ...]" + echo " $0 --env testnet" + exit 1 +fi + +echo "Source archive: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))" +echo "" + +resolve_nodes "$@" | while IFS='|' read -r user host pass; do + upload_to_node "$user" "$host" "$pass" + echo "" +done + +echo "Done. Now run: ./bin/orama install --vps-ip ..."