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.
This commit is contained in:
anonpenguin23 2026-02-16 10:01:35 +02:00
parent 25a167f9b4
commit 7163aad850
8 changed files with 143 additions and 150 deletions

View File

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

View File

@ -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 <vps-ip>
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()

View File

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

View File

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

View File

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

View File

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

View File

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

103
scripts/upload-source.sh Executable file
View File

@ -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 <vps-ip> [<vps-ip2> ...]
# ./scripts/upload-source.sh --env testnet # upload to all testnet nodes
#
# After uploading, run install:
# ./bin/orama install --vps-ip <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 <vps-ip> [<vps-ip2> ...]"
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 <ip> ..."