Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

36 changed files with 5951 additions and 1214 deletions

15
.env Normal file
View file

@ -0,0 +1,15 @@
# Wallarm Preflight Check Results
# Generated: 2026-04-21 15:47:43
# Script: ./wallarm-ct-check.sh
result=pass
os_name=darwin
os_version=25.3.0
architecture=x86_64
init_system=darwin
us_cloud_reachable=true
eu_cloud_reachable=true
registry_reachable=false
download_reachable=false
git_reachable=true

3
.gitignore vendored
View file

@ -11,6 +11,3 @@ notes/
wallarm
bin/
wallarm-upx
bin/
wallarm-linux-amd64
deploy

View file

@ -1,212 +0,0 @@
# Jump Server Setup for Wallarm Remote Assistance
This guide explains how to configure a Linux VM as a jump server so engineers
can securely access customer Wallarm nodes through the `wallarm --tunnel` feature.
## Architecture
```
Customer VM (VM-A) Jump Server (VM-B) Engineer
┌──────────────────┐ TLS:443 ┌──────────────────┐ SSH ┌──────────┐
│ wallarm binary │───────────►│ sshd │◄─────────│ You │
│ (reverse tunnel) │◄───────────│ │ │ │
│ │ │ localhost:9042 │ │ │
│ │ │ → VM-A:22 │ │ │
└──────────────────┘ └──────────────────┘ └──────────┘
```
VM-A opens an outbound TLS connection to VM-B on port 443 (looks like HTTPS to
firewalls). VM-B terminates TLS and proxies to its internal SSH server. VM-A
then requests a reverse port forward: VM-B:9042 → VM-A:22. The engineer SSH's
into VM-B, then jumps through localhost:9042 to reach VM-A.
## Prerequisites
- **Linux VM** (any distro) with a public IP or reachable via Zoraxy/Nginx reverse proxy
- **OpenSSH server** 7.6+
- **Port 443** accessible from customer environments (outbound only, no inbound needed)
## Step 1: Install and Configure SSH Server
```bash
# Install SSH server
apt-get install -y openssh-server # Debian/Ubuntu
yum install -y openssh-server # RHEL/CentOS
# Edit SSH config
nano /etc/ssh/sshd_config
```
Add or uncomment these lines:
```ini
# Required: allow TCP forwarding for the reverse tunnel
AllowTcpForwarding yes
# Required: keep idle tunnels alive
ClientAliveInterval 60
ClientAliveCountMax 3
# Optional: allow remote binds on non-localhost addresses
# (only needed if you want engineers to connect directly to VM-B:9042
# instead of SSH'ing into VM-B first)
GatewayPorts yes
```
Restart SSH:
```bash
systemctl restart sshd
```
## Step 2: Create the Tunnel User
This user is used by the `wallarm` binary on customer VMs to authenticate
and open the reverse tunnel. It should NOT have a shell — it exists only
for port forwarding.
```bash
# Create tunnel-only user (no shell, no home directory needed)
useradd -m -s /bin/false wallarm-tunnel
# Create SSH directory
mkdir -p ~wallarm-tunnel/.ssh
chmod 700 ~wallarm-tunnel/.ssh
```
### Option A: Password Authentication (simpler for customers)
```bash
# Set a password for the tunnel user
passwd wallarm-tunnel
```
The customer enters this password when prompted by `wallarm`.
### Option B: SSH Key Authentication (more secure)
Generate a key pair and share the private key with customers:
```bash
# On the jump server
ssh-keygen -t ed25519 -f ~wallarm-tunnel/.ssh/wallarm_tunnel -N "" -C "wallarm-tunnel"
cat ~wallarm-tunnel/.ssh/wallarm_tunnel.pub >> ~wallarm-tunnel/.ssh/authorized_keys
chmod 600 ~wallarm-tunnel/.ssh/authorized_keys
chown -R wallarm-tunnel:wallarm-tunnel ~wallarm-tunnel/.ssh
# Share the private key securely with customers
cat ~wallarm-tunnel/.ssh/wallarm_tunnel
```
The customer provides the key path when prompted by `wallarm`.
## Step 3: Zoraxy / Reverse Proxy (Optional)
If the jump server sits behind a Zoraxy edge proxy, configure it to forward
TLS:443 → internal SSH:22. This lets `wallarm` connect over port 443 (which
passes through most corporate firewalls).
### Zoraxy Configuration
1. Add a new proxy rule:
- **Domain**: `ssh.sechpoint.app`
- **Target**: `tcp://<jump-server-ip>:22`
- **TLS**: Enabled (auto-cert or custom)
2. No WebSocket or HTTP mode needed — Zoraxy proxies raw TCP.
### Manual Nginx Stream Proxy (Alternative)
```nginx
stream {
server {
listen 443 ssl;
proxy_pass <jump-server-ip>:22;
ssl_certificate /etc/ssl/certs/jump.crt;
ssl_certificate_key /etc/ssl/private/jump.key;
}
}
```
## Step 4: Test the Setup
### From the Jump Server Itself
```bash
# Verify SSH is listening
ss -tlnp | grep 22
# Verify tunnel user can authenticate (no shell expected)
ssh wallarm-tunnel@localhost echo test
# Expected: command fails (user has no shell), authentication succeeds
```
### From an External Machine (Customer VM)
```bash
# Test TLS connection (if Zoraxy is configured)
openssl s_client -connect ssh.sechpoint.app:443 </dev/null 2>/dev/null | head -5
# Test SSH over TLS
ssh -p 443 wallarm-tunnel@ssh.sechpoint.app
# Expected: "This account is currently not available" or immediate disconnect
# This confirms authentication works but no shell is allowed — correct.
```
## How wallarm Uses It
On the customer VM, the `wallarm` binary offers two paths:
1. **`wallarm --tunnel`** — starts tunnel with pre-configured credentials (build-time or env var)
2. **`wallarm` → Remote Assistance** — prompts for jumphost URL, username, password/key interactively
The binary does:
1. TLS dial to `ssh.sechpoint.app:443`
2. SSH authenticate as `wallarm-tunnel`
3. Request reverse forward: `0.0.0.0:9042 → localhost:22` (or `localhost:9042` if GatewayPorts is off)
4. Keep tunnel alive with 30s heartbeats
## Engineer Connection (Assistance Flow)
After the customer starts the tunnel, the engineer connects with two hops:
```bash
# Step 1: SSH into the jump server
ssh engineer@ssh.sechpoint.app
# Step 2: Jump through the tunnel to the customer VM
ssh root@localhost -p 9042
```
Or as a single command:
```bash
ssh -o ProxyJump=engineer@ssh.sechpoint.app root@localhost -p 9042
```
The engineer now has a root shell on the customer VM and can run the deployment
wizard or troubleshoot directly.
## Security Notes
- The `wallarm-tunnel` user has no shell (`/bin/false`) — authentication only succeeds for port forwarding
- The tunnel is outbound-only from the customer VM — no inbound ports opened
- TLS encrypts the connection end-to-end (VM-A → Zoraxy → VM-B)
- Add `Match User wallarm-tunnel` blocks in `sshd_config` to further restrict:
```ini
Match User wallarm-tunnel
PermitTTY no
PermitTunnel no
X11Forwarding no
AllowAgentForwarding no
ForceCommand /bin/false
```
## Troubleshooting
| Symptom | Check |
|---------|-------|
| "Connection refused" from customer VM | Jump server port 443 reachable? Zoraxy running? |
| "Permission denied" | Tunnel user password/key correct? `~wallarm-tunnel/.ssh/authorized_keys` permissions 600? |
| Tunnel opens but engineer can't reach VM-A | `AllowTcpForwarding yes` in sshd_config? SSH server restarted? |
| Tunnel drops after a few minutes | `ClientAliveInterval` set? Check firewall idle timeout |

36
Makefile Normal file
View file

@ -0,0 +1,36 @@
.PHONY: all linux-amd64 linux-arm64 clean test release
BINARY := wallarm
VERSION := $(shell git describe --tags --always 2>/dev/null || echo "dev")
LDFLAGS := -s -w -X main.version=$(VERSION)
# Embed tunnel key at build time: make TUNNEL_KEY=~/.wallarm/tunnel_key
ifdef TUNNEL_KEY
LDFLAGS += -X main.tunnelKey=$(shell cat $(TUNNEL_KEY))
endif
all: linux-amd64 linux-arm64
linux-amd64:
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/wallarm/
upx --best --lzma $(BINARY)-linux-amd64 -o $(BINARY)-linux-amd64.tmp 2>/dev/null && mv $(BINARY)-linux-amd64.tmp $(BINARY)-linux-amd64 || true
linux-arm64:
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/wallarm/
upx --best --lzma $(BINARY)-linux-arm64 -o $(BINARY)-linux-arm64.tmp 2>/dev/null && mv $(BINARY)-linux-arm64.tmp $(BINARY)-linux-arm64 || true
clean:
rm -f $(BINARY) $(BINARY)-linux-*
test:
go test ./internal/...
# Build and prepare for Gitea release.
# Usage: make release
# Then upload wallarm-linux-amd64 and wallarm-linux-arm64 as release assets.
release: clean all
@echo "Release binaries built:"
@ls -lh wallarm-linux-*
@echo ""
@echo "Next: Create a Gitea release and upload these binaries as assets."
@echo " Tag: v$(VERSION)"

116
README.md
View file

