#!/usr/bin/env bash # ============================================================================ # Volt Platform Installer — ArmoredGate # Universal installer for Debian/Ubuntu, RHEL/Rocky/Alma, Arch, SUSE # # Usage: # curl -fsSL https://get.armoredgate.com | sudo bash # curl -fsSL https://get.armoredgate.com | VOLT_VERSION=0.2.0 sudo bash # sudo bash install.sh # # Copyright (c) 2026 Armored Gate LLC. All rights reserved. # https://armoredgate.com # ============================================================================ set -euo pipefail # --------------------------------------------------------------------------- # Version & Download Configuration # --------------------------------------------------------------------------- VOLT_VERSION="${VOLT_VERSION:-0.2.0}" VOLT_CDN="https://cdn.armoredgate.com/releases" VOLT_USER="volt" VOLT_GROUP="volt" # Volt inter-node port range (configurable) VOLT_PORT_START="${VOLT_PORT_START:-7400}" VOLT_PORT_END="${VOLT_PORT_END:-7410}" # --------------------------------------------------------------------------- # Color & Output Helpers # --------------------------------------------------------------------------- if [ -t 1 ] && [ -t 2 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' RESET='' fi info() { echo -e "${BLUE}[INFO]${RESET} $*"; } success() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } fatal() { error "$@"; exit 1; } step() { echo -e "\n${CYAN}${BOLD}→ $*${RESET}"; } # --------------------------------------------------------------------------- # Cleanup Trap # --------------------------------------------------------------------------- CLEANUP_DIR="" cleanup() { local exit_code=$? if [ -n "$CLEANUP_DIR" ] && [ -d "$CLEANUP_DIR" ]; then rm -rf "$CLEANUP_DIR" fi if [ $exit_code -ne 0 ]; then echo "" error "Installation failed (exit code: $exit_code)" error "Check the output above for details." error "For help: https://docs.armoredgate.com/volt/install" fi } trap cleanup EXIT # --------------------------------------------------------------------------- # Banner # --------------------------------------------------------------------------- banner() { echo -e "${MAGENTA}${BOLD}" cat << 'BANNER' ██╗ ██╗ ██████╗ ██╗ ████████╗ ██║ ██║██╔═══██╗██║ ╚══██╔══╝ ██║ ██║██║ ██║██║ ██║ ╚██╗ ██╔╝██║ ██║██║ ██║ ╚████╔╝ ╚██████╔╝███████╗██║ ╚═══╝ ╚═════╝ ╚══════╝╚═╝ BANNER echo -e "${RESET}" echo -e " ${DIM}Volt Platform Installer v${VOLT_VERSION}${RESET}" echo -e " ${DIM}Secure containers (Voltainer) & VMs (Voltvisor)${RESET}" echo -e " ${DIM}Powered by systemd-nspawn · nftables · ArmoredGate${RESET}" echo "" } # --------------------------------------------------------------------------- # Preflight Checks # --------------------------------------------------------------------------- preflight() { step "Preflight checks" # Must be root if [ "$(id -u)" -ne 0 ]; then fatal "This installer must be run as root. Use: sudo bash install.sh" fi # Must be Linux if [ "$(uname -s)" != "Linux" ]; then fatal "Volt requires Linux. Detected: $(uname -s)" fi # Must have systemd if ! command -v systemctl &>/dev/null; then fatal "Volt requires systemd. This system does not appear to use systemd." fi # Check systemd is PID 1 if [ "$(cat /proc/1/comm 2>/dev/null)" != "systemd" ]; then warn "systemd does not appear to be PID 1. Volt may not function correctly." fi success "Running as root on Linux with systemd" } # --------------------------------------------------------------------------- # OS Detection # --------------------------------------------------------------------------- DISTRO="" DISTRO_FAMILY="" DISTRO_VERSION="" PKG_MANAGER="" detect_os() { step "Detecting operating system" if [ ! -f /etc/os-release ]; then fatal "Cannot detect OS: /etc/os-release not found" fi # shellcheck disable=SC1091 . /etc/os-release DISTRO="${ID}" DISTRO_VERSION="${VERSION_ID:-unknown}" case "$DISTRO" in ubuntu|debian|pop|linuxmint|elementary|zorin|kali) DISTRO_FAMILY="debian" PKG_MANAGER="apt" ;; rhel|centos|rocky|almalinux|ol|fedora|amzn) DISTRO_FAMILY="rhel" if command -v dnf &>/dev/null; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi ;; arch|manjaro|endeavouros|garuda) DISTRO_FAMILY="arch" PKG_MANAGER="pacman" ;; opensuse*|sles|suse) DISTRO_FAMILY="suse" PKG_MANAGER="zypper" ;; *) fatal "Unsupported distribution: $DISTRO. Supported: Debian/Ubuntu, RHEL/Rocky/Alma, Arch, SUSE" ;; esac success "Detected: ${PRETTY_NAME:-$DISTRO} (family: $DISTRO_FAMILY, pkg: $PKG_MANAGER)" } # --------------------------------------------------------------------------- # Architecture Detection # --------------------------------------------------------------------------- ARCH="" detect_arch() { step "Detecting architecture" local machine machine="$(uname -m)" case "$machine" in x86_64|amd64) ARCH="x86_64" ;; aarch64|arm64) ARCH="aarch64" ;; *) fatal "Unsupported architecture: $machine. Supported: x86_64, aarch64" ;; esac success "Architecture: $ARCH" } # --------------------------------------------------------------------------- # Install Prerequisites # --------------------------------------------------------------------------- install_prerequisites() { step "Installing prerequisites" local packages_to_install=() case "$DISTRO_FAMILY" in debian) info "Updating package index..." apt-get update -qq local debian_pkgs=(systemd-container nftables curl tar ca-certificates) for pkg in "${debian_pkgs[@]}"; do if ! dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"; then packages_to_install+=("$pkg") fi done if [ ${#packages_to_install[@]} -gt 0 ]; then info "Installing: ${packages_to_install[*]}" DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${packages_to_install[@]}" fi ;; rhel) local rhel_pkgs=(systemd-container nftables curl tar ca-certificates) for pkg in "${rhel_pkgs[@]}"; do if ! rpm -q "$pkg" &>/dev/null; then packages_to_install+=("$pkg") fi done if [ ${#packages_to_install[@]} -gt 0 ]; then info "Installing: ${packages_to_install[*]}" $PKG_MANAGER install -y -q "${packages_to_install[@]}" fi ;; arch) local arch_pkgs=(systemd nftables curl tar ca-certificates) # On Arch, systemd-nspawn is part of the 'systemd' package for pkg in "${arch_pkgs[@]}"; do if ! pacman -Qi "$pkg" &>/dev/null; then packages_to_install+=("$pkg") fi done if [ ${#packages_to_install[@]} -gt 0 ]; then info "Installing: ${packages_to_install[*]}" pacman -Sy --noconfirm --needed "${packages_to_install[@]}" fi ;; suse) local suse_pkgs=(systemd-container nftables curl tar ca-certificates) for pkg in "${suse_pkgs[@]}"; do if ! rpm -q "$pkg" &>/dev/null; then packages_to_install+=("$pkg") fi done if [ ${#packages_to_install[@]} -gt 0 ]; then info "Installing: ${packages_to_install[*]}" zypper --non-interactive install "${packages_to_install[@]}" fi ;; esac # Verify critical binaries exist for bin in systemd-nspawn nft curl tar; do if ! command -v "$bin" &>/dev/null; then fatal "Required binary not found after install: $bin" fi done success "All prerequisites installed" } # --------------------------------------------------------------------------- # Create System User & Group # --------------------------------------------------------------------------- create_user() { step "Creating Volt system user" if getent group "$VOLT_GROUP" &>/dev/null; then info "Group '$VOLT_GROUP' already exists" else groupadd --system "$VOLT_GROUP" success "Created group: $VOLT_GROUP" fi if id "$VOLT_USER" &>/dev/null; then info "User '$VOLT_USER' already exists" else useradd --system \ --gid "$VOLT_GROUP" \ --home-dir /var/lib/volt \ --no-create-home \ --shell /usr/sbin/nologin \ "$VOLT_USER" success "Created user: $VOLT_USER" fi } # --------------------------------------------------------------------------- # Create Directory Structure # --------------------------------------------------------------------------- create_directories() { step "Creating directory structure" local dirs=( /etc/volt /etc/nftables.d /var/lib/volt /var/lib/volt/cas /var/lib/volt/images /var/lib/volt/containers /var/lib/volt/volumes /var/lib/volt/networks /var/lib/volt/tmp /var/log/volt ) for dir in "${dirs[@]}"; do if [ ! -d "$dir" ]; then mkdir -p "$dir" info "Created: $dir" fi done # Set ownership chown -R "$VOLT_USER:$VOLT_GROUP" /var/lib/volt chown -R "$VOLT_USER:$VOLT_GROUP" /var/log/volt chmod 750 /var/lib/volt chmod 750 /var/log/volt chmod 755 /etc/volt success "Directory structure ready" } # --------------------------------------------------------------------------- # Download & Verify Volt Binaries # --------------------------------------------------------------------------- download_volt() { step "Downloading Volt v${VOLT_VERSION}" CLEANUP_DIR="$(mktemp -d)" local tmp_dir="$CLEANUP_DIR" local os="linux" local archive="volt-${os}-${ARCH}.tar.gz" local download_url="${VOLT_CDN}/${VOLT_VERSION}/${archive}" local checksum_url="${VOLT_CDN}/${VOLT_VERSION}/SHA256SUMS" info "Download URL: $download_url" info "Checksum URL: $checksum_url" # Download the archive info "Downloading Volt binary archive..." if ! curl -fsSL --retry 3 --retry-delay 2 -o "${tmp_dir}/${archive}" "$download_url"; then fatal "Failed to download Volt binary from: $download_url" fi # Download checksums info "Downloading SHA256 checksums..." if ! curl -fsSL --retry 3 --retry-delay 2 -o "${tmp_dir}/SHA256SUMS" "$checksum_url"; then warn "Could not download checksums. Skipping verification." else # Verify checksum info "Verifying SHA256 checksum..." cd "$tmp_dir" if ! sha256sum -c SHA256SUMS --ignore-missing 2>/dev/null | grep -q "${archive}: OK"; then # Try with shasum if sha256sum fails (some minimal installs) if command -v shasum &>/dev/null; then if ! shasum -a 256 -c SHA256SUMS --ignore-missing 2>/dev/null | grep -q "${archive}: OK"; then fatal "SHA256 checksum verification FAILED for ${archive}" fi else fatal "SHA256 checksum verification FAILED for ${archive}" fi fi success "SHA256 checksum verified" cd - >/dev/null fi # ----------------------------------------------------------------------- # GPG Signature Verification (placeholder) # TODO: When GPG signing is enabled, verify the signature: # 1. Import ArmoredGate signing key from ${VOLT_CDN}/keys/volt-release.asc # 2. Download ${checksum_url}.asc (detached signature of SHA256SUMS) # 3. gpg --verify SHA256SUMS.asc SHA256SUMS # 4. Fail hard if signature is invalid # ----------------------------------------------------------------------- info "GPG signature verification: not yet enabled (coming soon)" # Extract info "Extracting archive..." tar -xzf "${tmp_dir}/${archive}" -C "$tmp_dir" # Install binary if [ -f "${tmp_dir}/volt" ]; then install -m 0755 "${tmp_dir}/volt" /usr/local/bin/volt success "Installed: /usr/local/bin/volt" else # Try looking in a subdirectory local volt_bin volt_bin="$(find "$tmp_dir" -name "volt" -type f -executable 2>/dev/null | head -1)" if [ -n "$volt_bin" ]; then install -m 0755 "$volt_bin" /usr/local/bin/volt success "Installed: /usr/local/bin/volt" else fatal "Could not find 'volt' binary in archive" fi fi # Clean up temp rm -rf "$CLEANUP_DIR" CLEANUP_DIR="" success "Volt v${VOLT_VERSION} installed" } # --------------------------------------------------------------------------- # Write Default Configuration # --------------------------------------------------------------------------- write_config() { step "Writing default configuration" local config_file="/etc/volt/config.yaml" if [ -f "$config_file" ]; then info "Configuration already exists at $config_file — preserving" return fi local node_hostname node_hostname="$(hostname -f 2>/dev/null || hostname)" cat > "$config_file" << YAML # ============================================================================ # Volt Platform Configuration # Generated by installer on $(date -u '+%Y-%m-%dT%H:%M:%SZ') # Documentation: https://docs.armoredgate.com/volt/config # ============================================================================ # Node identity node: hostname: "${node_hostname}" # Unique node ID — generated on first start if empty id: "" # Runtime settings runtime: # Container backend: systemd-nspawn backend: nspawn # Root directory for Volt data data_dir: /var/lib/volt # Log directory log_dir: /var/log/volt # Max concurrent container starts max_concurrent_starts: 4 # Network configuration network: # Listen address for Volt API listen: "0.0.0.0:${VOLT_PORT_START}" # Inter-node communication port range port_range: "${VOLT_PORT_START}-${VOLT_PORT_END}" # Enable TLS for inter-node traffic tls: true # Content-addressable storage cas: dir: /var/lib/volt/cas # Max cache size (0 = unlimited) max_size: 0 # Logging logging: level: info format: json # Rotation: handled by systemd journal + logrotate file: /var/log/volt/volt.log # Security security: # Enable seccomp profiles for Voltainers seccomp: true # Enable Landlock LSM integration mac: auto # Rootless mode (experimental) rootless: false YAML chown root:$VOLT_GROUP "$config_file" chmod 640 "$config_file" success "Configuration written: $config_file" } # --------------------------------------------------------------------------- # Create systemd Service Unit # --------------------------------------------------------------------------- create_service() { step "Creating systemd service" local service_file="/etc/systemd/system/volt.service" cat > "$service_file" << 'UNIT' [Unit] Description=Volt Platform — Secure Container & VM Runtime Documentation=https://docs.armoredgate.com/volt After=network-online.target nftables.service Wants=network-online.target Requires=nftables.service [Service] Type=notify ExecStart=/usr/local/bin/volt daemon --config /etc/volt/config.yaml ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=5 TimeoutStartSec=30 TimeoutStopSec=30 # Security hardening User=root Group=root NoNewPrivileges=no ProtectSystem=strict ReadWritePaths=/var/lib/volt /var/log/volt /etc/volt /run ProtectHome=yes PrivateTmp=yes ProtectKernelTunables=no ProtectKernelModules=no ProtectControlGroups=no SystemCallArchitectures=native # Resource limits LimitNOFILE=1048576 LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity # Logging StandardOutput=journal StandardError=journal SyslogIdentifier=volt [Install] WantedBy=multi-user.target UNIT chmod 644 "$service_file" systemctl daemon-reload success "Created: $service_file" } # --------------------------------------------------------------------------- # Configure nftables Firewall # --------------------------------------------------------------------------- configure_firewall() { step "Configuring nftables firewall" # Disable conflicting firewall managers (idempotent) for svc in ufw firewalld; do if systemctl is-active "$svc" &>/dev/null; then warn "Disabling $svc in favor of nftables (uniform firewall)" systemctl stop "$svc" 2>/dev/null || true systemctl disable "$svc" 2>/dev/null || true fi done # Ensure nftables.d include directory exists mkdir -p /etc/nftables.d # Write the Volt firewall rules cat > /etc/nftables.d/volt.conf << NFTABLES #!/usr/sbin/nft -f # ============================================================================ # Volt Platform — nftables firewall rules # Generated by installer on $(date -u '+%Y-%m-%dT%H:%M:%SZ') # # This file is included by the main nftables configuration. # Edit port ranges as needed for your deployment. # ============================================================================ table inet volt_filter { chain input { type filter hook input priority 0; policy drop; # Allow established/related connections ct state established,related accept # Allow loopback iif "lo" accept # Allow ICMP (ping, path MTU discovery, etc.) ip protocol icmp accept ip6 nexthdr icmpv6 accept # Allow SSH tcp dport 22 accept # Allow HTTP and HTTPS tcp dport { 80, 443 } accept # Allow Volt inter-node communication tcp dport ${VOLT_PORT_START}-${VOLT_PORT_END} accept udp dport ${VOLT_PORT_START}-${VOLT_PORT_END} accept # Log and drop everything else # Uncomment for debugging: # log prefix "volt-drop: " limit rate 5/minute drop } chain forward { type filter hook forward priority 0; policy drop; # Allow forwarding for Voltainer networks # (Volt manages these rules dynamically) ct state established,related accept } chain output { type filter hook output priority 0; policy accept; # Allow all outgoing traffic } } NFTABLES chmod 644 /etc/nftables.d/volt.conf # Ensure the main nftables.conf includes our rules local nft_conf="/etc/nftables.conf" local include_line='include "/etc/nftables.d/*.conf"' if [ -f "$nft_conf" ]; then if ! grep -qF 'nftables.d' "$nft_conf"; then # Append include directive echo "" >> "$nft_conf" echo "# Volt platform firewall rules" >> "$nft_conf" echo "$include_line" >> "$nft_conf" info "Added include directive to $nft_conf" else info "Include directive already present in $nft_conf" fi else # Create minimal nftables.conf cat > "$nft_conf" << EOF #!/usr/sbin/nft -f # nftables configuration — managed by Volt installer flush ruleset $include_line EOF info "Created $nft_conf with Volt include" fi # Enable and start nftables systemctl enable nftables 2>/dev/null || true # Apply rules now if nft -f /etc/nftables.d/volt.conf 2>/dev/null; then success "Firewall rules applied" else warn "Could not apply firewall rules now (will apply on next nftables restart)" fi systemctl restart nftables 2>/dev/null || warn "Could not restart nftables service" success "nftables firewall configured" info "Rules: SSH(22), HTTP(80), HTTPS(443), Volt(${VOLT_PORT_START}-${VOLT_PORT_END})" } # --------------------------------------------------------------------------- # Security Hardening (sysctl) # --------------------------------------------------------------------------- apply_hardening() { step "Applying security hardening" local sysctl_file="/etc/sysctl.d/90-armored-hardening.conf" if [ -f "$sysctl_file" ]; then info "Hardening config already exists — preserving" else cat > "$sysctl_file" << 'SYSCTL' # ============================================================================ # ArmoredGate Security Hardening — sysctl settings # Applied by Volt installer # ============================================================================ # --- Network Security --- # Disable IP source routing net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0 net.ipv6.conf.default.accept_source_route = 0 # Disable ICMP redirects (prevent MITM) net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 # Enable reverse path filtering (anti-spoofing) net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 # Ignore ICMP broadcast requests net.ipv4.icmp_echo_ignore_broadcasts = 1 # Ignore bogus ICMP error responses net.ipv4.icmp_ignore_bogus_error_responses = 1 # Enable SYN cookies (SYN flood protection) net.ipv4.tcp_syncookies = 1 # Log martian packets net.ipv4.conf.all.log_martians = 1 net.ipv4.conf.default.log_martians = 1 # --- IP Forwarding (required for Voltainer networking) --- net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 # --- Kernel Security --- # Restrict dmesg access kernel.dmesg_restrict = 1 # Restrict kernel pointer exposure kernel.kptr_restrict = 2 # Disable SysRq (except sync + reboot) kernel.sysrq = 176 # Restrict ptrace scope kernel.yama.ptrace_scope = 1 # Restrict unprivileged user namespaces (if not needed) # Note: Some Voltainer configurations may need this enabled # kernel.unprivileged_userns_clone = 0 # --- Memory Protection --- # Randomize virtual address space (ASLR) kernel.randomize_va_space = 2 # Restrict core dumps fs.suid_dumpable = 0 # --- File System --- # Restrict access to kernel logs # Harden symlink/hardlink creation fs.protected_symlinks = 1 fs.protected_hardlinks = 1 fs.protected_fifos = 2 fs.protected_regular = 2 # --- Performance Tuning for Containers --- # Increase max file watchers (for many containers) fs.inotify.max_user_watches = 524288 fs.inotify.max_user_instances = 512 # Increase PID max for container density kernel.pid_max = 4194304 # Increase max open files fs.file-max = 2097152 # Connection tracking max (for nftables) net.netfilter.nf_conntrack_max = 1048576 SYSCTL info "Created: $sysctl_file" fi # Apply sysctl settings (non-fatal — some may not apply in containers/VMs) sysctl --system 2>/dev/null || warn "Some sysctl settings could not be applied (this is normal in some environments)" success "Security hardening applied" } # --------------------------------------------------------------------------- # Hostname Check # --------------------------------------------------------------------------- check_hostname() { step "Checking hostname" local current_hostname current_hostname="$(hostname)" if [ "$current_hostname" = "localhost" ] || [ "$current_hostname" = "localhost.localdomain" ] || [ -z "$current_hostname" ]; then warn "Hostname is set to '${current_hostname}' — Volt nodes should have unique hostnames" warn "Set a hostname with: hostnamectl set-hostname " warn "Example: hostnamectl set-hostname volt-node-01" else success "Hostname: $current_hostname" fi } # --------------------------------------------------------------------------- # Enable Service (but don't start yet) # --------------------------------------------------------------------------- enable_service() { step "Enabling Volt service" systemctl enable volt.service 2>/dev/null || true success "Volt service enabled (will start on boot)" info "Start now with: systemctl start volt" } # --------------------------------------------------------------------------- # Print Summary # --------------------------------------------------------------------------- print_summary() { echo "" echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════════${RESET}" echo -e "${GREEN}${BOLD} ✓ Volt Platform v${VOLT_VERSION} installed successfully!${RESET}" echo -e "${GREEN}${BOLD}════════════════════════════════════════════════════════════════${RESET}" echo "" echo -e " ${BOLD}Installed:${RESET}" echo -e " • volt CLI → /usr/local/bin/volt" echo -e " • Configuration → /etc/volt/config.yaml" echo -e " • Data directory → /var/lib/volt/" echo -e " • Log directory → /var/log/volt/" echo -e " • Systemd service → volt.service" echo -e " • Firewall rules → /etc/nftables.d/volt.conf" echo -e " • Kernel hardening → /etc/sysctl.d/90-armored-hardening.conf" echo "" echo -e " ${BOLD}Firewall (nftables):${RESET}" echo -e " • SSH (22) ${GREEN}✓ allowed${RESET}" echo -e " • HTTP/HTTPS (80, 443) ${GREEN}✓ allowed${RESET}" echo -e " • Volt inter-node (${VOLT_PORT_START}-${VOLT_PORT_END}) ${GREEN}✓ allowed${RESET}" echo -e " • All other inbound ${RED}✗ denied${RESET}" echo -e " • All outbound ${GREEN}✓ allowed${RESET}" echo "" echo -e " ${BOLD}Next steps:${RESET}" echo -e " 1. Review config: ${CYAN}nano /etc/volt/config.yaml${RESET}" echo -e " 2. Start Volt: ${CYAN}systemctl start volt${RESET}" echo -e " 3. Check status: ${CYAN}volt status${RESET}" echo -e " 4. Deploy first app: ${CYAN}volt run --name my-app ubuntu:24.04${RESET}" echo "" echo -e " ${BOLD}Documentation:${RESET} ${CYAN}https://docs.armoredgate.com/volt${RESET}" echo -e " ${BOLD}Uninstall:${RESET} ${CYAN}curl -fsSL https://get.armoredgate.com/uninstall.sh | sudo bash${RESET}" echo "" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { banner preflight detect_os detect_arch install_prerequisites create_user create_directories download_volt write_config create_service configure_firewall apply_hardening check_hostname enable_service print_summary } main "$@"