@ -1,78 +1,90 @@
# Wallarm Docker Node Manager
# Wallarm Native Node Manager
Single-command Docker-based deployment for Wallarm filtering nodes.
Multiple nodes on one host, no conflicts, zero native dependencies.
Single-binary deployment and management for Wallarm Native Nodes (connector mode, no Docker).
One command to get started, one TUI to manage everything.
## Quick Start
```bash
curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/setup.sh" | bash
sudo /opt/fw/deploy.sh
curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/deploy.sh" | bash
sudo ./deploy/wallarm
```
Setup installs Python + Docker, downloads deploy script. Menu handles everything.
That's it. The binary runs preflight checks, then opens an interactive TUI:
- **First run**: configuration wizard (cloud region, API token, node name, listen address)
- **Subsequent runs**: dashboard with node list, add/remove/configure actions
## Menu
## Features
- **Single binary** — 2MB, zero runtime dependencies, works on any Linux
- **Interactive TUI** — bubbletea-powered forms and dashboard
- **Preflight checks** — runs on every start: system, network, cloud reachability, resources
- **Multi-node** — manage multiple Wallarm nodes on the same host via systemd
- **Remote tunnel**`wallarm --tunnel` opens a reverse SSH tunnel over TLS:443 via Zoraxy
- **State persistence**`~/.wallarm/state.json` tracks all deployments
## Commands
```
[1] Deploy all Deploy all nodes from fw.conf
[2] Add a node Interactive config + deploy
[3] Status Show running containers
[4] Remove a node Stop + delete container
[5] Edit a node Update port, upstream, mode in fw.conf
wallarm Interactive TUI (wizard or dashboard)
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app
wallarm --version Show version
wallarm --help Show help
```
## Configuration
## Dashboard
`/opt/fw/app/fw.conf` — JSON with node configs:
```
📊 Wallarm Dashboard
Type: native | Cloud: EU (api.wallarm.com)
──────────────────────────────────────────────────
```json
{
"srv1": {
"token": "your-wallarm-token",
"cloud": "EU",
"port": "8081",
"upstream_ip": "10.1.0.10",
"upstream_port": "8081",
"mode": "monitoring"
}
}
Nodes:
● srv1 — running (0.0.0.0:8081)
● srv2 — running (0.0.0.0:8082)
Actions:
[a] Add node
[c] Configure
[r] Remove node
[t] Start tunnel
[q] Quit
```
## Architecture
```
deploy.py Single Python file, ~200 lines
├── Docker image: wallarm/node:6.13.0
├── Named volumes for persistence
└── Unique ports per node
/opt/fw/
├── deploy.sh Entry wrapper
├── app/
│ ├── main.py Deploy script
│ ├── fw.conf Node config
│ └── state.json Deployment state
└── wallarm-aio.sh Cached installer (native fallback)
wallarm/
├── cmd/wallarm/main.go Entrypoint: preflight → TUI
├── internal/
│ ├── shared/ System detection, validation, connectivity
│ ├── preflight/ Mandatory checks on every start
│ ├── state/ ~/.wallarm/state.json persistence
│ ├── native/ Systemd units, installer, node management
│ ├── tunnel/ Reverse SSH over TLS:443 via Zoraxy
│ └── ui/ Bubbletea TUI (wizard + dashboard)
├── bin/
│ └── wallarm-linux-amd64 Pre-built binary
├── deploy.sh One-command bootstrap
├── go.mod / go.sum
└── Makefile Cross-compile targets
```
## Requirements
## Building from Source
- Linux with systemd
- Docker (auto-installed by setup.sh)
- Python 3.10+ (auto-installed by setup.sh)
- Outbound to api.wallarm.com (EU) or us1.api.wallarm.com (US)
```bash
go build -ldflags "-s -w -X main.version=$(git describe --tags)" -o wallarm ./cmd/wallarm/
make linux-amd64 # Cross-compile
make all # All targets
```
## Remote Assistance
## Prerequisites
See [JUMP_SERVER.md](JUMP_SERVER.md) for jump server setup.
- Linux (systemd required)
- x86_64 or aarch64
- 2GB+ RAM, 10GB+ disk
- Outbound connectivity to Wallarm cloud (US/EU)
## Agent Rules (for AI assistants working on this project)
## License
**CRITICAL — These override all other considerations:**
1. **NEVER change code without explicit user approval.** Discuss ideas first. Ask before implementing.
2. **Production defaults must be explicit.** Never rely on Docker image defaults — nginx config, headers, client_max_body_size, upload limits must be set explicitly in our code. Defaults cause production outages.
3. **Header forwarding is not default.** `proxy_set_header X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` must be explicitly configured.
4. **client_max_body_size** must be set high enough for production (e.g., `1024m`) — default 1m blocks legitimate uploads.
5. **The user owns this project.** You are assisting, not leading. Propose, don't impose.
Proprietary — see repository for terms.

86
changelog.md Normal file
View file

@ -0,0 +1,86 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to date-based versioning (YYYY-MM.x).
## [2026-08.1] - 2026-08-01
### Fixed
- **setup.sh interactive prompt broken under `curl ... | bash`**: the deployment-type prompt and overwrite confirmation read from stdin, which is the script pipe (not the terminal) when piped to bash — the prompt was silently skipped and only Docker scripts were downloaded. setup.sh is now **non-interactive by default and downloads BOTH deployment types** (`docker/` + `native/`), so the native option is always available. Use `DEPLOYMENT_TYPE=docker|native` to download only one type.
### Added
- **Native deployment**: Wallarm filtering node can now be deployed directly on the OS **without Docker** via the unified manager `native/wallarm-native.sh` (Wallarm Native Node, go-node, `connector-server` mode)
- `--preflight` checks (root, systemd, architecture, required commands, installer + Wallarm cloud connectivity, disk/memory, listen-port availability); auto-run before `--install`
- Interactive parallel multi-node installation with per-node systemd template units (`wallarm-node@<name>.service`)
- `--config` (address/token/labels, safe env rewrite), `--remove`, `--status [NODE]`
- All-in-one installer from `repo.wallarm.com` (overridable via `WALLARM_INSTALLER_URL`/`WALLARM_INSTALLER_ARCH`)
- **Shared library**: `common/wallarm-lib.sh` extracted and reused by both deployment types
- Colors, logging (`log_message`, `fail_with_remediation`), early error handler
- System detection (OS/arch/init), network connectivity tests
- Preflight `.env` parsing (`load_env_file`), cloud region selection (`select_cloud_region`)
- Validation helpers (IP, CIDR, port), artifact download + checksum verification
### Changed
- **Repository structure** now separates deployment types:
- `docker/` all Docker-based scripts moved here (`git mv`, history preserved)
- `docker/binaries/` and `docker/images/` Docker artifacts moved into the Docker tree
- `native/` native (no-Docker) deployment, containing only the unified `wallarm-native.sh`
- `common/` shared library
- **Removed** the `native/wallarm-ct-*.sh` scripts (NGINX-module based native deployment) so the native deployment is represented solely by the unified `wallarm-native.sh`; the `wallarm-ct-*` family is now Docker-only
- **Artifact URLs** updated to the `docker/` prefix (`/docker/binaries/...`, `/docker/images/...`)
- **Docker scripts** refactored to source `common/wallarm-lib.sh` (removed duplicated helper functions; behavior preserved)
- **setup.sh** downloads the shared library and scripts per deployment type into `docker/`/`native/` (native = `wallarm-native.sh`); supports `DEPLOYMENT_TYPE=docker|native` to download only one type
- **README.md** rewritten to document both deployment types, the new structure, and the unified native manager
### Notes
- The `wallarm-ct-*` script family is Docker-only; native deployment uses `wallarm-native.sh`
- Native multi-node is supported via per-node systemd template units
- Docker deployment behavior is unchanged apart from the new directory layout
## [2026-04.1] - 2026-04-21
### Added
- Initial changelog file with versioning schema
- Date-based versioning system (YYYY-MM.x)
### Changed
- **Variable renaming**: All `GITLAB_*` variables renamed to `GIT_*` prefix
- `GITLAB_BASE_URL``GIT_BASE_URL`
- `GITLAB_RAW_URL``GIT_RAW_URL` (with updated path)
- `GITLAB_DOCKER_BINARY_URL``GIT_DOCKER_BINARY_URL`
- `GITLAB_DOCKER_CHECKSUM_URL``GIT_DOCKER_CHECKSUM_URL`
- `GITLAB_WALLARM_IMAGE_URL``GIT_WALLARM_IMAGE_URL`
- `GITLAB_WALLARM_CHECKSUM_URL``GIT_WALLARM_CHECKSUM_URL`
- `GITLAB_REACHABLE``GIT_REACHABLE`
- **URL structure**: Updated `GIT_RAW_URL` from `/-/raw/main` to `/raw/branch/main` path (corrected for download compatibility)
- **Terminology**: Replaced all "GitLab" references in comments and log messages with "Git Repositorys"
- **Documentation**: Updated README.md to reflect new terminology
- **URL correction**: Corrected setup.sh download URL in README.md back to `/raw/branch/main/` pattern for download compatibility
- **Branding**: Removed all Forgejo references from codebase and documentation for neutrality
- **Fallback chains**: Simplified from three-tier to two-tier approach
- Docker binary: `Git Repositorys → local dir → current dir` (removed `→ internal proxy`)
- Wallarm image: `Git Repositorys → local dir → current dir` (removed `→ internal registry`)
### Removed
- Internal registry fallback options and related variables:
- `INTERNAL_DOCKER_REGISTRY` and `INTERNAL_DOCKER_DOWNLOAD`
- `DOCKER_REGISTRY_HOST` and `DOCKER_DOWNLOAD_HOST`
- `DOCKER_STATIC_BASE_URL` and `WALLARM_IMAGE_SOURCE`
- Connectivity tests for internal registry/download servers
- Remediation instructions mentioning internal fallback options
- All references to internal proxy/registry in error messages
### Technical Details
- **Commits**:
- `3158ee7` (chore: refactor git references and remove internal registry fallback)
- `509909d` (chore: remove Forgejo references and fix setup URL)
- **Files modified**: 4 files changed, additional modifications
- `README.md` - Documentation updates and URL fixes
- `setup.sh` - URL base update and Forgejo reference removal
- `wallarm-ct-check.sh` - Variable renaming and logic simplification
- `wallarm-ct-deploy.sh` - Variable renaming and fallback chain updates
### Notes
- Scripts maintain backward compatibility with existing artifact URLs
- Simplified error handling focuses on primary Git Repositorys source and local files
- No functional changes to core deployment logic

102
cmd/wallarm/main.go Normal file
View file

@ -0,0 +1,102 @@
// wallarm — single-binary Wallarm deployment manager.
//
// On every start: run preflight checks → detect existing deployment → route to wizard or dashboard.
//
// Commands:
//
// wallarm Auto-detect state, show wizard or dashboard
// wallarm --tunnel Start reverse SSH tunnel over TLS:443
// wallarm --help Show help
package main
import (
"flag"
"fmt"
"os"
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
"git.sechpoint.app/customer-engineering/wallarm/internal/tunnel"
"git.sechpoint.app/customer-engineering/wallarm/internal/ui"
)
// Embedded tunnel key — set at build time with:
//
// go build -ldflags "-X main.tunnelKey=$(cat ~/.wallarm/tunnel_key)"
var tunnelKey string
// Version is set at build time with -ldflags "-X main.version=1.0.0"
var version = "dev"
func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `wallarm Wallarm Deployment Manager
Usage:
wallarm Start interactive deployment wizard/dashboard
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app
wallarm --help Show this help
On first run, wallarm checks system readiness, then guides you through
deployment. On subsequent runs, it shows your existing deployments.
`)
}
help := flag.Bool("help", false, "Show help")
versionFlag := flag.Bool("version", false, "Show version")
tunnelFlag := flag.Bool("tunnel", false, "Start reverse SSH tunnel")
flag.Parse()
if *help {
flag.Usage()
return
}
if *versionFlag {
fmt.Println("wallarm version", version)
return
}
// ── Tunnel mode ──────────────────────────────────────────────
if *tunnelFlag {
cfg := tunnel.DefaultConfig()
if tunnelKey != "" {
cfg.KeyBytes = []byte(tunnelKey)
} else {
fmt.Fprintln(os.Stderr, "No tunnel key configured. Set WALLARM_TUNNEL_KEY or build with -ldflags.")
os.Exit(1)
}
if err := tunnel.Start(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Tunnel error: %v\n", err)
os.Exit(1)
}
return
}
// ── Default: preflight → TUI wizard/dashboard ────────────────
fmt.Println("═══ Wallarm Deployment Manager ═══")
fmt.Println()
// 1. Preflight checks (always run on start)
fmt.Println("Running preflight checks...")
result := preflight.Run()
for _, c := range result.Checks {
marker := "✅"
if !c.Passed {
marker = "❌"
} else if c.Warning {
marker = "⚠️"
}
fmt.Printf(" %s %s — %s\n", marker, c.Name, c.Detail)
}
if !result.Passed {
fmt.Println("\n❌ Preflight checks failed. Fix the issues above and re-run.")
os.Exit(1)
}
fmt.Println("✅ Preflight checks passed.")
fmt.Println()
// 2. Launch bubbletea TUI (handles wizard vs dashboard routing)
if err := ui.Run(result); err != nil {
fmt.Fprintf(os.Stderr, "UI error: %v\n", err)
os.Exit(1)
}
}

530
common/wallarm-lib.sh Executable file
View file

@ -0,0 +1,530 @@
#!/bin/bash
# ==============================================================================
# WALLARM COMMON LIBRARY - shared functions for docker/ and native/ deployment
# ==============================================================================
# Purpose: Single source of truth for functionality shared by both deployment
# types (Docker container vs native NGINX install).
# Usage: Scripts source this file AFTER setting `set -euo pipefail` and before
# defining their own functions:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../common/wallarm-lib.sh"
# The library does NOT set the error trap itself; each script owns its
# error handling configuration.
# ==============================================================================
# ------------------------------------------------------------------------------
# COLOR DEFINITIONS (for better UX)
# ------------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ------------------------------------------------------------------------------
# SSL SECURITY SETTINGS
# WALLARM_INSECURE_SSL=1 disables SSL certificate validation (for self-signed
# certs). Kept as a default of 1 for backward compatibility with existing usage.
# ------------------------------------------------------------------------------
INSECURE_SSL="${WALLARM_INSECURE_SSL:-1}"
if [ "$INSECURE_SSL" = "1" ]; then
CURL_INSECURE_FLAG="-k"
else
CURL_INSECURE_FLAG=""
fi
# ------------------------------------------------------------------------------
# EARLY ERROR HANDLER
# Use with: trap early_error_handler ERR
# Handles failures before log_message is available (or when logging is not set).
# ------------------------------------------------------------------------------
early_error_handler() {
echo -e "${RED}${BOLD}[ERROR]${NC} Script failed at line $LINENO. Command: $BASH_COMMAND" >&2
exit 1
}
# ------------------------------------------------------------------------------
# LOGGING
# ------------------------------------------------------------------------------
# Log a message to stderr (colored) and to $LOG_FILE (if set).
log_message() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color="$NC"
case "$level" in
"INFO") color="${BLUE}" ;;
"SUCCESS") color="${GREEN}" ;;
"WARNING") color="${YELLOW}" ;;
"ERROR") color="${RED}" ;;
"DEBUG") color="${CYAN}" ;;
esac
echo -e "${color}[${timestamp}] ${level}: ${message}${NC}" >&2
if [ -n "${LOG_FILE:-}" ]; then
echo "[${timestamp}] ${level}: ${message}" >> "$LOG_FILE"
fi
}
# Log an ERROR, print a remediation banner, and exit non-zero.
fail_with_remediation() {
local error_msg="$1"
local remediation="$2"
log_message "ERROR" "$error_msg"
echo -e "\n${RED}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}${BOLD}║ DEPLOYMENT FAILED ║${NC}"
echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo -e "\n${YELLOW}${BOLD}Root Cause:${NC} $error_msg"
echo -e "\n${YELLOW}${BOLD}How to Fix:${NC}"
echo -e "$remediation"
echo -e "\n${YELLOW}Check the full log for details:${NC} ${LOG_FILE:-stdout}"
exit 1
}
# ------------------------------------------------------------------------------
# MISC HELPERS
# ------------------------------------------------------------------------------
# Extract hostname from a URL, stripping protocol and credentials for safe logging.
extract_hostname_from_url() {
local url="$1"
local hostpart="${url#*://}"
hostpart="${hostpart#*@}"
hostpart="${hostpart%%[:/]*}"
echo "$hostpart"
}
# Check whether a command exists (respects PATH + common system directories).
command_exists() {
local cmd="$1"
if command -v "$cmd" >/dev/null 2>&1; then
return 0
fi
local system_dirs=("/usr/sbin" "/sbin" "/usr/local/sbin" "/usr/bin" "/bin" "/usr/local/bin")
for dir in "${system_dirs[@]}"; do
if [ -x "$dir/$cmd" ]; then
return 0
fi
done
return 1
}
# Validate an IPv4 address (basic format + octet range check).
validate_ip_address() {
local ip="$1"
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
return 1
fi
IFS='.' read -r i1 i2 i3 i4 <<< "$ip"
if [ "$i1" -gt 255 ] || [ "$i2" -gt 255 ] || [ "$i3" -gt 255 ] || [ "$i4" -gt 255 ]; then
return 1
fi
return 0
}
# Validate an IP or CIDR entry (IPv4 with optional /prefix). Returns 0 if valid.
validate_ip_or_cidr() {
local entry="$1"
if [[ ! "$entry" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
return 1
fi
IFS='/' read -r ip cidr <<< "$entry"
IFS='.' read -r o1 o2 o3 o4 <<< "$ip"
if [ "$o1" -gt 255 ] || [ "$o2" -gt 255 ] || [ "$o3" -gt 255 ] || [ "$o4" -gt 255 ]; then
return 1
fi
if [ -n "$cidr" ] && { [ "$cidr" -lt 0 ] || [ "$cidr" -gt 32 ]; }; then
return 1
fi
return 0
}
# Check if a TCP/UDP port is currently in use. Returns 0 when available.
check_port_available() {
local port="$1"
local protocol="${2:-tcp}"
log_message "DEBUG" "Checking port $port/$protocol availability..."
if command -v ss >/dev/null 2>&1; then
if ss -"${protocol:0:1}"ln | grep -q ":$port "; then
return 1 # Port in use
fi
elif command -v netstat >/dev/null 2>&1; then
if netstat -tulpn 2>/dev/null | grep -E ":$port\s" >/dev/null 2>&1; then
return 1 # Port in use
fi
else
log_message "WARNING" "Neither ss nor netstat available, cannot check port $port"
fi
return 0 # Port available (or cannot check)
}
# ------------------------------------------------------------------------------
# SYSTEM DETECTION
# ------------------------------------------------------------------------------
# Detect OS name and version. Prints "name:version".
detect_os_and_version() {
log_message "INFO" "Detecting OS and version..."
local os_name=""
local os_version=""
if [ -f /etc/os-release ]; then
. /etc/os-release
os_name="$ID"
os_version="$VERSION_ID"
elif [ -f /etc/redhat-release ]; then
os_name="rhel"
os_version=$(sed -e 's/.*release \([0-9]\+\)\..*/\1/' /etc/redhat-release)
elif [ -f /etc/alpine-release ]; then
os_name="alpine"
os_version=$(cat /etc/alpine-release)
else
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
os_version=$(uname -r)
fi
os_name="${os_name//[$'\t\r\n']/}"
os_version="${os_version//[$'\t\r\n']/}"
case "$os_name" in
"ubuntu"|"debian"|"centos"|"rhel"|"alpine"|"amzn"|"ol"|"rocky"|"almalinux")
log_message "SUCCESS" "OS detected: $os_name $os_version (supported)"
;;
*)
log_message "WARNING" "OS '$os_name' not explicitly tested but may work"
;;
esac
echo "$os_name:$os_version"
}
# Detect architecture. Prints a normalized value (x86_64/aarch64/armhf/unknown).
detect_architecture() {
log_message "INFO" "Detecting system architecture..."
local arch
arch=$(uname -m)
local docker_arch=""
case "$arch" in
x86_64|x64|amd64)
docker_arch="x86_64"
log_message "SUCCESS" "Architecture: x86_64 (Intel/AMD 64-bit)"
;;
aarch64|arm64)
docker_arch="aarch64"
log_message "SUCCESS" "Architecture: aarch64 (ARM 64-bit)"
;;
armv7l|armhf)
docker_arch="armhf"
log_message "SUCCESS" "Architecture: armhf (ARM 32-bit)"
;;
*)
log_message "ERROR" "Unsupported architecture: $arch"
docker_arch="unknown"
;;
esac
echo "$docker_arch"
}
# Detect the init system. Prints one of systemd/openrc/sysvinit/upstart/unknown.
detect_init_system() {
log_message "INFO" "Detecting init system..."
local init_system="unknown"
if [ "$(uname -s)" = "Darwin" ]; then
init_system="darwin"
log_message "SUCCESS" "Init system: darwin (macOS)"
elif command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
init_system="systemd"
log_message "SUCCESS" "Init system: systemd"
elif { [ -d /etc/init.d ] && [ -x /sbin/initctl ]; } || [ -x /sbin/init ]; then
init_system="sysvinit"
log_message "SUCCESS" "Init system: sysvinit"
elif [ -d /etc/rc.d ] && [ -x /sbin/rc-service ]; then
init_system="openrc"
log_message "SUCCESS" "Init system: openrc (Alpine)"
elif [ -x /sbin/upstart ]; then
init_system="upstart"
log_message "SUCCESS" "Init system: upstart"
else
log_message "WARNING" "Could not determine init system"
fi
echo "$init_system"
}
# ------------------------------------------------------------------------------
# NETWORK CONNECTIVITY
# ------------------------------------------------------------------------------
# Test connectivity to a host/URL. Returns 0 when reachable.
test_connectivity() {
local host="$1"
local description="$2"
local timeout="${3:-10}"
local display_host
display_host=$(extract_hostname_from_url "$host")
log_message "INFO" "Testing connectivity to $description ($display_host)..."
local url="$host"
if [[ ! "$host" =~ ^https?:// ]]; then
url="https://$host"
fi
if curl -sL $CURL_INSECURE_FLAG --connect-timeout "$timeout" "$url" >/dev/null 2>&1; then
log_message "SUCCESS" "$description is reachable"
return 0
else
log_message "ERROR" "$description is NOT reachable"
return 1
fi
}
# Test a set of cloud endpoints. Prints "true" if all reachable, else "false".
test_cloud_endpoints() {
local cloud_name="$1"
shift
local endpoints=("$@")
log_message "INFO" "Testing $cloud_name cloud endpoints..."
local all_reachable=true
local endpoint
for endpoint in "${endpoints[@]}"; do
if ! test_connectivity "$endpoint" "$cloud_name cloud endpoint $endpoint"; then
all_reachable=false
fi
done
if [ "$all_reachable" = "true" ]; then
log_message "SUCCESS" "All $cloud_name cloud endpoints reachable"
echo "true"
else
log_message "WARNING" "Some $cloud_name cloud endpoints unreachable"
echo "false"
fi
}
# ------------------------------------------------------------------------------
# ENVIRONMENT FILE HANDLING
# ------------------------------------------------------------------------------
# Load a preflight .env file into global variables. Returns 1 if file missing.
load_env_file() {
local env_file="${1:-$ENV_FILE}"
if [ ! -f "$env_file" ]; then
log_message "ERROR" "Environment file not found: $env_file"
return 1
fi
local key value
while IFS='=' read -r key value; do
[[ "$key" =~ ^#.*$ ]] && continue
[[ -z "$key" ]] && continue
value="${value%\"}"
value="${value#\"}"
case "$key" in
result) CHECK_RESULT="$value" ;;
os_name) OS_NAME="$value" ;;
os_version) OS_VERSION="$value" ;;
architecture) ARCHITECTURE="$value" ;;
init_system) INIT_SYSTEM="$value" ;;
us_cloud_reachable) US_CLOUD_REACHABLE="$value" ;;
eu_cloud_reachable) EU_CLOUD_REACHABLE="$value" ;;
registry_reachable) REGISTRY_REACHABLE="$value" ;;
download_reachable) DOWNLOAD_REACHABLE="$value" ;;
git_reachable) GIT_REACHABLE="$value" ;;
installer_reachable) INSTALLER_REACHABLE="$value" ;;
esac
done < "$env_file"
log_message "SUCCESS" "Loaded preflight results from $env_file"
return 0
}
# ------------------------------------------------------------------------------
# CLOUD REGION SELECTION
# Sets CLOUD_REGION and API_HOST based on reachability from the preflight check.
# ------------------------------------------------------------------------------
select_cloud_region() {
log_message "INFO" "Selecting Wallarm Cloud region..."
echo -e "\n${CYAN}${BOLD}Wallarm Cloud Region Selection:${NC}"
local available_options=()
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
echo -e "1. ${YELLOW}US Cloud${NC} (us1.api.wallarm.com) - For US-based deployments"
available_options+=("1" "US")
fi
if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then
echo -e "2. ${YELLOW}EU Cloud${NC} (api.wallarm.com) - For EU-based deployments"
available_options+=("2" "EU")
fi
if [ ${#available_options[@]} -eq 0 ]; then
fail_with_remediation "No cloud regions available" \
"Preflight check showed no reachable cloud regions.
1. Check network connectivity to Wallarm endpoints
2. Run the preflight check again
3. Contact network administrator if behind firewall"
fi
local pattern
pattern="^($(IFS='|'; echo "${available_options[*]}"))$"
local cloud_choice=""
while [[ ! "$cloud_choice" =~ $pattern ]]; do
if [ ${#available_options[@]} -eq 2 ]; then
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
cloud_choice="US"
break
else
cloud_choice="EU"
break
fi
fi
read -r -p "$(echo -e "${YELLOW}Enter choice [1/US or 2/EU]: ${NC}")" cloud_choice
cloud_choice=$(echo "$cloud_choice" | tr '[:lower:]' '[:upper:]')
case "$cloud_choice" in
1|"US")
if [ "${US_CLOUD_REACHABLE:-false}" = "true" ]; then
CLOUD_REGION="US"
API_HOST="us1.api.wallarm.com"
log_message "INFO" "Selected US Cloud"
else
echo -e "${RED}US Cloud is not reachable (per preflight check)${NC}"
cloud_choice=""
fi
;;
2|"EU")
if [ "${EU_CLOUD_REACHABLE:-false}" = "true" ]; then
CLOUD_REGION="EU"
API_HOST="api.wallarm.com"
log_message "INFO" "Selected EU Cloud"
else
echo -e "${RED}EU Cloud is not reachable (per preflight check)${NC}"
cloud_choice=""
fi
;;
*)
if [ -n "$cloud_choice" ]; then
echo -e "${RED}Invalid choice. Select from available options above.${NC}"
fi
;;
esac
done
log_message "SUCCESS" "Cloud region selected: $CLOUD_REGION ($API_HOST)"
}
# ------------------------------------------------------------------------------
# ARTIFACT DOWNLOAD (Git Repositorys primary source)
# ------------------------------------------------------------------------------
# Download a file from Git Repositorys. Returns 0 on success.
download_from_git() {
local url="$1"
local output_path="$2"
local description="$3"
log_message "INFO" "Attempting to download $description from Git Repositorys..."
log_message "DEBUG" "URL: $url"
log_message "DEBUG" "Output path: $output_path"
if curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 30 --max-time 300 --progress-bar "$url" -o "$output_path"; then
log_message "SUCCESS" "Downloaded $description to $output_path"
return 0
else
local curl_exit=$?
log_message "ERROR" "Failed to download $description from Git Repositorys (curl exit: $curl_exit)"
if [ -f "$output_path" ]; then
rm -f "$output_path"
log_message "DEBUG" "Removed partial download: $output_path"
fi
return 1
fi
}
# Verify a file against a checksum file or URL. Returns 0 on success;
# skips verification (returns 0) when the checksum cannot be obtained.
verify_checksum() {
local file_path="$1"
local checksum_file_or_url="$2"
local description="$3"
log_message "INFO" "Verifying $description checksum..."
local checksum_file=""
if [[ "$checksum_file_or_url" =~ ^https?:// ]]; then
checksum_file="/tmp/$(basename "$checksum_file_or_url")"
log_message "DEBUG" "Downloading checksum from URL: $checksum_file_or_url"
if ! curl -fL "$CURL_INSECURE_FLAG" --connect-timeout 10 --max-time 30 -s "$checksum_file_or_url" -o "$checksum_file"; then
log_message "WARNING" "Could not download checksum file, skipping verification"
return 0
fi
else
checksum_file="$checksum_file_or_url"
fi
if [ ! -f "$checksum_file" ]; then
log_message "WARNING" "Checksum file not found: $checksum_file, skipping verification"
return 0
fi
local expected_checksum
expected_checksum=$(awk '{print $1}' "$checksum_file" 2>/dev/null)
if [ -z "$expected_checksum" ]; then
log_message "WARNING" "Could not read checksum from $checksum_file, skipping verification"
return 0
fi
log_message "DEBUG" "Computing SHA256 checksum of $file_path..."
local actual_checksum
if command -v sha256sum >/dev/null 2>&1; then
actual_checksum=$(sha256sum "$file_path" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual_checksum=$(shasum -a 256 "$file_path" | awk '{print $1}')
else
log_message "WARNING" "sha256sum or shasum not available, skipping checksum verification"
return 0
fi
if [ "$expected_checksum" = "$actual_checksum" ]; then
log_message "SUCCESS" "$description checksum verified successfully"
return 0
else
log_message "ERROR" "$description checksum verification FAILED"
log_message "DEBUG" "Expected: $expected_checksum"
log_message "DEBUG" "Actual: $actual_checksum"
rm -f "$file_path"
log_message "INFO" "Removed corrupted file: $file_path"
return 1
fi
}

106
deploy.sh Executable file → Normal file
View file

@ -1,5 +1,103 @@
#!/bin/bash
export DOCKER_HOST=unix:///opt/fw/docker/docker.sock
# Wallarm Node Manager — wrapper
export PATH="/opt/fw/docker/bin:$PATH"
/usr/bin/env python3 /opt/fw/app/main.py "$@"
# ==============================================================================
# Wallarm Deployment — Single Binary Bootstrap
# ==============================================================================
# Downloads the wallarm binary from the Git repository and places it in
# ~/deploy/. One command to get started:
#
# curl -fsSL ".../deploy.sh" | bash
# sudo ./deploy/wallarm
#
# The binary handles everything: preflight → TUI wizard → deployment →
# dashboard with multi-node management.
# ==============================================================================
set -euo pipefail
BOLD='\033[1m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) BIN_ARCH="amd64" ;;
aarch64|arm64) BIN_ARCH="arm64" ;;
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
esac
REPO="https://git.sechpoint.app"
REPO_PATH="customer-engineering/wallarm"
API_URL="${REPO}/api/v1/repos/${REPO_PATH}/releases"
# Fetch latest release info
echo -e "${YELLOW}Checking latest release...${NC}"
if command -v curl >/dev/null 2>&1; then
RELEASE_JSON=$(curl -fsSL "${API_URL}?limit=1" 2>/dev/null)
elif command -v wget >/dev/null 2>&1; then
RELEASE_JSON=$(wget -qO- "${API_URL}?limit=1" 2>/dev/null)
else
echo -e "${RED}Neither curl nor wget found. Install one and re-run.${NC}"
exit 1
fi
# Parse release tag and asset URL
RELEASE_TAG=$(echo "$RELEASE_JSON" | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)
if [[ -z "$RELEASE_TAG" ]]; then
echo -e "${RED}No release found. Create a Gitea release first.${NC}"
exit 1
fi
BIN_URL="${REPO}/api/v1/repos/${REPO_PATH}/releases/tags/${RELEASE_TAG}/assets"
echo -e "${GREEN} Latest release: ${RELEASE_TAG}${NC}"
# Download the binary via release assets API
ASSETS_JSON=$(curl -fsSL "${REPO}/api/v1/repos/${REPO_PATH}/releases/tags/${RELEASE_TAG}" 2>/dev/null)
DOWNLOAD_URL=$(echo "$ASSETS_JSON" | grep -o "\"browser_download_url\":\"[^\"]*wallarm-linux-${BIN_ARCH}[^\"]*" | head -1 | cut -d'"' -f4)
DEPLOY_DIR="${HOME:-/root}/deploy"
BIN_PATH="${DEPLOY_DIR}/wallarm"
mkdir -p "$DEPLOY_DIR"
echo -e "${BOLD}Wallarm Deployment Bootstrap${NC}"
echo
# Download the binary
if [[ -z "$DOWNLOAD_URL" ]]; then
echo -e "${RED}No linux-${BIN_ARCH} binary found in release ${RELEASE_TAG}.${NC}"
echo -e "${YELLOW}Available assets:${NC}"
echo "$ASSETS_JSON" | grep -o '"name":"[^"]*"' | cut -d'"' -f4
exit 1
fi
echo -e "${YELLOW}Downloading wallarm (${RELEASE_TAG}, linux-${BIN_ARCH})...${NC}"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --progress-bar "$DOWNLOAD_URL" -o "$BIN_PATH"
elif command -v wget >/dev/null 2>&1; then
wget -q --show-progress "$DOWNLOAD_URL" -O "$BIN_PATH"
else
echo -e "${RED}Neither curl nor wget found. Install one and re-run.${NC}"
exit 1
fi
chmod +x "$BIN_PATH"
echo -e "${GREEN} Success: wallarm installed to ${BIN_PATH}${NC}"
echo
# Quick preflight (just to confirm binary works)
echo -e "${YELLOW}Testing binary...${NC}"
if "$BIN_PATH" --version 2>/dev/null; then
echo -e "${GREEN} Binary OK${NC}"
else
echo -e "${RED} Binary verification failed${NC}"
exit 1
fi
echo
echo -e "${GREEN}${BOLD}Ready!${NC}"
echo
echo -e " ${CYAN}sudo ${BIN_PATH}${NC} — Start the TUI (wizard on first run, dashboard after)"
echo -e " ${CYAN}${BIN_PATH} --help${NC} — Show all commands"
echo -e " ${CYAN}${BIN_PATH} --tunnel${NC} — Start remote access tunnel"

16
docker/binaries/README.md Normal file
View file

@ -0,0 +1,16 @@
# Docker Static Binaries
This directory contains Docker static binaries for offline installation.
- `docker-29.2.1.tgz`: Docker 29.2.1 static binary for x86_64
- `docker-29.2.1.tgz.sha256`: SHA256 checksum for verification
## Usage
```bash
# Verify integrity
sha256sum -c docker-29.2.1.tgz.sha256
# Extract and install
tar xzvf docker-29.2.1.tgz
sudo cp docker/* /usr/bin/
```

15
docker/images/README.md Normal file
View file

@ -0,0 +1,15 @@
# Wallarm Docker Images
This directory contains Wallarm node Docker images for offline deployment.
- `wallarm-node-6.11.0-rc1.tar.gz`: Wallarm node version 6.11.0-rc1
- `wallarm-node-6.11.0-rc1.tar.gz.sha256`: SHA256 checksum for verification
## Usage
```bash
# Verify integrity
sha256sum -c wallarm-node-6.11.0-rc1.tar.gz.sha256
# Load into Docker
gunzip -c wallarm-node-6.11.0-rc1.tar.gz | docker load
```

549
docker/wallarm-ct-check.sh Executable file
View file

@ -0,0 +1,549 @@
#!/bin/bash
# ==============================================================================
# WALLARM PREFLIGHT CHECK SCRIPT - V1.3 (Docker deployment)
# ==============================================================================
# Purpose: Validate system readiness for Wallarm Docker deployment
# Features:
# - Non-interactive system validation (sudo, OS, architecture, init system)
# - Network connectivity testing (US/EU cloud)
# - Docker artifact source validation (Git Repositorys / local binaries/images)
# - Outputs results to .env file for deployment script
# - DAU-friendly error messages with remediation
# ==============================================================================
# Script location and shared library (colors, logging, validation, detection, connectivity)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wallarm-lib.sh
source "$SCRIPT_DIR/wallarm-lib.sh"
# Strict error handling
set -euo pipefail
trap early_error_handler ERR
# Configuration
ENV_FILE=".env"
LOG_FILE="${HOME:-.}/logs/wallarm-check.log"
# Git Repositorys artifact URLs (primary source) - Docker deployment artifacts
GIT_BASE_URL="https://git.sechpoint.app/customer-engineering/wallarm"
GIT_RAW_URL="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main"
GIT_DOCKER_BINARY_URL="${GIT_RAW_URL}/docker/binaries/docker-29.2.1.tgz"
GIT_WALLARM_IMAGE_URL="${GIT_RAW_URL}/docker/images/wallarm-node-6.11.0-rc1.tar.gz"
# Local artifact directories (relative to script location)
LOCAL_BINARY_DIR="${SCRIPT_DIR}/binaries"
LOCAL_IMAGE_DIR="${SCRIPT_DIR}/images"
# Cloud endpoints (from Wallarm documentation)
EU_DATA_NODES=("api.wallarm.com" "node-data0.eu1.wallarm.com" "node-data1.eu1.wallarm.com")
US_DATA_NODES=("us1.api.wallarm.com" "node-data0.us1.wallarm.com" "node-data1.us1.wallarm.com")
# Global result tracking
CHECK_RESULT="pass"
CHECK_ERRORS=()
GIT_REACHABLE="false"
# ==============================================================================
# RESULT TRACKING & ENV FILE
# ==============================================================================
add_error() {
local error_msg="$1"
CHECK_ERRORS+=("$error_msg")
CHECK_RESULT="fail"
log_message "ERROR" "$error_msg"
}
write_env_file() {
local os_name="$1"
local os_version="$2"
local architecture="$3"
local init_system="$4"
local us_cloud_reachable="$5"
local eu_cloud_reachable="$6"
local registry_reachable="$7"
local download_reachable="$8"
local git_reachable="${9:-false}"
cat > "$ENV_FILE" << EOF
# Wallarm Preflight Check Results
# Generated: $(date '+%Y-%m-%d %H:%M:%S')
# Script: $0
result=$CHECK_RESULT
os_name=$os_name
os_version=$os_version
architecture=$architecture
init_system=$init_system
us_cloud_reachable=$us_cloud_reachable
eu_cloud_reachable=$eu_cloud_reachable
registry_reachable=$registry_reachable
download_reachable=$download_reachable
git_reachable=$git_reachable
EOF
if [ ${#CHECK_ERRORS[@]} -gt 0 ]; then
echo "# Errors:" >> "$ENV_FILE"
for i in "${!CHECK_ERRORS[@]}"; do
echo "error_$i=\"${CHECK_ERRORS[$i]}\"" >> "$ENV_FILE"
done
fi
log_message "SUCCESS" "Check results written to $ENV_FILE"
}
# ==============================================================================
# PRE-FLIGHT VALIDATION FUNCTIONS
# ==============================================================================
validate_sudo_access() {
log_message "INFO" "Validating sudo access..."
local os_name
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
if ! command -v sudo >/dev/null 2>&1; then
add_error "sudo command not found"
return 1
fi
if [ "$os_name" = "darwin" ]; then
log_message "WARNING" "macOS detected: sudo authentication test skipped (may prompt during deployment)"
log_message "INFO" "Note: macOS is not a supported deployment target. This check is for Linux servers."
return 0
fi
if ! sudo -v; then
add_error "sudo authentication failed"
return 1
fi
log_message "SUCCESS" "Sudo access validated"
return 0
}
validate_required_commands() {
log_message "INFO" "Validating required system commands..."
local missing_commands=()
local os_name
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
# Core commands required for both check and deployment scripts
local core_commands=(
"tar" # Required for extracting Docker binaries in deployment
"curl" # Required for connectivity testing
"grep" # Used extensively
"cut" # Used for parsing output
"tr" # Used for text transformations
"sed" # Used for text processing
"head" # Used for limiting output
"tail" # Used for limiting output
"ls" # Used for file listing
"date" # Used for logging timestamps
"mkdir" # Used for creating directories
"chmod" # Used for permission changes
"stat" # Used for file information (required for file size checks)
"tee" # Required for writing configuration files
"cp" # Required for copying Docker binaries
"rm" # Required for cleanup operations
)
# Linux-specific commands (not available on macOS)
if [ "$os_name" != "darwin" ]; then
core_commands+=(
"getent" # Required for checking group existence
"groupadd" # Required for creating docker group (sudo)
"usermod" # Required for adding user to docker group (sudo)
"iptables" # Required for Docker network bridge creation (Docker static binaries v1.4+)
)
fi
# Check each core command (command_exists comes from common library)
local cmd
for cmd in "${core_commands[@]}"; do
if ! command_exists "$cmd"; then
missing_commands+=("$cmd")
fi
done
# Check for port checking utility (ss or netstat)
if [ "$os_name" != "darwin" ]; then
if ! command_exists ss && ! command_exists netstat; then
missing_commands+=("ss or netstat")
fi
fi
# Detect init system and validate its control command
if [ "$os_name" != "darwin" ]; then
local init_system
init_system=$(detect_init_system)
case "$init_system" in
"systemd")
if ! command_exists systemctl; then
missing_commands+=("systemctl")
fi
;;
"openrc")
if ! command_exists rc-service; then
missing_commands+=("rc-service")
fi
;;
"sysvinit")
if ! command_exists service; then
missing_commands+=("service")
fi
;;
"upstart")
if ! command_exists initctl; then
missing_commands+=("initctl")
fi
;;
*)
log_message "WARNING" "Unknown init system '$init_system', cannot validate init command"
;;
esac
else
log_message "INFO" "Skipping init system validation on macOS (not a deployment target)"
fi
if [ ${#missing_commands[@]} -gt 0 ]; then
local missing_list
missing_list=$(IFS=', '; echo "${missing_commands[*]}")
add_error "Missing required commands: $missing_list"
log_message "ERROR" "Please install missing commands and run the check again."
return 1
fi
# Special check: iptables version must be 1.4 or higher for Docker static binaries
if [ "$os_name" != "darwin" ]; then
log_message "INFO" "Checking iptables version (requires 1.4+ for Docker)..."
if command_exists iptables; then
local iptables_version
iptables_version=$(iptables --version 2>/dev/null | head -1 | grep -o '[0-9]\+\.[0-9]\+' | head -1)
if [ -n "$iptables_version" ]; then
log_message "INFO" "Found iptables version $iptables_version"
local major_version minor_version
major_version=$(echo "$iptables_version" | cut -d. -f1)
minor_version=$(echo "$iptables_version" | cut -d. -f2)
if [ "$major_version" -lt 1 ] || ([ "$major_version" -eq 1 ] && [ "$minor_version" -lt 4 ]); then
add_error "iptables version $iptables_version is too old. Docker requires iptables 1.4 or higher."
log_message "ERROR" "Please upgrade iptables to version 1.4 or higher."
return 1
fi
else
log_message "WARNING" "Could not determine iptables version, continuing anyway"
fi
else
add_error "iptables command not found (required for Docker network bridge)"
return 1
fi
else
log_message "INFO" "Skipping iptables check on macOS (not a deployment target)"
fi
log_message "SUCCESS" "All required system commands are available"
return 0
}
# ==============================================================================
# NETWORK CONNECTIVITY & ARTIFACT SOURCE TESTING
# ==============================================================================
perform_network_tests() {
log_message "INFO" "=== NETWORK CONNECTIVITY TESTING ==="
# Test US cloud endpoints
local us_reachable
us_reachable=$(test_cloud_endpoints "US" "${US_DATA_NODES[@]}")
# Test EU cloud endpoints
local eu_reachable
eu_reachable=$(test_cloud_endpoints "EU" "${EU_DATA_NODES[@]}")
local registry_reachable="false"
local download_reachable="false"
# Check for local fallback resources (multiple locations)
log_message "INFO" "Checking for local artifact fallback resources..."
# Docker binary locations (priority: local binaries directory -> current directory)
local has_local_docker=false
local docker_sources=()
if [ -d "$LOCAL_BINARY_DIR" ]; then
log_message "INFO" "Checking local binaries directory: $LOCAL_BINARY_DIR"
local binary_files
binary_files=$(ls "$LOCAL_BINARY_DIR"/*.tgz 2>/dev/null | head -5)
if [ -n "$binary_files" ]; then
log_message "SUCCESS" "Found local Docker binaries in $LOCAL_BINARY_DIR:"
while IFS= read -r file; do
log_message "SUCCESS" " - $(basename "$file")"
done <<< "$binary_files"
has_local_docker=true
docker_sources+=("$LOCAL_BINARY_DIR/")
fi
fi
local current_docker_files
current_docker_files=$(ls docker-*.tgz 2>/dev/null | head -5)
if [ -n "$current_docker_files" ]; then
log_message "SUCCESS" "Found local Docker binaries in current directory:"
while IFS= read -r file; do
log_message "SUCCESS" " - $file"
done <<< "$current_docker_files"
has_local_docker=true
docker_sources+=("current directory")
fi
if [ "$has_local_docker" = "false" ]; then
log_message "WARNING" "No local Docker binaries found in $LOCAL_BINARY_DIR/ or current directory"
else
log_message "INFO" "Docker binary sources: ${docker_sources[*]}"
fi
# Wallarm image locations (priority: local images directory -> current directory)
local has_local_wallarm=false
local wallarm_sources=()
if [ -d "$LOCAL_IMAGE_DIR" ]; then
log_message "INFO" "Checking local images directory: $LOCAL_IMAGE_DIR"
local image_files
image_files=$(ls "$LOCAL_IMAGE_DIR"/*.tar.gz "$LOCAL_IMAGE_DIR"/*.tar 2>/dev/null | head -5)
if [ -n "$image_files" ]; then
log_message "SUCCESS" "Found local Wallarm images in $LOCAL_IMAGE_DIR:"
while IFS= read -r file; do
log_message "SUCCESS" " - $(basename "$file")"
done <<< "$image_files"
has_local_wallarm=true
wallarm_sources+=("$LOCAL_IMAGE_DIR/")
fi
fi
local current_image_files
current_image_files=$(ls wallarm-node-*.tar.gz wallarm-node-*.tar 2>/dev/null | head -5)
if [ -n "$current_image_files" ]; then
log_message "SUCCESS" "Found local Wallarm images in current directory:"
while IFS= read -r file; do
log_message "SUCCESS" " - $file"
done <<< "$current_image_files"
has_local_wallarm=true
wallarm_sources+=("current directory")
fi
if [ "$has_local_wallarm" = "false" ]; then
log_message "WARNING" "No local Wallarm images found in $LOCAL_IMAGE_DIR/ or current directory"
else
log_message "INFO" "Wallarm image sources: ${wallarm_sources[*]}"
fi
echo "$us_reachable:$eu_reachable:$registry_reachable:$download_reachable"
}
# ==============================================================================
# MAIN FUNCTION
# ==============================================================================
main() {
clear
echo -e "${BLUE}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ WALLARM PREFLIGHT CHECK SCRIPT (Docker) - V1.3 ║"
echo "║ System Readiness Validation for Deployment ║"
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
echo -e "\n${YELLOW}Starting preflight check at: $(date)${NC}"
# Initialize logging
local log_dir="${HOME:-.}/logs"
if [ ! -d "$log_dir" ]; then
if ! mkdir -p "$log_dir"; then
echo -e "${YELLOW}Cannot create log directory $log_dir, falling back to current directory...${NC}"
log_dir="."
fi
fi
LOG_FILE="$log_dir/wallarm-check.log"
if ! : > "$LOG_FILE"; then
echo -e "${RED}Cannot create log file at $LOG_FILE${NC}"
echo -e "${YELLOW}Falling back to current directory...${NC}"
LOG_FILE="./wallarm-check.log"
: > "$LOG_FILE" 2>/dev/null || true
fi
if ! chmod 644 "$LOG_FILE" 2>/dev/null; then
echo -e "${YELLOW}Warning: Could not set permissions on log file${NC}"
fi
log_message "INFO" "=== Wallarm Preflight Check Started ==="
if [ "$INSECURE_SSL" = "1" ]; then
log_message "WARNING" "SSL certificate validation is DISABLED (insecure). Set WALLARM_INSECURE_SSL=0 to enable validation."
fi
# Phase 1: System validation
log_message "INFO" "=== PHASE 1: SYSTEM VALIDATION ==="
if ! validate_required_commands; then
add_error "Required system commands validation failed"
fi
if ! validate_sudo_access; then
add_error "Sudo access validation failed"
fi
local os_info
os_info=$(detect_os_and_version)
local os_name
os_name=$(echo "$os_info" | cut -d: -f1)
local os_version
os_version=$(echo "$os_info" | cut -d: -f2)
local architecture
architecture=$(detect_architecture)
if [ "$architecture" = "unknown" ]; then
add_error "Unsupported architecture detected"
fi
local init_system
init_system=$(detect_init_system)
log_message "SUCCESS" "System validation completed:"
log_message "SUCCESS" " OS: $os_name $os_version"
log_message "SUCCESS" " Architecture: $architecture"
log_message "SUCCESS" " Init System: $init_system"
# Phase 2: Network connectivity testing
log_message "INFO" "=== PHASE 2: NETWORK CONNECTIVITY TESTING ==="
log_message "INFO" "Testing connectivity to Git Repositorys artifact repository..."
GIT_REACHABLE="false"
if test_connectivity "$GIT_DOCKER_BINARY_URL" "Git Repositorys Docker artifact"; then
GIT_REACHABLE="true"
log_message "SUCCESS" "Git Repositorys Docker artifact is reachable (primary source)"
else
log_message "WARNING" "Git Repositorys Docker artifact is not reachable - will use fallback sources"
fi
local network_results
network_results=$(perform_network_tests)
local us_reachable
us_reachable=$(echo "$network_results" | cut -d: -f1)
local eu_reachable
eu_reachable=$(echo "$network_results" | cut -d: -f2)
local registry_reachable
registry_reachable=$(echo "$network_results" | cut -d: -f3)
local download_reachable
download_reachable=$(echo "$network_results" | cut -d: -f4)
# Critical check: Need at least one source for Docker and Wallarm
# Priority: Git Repositorys (primary) -> local files
if [ "$GIT_REACHABLE" = "true" ]; then
log_message "SUCCESS" "Git Repositorys artifact repository is reachable (primary source available)"
else
log_message "WARNING" "Git Repositorys artifact repository is not reachable - checking fallback sources"
local has_local_docker=false
local has_local_wallarm=false
if [ -d "$LOCAL_BINARY_DIR" ] && [ -n "$(ls "$LOCAL_BINARY_DIR"/*.tgz 2>/dev/null)" ]; then
has_local_docker=true
log_message "INFO" "Found local Docker binaries in $LOCAL_BINARY_DIR/"
elif [ -n "$(ls docker-*.tgz 2>/dev/null)" ]; then
has_local_docker=true
log_message "INFO" "Found local Docker binaries in current directory"
fi
if [ -d "$LOCAL_IMAGE_DIR" ] && [ -n "$(ls "$LOCAL_IMAGE_DIR"/*.tar.gz "$LOCAL_IMAGE_DIR"/*.tar 2>/dev/null)" ]; then
has_local_wallarm=true
log_message "INFO" "Found local Wallarm images in $LOCAL_IMAGE_DIR/"
elif [ -n "$(ls wallarm-node-*.tar.gz wallarm-node-*.tar 2>/dev/null)" ]; then
has_local_wallarm=true
log_message "INFO" "Found local Wallarm images in current directory"
fi
local has_sufficient_resources=true
if [ "$has_local_docker" = "false" ]; then
log_message "ERROR" "No Docker binary source available"
log_message "ERROR" " - Git Repositorys artifacts unreachable: $GIT_RAW_URL"
log_message "ERROR" " - Local binaries not found in $LOCAL_BINARY_DIR/ or current directory"
has_sufficient_resources=false
fi
if [ "$has_local_wallarm" = "false" ]; then
log_message "ERROR" "No Wallarm image source available"
log_message "ERROR" " - Git Repositorys artifacts unreachable: $GIT_RAW_URL"
log_message "ERROR" " - Local images not found in $LOCAL_IMAGE_DIR/ or current directory"
has_sufficient_resources=false
fi
if [ "$has_sufficient_resources" = "false" ]; then
add_error "Insufficient resources: Need at least one source for Docker and Wallarm artifacts.
Possible sources:
1. Git Repositorys (primary): Ensure network access to $GIT_RAW_URL
2. Local files: Place artifacts in:
- Docker binary: $LOCAL_BINARY_DIR/docker-29.2.1.tgz or current directory
- Wallarm image: $LOCAL_IMAGE_DIR/wallarm-node-6.11.0-rc1.tar.gz or current directory"
fi
fi
log_message "SUCCESS" "Network testing completed:"
log_message "SUCCESS" " Git Repositorys Artifacts Reachable: $GIT_REACHABLE"
log_message "SUCCESS" " US Cloud Reachable: $us_reachable"
log_message "SUCCESS" " EU Cloud Reachable: $eu_reachable"
log_message "SUCCESS" " Fallback Registry Reachable: $registry_reachable"
log_message "SUCCESS" " Fallback Download Reachable: $download_reachable"
# Phase 3: Write results
log_message "INFO" "=== PHASE 3: WRITING RESULTS ==="
write_env_file "$os_name" "$os_version" "$architecture" "$init_system" \
"$us_reachable" "$eu_reachable" "$registry_reachable" "$download_reachable" \
"$GIT_REACHABLE"
# Final summary
if [ "$CHECK_RESULT" = "pass" ]; then
log_message "SUCCESS" "=== PREFLIGHT CHECK PASSED ==="
echo -e "\n${GREEN}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ PREFLIGHT CHECK PASSED - SYSTEM READY ║${NC}"
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo -e "\n${CYAN}System is ready for Wallarm Docker deployment.${NC}"
echo -e "${YELLOW}Check results: $ENV_FILE${NC}"
echo -e "${YELLOW}Full log: $LOG_FILE${NC}"
echo -e "\n${GREEN}Next step: Run ./docker/wallarm-ct-deploy.sh to proceed with deployment${NC}"
exit 0
else
log_message "ERROR" "=== PREFLIGHT CHECK FAILED ==="
echo -e "\n${RED}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}${BOLD}║ PREFLIGHT CHECK FAILED - SYSTEM NOT READY ║${NC}"
echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo -e "\n${YELLOW}${BOLD}Issues found:${NC}"
for error in "${CHECK_ERRORS[@]}"; do
echo -e " ${RED}${NC} $error"
done
echo -e "\n${YELLOW}Check results: $ENV_FILE${NC}"
echo -e "${YELLOW}Full log: $LOG_FILE${NC}"
echo -e "\n${CYAN}Please fix the issues above and run the check again.${NC}"
exit 1
fi
}
# ==============================================================================
# SCRIPT EXECUTION
# ==============================================================================
# Ensure we're in bash
if [ -z "$BASH_VERSION" ]; then
echo "Error: This script must be run with bash" >&2
exit 1
fi
# Run main function
main "$@"

1653
docker/wallarm-ct-deploy.sh Executable file

File diff suppressed because it is too large Load diff

268
docker/wallarm-ct-reconfigure.sh Executable file
View file

@ -0,0 +1,268 @@
#!/bin/bash
# ==============================================================================
# WALLARM RECONFIGURATION SCRIPT - V1.1 (Docker deployment)
# ==============================================================================
# Purpose: Modify nginx configuration of an existing Wallarm Docker node
# Features:
# - Update set_real_ip_from (trusted proxy IPs/CIDRs)
# - Change wallarm_mode (monitoring/block)
# - Backup current config before changes
# - Interactive prompts with validation
# ==============================================================================
# Script location and shared library (colors, logging, validation)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wallarm-lib.sh
source "$SCRIPT_DIR/wallarm-lib.sh"
set -euo pipefail
trap early_error_handler ERR
# ==============================================================================
# CHECK FOR SUDO / ROOT PRIVILEGES
# ==============================================================================
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}${BOLD}ERROR:${NC} This script must be run with sudo or as root."
echo -e "${YELLOW}Please run: sudo $0${NC}"
exit 1
fi
# ==============================================================================
# CONFIGURATION
# ==============================================================================
INSTANCE_DIR="/opt"
INSTANCE_NAME=""
# ==============================================================================
# FUNCTIONS
# ==============================================================================
# Function to find the Wallarm instance directory
find_wallarm_instance() {
local dirs=()
while IFS= read -r dir; do
if [[ -d "$dir" && -f "$dir/nginx.conf" && -f "$dir/start.sh" ]]; then
dirs+=("$dir")
fi
done < <(find "$INSTANCE_DIR" -maxdepth 1 -type d -name "wallarm-*" 2>/dev/null)
if [ ${#dirs[@]} -eq 0 ]; then
echo -e "${RED}No Wallarm instance found in $INSTANCE_DIR.${NC}"
exit 1
elif [ ${#dirs[@]} -eq 1 ]; then
INSTANCE_DIR="${dirs[0]}"
INSTANCE_NAME=$(basename "$INSTANCE_DIR")
echo -e "${GREEN}Found instance: $INSTANCE_NAME${NC}"
else
echo -e "${YELLOW}Multiple Wallarm instances found:${NC}"
for i in "${!dirs[@]}"; do
echo "$((i+1)). $(basename "${dirs[$i]}")"
done
read -r -p "Select instance number: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#dirs[@]} ]; then
INSTANCE_DIR="${dirs[$((choice-1))]}"
INSTANCE_NAME=$(basename "$INSTANCE_DIR")
else
echo -e "${RED}Invalid selection.${NC}"
exit 1
fi
fi
}
# Validate IP/CIDR format
validate_proxy() {
local proxy="$1"
if [[ "$proxy" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then
IFS='/' read -r ip cidr <<< "$proxy"
IFS='.' read -r o1 o2 o3 o4 <<< "$ip"
if [ "$o1" -le 255 ] && [ "$o2" -le 255 ] && [ "$o3" -le 255 ] && [ "$o4" -le 255 ]; then
if [ -z "$cidr" ] || ( [ "$cidr" -ge 0 ] && [ "$cidr" -le 32 ] ); then
return 0
fi
fi
fi
return 1
}
# Parse current configuration to get existing values
parse_current_config() {
local config_file="$1"
# Get current wallarm_mode
current_mode=$(grep -oP 'wallarm_mode\s+\K\S+' "$config_file" | head -1)
# Get current set_real_ip_from lines
current_proxies=$(grep -oP 'set_real_ip_from\s+\K\S+' "$config_file")
}
# Update configuration
update_config() {
local config_file="$1"
local backup_file="$config_file.backup.$(date +%Y%m%d_%H%M%S)"
echo -e "${YELLOW}Backing up current config to $backup_file${NC}"
cp "$config_file" "$backup_file"
# Read new values interactively
echo -e "\n${CYAN}${BOLD}Current set_real_ip_from entries:${NC}"
if [ -n "$current_proxies" ]; then
echo "$current_proxies" | while read -r proxy; do
echo " $proxy"
done
else
echo " (none)"
fi
echo -e "\n${YELLOW}Do you want to change the trusted proxy IPs/CIDRs? (y/N)${NC}"
read -r change_proxy
if [[ "$change_proxy" =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Enter new trusted proxy IPs/CIDRs (space-separated, or empty to remove all):${NC}"
read -r new_proxies_input
new_proxies=()
if [[ -n "$new_proxies_input" ]]; then
IFS=' ' read -ra proxy_array <<< "$new_proxies_input"
for proxy in "${proxy_array[@]}"; do
proxy=$(echo "$proxy" | xargs)
if validate_proxy "$proxy"; then
new_proxies+=("$proxy")
else
echo -e "${RED}Invalid format: $proxy. Skipping.${NC}"
fi
done
fi
else
# Keep existing
while read -r proxy; do
new_proxies+=("$proxy")
done <<< "$current_proxies"
fi
echo -e "\n${CYAN}${BOLD}Current wallarm_mode:${NC} ${current_mode:-not set}"
echo -e "${YELLOW}Do you want to change the wallarm_mode? (y/N)${NC}"
read -r change_mode
if [[ "$change_mode" =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}Select new mode:${NC}"
echo "1. monitoring"
echo "2. block"
read -r mode_choice
case "$mode_choice" in
1) new_mode="monitoring" ;;
2) new_mode="block" ;;
*) echo -e "${RED}Invalid choice, keeping current mode.${NC}"; new_mode="$current_mode" ;;
esac
else
new_mode="$current_mode"
fi
# Now rebuild the config file
# We'll create a temporary file and replace the original
temp_config=$(mktemp)
# Read original config line by line and modify as needed
in_server_block=false
while IFS= read -r line; do
# Detect start of server block
if [[ "$line" =~ ^[[:space:]]*server[[:space:]]*{ ]]; then
in_server_block=true
fi
# If we are inside server block, we may need to replace lines
if $in_server_block; then
# Replace set_real_ip_from lines with new ones
if [[ "$line" =~ ^[[:space:]]*set_real_ip_from[[:space:]]+ ]]; then
# Skip original set_real_ip_from lines (will be added later)
continue
fi
# Replace wallarm_mode line
if [[ "$line" =~ ^[[:space:]]*wallarm_mode[[:space:]]+ ]]; then
# We'll add new line after processing all lines
continue
fi
fi
# Write line to temp file
echo "$line" >> "$temp_config"
# After writing the line, if we are at the end of the server block, we may need to insert new directives
if $in_server_block && [[ "$line" =~ ^[[:space:]]*}$ ]]; then
in_server_block=false
# Insert the new set_real_ip_from lines just before the closing brace
if [ ${#new_proxies[@]} -gt 0 ]; then
for proxy in "${new_proxies[@]}"; do
echo " set_real_ip_from $proxy;" >> "$temp_config"
done
echo " real_ip_header X-Real-IP;" >> "$temp_config"
echo " real_ip_recursive on;" >> "$temp_config"
elif [ -n "$current_proxies" ]; then
# If we removed all proxies, we should also remove the real_ip_header and real_ip_recursive lines
# But that's tricky; we'll just not add them, but they might remain in the file if they were separate.
# Simpler: after rebuild, we need to ensure they are not there. We'll do a final cleanup.
echo -e "${YELLOW}Removing all set_real_ip_from directives.${NC}"
fi
# Insert new wallarm_mode
if [ -n "$new_mode" ]; then
echo " wallarm_mode $new_mode;" >> "$temp_config"
fi
fi
done < "$config_file"
# After building the temp file, we need to ensure any leftover real_ip_header lines are removed if no proxies.
if [ ${#new_proxies[@]} -eq 0 ]; then
# Remove lines containing real_ip_header and real_ip_recursive if they exist
sed -i '/real_ip_header/d' "$temp_config"
sed -i '/real_ip_recursive/d' "$temp_config"
fi
# Replace the original config with the new one
mv "$temp_config" "$config_file"
chmod 644 "$config_file"
echo -e "${GREEN}Configuration updated.${NC}"
}
restart_container() {
local container_name="$1"
echo -e "${YELLOW}Restarting container $container_name to apply changes...${NC}"
if docker ps --format "{{.Names}}" | grep -q "^$container_name$"; then
docker restart "$container_name"
echo -e "${GREEN}Container restarted.${NC}"
else
echo -e "${RED}Container $container_name is not running. Starting it...${NC}"
if [ -f "$INSTANCE_DIR/start.sh" ]; then
"$INSTANCE_DIR/start.sh"
else
echo -e "${RED}No start script found. Please start manually: docker start $container_name${NC}"
exit 1
fi
fi
}
main() {
echo -e "${BLUE}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ WALLARM RECONFIGURATION SCRIPT - V1.0 ║"
echo "║ Modify nginx.conf (trusted proxies / mode) ║"
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
find_wallarm_instance
local config_file="$INSTANCE_DIR/nginx.conf"
if [ ! -f "$config_file" ]; then
echo -e "${RED}Configuration file not found: $config_file${NC}"
exit 1
fi
parse_current_config "$config_file"
update_config "$config_file"
echo -e "${YELLOW}Do you want to restart the container now? (Y/n)${NC}"
read -r restart_choice
if [[ ! "$restart_choice" =~ ^[Nn]$ ]]; then
restart_container "$INSTANCE_NAME"
else
echo -e "${YELLOW}Changes will take effect after container restart.${NC}"
echo -e "You can restart later with: docker restart $INSTANCE_NAME"
fi
echo -e "\n${GREEN}${BOLD}Reconfiguration completed.${NC}"
}
main "$@"

512
docker/wallarm-ct-uninstall.sh Executable file
View file

@ -0,0 +1,512 @@
#!/bin/bash
# ==============================================================================
# WALLARM UNINSTALL SCRIPT - V1.1 (Docker deployment)
# ==============================================================================
# Purpose: Safely remove a Wallarm Docker node and cleanup Docker installation
# Features:
# - Interactive confirmation with safety checks
# - Stops and removes Wallarm container and image
# - Removes Docker service files created by deployment script
# - Optional cleanup of Docker binaries (if no other containers exist)
# - Preserves user data and logs (with option to remove)
# - DAU-friendly warnings and confirmations
# ==============================================================================
# Script location and shared library (colors, logging, validation)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wallarm-lib.sh
source "$SCRIPT_DIR/wallarm-lib.sh"
# Strict error handling
set -euo pipefail
trap early_error_handler ERR
# ==============================================================================
# FUNCTIONS
# ==============================================================================
# Ask for confirmation
confirm() {
local prompt="$1"
local default="${2:-n}"
local options="[y/N]"
if [ "$default" = "y" ]; then
options="[Y/n]"
fi
echo -e -n "${YELLOW}${prompt} ${options}${NC} "
read -r response
case "$response" in
[yY][eE][sS]|[yY])
return 0
;;
[nN][oO]|[nN])
return 1
;;
"")
# Use default
if [ "$default" = "y" ]; then
return 0
else
return 1
fi
;;
*)
# Invalid input, treat as no
return 1
;;
esac
}
# Check if running as root or with sudo
check_sudo() {
if [ "$EUID" -ne 0 ]; then
log_message "INFO" "This script requires sudo privileges"
if ! sudo -n true 2>/dev/null; then
log_message "INFO" "Please enter your sudo password when prompted"
sudo -v
fi
fi
}
# Detect init system
detect_init_system() {
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
echo "systemd"
elif [ -d /run/openrc ]; then
echo "openrc"
elif [ -f /etc/init.d/docker ]; then
echo "sysvinit"
else
echo "unknown"
fi
}
# Check if Docker is installed and running
check_docker() {
if ! command -v docker >/dev/null 2>&1; then
log_message "WARNING" "Docker command not found"
return 1
fi
if ! sudo docker info >/dev/null 2>&1; then
log_message "WARNING" "Docker is not running"
return 1
fi
return 0
}
# Check for other Docker containers (besides Wallarm)
check_other_containers() {
local wallarm_container="wallarm-node"
local all_containers
all_containers=$(sudo docker ps -a -q 2>/dev/null | wc -l)
local wallarm_containers
wallarm_containers=$(sudo docker ps -a --filter "name=${wallarm_container}" -q 2>/dev/null | wc -l)
if [ "$all_containers" -gt "$wallarm_containers" ]; then
log_message "WARNING" "Found other Docker containers besides Wallarm"
sudo docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" | grep -v "$wallarm_container" || true
return 0 # Other containers exist
fi
return 1 # Only Wallarm containers or no containers
}
# Stop and remove Wallarm container
remove_wallarm_container() {
local container_name="wallarm-node"
log_message "INFO" "Looking for Wallarm container..."
if sudo docker ps -a --filter "name=${container_name}" --format "{{.Names}}" | grep -q "${container_name}"; then
log_message "INFO" "Found Wallarm container: ${container_name}"
# Stop container if running
if sudo docker ps --filter "name=${container_name}" --filter "status=running" --format "{{.Names}}" | grep -q "${container_name}"; then
log_message "INFO" "Stopping Wallarm container..."
sudo docker stop "${container_name}" || {
log_message "WARNING" "Failed to stop container, attempting force stop"
sudo docker kill "${container_name}" 2>/dev/null || true
}
fi
# Remove container
log_message "INFO" "Removing Wallarm container..."
sudo docker rm -f "${container_name}" 2>/dev/null || {
log_message "WARNING" "Failed to remove container, it may already be removed"
}
log_message "SUCCESS" "Wallarm container removed"
else
log_message "INFO" "No Wallarm container found"
fi
}
# Remove Wallarm image
remove_wallarm_image() {
local image_name="wallarm/node"
log_message "INFO" "Looking for Wallarm image..."
if sudo docker images --format "{{.Repository}}" | grep -q "^${image_name}"; then
log_message "INFO" "Found Wallarm image: ${image_name}"
# Check if image is used by any containers
local used_by
used_by=$(sudo docker ps -a --filter "ancestor=${image_name}" -q 2>/dev/null | wc -l)
if [ "$used_by" -gt 0 ]; then
log_message "WARNING" "Image ${image_name} is still in use by containers, skipping removal"
return
fi
# Remove image
log_message "INFO" "Removing Wallarm image..."
sudo docker rmi "${image_name}:latest" 2>/dev/null || {
log_message "WARNING" "Failed to remove image, it may be in use or already removed"
}
# Also try to remove by ID if tag removal failed
local image_id
image_id=$(sudo docker images --filter "reference=${image_name}" --format "{{.ID}}" 2>/dev/null | head -1)
if [ -n "$image_id" ]; then
sudo docker rmi -f "$image_id" 2>/dev/null || true
fi
log_message "SUCCESS" "Wallarm image removed"
else
log_message "INFO" "No Wallarm image found"
fi
}
# Remove Docker service files (created by deployment script)
remove_docker_service_files() {
local init_system
init_system=$(detect_init_system)
log_message "INFO" "Removing Docker service files for init system: ${init_system}"
case "$init_system" in
"systemd")
# Stop and disable Docker service
if sudo systemctl is-active docker --quiet 2>/dev/null; then
log_message "INFO" "Stopping Docker service..."
sudo systemctl stop docker 2>/dev/null || true
fi
if sudo systemctl is-enabled docker --quiet 2>/dev/null; then
log_message "INFO" "Disabling Docker service..."
sudo systemctl disable docker 2>/dev/null || true
fi
# Remove systemd unit files (if they exist and were created by our script)
local systemd_files=(
"/etc/systemd/system/docker.socket"
"/etc/systemd/system/docker.service"
"/usr/lib/systemd/system/docker.socket"
"/usr/lib/systemd/system/docker.service"
)
for file in "${systemd_files[@]}"; do
if [ -f "$file" ]; then
log_message "INFO" "Removing systemd file: $file"
sudo rm -f "$file"
fi
done
sudo systemctl daemon-reload 2>/dev/null || true
;;
"openrc")
# Stop and remove from runlevels
if sudo rc-service docker status 2>/dev/null | grep -q "started"; then
log_message "INFO" "Stopping Docker service (OpenRC)..."
sudo rc-service docker stop 2>/dev/null || true
fi
if [ -f /etc/init.d/docker ]; then
log_message "INFO" "Removing OpenRC init script..."
sudo rc-update del docker default 2>/dev/null || true
sudo rm -f /etc/init.d/docker
fi
;;
"sysvinit")
# Stop service
if [ -f /etc/init.d/docker ]; then
log_message "INFO" "Stopping Docker service (SysV init)..."
sudo service docker stop 2>/dev/null || true
# Remove from startup
if command -v update-rc.d >/dev/null 2>&1; then
sudo update-rc.d -f docker remove 2>/dev/null || true
elif command -v chkconfig >/dev/null 2>&1; then
sudo chkconfig --del docker 2>/dev/null || true
fi
log_message "INFO" "Removing SysV init script..."
sudo rm -f /etc/init.d/docker
fi
;;
*)
log_message "WARNING" "Unknown init system, skipping service file cleanup"
;;
esac
log_message "SUCCESS" "Docker service files removed"
}
# Remove Docker binaries (optional, only if no other containers exist)
remove_docker_binaries() {
local docker_binaries=(
"/usr/bin/docker"
"/usr/bin/dockerd"
"/usr/bin/docker-init"
"/usr/bin/docker-proxy"
"/usr/bin/containerd"
"/usr/bin/containerd-shim"
"/usr/bin/containerd-shim-runc-v1"
"/usr/bin/containerd-shim-runc-v2"
"/usr/bin/runc"
)
log_message "INFO" "Checking Docker binaries..."
local binaries_found=0
for binary in "${docker_binaries[@]}"; do
if [ -f "$binary" ]; then
binaries_found=$((binaries_found + 1))
fi
done
if [ "$binaries_found" -eq 0 ]; then
log_message "INFO" "No Docker binaries found in /usr/bin/"
return
fi
if confirm "Remove Docker binaries from /usr/bin/? (Only do this if Docker was installed by wallarm-ct-deploy.sh)" "n"; then
log_message "WARNING" "Removing Docker binaries..."
for binary in "${docker_binaries[@]}"; do
if [ -f "$binary" ]; then
log_message "INFO" "Removing $binary"
sudo rm -f "$binary"
fi
done
# Also remove CNI plugins if they exist
if [ -d "/opt/cni/bin" ]; then
log_message "INFO" "Removing CNI plugins from /opt/cni/bin/"
sudo rm -rf /opt/cni/bin/*
fi
log_message "SUCCESS" "Docker binaries removed"
else
log_message "INFO" "Skipping Docker binary removal"
fi
}
# Remove Docker configuration files
remove_docker_config() {
local config_files=(
"/etc/docker/daemon.json"
"/etc/containerd/config.toml"
"/var/lib/docker" # Warning: This removes all Docker data!
)
log_message "INFO" "Checking Docker configuration files..."
# Only remove daemon.json if it was created by our script
if [ -f "/etc/docker/daemon.json" ]; then
log_message "INFO" "Found /etc/docker/daemon.json"
if grep -q "storage-driver.*vfs" "/etc/docker/daemon.json" 2>/dev/null; then
log_message "INFO" "This appears to be the VFS configuration from wallarm-ct-deploy.sh"
if confirm "Remove /etc/docker/daemon.json?" "n"; then
sudo rm -f "/etc/docker/daemon.json"
log_message "SUCCESS" "Docker configuration removed"
fi
else
log_message "WARNING" "/etc/docker/daemon.json doesn't appear to be from wallarm-ct-deploy.sh, skipping"
fi
fi
# Warn about Docker data directory
if [ -d "/var/lib/docker" ]; then
log_message "WARNING" "/var/lib/docker contains Docker data (images, containers, volumes)"
log_message "WARNING" "Removing this directory will delete ALL Docker data on the system"
if confirm "Remove /var/lib/docker? (WARNING: Deletes ALL Docker data)" "n"; then
log_message "WARNING" "Removing /var/lib/docker - this may take a while..."
sudo rm -rf /var/lib/docker
log_message "SUCCESS" "Docker data directory removed"
fi
fi
}
# Remove docker group (if empty)
remove_docker_group() {
log_message "INFO" "Checking docker group..."
if getent group docker >/dev/null; then
local group_users
group_users=$(getent group docker | cut -d: -f4)
if [ -z "$group_users" ]; then
log_message "INFO" "Docker group exists and has no users"
if confirm "Remove docker group?" "n"; then
sudo groupdel docker 2>/dev/null || {
log_message "WARNING" "Failed to remove docker group (may be system group)"
}
log_message "SUCCESS" "Docker group removed"
fi
else
log_message "WARNING" "Docker group has users: $group_users"
log_message "INFO" "Skipping docker group removal (users still present)"
fi
else
log_message "INFO" "Docker group not found"
fi
}
# Remove Wallarm-specific files and logs
remove_wallarm_files() {
local wallarm_files=(
"$HOME/wallarm-start.sh"
"$HOME/wallarm-stop.sh"
"$HOME/wallarm-status.sh"
"/usr/local/bin/wallarm-start"
"/usr/local/bin/wallarm-stop"
"/usr/local/bin/wallarm-status"
)
log_message "INFO" "Removing Wallarm scripts and logs..."
# Remove scripts
for file in "${wallarm_files[@]}"; do
if [ -f "$file" ]; then
log_message "INFO" "Removing $file"
sudo rm -f "$file"
fi
done
# Remove log directory (if empty)
local log_dir="$HOME/logs"
if [ -d "$log_dir" ]; then
log_message "INFO" "Found log directory: $log_dir"
if [ -z "$(ls -A "$log_dir" 2>/dev/null)" ]; then
log_message "INFO" "Log directory is empty, removing..."
sudo rmdir "$log_dir" 2>/dev/null || true
else
log_message "INFO" "Log directory contains files, preserving..."
fi
fi
# Remove .env file if it exists
if [ -f ".env" ]; then
log_message "INFO" "Removing .env file..."
rm -f ".env"
fi
log_message "SUCCESS" "Wallarm files cleaned up"
}
# Main uninstall function
main() {
echo -e "${CYAN}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ WALLARM UNINSTALLATION ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e "${YELLOW}This script will remove Wallarm filtering node and cleanup Docker installation.${NC}"
echo -e "${YELLOW}You will be asked for confirmation before each destructive operation.${NC}"
echo ""
if ! confirm "Do you want to continue with the uninstallation?" "n"; then
log_message "INFO" "Uninstallation cancelled by user"
exit 0
fi
# Check sudo
check_sudo
# Check Docker
if check_docker; then
log_message "INFO" "Docker is installed and running"
# Check for other containers
if check_other_containers; then
log_message "WARNING" "Other Docker containers exist on this system"
echo -e "${YELLOW}Warning: Removing Docker may affect other containers.${NC}"
echo -e "${YELLOW}Consider leaving Docker installed if you need it for other purposes.${NC}"
echo ""
fi
else
log_message "WARNING" "Docker is not running or not installed"
fi
# Step 1: Remove Wallarm container and image
echo ""
echo -e "${CYAN}${BOLD}Step 1: Remove Wallarm container and image${NC}"
if confirm "Stop and remove Wallarm container and image?" "y"; then
remove_wallarm_container
remove_wallarm_image
else
log_message "INFO" "Skipping Wallarm container/image removal"
fi
# Step 2: Remove Docker service files
echo ""
echo -e "${CYAN}${BOLD}Step 2: Remove Docker service files${NC}"
if confirm "Remove Docker service files (systemd/OpenRC/SysV init scripts)?" "y"; then
remove_docker_service_files
else
log_message "INFO" "Skipping Docker service file removal"
fi
# Step 3: Optional Docker binary removal
echo ""
echo -e "${CYAN}${BOLD}Step 3: Docker binaries and configuration${NC}"
remove_docker_binaries
remove_docker_config
# Step 4: Remove docker group
echo ""
echo -e "${CYAN}${BOLD}Step 4: System cleanup${NC}"
remove_docker_group
# Step 5: Remove Wallarm files
echo ""
echo -e "${CYAN}${BOLD}Step 5: Wallarm files and logs${NC}"
if confirm "Remove Wallarm scripts and log files?" "y"; then
remove_wallarm_files
else
log_message "INFO" "Skipping Wallarm file cleanup"
fi
# Final message
echo ""
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ UNINSTALLATION COMPLETE ║${NC}"
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${GREEN}Wallarm filtering node has been removed.${NC}"
echo ""
echo -e "${YELLOW}Note:${NC}"
echo -e " • Docker may still be installed on your system"
echo -e " • Docker data in /var/lib/docker may still exist"
echo -e " • User may still be in docker group (check with 'groups')"
echo ""
echo -e "To completely remove Docker, you may need to:"
echo -e " 1. Remove Docker package using your system's package manager"
echo -e " 2. Remove /var/lib/docker directory (contains all Docker data)"
echo -e " 3. Remove user from docker group: sudo gpasswd -d \$USER docker"
echo ""
}
# Run main function
main "$@"

91
docker/wallarm-docker.sh Normal file
View file

@ -0,0 +1,91 @@
#!/bin/bash
# ==============================================================================
# Wallarm Docker Node Manager - Unified single-script manager
# ==============================================================================
# Delegates to the individual docker/* scripts for preflight, deployment,
# reconfiguration, and removal — providing a single entry point matching the
# native/wallarm-native.sh command interface.
#
# Commands:
# --preflight Run preflight checks only (no installation).
# --install Interactive deployment of a Wallarm Docker node.
# --config Reconfigure an existing Docker node.
# --remove Remove a Docker node completely.
# --status Show running Wallarm containers.
# --help|-h Show help.
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Shell script files this wrapper delegates to (must be in the same directory)
CHECK_SCRIPT="${SCRIPT_DIR}/wallarm-ct-check.sh"
DEPLOY_SCRIPT="${SCRIPT_DIR}/wallarm-ct-deploy.sh"
RECONF_SCRIPT="${SCRIPT_DIR}/wallarm-ct-reconfigure.sh"
REMOVE_SCRIPT="${SCRIPT_DIR}/wallarm-ct-uninstall.sh"
show_help() {
cat <<EOF
Usage: $0 COMMAND [OPTIONS]
Commands:
--preflight Run system validation and preflight checks
--install Interactive deployment of a Wallarm Docker node
--config Reconfigure an existing Docker node (proxies, mode)
--remove Uninstall a Wallarm Docker node
--status Show running Wallarm containers
Examples:
$0 --preflight
sudo $0 --install
sudo $0 --config
sudo $0 --remove
EOF
}
# Ensure the delegated script exists
require_script() {
local script="$1"
local name="$2"
if [[ ! -x "$script" ]]; then
echo "!!! $name script not found or not executable: $script" >&2
echo " Run setup.sh first to download all deployment scripts." >&2
exit 1
fi
}
case "${1:-}" in
--preflight)
require_script "$CHECK_SCRIPT" "Preflight (wallarm-ct-check.sh)"
exec "$CHECK_SCRIPT" "${@:2}"
;;
--install)
require_script "$DEPLOY_SCRIPT" "Deploy (wallarm-ct-deploy.sh)"
exec sudo "$DEPLOY_SCRIPT" "${@:2}"
;;
--config)
require_script "$RECONF_SCRIPT" "Reconfigure (wallarm-ct-reconfigure.sh)"
exec sudo "$RECONF_SCRIPT" "${@:2}"
;;
--remove)
require_script "$REMOVE_SCRIPT" "Uninstall (wallarm-ct-uninstall.sh)"
exec sudo "$REMOVE_SCRIPT" "${@:2}"
;;
--status)
echo "Wallarm Docker Nodes:"
if command -v docker >/dev/null 2>&1; then
docker ps --filter "name=wallarm-" \
--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || true
else
echo " Docker is not installed or not in PATH."
fi
;;
--help|-h)
show_help
;;
*)
show_help
exit 1
;;
esac

38
go.mod Normal file
View file

@ -0,0 +1,38 @@
module git.sechpoint.app/customer-engineering/wallarm
go 1.24.0
toolchain go1.24.4
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/crypto v0.36.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

77
go.sum Normal file
View file

@ -0,0 +1,77 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw=
github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=

218
internal/native/native.go Normal file
View file

@ -0,0 +1,218 @@
// Package native implements Wallarm Native Node deployment (no Docker).
// Ported from native/wallarm-native.sh.
package native
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
// Constants matching the bash script
const (
BaseDir = "/opt/wallarm"
NodesDir = BaseDir + "/nodes"
SystemdTemplate = "/etc/systemd/system/wallarm-node@.service"
InstallerBaseURL = "https://repo.wallarm.com/linux/wallarm-native-node/latest/all-in-one"
InstallerArch = "x86_64"
)
// InstallerURL returns the all-in-one installer URL.
var InstallerURL = fmt.Sprintf("%s/wallarm-native-node-aio-%s-latest.sh", InstallerBaseURL, InstallerArch)
// InstallNode deploys a single native Wallarm node.
// node: node metadata, apiToken: Wallarm API token, apiHost: cloud API host, labels: optional node labels.
func InstallNode(node state.Node, apiToken, apiHost, labels string) error {
if apiToken == "" {
return fmt.Errorf("API token required")
}
if apiHost == "" {
return fmt.Errorf("API host required")
}
workDir := filepath.Join(NodesDir, node.Name)
installerPath := filepath.Join(workDir, "wallarm-native-node-aio.sh")
// 1. Create directories
for _, d := range []string{
filepath.Join(workDir, "etc"),
filepath.Join(workDir, "var", "log"),
filepath.Join(workDir, "var", "run"),
} {
if err := os.MkdirAll(d, 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", d, err)
}
}
// 2. Write go-node.yaml config
configPath := filepath.Join(workDir, "etc", "go-node.yaml")
config := fmt.Sprintf(`mode: connector-server
connector:
address: "%s"
`, node.Address)
if err := os.WriteFile(configPath, []byte(config), 0644); err != nil {
return fmt.Errorf("write config: %w", err)
}
// 3. Download installer if missing
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
fmt.Printf("[%s] Downloading installer...\n", node.Name)
cmd := exec.Command("curl", "-fsSL", "-o", installerPath, InstallerURL)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("download installer: %w\n%s", err, string(out))
}
if err := os.Chmod(installerPath, 0755); err != nil {
return fmt.Errorf("chmod installer: %w", err)
}
}
// 4. Write environment file
envFile := filepath.Join(workDir, "env")
envContent := fmt.Sprintf(`WALLARM_API_TOKEN=%s
WALLARM_API_HOST=%s
WALLARM_LABELS=%s
WALLARM_CONFIG_PATH=%s
`, apiToken, apiHost, labels, configPath)
if err := os.WriteFile(envFile, []byte(envContent), 0600); err != nil {
return fmt.Errorf("write env: %w", err)
}
// 5. Run the all-in-one installer
fmt.Printf("[%s] Running Wallarm installer...\n", node.Name)
installCmd := exec.Command(installerPath, "install",
"--", "--config-dir", filepath.Join(workDir, "etc"),
"--", "--log-dir", filepath.Join(workDir, "var", "log"),
"--", "--pid-dir", filepath.Join(workDir, "var", "run"),
)
installCmd.Dir = workDir
installCmd.Env = append(os.Environ(),
"WALLARM_API_TOKEN="+apiToken,
"WALLARM_API_HOST="+apiHost,
"WALLARM_LABELS="+labels,
"WALLARM_CONFIG_PATH="+configPath,
)
logFile, err := os.Create(filepath.Join(workDir, "install.log"))
if err == nil {
installCmd.Stdout = logFile
installCmd.Stderr = logFile
defer logFile.Close()
}
if err := installCmd.Run(); err != nil {
return fmt.Errorf("installation failed — check %s/install.log: %w", workDir, err)
}
// 6. Enable and start systemd service
serviceName := "wallarm-node@" + node.Name
for _, action := range []string{"enable", "start"} {
cmd := exec.Command("systemctl", action, serviceName)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("systemctl %s %s: %w\n%s", action, serviceName, err, string(out))
}
}
fmt.Printf("[%s] Installation successful. Service: %s\n", node.Name, serviceName)
return nil
}
// RemoveNode stops the systemd service and deletes the node directory.
func RemoveNode(nodeName string) error {
serviceName := "wallarm-node@" + nodeName
workDir := filepath.Join(NodesDir, nodeName)
// Stop and disable
for _, action := range []string{"stop", "disable"} {
exec.Command("systemctl", action, serviceName).Run() // ignore errors
}
// Remove directory
if err := os.RemoveAll(workDir); err != nil {
return fmt.Errorf("remove %s: %w", workDir, err)
}
fmt.Printf("Node %s removed.\n", nodeName)
return nil
}
// Status returns the systemd status for a node, or all nodes if nodeName is empty.
func Status(nodeName string) (string, error) {
if nodeName != "" {
cmd := exec.Command("systemctl", "status", "wallarm-node@"+nodeName, "--no-pager")
out, err := cmd.CombinedOutput()
return string(out), err
}
entries, err := os.ReadDir(NodesDir)
if err != nil {
return "", err
}
var sb strings.Builder
sb.WriteString("Wallarm Nodes:\n")
for _, e := range entries {
if e.IsDir() {
name := e.Name()
sb.WriteString(fmt.Sprintf("--- %s ---\n", name))
cmd := exec.Command("systemctl", "status", "wallarm-node@"+name, "--no-pager")
out, err := cmd.CombinedOutput()
if err != nil {
sb.WriteString(string(out))
} else {
lines := strings.Split(string(out), "\n")
for i, l := range lines {
if i >= 5 {
break
}
sb.WriteString(l + "\n")
}
}
sb.WriteString("\n")
}
}
return sb.String(), nil
}
// GenerateSystemdTemplate creates the wallarm-node@.service template unit if it doesn't exist.
func GenerateSystemdTemplate() error {
if _, err := os.Stat(SystemdTemplate); err == nil {
return nil // already exists
}
content := fmt.Sprintf(`[Unit]
Description=Wallarm Native Node - %%I
After=network.target
[Service]
Type=simple
WorkingDirectory=%s/%%i
EnvironmentFile=%s/%%i/env
ExecStart=%s/%%i/wallarm-native-node-aio.sh start
ExecStop=%s/%%i/wallarm-native-node-aio.sh stop
Restart=on-failure
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
`, NodesDir, NodesDir, NodesDir, NodesDir)
if err := os.WriteFile(SystemdTemplate, []byte(content), 0644); err != nil {
return fmt.Errorf("write systemd template: %w", err)
}
cmd := exec.Command("systemctl", "daemon-reload")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("daemon-reload: %w\n%s", err, string(out))
}
return nil
}
// CreateNodesDir ensures the /opt/wallarm/nodes directory exists.
func CreateNodesDir() error {
return os.MkdirAll(NodesDir, 0755)
}

View file

@ -0,0 +1,169 @@
// Package preflight runs system readiness checks before any deployment.
// It is called on every binary start and returns a structured report.
package preflight
import (
"fmt"
"os"
"git.sechpoint.app/customer-engineering/wallarm/internal/shared"
)
// Result holds the outcome of all preflight checks.
type Result struct {
Passed bool `json:"passed"`
Checks []Check `json:"checks"`
USReachable bool `json:"us_reachable"`
EUReachable bool `json:"eu_reachable"`
}
// Check represents a single preflight check.
type Check struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Detail string `json:"detail,omitempty"`
Warning bool `json:"warning,omitempty"`
}
// Cloud endpoints (from wallarm-lib.sh)
var euEndpoints = []string{
"api.wallarm.com:443",
"node-data0.eu1.wallarm.com:443",
"node-data1.eu1.wallarm.com:443",
}
var usEndpoints = []string{
"us1.api.wallarm.com:443",
"node-data0.us1.wallarm.com:443",
"node-data1.us1.wallarm.com:443",
}
// Run executes all preflight checks and returns the result.
func Run() Result {
r := Result{Passed: true}
// 1. Root check
if os.Geteuid() != 0 {
r.Checks = append(r.Checks, Check{
Name: "root", Passed: false, Detail: "must run as root for package installation and system config",
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "root", Passed: true})
}
// 2. Init system
initSys := shared.InitSystem()
if initSys != "systemd" {
r.Checks = append(r.Checks, Check{
Name: "init", Passed: false, Detail: fmt.Sprintf("requires systemd, detected: %s", initSys),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "init", Passed: true, Detail: "systemd"})
}
// 3. Architecture
arch := shared.Arch()
supported := arch == "x86_64" || arch == "aarch64"
r.Checks = append(r.Checks, Check{
Name: "arch", Passed: supported, Detail: arch,
})
if !supported {
r.Passed = false
}
// 4. OS
id, ver := shared.OSInfo()
r.Checks = append(r.Checks, Check{Name: "os", Passed: true, Detail: id + " " + ver})
// 5. Required commands
requiredCmds := []string{"curl", "systemctl", "sed", "mkdir", "rm"}
for _, cmd := range requiredCmds {
ok := shared.CommandExists(cmd)
r.Checks = append(r.Checks, Check{Name: "cmd:" + cmd, Passed: ok})
if !ok {
r.Passed = false
}
}
// 6. Installer reachability
installerOk := shared.HTTPHead("https://repo.wallarm.com")
r.Checks = append(r.Checks, Check{
Name: "installer_reachable", Passed: installerOk,
Detail: "repo.wallarm.com",
})
if !installerOk {
r.Checks[len(r.Checks)-1].Warning = true
}
// 7. Cloud endpoints
r.USReachable = checkEndpoints(usEndpoints)
r.EUReachable = checkEndpoints(euEndpoints)
r.Checks = append(r.Checks, Check{
Name: "cloud:US", Passed: r.USReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(usEndpoints), len(usEndpoints)),
})
r.Checks = append(r.Checks, Check{
Name: "cloud:EU", Passed: r.EUReachable,
Detail: fmt.Sprintf("%d/%d reachable", countReachable(euEndpoints), len(euEndpoints)),
})
if !r.USReachable && !r.EUReachable {
r.Passed = false
}
// 8. Disk space (>= 2GB)
free, err := shared.FreeDiskMB("/opt")
if err == nil && free < 2048 {
r.Checks = append(r.Checks, Check{
Name: "disk", Passed: false,
Detail: fmt.Sprintf("%d MB free (need >= 2048 MB)", free),
})
r.Passed = false
} else {
r.Checks = append(r.Checks, Check{Name: "disk", Passed: true, Detail: fmt.Sprintf("%d MB free", free)})
}
// 9. Memory (>= 2GB, warning only)
mem, err := shared.FreeMemoryMB()
if err == nil && mem < 2048 {
r.Checks = append(r.Checks, Check{
Name: "memory", Passed: true, Warning: true,
Detail: fmt.Sprintf("%d MB (2GB+ recommended)", mem),
})
} else {
r.Checks = append(r.Checks, Check{Name: "memory", Passed: true, Detail: fmt.Sprintf("%d MB", mem)})
}
return r
}
func checkEndpoints(endpoints []string) bool {
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
return true
}
}
return false
}
func countReachable(endpoints []string) int {
n := 0
for _, ep := range endpoints {
host, _ := splitHostPort(ep)
if shared.TCPConnect(host, 443) {
n++
}
}
return n
}
func splitHostPort(addr string) (string, string) {
for i := len(addr) - 1; i >= 0; i-- {
if addr[i] == ':' {
return addr[:i], addr[i+1:]
}
}
return addr, ""
}

174
internal/shared/shared.go Normal file
View file

@ -0,0 +1,174 @@
// Package shared provides validation, connectivity, and system detection
// utilities ported from the bash wallarm-lib.sh library.
package shared
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
)
// ─── System Detection ────────────────────────────────────────────────
// InitSystem returns the detected init system: systemd, openrc, sysvinit, upstart, or unknown.
func InitSystem() string {
if runtime.GOOS == "darwin" {
return "darwin"
}
if _, err := exec.LookPath("systemctl"); err == nil {
return "systemd"
}
if _, err := os.Stat("/sbin/openrc-run"); err == nil {
return "openrc"
}
if _, err := os.Stat("/etc/init.d"); err == nil {
return "sysvinit"
}
if _, err := os.Stat("/sbin/upstart"); err == nil {
return "upstart"
}
return "unknown"
}
// OSInfo returns (os_id, version_id) from /etc/os-release.
func OSInfo() (id, version string) {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return strings.ToLower(runtime.GOOS), "unknown"
}
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "ID=") {
id = strings.Trim(strings.TrimPrefix(line, "ID="), `"`)
}
if strings.HasPrefix(line, "VERSION_ID=") {
version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), `"`)
}
}
if id == "" {
id = strings.ToLower(runtime.GOOS)
}
return id, version
}
// Arch returns the normalized architecture: x86_64, aarch64, or armhf.
func Arch() string {
switch runtime.GOARCH {
case "amd64":
return "x86_64"
case "arm64":
return "aarch64"
case "arm":
return "armhf"
default:
return runtime.GOARCH
}
}
// ─── Validation ──────────────────────────────────────────────────────
// ValidateIP checks whether s is a valid IPv4 address.
func ValidateIP(s string) error {
parts := strings.Split(s, ".")
if len(parts) != 4 {
return fmt.Errorf("invalid IPv4: %s", s)
}
for _, p := range parts {
n, err := strconv.Atoi(p)
if err != nil || n < 0 || n > 255 {
return fmt.Errorf("invalid IPv4 octet: %s", p)
}
}
return nil
}
// ValidateCIDR checks whether s is a valid IPv4 address with optional /prefix.
func ValidateCIDR(s string) error {
ip := s
prefix := ""
if idx := strings.IndexByte(s, '/'); idx != -1 {
ip, prefix = s[:idx], s[idx+1:]
}
if err := ValidateIP(ip); err != nil {
return err
}
if prefix != "" {
n, err := strconv.Atoi(prefix)
if err != nil || n < 0 || n > 32 {
return fmt.Errorf("invalid CIDR prefix: %s", prefix)
}
}
return nil
}
// CommandExists returns true if cmd is in PATH or in common system directories.
func CommandExists(cmd string) bool {
if _, err := exec.LookPath(cmd); err == nil {
return true
}
for _, dir := range []string{"/usr/sbin", "/sbin", "/usr/local/sbin", "/usr/bin", "/bin", "/usr/local/bin"} {
if _, err := os.Stat(dir + "/" + cmd); err == nil {
return true
}
}
return false
}
// ─── Resource Checks ─────────────────────────────────────────────────
// FreeDiskMB returns available disk space in MB for the given path.
func FreeDiskMB(path string) (int64, error) {
cmd := exec.Command("df", "-k", path)
out, err := cmd.Output()
if err != nil {
return 0, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, fmt.Errorf("unexpected df output")
}
fields := strings.Fields(lines[1])
if len(fields) < 4 {
return 0, fmt.Errorf("unexpected df fields")
}
kb, err := strconv.ParseInt(fields[3], 10, 64)
if err != nil {
return 0, err
}
return kb / 1024, nil // convert KB to MB
}
// FreeMemoryMB returns available memory in MB.
func FreeMemoryMB() (int64, error) {
cmd := exec.Command("free", "-m")
out, err := cmd.Output()
if err != nil {
return 0, err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, fmt.Errorf("unexpected free output")
}
fields := strings.Fields(lines[1])
if len(fields) < 2 {
return 0, fmt.Errorf("unexpected free fields")
}
return strconv.ParseInt(fields[1], 10, 64)
}
// ─── Connectivity ────────────────────────────────────────────────────
// TCPConnect tests whether host:port accepts a TCP connection.
func TCPConnect(host string, port int) bool {
cmd := exec.Command("timeout", "5", "bash", "-c",
fmt.Sprintf("echo >/dev/tcp/%s/%d 2>/dev/null", host, port))
return cmd.Run() == nil
}
// HTTPHead returns true if the URL returns a successful status.
func HTTPHead(url string) bool {
cmd := exec.Command("curl", "-fsSL", "--connect-timeout", "10", url)
return cmd.Run() == nil
}

84
internal/state/state.go Normal file
View file

@ -0,0 +1,84 @@
// Package state manages the persistent deployment state file (~/.wallarm/state.json).
package state
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// StateDir is where wallarm stores its state.
const StateDir = ".wallarm"
// State represents the persistent deployment state.
type State struct {
DeploymentType string `json:"deployment_type,omitempty"` // docker or native
CloudRegion string `json:"cloud_region,omitempty"` // US or EU
APIHost string `json:"api_host,omitempty"` // e.g., api.wallarm.com
APIToken string `json:"api_token,omitempty"` // Wallarm API token (sensitive)
Nodes []Node `json:"nodes"`
}
// Node represents a single deployed Wallarm node (docker container or native systemd unit).
type Node struct {
Name string `json:"name"`
Type string `json:"type"` // docker or native
Address string `json:"address,omitempty"` // listen address for native
Port int `json:"port,omitempty"` // ingress port for docker
UpstreamIP string `json:"upstream_ip,omitempty"` // docker
UpstreamPort int `json:"upstream_port,omitempty"` // docker
Status string `json:"status"` // running, stopped, unknown
CreatedAt string `json:"created_at"`
}
// Path returns the full path to the state file.
func Path() string {
home, err := os.UserHomeDir()
if err != nil {
home = "/root"
}
return filepath.Join(home, StateDir, "state.json")
}
// Load reads and parses the state file. Returns nil if the file doesn't exist.
func Load() (*State, error) {
p := Path()
data, err := os.ReadFile(p)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read state: %w", err)
}
var s State
if err := json.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("parse state: %w", err)
}
return &s, nil
}
// Save writes the state to disk, creating directories as needed.
func Save(s *State) error {
p := Path()
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
return fmt.Errorf("create state dir: %w", err)
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return fmt.Errorf("marshal state: %w", err)
}
if err := os.WriteFile(p, data, 0600); err != nil {
return fmt.Errorf("write state: %w", err)
}
return nil
}
// HasDeployment returns true if a state file exists with at least one node.
func HasDeployment() bool {
s, err := Load()
if err != nil || s == nil {
return false
}
return len(s.Nodes) > 0
}

146
internal/tunnel/tunnel.go Normal file
View file

@ -0,0 +1,146 @@
// Package tunnel provides a reverse SSH tunnel over TLS:443 via a Zoraxy edge proxy.
// It opens an outbound TLS connection to the configured jumphost, authenticates
// via SSH, and establishes a reverse port forward so you can reach the target VM
// through sechpoint.app.
package tunnel
import (
"crypto/tls"
"fmt"
"io"
"net"
"os"
"os/signal"
"time"
"golang.org/x/crypto/ssh"
)
// Config holds the tunnel connection parameters.
type Config struct {
Jumphost string // e.g., "ssh.sechpoint.app:443"
RemotePort int // Port on the jumphost that forwards to target's SSH
LocalSSHPort int // SSH port on the target VM (usually 22)
User string // SSH user on the jumphost
KeyPath string // Path to private key for authentication
KeyBytes []byte // Raw private key bytes (takes precedence over KeyPath)
}
// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
return Config{
Jumphost: "ssh.sechpoint.app:443",
RemotePort: 9042,
LocalSSHPort: 22,
User: "wallarm-tunnel",
}
}
// Start opens a reverse SSH tunnel over TLS and keeps it alive.
// It blocks until SIGINT or connection failure.
func Start(cfg Config) error {
// Load the private key
var signer ssh.Signer
if len(cfg.KeyBytes) > 0 {
var err error
signer, err = ssh.ParsePrivateKey(cfg.KeyBytes)
if err != nil {
return fmt.Errorf("parse embedded key: %w", err)
}
} else {
keyBytes, err := os.ReadFile(cfg.KeyPath)
if err != nil {
return fmt.Errorf("read key %s: %w", cfg.KeyPath, err)
}
signer, err = ssh.ParsePrivateKey(keyBytes)
if err != nil {
return fmt.Errorf("parse key: %w", err)
}
}
sshConfig := &ssh.ClientConfig{
User: cfg.User,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil // Accept all host keys (trusted infrastructure)
},
Timeout: 10 * time.Second,
}
// TLS dial to the Zoraxy edge (port 443)
tlsConn, err := tls.Dial("tcp", cfg.Jumphost, &tls.Config{
InsecureSkipVerify: false,
})
if err != nil {
return fmt.Errorf("TLS dial %s: %w", cfg.Jumphost, err)
}
// SSH over TLS
sshConn, chans, reqs, err := ssh.NewClientConn(tlsConn, cfg.Jumphost, sshConfig)
if err != nil {
tlsConn.Close()
return fmt.Errorf("SSH handshake: %w", err)
}
client := ssh.NewClient(sshConn, chans, reqs)
defer client.Close()
// Request reverse port forward: jumphost:RemotePort -> localhost:LocalSSHPort
remoteAddr := fmt.Sprintf("0.0.0.0:%d", cfg.RemotePort)
localAddr := fmt.Sprintf("localhost:%d", cfg.LocalSSHPort)
listener, err := client.Listen("tcp", remoteAddr)
if err != nil {
return fmt.Errorf("remote listen %s: %w", remoteAddr, err)
}
defer listener.Close()
fmt.Printf("Tunnel established: %s -> %s\n", remoteAddr, localAddr)
fmt.Printf("Connect: ssh -p %d root@%s\n", cfg.RemotePort, cfg.Jumphost)
// Handle incoming connections on the remote listener
go func() {
for {
remoteConn, err := listener.Accept()
if err != nil {
return
}
go forwardConnection(remoteConn, localAddr)
}
}()
// Keep alive until signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
// Heartbeat every 30s
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-sigCh:
fmt.Println("\nTunnel closed.")
return nil
case <-ticker.C:
_, _, err := client.SendRequest("keepalive@wallarm", true, nil)
if err != nil {
return fmt.Errorf("keepalive failed: %w", err)
}
}
}
}
func forwardConnection(remoteConn net.Conn, localAddr string) {
defer remoteConn.Close()
localConn, err := net.DialTimeout("tcp", localAddr, 10*time.Second)
if err != nil {
return
}
defer localConn.Close()
go func() {
io.Copy(localConn, remoteConn)
remoteConn.Close()
}()
io.Copy(remoteConn, localConn)
}

339
internal/ui/ui.go Normal file
View file

@ -0,0 +1,339 @@
// Package ui provides the bubbletea terminal UI for wallarm:
// - Wizard: guides new deployments (type → region → config → deploy)
// - Dashboard: lists existing nodes with actions (add, config, remove, tunnel)
package ui
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"git.sechpoint.app/customer-engineering/wallarm/internal/native"
"git.sechpoint.app/customer-engineering/wallarm/internal/preflight"
"git.sechpoint.app/customer-engineering/wallarm/internal/state"
)
// Styles
var (
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).MarginBottom(1)
goodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("227"))
badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true)
)
// Model is the top-level bubbletea model.
type Model struct {
state stateView
width int
height int
status string // feedback message during deployment
deploying bool // true while deployment is in progress
}
type stateView int
const (
viewPreflight stateView = iota
viewRoute
viewWizard
viewDashboard
viewDone
viewError
)
var preflightResult preflight.Result
// deployCompleteMsg is sent when deployment finishes.
type deployCompleteMsg struct {
err error
}
// Run starts the bubbletea TUI.
func Run(r preflight.Result) error {
preflightResult = r
m := Model{state: viewRoute}
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return err
}
return nil
}
func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "1", "2":
if m.state == viewWizard && !m.deploying {
return m, m.startDeploy()
}
case "enter":
if m.state == viewDone || m.state == viewError {
return m, tea.Quit
}
}
case deployCompleteMsg:
m.deploying = false
if msg.err != nil {
m.status = fmt.Sprintf("Deployment failed: %v", msg.err)
m.state = viewError
} else {
m.status = "Deployment successful!"
m.state = viewDone
}
return m, nil
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
switch m.state {
case viewRoute:
if state.HasDeployment() {
m.state = viewDashboard
} else {
m.state = viewWizard
}
}
return m, nil
}
func (m Model) View() string {
switch m.state {
case viewRoute:
return ""
case viewWizard:
return wizardView(m)
case viewDashboard:
return dashboardView(m)
case viewDone:
return goodStyle.Render("✅ " + m.status) + "\n\n" + dimStyle.Render("Press Enter to exit.")
case viewError:
return badStyle.Render("❌ " + m.status) + "\n\n" + dimStyle.Render("Press Enter to exit.")
}
return ""
}
// startDeploy runs the Huh form in a goroutine, then deploys.
func (m Model) startDeploy() tea.Cmd {
return func() tea.Msg {
m.deploying = true
s, err := runDeployForm()
if err != nil {
return deployCompleteMsg{err}
}
// Deploy native node
err = deployNative(s)
if err == nil {
state.Save(&s)
}
return deployCompleteMsg{err}
}
}
// ─── Wizard ──────────────────────────────────────────────────────────
func wizardView(m Model) string {
s := titleStyle.Render("🛡️ Wallarm Setup Wizard") + "\n"
if m.deploying {
s += dimStyle.Render("Configuring deployment — follow the prompts below...") + "\n"
return s
}
s += dimStyle.Render("No existing deployment found. Let's set one up.") + "\n\n"
s += activeStyle.Render("Wallarm Native Node Deployment") + "\n"
s += " Press Enter to begin configuration" + "\n\n"
s += activeStyle.Render("Step 2: Cloud region") + "\n"
r := preflightResult
if r.USReachable {
s += goodStyle.Render(" US") + " — us1.api.wallarm.com (reachable)\n"
} else {
s += dimStyle.Render(" US — not reachable") + "\n"
}
if r.EUReachable {
s += goodStyle.Render(" EU") + " — api.wallarm.com (reachable)\n"
} else {
s += dimStyle.Render(" EU — not reachable") + "\n"
}
s += dimStyle.Render("\nPress q to quit")
return s
}
// runDeployForm collects configuration via huh interactive forms.
func runDeployForm() (state.State, error) {
var s state.State
s.DeploymentType = "native"
// Step 1: Cloud region
regionOptions := []huh.Option[string]{}
if preflightResult.USReachable {
regionOptions = append(regionOptions, huh.NewOption("US (us1.api.wallarm.com)", "US"))
}
if preflightResult.EUReachable {
regionOptions = append(regionOptions, huh.NewOption("EU (api.wallarm.com)", "EU"))
}
if len(regionOptions) == 0 {
return s, fmt.Errorf("no cloud regions reachable")
}
var region string
if len(regionOptions) == 1 {
region = regionOptions[0].Value // auto-select
} else {
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Choose Wallarm cloud region").
Options(regionOptions...).
Value(&region),
),
).WithTheme(huh.ThemeCharm()).Run()
if err != nil {
return s, err
}
}
s.CloudRegion = region
if region == "US" {
s.APIHost = "us1.api.wallarm.com"
} else {
s.APIHost = "api.wallarm.com"
}
// Step 2: API Token
var apiToken string
err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Wallarm API Token (Deploy role)").
Placeholder("Paste your token here").
EchoMode(huh.EchoModePassword).
Value(&apiToken).
Validate(func(v string) error {
if len(v) < 10 {
return fmt.Errorf("token too short")
}
return nil
}),
),
).WithTheme(huh.ThemeCharm()).Run()
if err != nil {
return s, err
}
s.APIToken = apiToken
// Step 3: Node configuration
var nodeName, address, labels string
nodeForm := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Node name").Value(&nodeName).Validate(func(v string) error {
if v == "" {
return fmt.Errorf("required")
}
return nil
}),
huh.NewInput().Title("Listen address (IP:Port)").Placeholder("0.0.0.0:8081").Value(&address).Validate(func(v string) error {
if v == "" {
return fmt.Errorf("required")
}
return nil
}),
huh.NewInput().Title("Labels (optional)").Placeholder("group=prod").Value(&labels),
),
)
err = nodeForm.WithTheme(huh.ThemeCharm()).Run()
if err != nil {
return s, err
}
s.Nodes = append(s.Nodes, state.Node{
Name: nodeName,
Type: "native",
Address: address,
Status: "deploying",
CreatedAt: time.Now().Format(time.RFC3339),
})
return s, nil
}
// deployNative executes the native deployment.
func deployNative(s state.State) error {
if err := native.CreateNodesDir(); err != nil {
return err
}
if err := native.GenerateSystemdTemplate(); err != nil {
return err
}
for _, node := range s.Nodes {
labels := "group=" + node.Name
if err := native.InstallNode(node, s.APIToken, s.APIHost, labels); err != nil {
return err
}
}
return nil
}
// ─── Dashboard ───────────────────────────────────────────────────────
func dashboardView(m Model) string {
s, err := state.Load()
if err != nil || s == nil {
return badStyle.Render("Error loading state.") + "\nPress q to quit"
}
out := titleStyle.Render("📊 Wallarm Dashboard") + "\n"
out += fmt.Sprintf("Type: %s | Cloud: %s (%s)\n", s.DeploymentType, s.CloudRegion, s.APIHost)
out += dimStyle.Render(strings.Repeat("─", 50)) + "\n\n"
if len(s.Nodes) == 0 {
out += dimStyle.Render("No nodes deployed yet.") + "\n\n"
} else {
out += activeStyle.Render("Nodes:") + "\n"
for _, n := range s.Nodes {
marker := "●"
style := goodStyle
if n.Status != "running" {
marker = "○"
style = warnStyle
}
detail := n.Address
if detail == "" && n.Port != 0 {
detail = fmt.Sprintf(":%d → %s:%d", n.Port, n.UpstreamIP, n.UpstreamPort)
}
out += style.Render(fmt.Sprintf(" %s %s — %s", marker, n.Name, n.Status))
if detail != "" {
out += dimStyle.Render(fmt.Sprintf(" (%s)", detail))
}
out += "\n"
}
out += "\n"
}
out += "Actions:\n"
out += fmt.Sprintf(" %s\n", activeStyle.Render("[a] Add node"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[c] Configure"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[r] Remove node"))
out += fmt.Sprintf(" %s\n", activeStyle.Render("[t] Start tunnel"))
out += fmt.Sprintf(" %s\n", dimStyle.Render("[q] Quit"))
return out
}

498
native/wallarm-native.sh Executable file
View file

@ -0,0 +1,498 @@
#!/bin/bash
# ==============================================================================
# Wallarm Native Node Manager - Install, Configure, Remove, and Control
# ==============================================================================
# Unified single-script manager for the Wallarm Native Node (connector mode,
# NO Docker). Manages multiple isolated nodes under ${BASE_DIR}/nodes with a
# systemd template unit (wallarm-node@<name>.service).
#
# NOTE: This targets the Wallarm Native Node product (go-node, connector-server
# mode, all-in-one installer) - distinct from the NGINX-module based native
# deployment in ./wallarm-ct-deploy.sh.
#
# Commands:
# --preflight Run preflight checks only (no installation).
# --install Interactive installation of one or more nodes (parallel).
# --config Update an existing node's configuration.
# Options: --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
# --remove Remove a node completely.
# Options: --node NAME
# --status [NODE] Show systemd status for a node, or all nodes.
# --help|-h Show help.
# ==============================================================================
set -euo pipefail
# Script location and shared library (logging, detection, connectivity, validation)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./wallarm-lib.sh
source "$SCRIPT_DIR/wallarm-lib.sh"
# --- Constants ---
BASE_DIR="/opt/wallarm"
NODES_DIR="${BASE_DIR}/nodes"
SYSTEMD_TEMPLATE="/etc/systemd/system/wallarm-node@.service"
# Wallarm Native Node all-in-one installer (latest, x86_64 by default)
# Override WALLARM_INSTALLER_URL to pin a version; WALLARM_INSTALLER_ARCH to
# select the architecture suffix.
INSTALLER_BASE_URL="https://repo.wallarm.com/linux/wallarm-native-node/latest/all-in-one"
INSTALLER_ARCH="${WALLARM_INSTALLER_ARCH:-x86_64}"
INSTALLER_URL="${WALLARM_INSTALLER_URL:-${INSTALLER_BASE_URL}/wallarm-native-node-aio-${INSTALLER_ARCH}-latest.sh}"
# Wallarm cloud endpoints (for connectivity checks)
EU_DATA_NODES=("api.wallarm.com" "node-data0.eu1.wallarm.com" "node-data1.eu1.wallarm.com")
US_DATA_NODES=("us1.api.wallarm.com" "node-data0.us1.wallarm.com" "node-data1.us1.wallarm.com")
# Cloud region selection (populated by preflight, used by select_cloud_region)
US_CLOUD_REACHABLE="false"
EU_CLOUD_REACHABLE="false"
CLOUD_REGION=""
API_HOST=""
# --- Helper functions ---
log() { echo ">>> $*"; }
err() { echo "!!! $*" >&2; }
check_root() {
if [[ $EUID -ne 0 ]]; then
err "This script must be run as root (for systemd and /opt write access)."
exit 1
fi
}
ensure_dirs() {
mkdir -p "${NODES_DIR}"
}
generate_systemd_template() {
if [[ ! -f "${SYSTEMD_TEMPLATE}" ]]; then
log "Creating systemd template: ${SYSTEMD_TEMPLATE}"
cat > "${SYSTEMD_TEMPLATE}" <<EOF
[Unit]
Description=Wallarm Native Node - %I
After=network.target
[Service]
Type=simple
WorkingDirectory=${NODES_DIR}/%i
EnvironmentFile=${NODES_DIR}/%i/env
ExecStart=${NODES_DIR}/%i/wallarm-native-node-aio.sh start
ExecStop=${NODES_DIR}/%i/wallarm-native-node-aio.sh stop
Restart=on-failure
RestartSec=5
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
fi
}
write_env_file() {
local node_name="$1"
local api_token="$2"
local api_host="$3"
local labels="$4"
local config_path="${NODES_DIR}/${node_name}/etc/go-node.yaml"
local env_file="${NODES_DIR}/${node_name}/env"
cat > "${env_file}" <<EOF
WALLARM_API_TOKEN=${api_token}
WALLARM_API_HOST=${api_host}
WALLARM_LABELS=${labels}
WALLARM_CONFIG_PATH=${config_path}
EOF
chmod 600 "${env_file}" # token is sensitive
}
# --- Preflight checks ---
run_preflight() {
local failed=0
log "Running preflight checks..."
# 1. Root privileges
if [[ $EUID -ne 0 ]]; then
err "Preflight failed: must be run as root."
failed=1
fi
# 2. Init system must be systemd (template unit management)
local init_system
init_system=$(detect_init_system)
if [[ "$init_system" != "systemd" ]]; then
err "Preflight failed: this manager requires systemd (detected: $init_system)."
failed=1
fi
# 3. Architecture
local arch
arch=$(detect_architecture)
if [[ "$arch" != "x86_64" && "$arch" != "aarch64" ]]; then
err "Preflight failed: unsupported architecture '$arch' for the native node installer."
failed=1
fi
log "Architecture: ${arch} (installer suffix: ${INSTALLER_ARCH})"
# 4. Required commands
local required_cmds=(curl systemctl sed mkdir rm sleep)
local missing=()
local cmd
for cmd in "${required_cmds[@]}"; do
if ! command_exists "$cmd"; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
err "Preflight failed: missing required commands: ${missing[*]}"
failed=1
fi
# 5. Installer reachability
if ! test_connectivity "$INSTALLER_URL" "Wallarm native node installer"; then
err "Preflight failed: installer not reachable: $INSTALLER_URL"
failed=1
fi
# 6. Wallarm cloud reachability (must have at least one reachable region)
local us eu
us=$(test_cloud_endpoints "US" "${US_DATA_NODES[@]}")
eu=$(test_cloud_endpoints "EU" "${EU_DATA_NODES[@]}")
US_CLOUD_REACHABLE="$us"
EU_CLOUD_REACHABLE="$eu"
if [[ "$us" != "true" && "$eu" != "true" ]]; then
err "Preflight failed: no Wallarm cloud region reachable (US/EU)."
failed=1
fi
# 7. Disk space (>= 2GB free on the nodes volume)
local avail_kb
avail_kb=$(df -k "$BASE_DIR" 2>/dev/null | awk 'NR==2 {print $4}' || true)
if [[ -n "$avail_kb" ]] && (( avail_kb < 2097152 )); then
err "Preflight failed: insufficient disk space on $BASE_DIR (need >= 2GB free)."
failed=1
fi
# 8. Memory (>= 2GB recommended; warning only)
if command_exists free; then
local mem_mb
mem_mb=$(free -m 2>/dev/null | awk '/Mem:/ {print $2}')
if [[ -n "$mem_mb" ]] && (( mem_mb < 2048 )); then
err "Warning: only ${mem_mb}MB RAM detected (2GB+ recommended)."
fi
fi
if [[ $failed -ne 0 ]]; then
err "Preflight check FAILED. Resolve the issues above and re-run."
return 1
fi
log "Preflight checks passed."
return 0
}
# Validate a listen address (IP:PORT) and check its port is free
check_listen_port() {
local address="$1"
local port="${address##*:}"
if [[ ! "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
err "Invalid listen address (expected IP:PORT): $address"
return 1
fi
if ! check_port_available "$port"; then
err "Listen port $port (for $address) is already in use."
return 1
fi
return 0
}
# --- Core actions ---
install_single_node() {
local node_name="$1"
local listen_address="$2"
local api_token="$3"
local api_host="$4"
local labels="${5:-group=${node_name}}"
local work_dir="${NODES_DIR}/${node_name}"
local installer_path="${work_dir}/wallarm-native-node-aio.sh"
log "[${node_name}] Installing (listening on ${listen_address})..."
mkdir -p "${work_dir}/etc" "${work_dir}/var/log" "${work_dir}/var/run"
# 1. Write config
cat > "${work_dir}/etc/go-node.yaml" <<EOF
mode: connector-server
connector:
address: "${listen_address}"
EOF
# 2. Download installer if missing
if [[ ! -f "${installer_path}" ]]; then
log "[${node_name}] Downloading installer..."
curl -fsSL -o "${installer_path}" "${INSTALLER_URL}" || {
err "[${node_name}] Download failed"
return 1
}
chmod +x "${installer_path}"
fi
# 3. Write environment file (used by systemd and manual scripts)
write_env_file "${node_name}" "${api_token}" "${api_host}" "${labels}"
# 4. Run installer
cd "${work_dir}" || return 1
if WALLARM_API_TOKEN="${api_token}" \
WALLARM_API_HOST="${api_host}" \
WALLARM_LABELS="${labels}" \
WALLARM_CONFIG_PATH="${work_dir}/etc/go-node.yaml" \
./wallarm-native-node-aio.sh install \
-- --config-dir "${work_dir}/etc" \
-- --log-dir "${work_dir}/var/log" \
-- --pid-dir "${work_dir}/var/run" \
> "${work_dir}/install.log" 2>&1; then
log "[${node_name}] Installation successful."
# Enable and start the systemd service
systemctl enable "wallarm-node@${node_name}" 2>/dev/null || true
systemctl start "wallarm-node@${node_name}"
log "[${node_name}] Service started (systemctl status wallarm-node@${node_name})"
else
err "[${node_name}] Installation failed. Check ${work_dir}/install.log"
return 1
fi
}
cmd_preflight() {
check_root
echo ""
if run_preflight; then
log "Preflight passed - system ready for --install."
exit 0
else
exit 1
fi
}
cmd_install() {
check_root
if ! run_preflight; then
exit 1
fi
ensure_dirs
generate_systemd_template
read -p "Enter Wallarm API Token (with Deploy role): " WALLARM_API_TOKEN
if [[ -z "$WALLARM_API_TOKEN" ]]; then
err "API Token cannot be empty."
exit 1
fi
# Select Wallarm cloud region (US/EU)
select_cloud_region
echo ""
echo "Enter each node's name and listening address (format: name IP:Port)"
echo "Example: node1 0.0.0.0:8081"
echo "Leave name blank to finish."
declare -a NODE_NAMES=()
declare -a NODE_ADDRESSES=()
while true; do
read -p "Node name (blank to stop): " name
[[ -z "$name" ]] && break
read -p "Listening address (e.g., 0.0.0.0:8081): " address
if [[ -z "$address" ]]; then
err "Address cannot be empty, skipping."
continue
fi
NODE_NAMES+=("$name")
NODE_ADDRESSES+=("$address")
done
if [[ ${#NODE_NAMES[@]} -eq 0 ]]; then
err "No nodes provided."
exit 1
fi
# Validate listen ports before installing anything
local address
for address in "${NODE_ADDRESSES[@]}"; do
if ! check_listen_port "$address"; then
exit 1
fi
done
echo ""
echo "Will install ${#NODE_NAMES[@]} nodes in parallel:"
for i in "${!NODE_NAMES[@]}"; do
echo " - ${NODE_NAMES[$i]} -> ${NODE_ADDRESSES[$i]}"
done
read -p "Proceed? (y/N): " confirm
[[ ! "$confirm" =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; }
echo ""
log "Starting parallel installations..."
declare -a INSTALL_PIDS=()
for i in "${!NODE_NAMES[@]}"; do
install_single_node "${NODE_NAMES[$i]}" "${NODE_ADDRESSES[$i]}" "$WALLARM_API_TOKEN" "$API_HOST" &
INSTALL_PIDS+=($!)
done
FAILED=0
local pid
for pid in "${INSTALL_PIDS[@]}"; do
wait "$pid" || ((FAILED++))
done
if [[ $FAILED -eq 0 ]]; then
log "All nodes installed and started via systemd."
else
err "$FAILED node(s) failed. Check individual install.log files."
fi
}
cmd_config() {
# Usage: --config --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
check_root
local node_name="" address="" token="" labels=""
while [[ $# -gt 0 ]]; do
case "$1" in
--node) [[ $# -ge 2 ]] || { err "--node requires a value"; exit 1; }; node_name="$2"; shift 2 ;;
--address) [[ $# -ge 2 ]] || { err "--address requires a value"; exit 1; }; address="$2"; shift 2 ;;
--token) [[ $# -ge 2 ]] || { err "--token requires a value"; exit 1; }; token="$2"; shift 2 ;;
--labels) [[ $# -ge 2 ]] || { err "--labels requires a value"; exit 1; }; labels="$2"; shift 2 ;;
*) err "Unknown config option: $1"; exit 1 ;;
esac
done
if [[ -z "$node_name" ]]; then
err "Missing --node"
exit 1
fi
local work_dir="${NODES_DIR}/${node_name}"
if [[ ! -d "$work_dir" ]]; then
err "Node '$node_name' does not exist in ${NODES_DIR}"
exit 1
fi
# Update config file
if [[ -n "$address" ]]; then
if ! check_listen_port "$address"; then
exit 1
fi
log "Updating listening address to $address"
sed -i "s|^\([[:space:]]*address: \).*|\1\"${address}\"|" "${work_dir}/etc/go-node.yaml"
fi
# Update env file if token or labels provided (rewrite to avoid sed escaping issues)
if [[ -n "$token" || -n "$labels" ]]; then
local env_file="${work_dir}/env"
[[ -f "$env_file" ]] || { err "env file not found"; exit 1; }
local current_token current_labels
current_token=$(grep '^WALLARM_API_TOKEN=' "$env_file" | cut -d= -f2-)
current_labels=$(grep '^WALLARM_LABELS=' "$env_file" | cut -d= -f2-)
write_env_file "$node_name" "${token:-$current_token}" "${labels:-$current_labels}"
log "Token/labels updated for $node_name."
fi
log "Configuration updated for $node_name. Restart with: systemctl restart wallarm-node@${node_name}"
}
cmd_remove() {
check_root
local node_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--node) [[ $# -ge 2 ]] || { err "--node requires a value"; exit 1; }; node_name="$2"; shift 2 ;;
*) err "Unknown remove option: $1"; exit 1 ;;
esac
done
if [[ -z "$node_name" ]]; then
err "Missing --node"
exit 1
fi
local work_dir="${NODES_DIR}/${node_name}"
if [[ ! -d "$work_dir" ]]; then
err "Node '$node_name' does not exist."
exit 1
fi
log "Stopping and disabling service..."
systemctl stop "wallarm-node@${node_name}" 2>/dev/null || true
systemctl disable "wallarm-node@${node_name}" 2>/dev/null || true
log "Removing directory ${work_dir}..."
rm -rf "$work_dir"
log "Node $node_name removed."
}
cmd_status() {
# Show systemd status for all found nodes or a specific one
local node_name="${1:-}"
if [[ -n "$node_name" ]]; then
systemctl status "wallarm-node@${node_name}" --no-pager
else
echo "Wallarm Nodes status:"
local dir name
for dir in "${NODES_DIR}"/*/; do
if [[ -d "$dir" ]]; then
name=$(basename "$dir")
echo "--- $name ---"
systemctl status "wallarm-node@${name}" --no-pager | head -5
echo ""
fi
done
fi
}
# --- Help ---
show_help() {
cat <<EOF
Usage: $0 [COMMAND] [OPTIONS]
Commands:
--preflight Run preflight checks only (no installation).
--install Interactive installation of one or more nodes (parallel).
--config Update an existing node's configuration.
Options: --node NAME --address IP:PORT [--token TOKEN] [--labels LABELS]
--remove Remove a node completely.
Options: --node NAME
--status [NODE] Show systemd status for a node, or all nodes.
Environment:
WALLARM_INSTALLER_URL Override the all-in-one installer URL (default: repo.wallarm.com latest).
WALLARM_INSTALLER_ARCH Installer architecture suffix (default: x86_64).
Examples:
$0 --preflight
$0 --install
$0 --config --node node1 --address 0.0.0.0:9090
$0 --remove --node node2
$0 --status
EOF
}
# --- Main argument parsing ---
if [[ $# -eq 0 ]]; then
show_help
exit 0
fi
case "$1" in
--preflight) shift; cmd_preflight "$@" ;;
--install) shift; cmd_install "$@" ;;
--config) shift; cmd_config "$@" ;;
--remove) shift; cmd_remove "$@" ;;
--status) shift; cmd_status "$@" ;;
--help|-h) show_help ;;
*) err "Unknown command: $1"; show_help; exit 1 ;;
esac

View file

@ -1,26 +0,0 @@
#!/bin/bash
# remove.sh — uninstall a running Wallarm node
set -e
NAME="${1:-}"
if [ -z "$NAME" ]; then
echo "Usage: $0 <node-name>"
echo " $0 --all Remove all nodes"
exit 1
fi
if [ "$NAME" = "--all" ]; then
docker ps -q --filter 'name=wallarm-' | xargs -r docker rm -f
rm -f /opt/fw/app/state.json
echo "All nodes removed."
else
docker rm -f "wallarm-${NAME}" 2>/dev/null
python3 -c "
import json
try:
with open('/opt/fw/app/state.json') as f: s = json.load(f)
s['nodes'] = [n for n in s.get('nodes',[]) if n['name'] != '${NAME}']
with open('/opt/fw/app/state.json','w') as f: json.dump(s, f, indent=2)
except: pass
" 2>/dev/null || true
echo "Node ${NAME} removed."
fi

276
setup.sh Normal file → Executable file
View file

@ -1,129 +1,191 @@
#!/bin/bash
# ==============================================================================
# Wallarm Docker Deployment Bootstrap
# Wallarm Deployment Setup Script
# ==============================================================================
# Flow: check connectivity → install Docker → download deploy script
# All under /opt/fw/ — self-contained, user-editable config.
# Downloads Wallarm deployment scripts from the Git repository and places
# them in a single deploy/ directory.
#
# curl -fsSL ".../setup.sh" | bash
# sudo /opt/fw/deploy.sh
# When run interactively (or piped via curl|bash to a terminal), asks which
# deployment type to download:
# 1. docker - Wallarm filtering node as a Docker container
# 2. native - Wallarm filtering node installed directly on the OS (no Docker)
# 3. both - All scripts (docker + native)
#
# Falls back to downloading BOTH only when fully headless (no /dev/tty).
# Override with the DEPLOYMENT_TYPE env var:
# DEPLOYMENT_TYPE=native curl -fsSL ".../setup.sh" | bash
# DEPLOYMENT_TYPE=docker curl -fsSL ".../setup.sh" | bash
#
# Downloads the repo archive once, then copies only the needed folders
# (common + docker and/or native) into deploy/. No per-file curl calls.
# ==============================================================================
set -euo pipefail
BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
REPO="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main"
WALLARM_DIR="/opt/fw"
OFFLINE_DIR="${WALLARM_DIR}/offline"
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
echo -e "${BOLD}Wallarm Bootstrap${NC}"
# Git repository archive URL (Gitea-style)
REPO_ARCHIVE="https://git.sechpoint.app/customer-engineering/wallarm/archive/main.tar.gz"
# ── 1. Connectivity check ─────────────────────────────────────────────
echo -e "${YELLOW}Checking connectivity...${NC}"
if curl -fsSL --connect-timeout 5 -o /dev/null https://git.sechpoint.app 2>/dev/null; then
echo -e "${GREEN} git.sechpoint.app reachable${NC}"
# Temp directory for extracted archive
TEMP_DIR=$(mktemp -d /tmp/wallarm-setup.XXXXXX)
trap 'rm -rf "$TEMP_DIR"' EXIT
# Detect download command
if command -v curl >/dev/null 2>&1; then
DOWNLOAD_NAME="curl"
elif command -v wget >/dev/null 2>&1; then
DOWNLOAD_NAME="wget"
else
echo -e "${RED} Cannot reach git.sechpoint.app${NC}"
echo -e "${RED} Check network/firewall. Aborting.${NC}"
echo -e "${RED}${BOLD}ERROR:${NC} Neither curl nor wget is installed."
echo -e "Please install one of them and run this script again."
exit 1
fi
# ── 2. Ensure curl (for downloads) ────────────────────────────────────
if ! command -v curl >/dev/null 2>&1; then
apt-get update -qq 2>/dev/null && apt-get install -y -qq curl 2>/dev/null || true
fi
# --- Archive download and extraction ---
# ── 3. Ensure Python 3.10+ ────────────────────────────────────────────
PYTHON_BIN=""
if command -v python3 >/dev/null 2>&1 && python3 -c "import sys; exit(0 if sys.version_info >= (3,10) else 1)" 2>/dev/null; then
PYTHON_BIN="python3"
else
echo -e "${YELLOW}Installing Python 3...${NC}"
apt-get update -qq 2>/dev/null && apt-get install -y -qq python3 2>/dev/null || \
yum install -y -q python3 2>/dev/null || \
dnf install -y -q python3 2>/dev/null || \
apk add --no-cache python3 2>/dev/null || true
PYTHON_BIN="python3"
fi
echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}"
# ── 4. Install Docker under /opt/fw/docker/ ───────────────────────────
DOCKER_DIR="${WALLARM_DIR}/docker"
DOCKER_URL="${DOCKER_DOWNLOAD_URL:-https://download.docker.com/linux/static/stable/x86_64/docker-29.2.1.tgz}"
WALLARM_IMAGE_URL="${WALLARM_IMAGE_URL:-https://git.sechpoint.app/customer-engineering/wallarm/releases/download/v1.0.0/wallarm-node-6.11.0-rc1.tar.gz}"
if ! command -v docker >/dev/null 2>&1; then
echo -e "${YELLOW}Downloading Docker...${NC}"
mkdir -p "${DOCKER_DIR}/bin" "${DOCKER_DIR}/data"
if [ -f "${OFFLINE_DIR}/docker-29.2.1.tgz" ]; then
echo -e "${GREEN} Using offline binary${NC}"
tar -xzf "${OFFLINE_DIR}/docker-29.2.1.tgz" -C "${DOCKER_DIR}/bin/"
download_archive() {
echo -e "${YELLOW}Downloading deployment scripts from repository...${NC}"
if [ "$DOWNLOAD_NAME" = "curl" ]; then
curl -fsSL "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null
else
curl -fsSL "${DOCKER_URL}" -o /tmp/docker.tgz 2>/dev/null && \
tar --strip-components=1 -xzf /tmp/docker.tgz -C "${DOCKER_DIR}/bin/" && \
rm -f /tmp/docker.tgz
wget -qO- "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null
fi
# Start dockerd with custom data root
if [ -x "${DOCKER_DIR}/bin/dockerd" ]; then
# Symlink binaries
ln -sf "${DOCKER_DIR}/bin/docker" /usr/bin/docker 2>/dev/null
ln -sf "${DOCKER_DIR}/bin/dockerd" /usr/bin/dockerd 2>/dev/null
# Start containerd first, then dockerd — and make them reboot-resistant
cat > /etc/systemd/system/wallarm-containerd.service << UNIT
[Unit]
Description=Wallarm containerd
After=network.target
[Service]
ExecStart=${DOCKER_DIR}/bin/containerd
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
cat > /etc/systemd/system/wallarm-dockerd.service << UNIT
[Unit]
Description=Wallarm Docker daemon
After=wallarm-containerd.service
Requires=wallarm-containerd.service
[Service]
ExecStart=${DOCKER_DIR}/bin/dockerd --data-root ${DOCKER_DIR}/data --exec-root ${DOCKER_DIR}/exec --pidfile ${DOCKER_DIR}/docker.pid --userland-proxy=false
Restart=always
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable wallarm-containerd wallarm-dockerd
systemctl start wallarm-containerd wallarm-dockerd
export PATH="${DOCKER_DIR}/bin:$PATH"
if [[ ! -d "$TEMP_DIR/common" ]]; then
echo -e "${RED}${BOLD}ERROR:${NC} Failed to download or extract repository archive."
echo -e "Check network connectivity to: ${REPO_ARCHIVE}"
exit 1
fi
fi
# ── 5. Pre-load Wallarm image if available ────────────────────────────
if command -v docker >/dev/null 2>&1; then
if [ -f "${OFFLINE_DIR}/wallarm-node.tar" ]; then
echo -e "${YELLOW}Loading offline Wallarm image...${NC}"
docker load -i "${OFFLINE_DIR}/wallarm-node.tar" 2>/dev/null || true
fi
fi
# ── 6. Download deploy script ─────────────────────────────────────────
APP_DIR="${WALLARM_DIR}/app"
mkdir -p "${APP_DIR}"
echo -e "${YELLOW}Downloading deploy script...${NC}"
curl -fsSL "${REPO}/sources/python/main.py" -o "${APP_DIR}/main.py"
curl -fsSL "${REPO}/../deploy.sh" -o "${WALLARM_DIR}/deploy.sh" 2>/dev/null || {
# Fallback: create wrapper
cat > "${WALLARM_DIR}/deploy.sh" << 'WRAPPER'
#!/bin/bash
exec python3 /opt/fw/app/main.py "$@"
WRAPPER
echo -e "${GREEN} Success: archive downloaded and extracted.${NC}"
}
chmod +x "${APP_DIR}/main.py" "${WALLARM_DIR}/deploy.sh"
deploy_type() {
local type="$1"
# Copy common library (always needed)
cp "$TEMP_DIR/common/"* "deploy/" 2>/dev/null || true
# Copy the selected deployment type's folder
if [[ -d "$TEMP_DIR/$type" ]]; then
cp "$TEMP_DIR/$type/"* "deploy/" 2>/dev/null || true
chmod +x deploy/*.sh 2>/dev/null || true
echo -e "${GREEN} Copied: common + ${type} scripts → deploy/${NC}"
else
echo -e "${RED} Folder '$type' not found in archive.${NC}"
exit 1
fi
}
# Main
clear 2>/dev/null || true
echo -e "${BLUE}${BOLD}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ WALLARM DEPLOYMENT SETUP SCRIPT ║"
echo "║ Downloads all necessary deployment tools ║"
echo "╚══════════════════════════════════════════════════════════════╝${NC}"
echo
# Decide which deployment type(s) to download
DEPLOY_TYPES=()
# Priority 1: explicit DEPLOYMENT_TYPE env var (non-interactive override)
if [[ "${DEPLOYMENT_TYPE:-}" =~ ^(docker|native)$ ]]; then
DEPLOY_TYPES+=("$DEPLOYMENT_TYPE")
echo -e "${GREEN}Downloading only: $DEPLOYMENT_TYPE (from DEPLOYMENT_TYPE env var)${NC}"
# Priority 2 + 3: try interactive prompt via terminal or /dev/tty
else
# Determine where we can read user input from
INTERACTIVE=""
if [[ -t 0 ]]; then
INTERACTIVE="/dev/stdin"
elif [[ -c /dev/tty ]]; then
INTERACTIVE="/dev/tty"
fi
if [[ -n "$INTERACTIVE" ]]; then
echo -e "${CYAN}Which deployment type do you need?${NC}"
echo -e " ${YELLOW}1${NC}) Docker only — Wallarm node as a container"
echo -e " ${YELLOW}2${NC}) Native only — Wallarm node directly on this OS (no Docker)"
echo -e " ${YELLOW}3${NC}) Both — docker + native scripts"
echo
while true; do
read -r -p "$(echo -e "${YELLOW}Enter choice [1/2/3]: ${NC}")" choice <"$INTERACTIVE" 2>/dev/null || {
echo -e "${YELLOW}Input unavailable, defaulting to BOTH.${NC}"
DEPLOY_TYPES+=("docker" "native")
break
}
case "$choice" in
1) DEPLOY_TYPES+=("docker"); break ;;
2) DEPLOY_TYPES+=("native"); break ;;
3) DEPLOY_TYPES+=("docker" "native"); break ;;
*) echo -e "${RED}Invalid choice. Enter 1, 2, or 3.${NC}" ;;
esac
done
echo
else
# Fully headless — default to both
DEPLOY_TYPES+=("docker" "native")
echo -e "${CYAN}No terminal available: downloading BOTH deployment types.${NC}"
echo -e "${YELLOW}To download only one type, set DEPLOYMENT_TYPE=docker or DEPLOYMENT_TYPE=native${NC}"
fi
fi
echo
# Download archive once, then copy only the selected folders
echo
mkdir -p "deploy"
download_archive
for type in "${DEPLOY_TYPES[@]}"; do
deploy_type "$type"
done
# Clean exit trap removes temp dir
# Patch library sourcing path for flat deploy/ structure
# (repo scripts still reference ../common/ — fix until they're updated)
for script in deploy/*.sh; do
[[ "$script" == "deploy/wallarm-lib.sh" ]] && continue
sed -i 's|source "\$SCRIPT_DIR/\.\./common/wallarm-lib\.sh"|source "$SCRIPT_DIR/wallarm-lib.sh"|' "$script"
sed -i 's|# shellcheck source=\.\./common/wallarm-lib\.sh|# shellcheck source=./wallarm-lib.sh|' "$script"
done
echo
echo -e "${GREEN}${BOLD}Ready!${NC}"
echo -e " ${GREEN}sudo /opt/fw/deploy.sh${NC}"
echo -e "${GREEN}${BOLD}Setup complete - requested scripts downloaded!${NC}"
echo
echo -e " Config: /opt/fw/app/fw.conf"
echo -e " Docker: /opt/fw/docker/"
echo -e " Offline: /opt/fw/offline/ (drop binaries here for air-gapped)"
# Show next steps only for the deployment types that were downloaded
for deploy_type in "${DEPLOY_TYPES[@]}"; do
case "$deploy_type" in
docker)
echo -e "${CYAN}Docker deployment next steps:${NC}"
echo -e " 1. Run the preflight check: ${YELLOW}./deploy/wallarm-docker.sh --preflight${NC}"
echo -e " 2. Deploy a Wallarm node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --install${NC}"
echo -e " 3. Reconfigure existing node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --config${NC}"
echo -e " 4. Uninstall a node: ${YELLOW}sudo ./deploy/wallarm-docker.sh --remove${NC}"
echo -e " 5. Show node status: ${YELLOW}./deploy/wallarm-docker.sh --status${NC}"
echo
;;
native)
echo -e "${CYAN}Native deployment next steps (no Docker):${NC}"
echo -e " 1. Run the preflight check: ${YELLOW}sudo ./deploy/wallarm-native.sh --preflight${NC}"
echo -e " 2. Deploy Wallarm nodes: ${YELLOW}sudo ./deploy/wallarm-native.sh --install${NC}"
echo -e " 3. Update a node's config: ${YELLOW}sudo ./deploy/wallarm-native.sh --config --node NAME --address IP:PORT${NC}"
echo -e " 4. Remove a node: ${YELLOW}sudo ./deploy/wallarm-native.sh --remove --node NAME${NC}"
echo -e " 5. Show node status: ${YELLOW}./deploy/wallarm-native.sh --status${NC}"
echo
;;
esac
done
echo -e "${YELLOW}Note: Some scripts require sudo. Run them with: sudo ./<script>${NC}"
echo -e "${YELLOW}Make sure you have the required information ready (see documentation).${NC}"

View file

@ -1 +0,0 @@
Docker static binaries for offline/air-gapped deployment.

View file

@ -1 +0,0 @@
Wallarm Docker images for offline/air-gapped deployment.

View file

@ -1,252 +0,0 @@
#!/usr/bin/env python3
"""Wallarm Docker Node Manager"""
import json, os, sys, time, subprocess, shutil
VERSION = "2.0.0"
BASE = "/opt/fw"
APP = f"{BASE}/app"
CONF = f"{APP}/fw.conf"
STATE = f"{APP}/state.json"
DOCKER_DIR = f"{BASE}/docker"
OFFLINE_DIR = f"{BASE}/offline"
WALLARM_IMAGE = os.environ.get("WALLARM_IMAGE", "wallarm/node:6.11.0-rc1")
# Ensure Docker is in PATH
os.environ["PATH"] = f"{DOCKER_DIR}/bin:{os.environ.get('PATH', '')}"
def run(cmd, timeout=120):
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
def load_config():
if not os.path.exists(CONF): return {}
with open(CONF) as f: return json.load(f)
def load_state():
if not os.path.exists(STATE): return {"nodes": []}
with open(STATE) as f: return json.load(f)
def save_state(s):
os.makedirs(APP, exist_ok=True)
with open(STATE, "w") as f: json.dump(s, f, indent=2)
# ─── Docker ────────────────────────────────────────────────────────────
def install_docker():
if shutil.which("docker"):
return
print("Installing Docker...")
run("apt-get update -qq && apt-get install -y -qq docker.io 2>/dev/null || yum install -y -q docker 2>/dev/null", timeout=120)
run("systemctl enable docker && systemctl start docker")
def deploy_container(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
# Ensure Docker is running
install_docker()
# Pull image if needed
if run(f"docker images -q {WALLARM_IMAGE}").stdout.strip() == "":
print(f"[{name}] Pulling Wallarm image...")
run(f"docker pull {WALLARM_IMAGE}", timeout=300)
# Remove old container if exists
container = f"wallarm-{name}"
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
# Run container
monitoring_port = int(port) + 10
print(f"[{name}] Starting container on port {port}...")
cmd = f"""docker run -d \
--name {container} \
--restart unless-stopped \
-p {port}:{port} \
-v wallarm-data-{name}:/opt/wallarm \
-p {monitoring_port}:{monitoring_port} \
-e WALLARM_API_TOKEN={token} \
-e WALLARM_API_HOST={api_host} \
-e WALLARM_MODE={mode} \
-e NGINX_PORT={port}"""
if upstream_ip:
cmd += f" -e WALLARM_UPSTREAM=http://{upstream_ip}:{upstream_port}"
cmd += f" {WALLARM_IMAGE}"
r = run(cmd, timeout=30)
if r.returncode != 0:
print(f"{name}: {r.stderr}")
return False
# Save state
s = load_state()
s.setdefault("nodes", []).append({
"name": name, "type": "docker", "port": int(port),
"upstream_ip": upstream_ip, "upstream_port": int(upstream_port),
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
})
save_state(s)
print(f"{name} running on port {port} (docker ps --filter name={container})")
return True
def remove_container(name):
container = f"wallarm-{name}"
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
shutil.rmtree(f"/opt/wallarm-{name}", ignore_errors=True)
s = load_state()
s["nodes"] = [n for n in s.get("nodes", []) if n["name"] != name]
save_state(s)
print(f"{name} removed")
def container_status():
r = run("docker ps --filter 'name=wallarm-' --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'")
if not r.stdout.strip():
print("No containers running.")
return
print("\n─── Containers ───")
for line in r.stdout.strip().split("\n"):
print(f" {line}")
# ─── Deploy All ───────────────────────────────────────────────────────
def deploy_all():
cfg = load_config()
nodes = cfg.get("nodes", cfg) # support both {"nodes":{...}} and flat format
if not nodes:
print("No nodes in fw.conf")
return
for name, nc in nodes.items():
if not isinstance(nc, dict): continue
deploy_container(name, nc["token"], nc.get("cloud","EU"),
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
str(nc.get("upstream_port","80")), nc.get("labels",""),
nc.get("mode","monitoring"))
# ─── Menu ─────────────────────────────────────────────────────────────
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
return deploy_all()
print(f"═══ Wallarm Node Manager v{VERSION} ═══\n")
print("Preflight checks...")
if not shutil.which("docker"):
print(" ❌ Docker not found. Run setup.sh first.")
sys.exit(1)
print(" ✅ Docker ready\n")
cfg = load_config()
deployed = {n["name"] for n in load_state().get("nodes", [])}
while True:
undeployed = [k for k in cfg if k not in deployed]
print("─── Menu ───")
if undeployed:
print(f" Nodes ready to deploy: {', '.join(undeployed)}")
else:
print(" No nodes pending deployment.")
if undeployed:
print(" [1] Deploy all pending nodes")
print(" [2] Add a new node")
if deployed:
print(" [3] Show status")
print(" [4] Remove a node")
print(" [q] Quit")
c = input("\nChoice: ").strip()
if c == "1" and undeployed:
for name in undeployed:
nc = cfg[name]
print(f"\nDeploying {name}...")
deploy_container(name, nc["token"], nc.get("cloud","EU"),
str(nc.get("port","8081")), nc.get("upstream_ip",""),
str(nc.get("upstream_port","80")), "",
nc.get("mode","monitoring"))
elif c == "2":
add_node()
cfg = load_config()
deployed = {n["name"] for n in load_state().get("nodes", [])}
elif c == "3" and deployed:
container_status()
elif c == "4" and deployed:
remove_menu()
elif c.lower() == "q":
break
def create_config():
print("\n─── Create Node Config ───")
name = input("Node name: ").strip()
if not name: return
token = input("Wallarm token: ").strip()
cloud = input("Cloud [EU]: ").strip() or "EU"
port = input("Port [8081]: ").strip() or "8081"
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
upstream_port = input("Upstream port [80]: ").strip() or "80"
mode = input("Mode [monitoring]: ").strip() or "monitoring"
cfg = load_config()
cfg[name] = {"token": token, "cloud": cloud, "port": port,
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
"labels": f"group={name}", "mode": mode}
os.makedirs(APP, exist_ok=True)
with open(CONF, "w") as f:
json.dump(cfg, f, indent=2)
print(f"{name} added to fw.conf")
def add_node():
print("\n─── Add New Node ───")
name = input("Node name: ").strip()
if not name: return
token = input("Wallarm token: ").strip()
if not token: print("Token required."); return
cloud = input("Cloud region [EU]: ").strip() or "EU"
port = input("Port [8081]: ").strip() or "8081"
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
upstream_port = input("Upstream port [80]: ").strip() or "80"
mode = input("Mode [monitoring]: ").strip() or "monitoring"
# Save to fw.conf
cfg = load_config()
cfg[name] = {"token": token, "cloud": cloud, "port": port,
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
"labels": f"group={name}", "mode": mode}
os.makedirs(APP, exist_ok=True)
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
# Deploy immediately
print(f"\nDeploying {name}...")
deploy_container(name, token, cloud, port, upstream_ip, upstream_port, f"group={name}", mode)
def remove_menu():
s = load_state().get("nodes", [])
if not s: print("No nodes deployed."); return
print("\n─── Remove Node ───")
for i, n in enumerate(s):
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
c = input("\nSelect number: ").strip()
if not c.isdigit() or int(c) < 1 or int(c) > len(s): return
remove_container(s[int(c)-1]["name"])
def edit_menu():
cfg = load_config()
if not cfg: print("No nodes in fw.conf"); return
print("\n─── Edit Node ───")
names = list(cfg.keys())
for i, name in enumerate(names):
nc = cfg[name]
print(f" [{i+1}] {name} (port {nc.get('port','')})")
c = input("\nSelect number: ").strip()
if not c.isdigit() or int(c) < 1 or int(c) > len(names): return
name = names[int(c)-1]
nc = cfg[name]
print(f"\nEditing {name}. Leave blank to keep current value:")
for key, prompt in [("port","Port"), ("upstream_ip","Upstream IP"),
("upstream_port","Upstream port"), ("mode","Mode")]:
v = input(f"{prompt} [{nc.get(key,'')}]: ").strip()
if v: nc[key] = v
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
print(f"{name} updated in fw.conf. Re-deploy to apply changes.")
if __name__ == "__main__":
main()

View file

@ -1,471 +0,0 @@
#!/usr/bin/env python3
"""Wallarm Docker Node Manager"""
import json, os, sys, time, subprocess, shutil
VERSION = "2.0.0"
BASE = "/opt/fw"
APP = f"{BASE}/app"
CONF = f"{APP}/fw.conf"
STATE = f"{APP}/state.json"
DOCKER_DIR = f"{BASE}/docker"
OFFLINE_DIR = f"{BASE}/offline"
WALLARM_IMAGE = os.environ.get("WALLARM_IMAGE", "wallarm/node:6.11.0-rc1")
# Ensure Docker is in PATH
os.environ["PATH"] = f"{DOCKER_DIR}/bin:{os.environ.get('PATH', '')}"
def run(cmd, timeout=120):
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
def load_config():
if not os.path.exists(CONF): return {}
with open(CONF) as f: return json.load(f)
def load_state():
if not os.path.exists(STATE): return {"nodes": []}
with open(STATE) as f: return json.load(f)
def save_state(s):
os.makedirs(APP, exist_ok=True)
with open(STATE, "w") as f: json.dump(s, f, indent=2)
# ─── Docker ────────────────────────────────────────────────────────────
def install_docker():
if shutil.which("docker"):
return
print("Installing Docker...")
run("apt-get update -qq && apt-get install -y -qq docker.io 2>/dev/null || yum install -y -q docker 2>/dev/null", timeout=120)
run("systemctl enable docker && systemctl start docker")
def deploy_container(name, token, cloud, port, upstream_ip, upstream_port, labels, mode):
api_host = "us1.api.wallarm.com" if cloud == "US" else "api.wallarm.com"
# Ensure Docker is running
install_docker()
# Pull image if needed
if run(f"docker images -q {WALLARM_IMAGE}").stdout.strip() == "":
print(f"[{name}] Pulling Wallarm image...")
run(f"docker pull {WALLARM_IMAGE}", timeout=300)
# Remove old container
container = f"wallarm-{name}"
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
host_dir = f"{BASE}/{name}"
os.makedirs(host_dir, exist_ok=True)
# Generate start.sh
upstream = f"{upstream_ip}:{upstream_port}"
start_path = f"{host_dir}/start.sh"
with open(start_path, "w") as f:
f.write(f"""#!/bin/bash
# Wallarm node: {name}
docker rm -f {container} 2>/dev/null || true
docker run -d \\
--name {container} \\
--restart always \\
-p {port}:80 \\
-e WALLARM_API_TOKEN='{token}' \\
-e WALLARM_API_HOST={api_host} \\
-e WALLARM_MODE={mode} \\
-e NGINX_BACKEND={upstream} \\
-v {host_dir}/nginx.conf:/etc/nginx/http.d/default.conf:ro \\
{WALLARM_IMAGE}
sleep 3
docker ps --filter name={container} --format '{{{{.Status}}}}'
""")
os.chmod(start_path, 0o755)
# Write nginx.conf (always regenerate to apply template updates)
nginx_conf = f"{host_dir}/nginx.conf"
with open(nginx_conf, "w") as f:
f.write(f"""# Wallarm node: {name}
# Mount with: -v $(pwd)/nginx.conf:/etc/nginx/http.d/default.conf:ro
server {{
listen 80;
server_name _;
client_max_body_size 1024m;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# Pass all headers through — preserve auth cookies
proxy_pass_request_headers on;
location /wallarm-status {{
wallarm_status on;
allow 127.0.0.0/8;
deny all;
}}
location / {{
proxy_pass http://{upstream};
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}}
}}
""")
print(f"[{name}] Starting...")
r = run(f"bash {start_path}", timeout=30)
if r.returncode != 0 or "Error" in r.stderr or "Error" in r.stdout:
print(f"{name}: {r.stderr or r.stdout}")
return False
# Verify container is running
time.sleep(2)
check = run(f"docker inspect -f '{{{{.State.Running}}}}' {container} 2>/dev/null")
if check.stdout.strip() != "true":
logs = run(f"docker logs {container} 2>&1").stdout[-500:]
print(f"{name} failed to start:\n{logs}")
return False
# Save state (dedup)
s = load_state()
s.setdefault("nodes", [])
s["nodes"] = [n for n in s["nodes"] if n["name"] != name] # remove old
s["nodes"].append({
"name": name, "type": "docker", "port": int(port),
"upstream_ip": upstream_ip, "upstream_port": int(upstream_port),
"status": "running", "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ")
})
save_state(s)
print(f"{name} running on port {port} (docker ps --filter name={container})")
return True
def remove_container(name):
container = f"wallarm-{name}"
run(f"docker rm -f {container} 2>/dev/null", timeout=10)
shutil.rmtree(f"/opt/wallarm-{name}", ignore_errors=True)
s = load_state()
s["nodes"] = [n for n in s.get("nodes", []) if n["name"] != name]
save_state(s)
print(f"{name} removed")
def container_status():
r = run("docker ps --filter 'name=wallarm-' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null")
if not r.stdout.strip():
print("No containers running.")
return
print("\n─── Containers ───")
print(r.stdout)
# ─── Deploy All ───────────────────────────────────────────────────────
def deploy_all():
cfg = load_config()
nodes = cfg.get("nodes", cfg) # support both {"nodes":{...}} and flat format
if not nodes:
print("No nodes in fw.conf")
return
for name, nc in nodes.items():
if not isinstance(nc, dict): continue
deploy_container(name, nc["token"], nc.get("cloud","EU"),
str(nc.get("port","8081")), nc.get("upstream_ip","127.0.0.1"),
str(nc.get("upstream_port","80")), nc.get("labels",""),
nc.get("mode","monitoring"))
# ─── Menu ─────────────────────────────────────────────────────────────
def main():
if len(sys.argv) > 1 and sys.argv[1] == "--deploy-all":
return deploy_all()
print(f"═══ Wallarm Node Manager v{VERSION} ═══\n")
print("Preflight checks...")
if not shutil.which("docker"):
print(" ❌ Docker not found. Run setup.sh first.")
sys.exit(1)
print(" ✅ Docker ready\n")
cfg = load_config()
deployed = {n["name"] for n in load_state().get("nodes", [])}
while True:
undeployed = [k for k in cfg if k not in deployed]
print("\n─── Menu ───")
print(" [1] Add a new node")
n = 2
node_map = {}
ops = {}
for name in undeployed:
print(f" [{n}] Deploy {name} (port {cfg[name].get('port','?')})")
node_map[str(n)] = name
n += 1
if deployed:
print(f" [{n}] Update nodes"); ops['update'] = str(n); n += 1
print(f" [{n}] Edit nodes"); ops['edit'] = str(n); n += 1
print(f" [{n}] Delete a node"); ops['del'] = str(n); n += 1
print(f" [{n}] Status"); ops['status'] = str(n); n += 1
print(f" [{n}] Remote tunnel"); ops['tunnel'] = str(n); n += 1
print(f" [{n}] Debug (tap proxy)"); ops['debug'] = str(n); n += 1
print(" [q] Quit")
c = input("\nChoice: ").strip()
if c == "1":
add_node()
cfg = load_config()
deployed = {n["name"] for n in load_state().get("nodes", [])}
elif c in node_map:
name = node_map[c]
nc = cfg[name]
deploy_container(name, nc["token"], nc.get("cloud","EU"),
str(nc.get("port","8081")), nc.get("upstream_ip",""),
str(nc.get("upstream_port","80")), "",
nc.get("mode","monitoring"))
deployed.add(name)
elif c == ops.get('update') and deployed:
update_nodes()
elif c == ops.get('edit') and deployed:
edit_nodes()
elif c == ops.get('del') and deployed:
remove_menu()
elif c == ops.get('status'):
container_status()
elif c == ops.get('tunnel'):
start_tunnel()
elif c == ops.get('debug'):
start_debug()
elif c.lower() == "q":
break
def add_node():
print("\n─── Add New Node ───")
name = input("Node name: ").strip()
if not name: return
token = input("Wallarm token: ").strip()
if not token: print("Token required."); return
print("Cloud region:")
print(" [1] EU (api.wallarm.com)")
print(" [2] US (us1.api.wallarm.com)")
c = input("Choose [1]: ").strip()
cloud = "EU" if c == "2" else "EU"
if c == "2": cloud = "US"
port = input("Port [8081]: ").strip() or "8081"
upstream_ip = input("Upstream IP [127.0.0.1]: ").strip() or "127.0.0.1"
upstream_port = input("Upstream port [80]: ").strip() or "80"
print("\nTraffic mode:")
print(" [1] monitoring — detect only")
print(" [2] safe_blocking — block definitely malicious")
print(" [3] block — block all attacks")
print(" [4] off — disable")
m = input("Choose [1]: ").strip()
mode = {"1":"monitoring","2":"safe_blocking","3":"block","4":"off"}.get(m, "monitoring")
# Save to fw.conf
cfg = load_config()
cfg[name] = {"token": token, "cloud": cloud, "port": port,
"upstream_ip": upstream_ip, "upstream_port": upstream_port,
"labels": f"group={name}", "mode": mode}
os.makedirs(APP, exist_ok=True)
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
# Deploy immediately
print(f"\nDeploying {name}...")
deploy_container(name, token, cloud, port, upstream_ip, upstream_port, f"group={name}", mode)
def remove_menu():
s = load_state().get("nodes", [])
if not s: print("No nodes deployed."); return
print("\n─── Remove Node ───")
for i, n in enumerate(s):
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
c = input("\nSelect number: ").strip()
if not c.isdigit() or int(c) < 1 or int(c) > len(s): return
remove_container(s[int(c)-1]["name"])
def edit_menu():
cfg = load_config()
if not cfg: print("No nodes in fw.conf"); return
print("\n─── Edit Node ───")
names = list(cfg.keys())
for i, name in enumerate(names):
nc = cfg[name]
print(f" [{i+1}] {name} (port {nc.get('port','')})")
c = input("\nSelect number: ").strip()
if not c.isdigit() or int(c) < 1 or int(c) > len(names): return
name = names[int(c)-1]
nc = cfg[name]
print(f"\nEditing {name}. Leave blank to keep current value:")
for key, prompt in [("port","Port"), ("upstream_ip","Upstream IP"),
("upstream_port","Upstream port"), ("mode","Mode")]:
v = input(f"{prompt} [{nc.get(key,'')}]: ").strip()
if v: nc[key] = v
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
print(f"{name} updated in fw.conf. Re-deploy to apply changes.")
main()
def start_tunnel():
host = input("Jumphost URL [ssh.sechpoint.app:443]: ").strip() or "ssh.sechpoint.app:443"
user = input("Username [wallarm-tunnel]: ").strip() or "wallarm-tunnel"
pw = input("Password or SSH key path: ").strip()
if not pw: print("No credentials."); return
print(f"\nShare: ssh -p 9042 {user}@{host}\nCtrl+C to close.\n")
# Requires paramiko: pip install paramiko
try:
import paramiko, socket, threading
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host.split(":")[0], 443))
t = paramiko.Transport(sock)
if pw.startswith("/"): t.connect(username=user, key_filename=pw)
else: t.connect(username=user, password=pw)
t.request_port_forward("", 9042, "localhost", 22)
print("Tunnel active. Ctrl+C to close.")
threading.Event().wait()
except ImportError:
print("Install paramiko: pip3 install paramiko")
except Exception as e:
print(f"Tunnel failed: {e}")
def edit_nodes():
s = load_state().get("nodes", [])
if not s: print("No nodes."); return
print("\n─── Edit Node ───")
for i, n in enumerate(s):
r = run(f"docker inspect -f '{{{{.State.Status}}}}' wallarm-{n['name']} 2>/dev/null")
st = "" if r.stdout.strip() == "running" else ""
print(f" [{i+1}] {st} {n['name']} (port {n.get('port','')})")
print(" [b] Back")
c = input("\nSelect node: ").strip()
if c.lower() == 'b': return
if not c.isdigit(): return
idx = int(c) - 1
if idx < 0 or idx >= len(s): return
n = s[idx]
cfg = load_config()
nc = cfg.get(n['name'], {})
while True:
print(f"\n─── Editing: {n['name']} ───")
print(f" [1] Token: {'*'*8}")
print(f" [2] Cloud: {nc.get('cloud','EU')}")
print(f" [3] Port: {n.get('port','')}")
print(f" [4] Upstream: {nc.get('upstream_ip','')}:{nc.get('upstream_port','')}")
print(f" [5] Mode: {nc.get('mode','monitoring')}")
print(f" [s] Save & restart")
print(f" [b] Back")
c = input("\nEdit field: ").strip()
if c == '1': nc['token'] = input("New token: ").strip() or nc.get('token','')
elif c == '2':
print(" [1] EU [2] US")
r = input("Cloud: ").strip()
nc['cloud'] = "US" if r == "2" else "EU"
elif c == '3': nc['port'] = input("Port: ").strip() or nc.get('port','')
elif c == '4':
up = input("Upstream (ip:port): ").strip()
if ':' in up:
ip, p = up.split(':')
nc['upstream_ip'] = ip; nc['upstream_port'] = p
elif c == '5':
print(" [1] monitoring [2] safe_blocking [3] block [4] off")
m = input("Mode: ").strip()
nc['mode'] = {"1":"monitoring","2":"safe_blocking","3":"block","4":"off"}.get(m, nc.get('mode','monitoring'))
elif c.lower() == 's':
with open(CONF, "w") as f: json.dump(cfg, f, indent=2)
deploy_container(n['name'], nc['token'], nc.get('cloud','EU'),
str(nc.get('port','')), nc.get('upstream_ip',''),
str(nc.get('upstream_port','')), "",
nc.get('mode','monitoring'))
print(f"{n['name']} updated & restarted.")
break
elif c.lower() == 'b': break
def remove_menu():
s = load_state().get("nodes", [])
if not s: print("No nodes."); return
print("\n─── Delete Node ───")
for i, n in enumerate(s):
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
print(" [b] Back")
c = input("\nSelect: ").strip()
if c.lower() == 'b': return
if not c.isdigit(): return
idx = int(c) - 1
if 0 <= idx < len(s):
remove_container(s[idx]["name"])
def start_debug():
s = load_state().get("nodes", [])
if not s: print("No nodes."); return
print("\n─── Debug (Tap Proxy) ───")
for i, n in enumerate(s):
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
c = input("\nSelect node: ").strip()
if not c.isdigit(): return
idx = int(c) - 1
if idx < 0 or idx >= len(s): return
n = s[idx]
port = str(n.get('port', '8081'))
backend = f"{n.get('upstream_ip','127.0.0.1')}:{n.get('upstream_port','80')}"
logdir = f"{BASE}/{n['name']}"
os.makedirs(logdir, exist_ok=True)
tap_script = f"{BASE}/app/tap-proxy.py"
# Kill any stale tap proxy on this port
run(f"kill -9 $(ss -tlnp 'sport = :{port}' | grep -oP 'pid=\\K[0-9]+') 2>/dev/null", timeout=5)
# Stop container, start tap
print(f"\nStopping wallarm-{n['name']}...")
run(f"docker stop wallarm-{n['name']} 2>/dev/null", timeout=10)
run(f"docker rm -f wallarm-{n['name']} 2>/dev/null", timeout=5)
# Wait for port to free
import time
for _ in range(10):
r = run(f"ss -tlnp | grep ':{port}' 2>/dev/null")
if not r.stdout.strip():
break
time.sleep(1)
else:
print(f"Port {port} still in use — force killing...")
run(f"ss -K 'sport = :{port}' 2>/dev/null; kill -9 $(ss -tlnp 'sport = :{port}' | grep -oP 'pid=\\K[0-9]+') 2>/dev/null", timeout=5)
time.sleep(2)
print(f"Tap proxy :{port}{backend}")
print(f"Log: {logdir}/tap-*.log")
print("Press Ctrl+C to stop — container will restart automatically.\n")
try:
import subprocess
subprocess.call(["python3", tap_script, port, backend, logdir])
except KeyboardInterrupt:
pass
finally:
print(f"\nRestarting wallarm-{n['name']}...")
run(f"docker start wallarm-{n['name']} 2>/dev/null || bash {BASE}/{n['name']}/start.sh", timeout=30)
def update_nodes():
"""Rebuild containers using current config — no config changes, just apply template updates."""
cfg = load_config()
s = load_state().get("nodes", [])
if not s: print("No nodes."); return
print("\n─── Update Nodes ───")
for i, n in enumerate(s):
print(f" [{i+1}] {n['name']} (port {n.get('port','')})")
print(" [a] All [b] Back")
c = input("\nSelect: ").strip()
if c.lower() == 'b': return
nodes_to_update = s if c.lower() == 'a' else ([s[int(c)-1]] if c.isdigit() and 1 <= int(c) <= len(s) else [])
if not nodes_to_update: return
for n in nodes_to_update:
name = n['name']
nc = cfg.get(name, {})
if not nc: continue
print(f"\nUpdating {name}...")
deploy_container(name, nc["token"], nc.get("cloud","EU"),
str(nc.get("port","8081")), nc.get("upstream_ip",""),
str(nc.get("upstream_port","80")), "",
nc.get("mode","monitoring"))
print(f"{name} updated.")
if __name__ == "__main__":
main()

View file

@ -1,85 +0,0 @@
#!/usr/bin/env python3
"""Tap Proxy — logs HTTP headers. Usage: python3 tap-proxy.py <port> <backend_host:port> <log_dir>"""
import http.server, urllib.request, sys, socket
from datetime import datetime
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8081
BACKEND = sys.argv[2] if len(sys.argv) > 2 else "127.0.0.1:80"
LOG = (sys.argv[3] if len(sys.argv) > 3 else "/tmp") + "/tap-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".log"
BACKEND_URL = "http://" + BACKEND
# Don't follow redirects — preserve Set-Cookie
class NoRedirect(urllib.request.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
fp.status, fp.reason = code, msg
return fp
http_error_301 = http_error_302
http_error_303 = http_error_302
http_error_307 = http_error_302
http_error_308 = http_error_302
_opener = urllib.request.build_opener(NoRedirect)
class Tap(http.server.BaseHTTPRequestHandler):
def do_GET(self): self._tap('GET')
def do_POST(self): self._tap('POST')
def do_PUT(self): self._tap('PUT')
def do_DELETE(self): self._tap('DELETE')
def _tap(self, method):
now = datetime.now().strftime('%H:%M:%S')
msg = "\n" + "="*60 + "\n[" + now + "] " + method + " " + self.path
msg += "\nClient: " + self.client_address[0] + "\n" + "-"*60 + "\n"
for k, v in sorted(self.headers.items()):
msg += " " + k + ": " + v + "\n"
# Handle chunked transfer-encoding
body = None
cl = self.headers.get('Content-Length')
te = self.headers.get('Transfer-Encoding', '').lower()
if cl:
body = self.rfile.read(int(cl))
elif 'chunked' in te:
body = b''
while True:
line = self.rfile.readline().strip()
if not line: continue
size = int(line, 16)
if size == 0: break
body += self.rfile.read(size)
self.rfile.readline()
while True:
line = self.rfile.readline().strip()
if not line: break
req = urllib.request.Request(BACKEND_URL + self.path, data=body, method=method)
# Pass through all headers including Host
for k, v in self.headers.items():
if k.lower() not in ('connection',):
req.add_header(k, v)
try:
resp = _opener.open(req, timeout=30)
msg += "\nRESPONSE: " + str(resp.status) + " " + str(resp.reason) + "\n"
for k, v in resp.getheaders():
msg += " " + k + ": " + v + "\n"
self.send_response(resp.status)
for k, v in resp.getheaders():
if k.lower() not in ('transfer-encoding', 'connection'):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.read())
except Exception as e:
msg += "\nBACKEND: " + str(e) + "\n"
self.send_response(502)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b"Backend unreachable: " + str(e).encode())
with open(LOG, "a") as f:
f.write(msg)
def log_message(self, *args): pass
print("Tap proxy :{0} -> {1}\nLog: {2}\nCtrl+C to stop".format(PORT, BACKEND_URL, LOG))
http.server.HTTPServer(('0.0.0.0', PORT), Tap).serve_forever()