Compare commits

..

163 commits
v1.0.0 ... main

Author SHA1 Message Date
admin
e3f467c20c fix: dedup state.json, prevent duplicate node entries 2026-08-08 14:44:20 +00:00
admin
a8084981b4 fix: always regenerate nginx.conf on deploy/update 2026-08-08 14:42:11 +00:00
admin
3a80e1067a fix: force reorder — all defs before if __name__ 2026-08-08 14:39:12 +00:00
admin
3149b15cef feat: update nodes — rebuild container from fw.conf, preserve config 2026-08-08 14:36:25 +00:00
admin
6fd758bb1f fix: apply WAF plugin team fixes — Host header, chunked encoding, no redirect follow
- nginx.conf: restore proxy_set_header for Host, X-Real-IP, X-Forwarded-For
- tap-proxy: preserve Host header, handle chunked POST, don't follow redirects
- Credits: zoraxy-waf plugin team
2026-08-08 14:30:51 +00:00
admin
98bd0d7d23 fix: tap proxy survives backend errors, returns 502 with details 2026-08-08 10:31:32 +00:00
admin
348bf39517 fix: kill stale tap proxy on port before starting new debug 2026-08-08 07:32:05 +00:00
admin
d955bbff0c fix: run tap proxy interactively — user controls stop with Ctrl+C 2026-08-08 07:21:47 +00:00
admin
f5eaf58e9b fix: use ss+kill instead of fuser (not installed) 2026-08-08 05:57:41 +00:00
admin
79707c9395 fix: wait for port to free after docker stop in debug mode 2026-08-08 05:31:29 +00:00
admin
853aad8a36 fix: standalone tap-proxy.py — no more nested f-string mess 2026-08-07 17:33:58 +00:00
admin
c17a44f077 fix: escape curly braces in tap proxy f-string template 2026-08-07 17:31:42 +00:00
admin
448a05856a fix: move start_debug before main() call 2026-08-07 17:28:33 +00:00
admin
6f1037950a feat: debug mode — tap proxy logs all headers, saves to file 2026-08-07 17:21:40 +00:00
admin
85f6d42920 fix: mount nginx.conf, add proxy_pass_request_headers on 2026-08-07 16:54:30 +00:00
admin
8bbf2ccc25 fix: pass headers through from ingress controller — no overwrite 2026-08-07 16:20:48 +00:00
admin
c2bfffa668 fix: verify container actually running after start, show logs on failure 2026-08-04 08:00:58 +00:00
admin
678fa83f31 feat: full edit menu (5 fields), delete [b] back, fix status display 2026-08-04 07:53:26 +00:00
admin
9c6ddd2da1 fix: use NGINX_BACKEND env var (image auto-generates nginx) 2026-08-03 17:38:53 +00:00
admin
202f0e2dc8 fix: start.sh per node — declarative config + startup, no compose needed 2026-08-03 17:28:57 +00:00
admin
a2650e5c36 fix: flat structure — compose.yaml + nginx.conf, production defaults
- /opt/fw/{name}/compose.yaml (flat, no subdirs)
- /opt/fw/{name}/nginx.conf (client_max_body_size 1024m, timeouts 300s)
- restart: always in compose + systemd units for dockerd/containerd
2026-08-03 17:20:14 +00:00
admin
30fb7b2010 docs: add agent rules — discuss first, explicit production defaults 2026-08-03 17:14:29 +00:00
admin
f9c2771984 feat: docker-compose.yml per node, minimal nginx config
- /opt/fw/{name}/docker-compose.yml — declarative, editable
- /opt/fw/{name}/nginx/default.conf — user-editable
- docker compose up -d for deployment
- Remove rate limits and header manipulation (app handles it)
2026-08-03 17:10:23 +00:00
admin
c4f2c8fadb fix: proper nginx config with proxy headers, mount http.d/default.conf 2026-08-03 17:04:09 +00:00
admin
643a8fbe23 fix: reboot-resistant systemd units for containerd + dockerd 2026-08-03 17:02:05 +00:00
admin
f72ea3f348 feat: default nginx config on host, editable 2026-08-03 16:59:10 +00:00
admin
3d8b85d918 fix: host directory mounts for editable config (/opt/fw/{name}/) 2026-08-03 16:58:52 +00:00
admin
9d10516a63 fix: move main() call to end of file after all function defs 2026-08-03 16:11:02 +00:00
admin
6a79d6b5b6 fix: ops dict declared at top of loop, remove duplicate 2026-08-03 16:09:13 +00:00
admin
30d247c411 fix: numbered cloud region selection 2026-08-03 16:07:41 +00:00
admin
0171438505 fix: dynamic menu numbering via ops dict 2026-08-03 16:06:40 +00:00
admin
30c24da698 fix: numbered mode selection, remove duplicate create_config 2026-08-03 15:50:44 +00:00
admin
f3c82258ac fix: start containerd before dockerd 2026-08-03 15:46:12 +00:00
admin
d8b40f4999 fix: add --userland-proxy=false to dockerd 2026-08-03 15:44:31 +00:00
admin
f394d5fa97 fix: docker symlink to /usr/bin, DOCKER_HOST for custom socket 2026-08-03 15:43:54 +00:00
admin
1d4b6c9673 feat: edit nodes, delete, status as separate menu items 2026-08-03 15:41:53 +00:00
admin
b981b82c5d feat: manage nodes submenu with restart/delete 2026-08-03 15:40:20 +00:00
admin
c6e163a767 feat: individual node deploy options in menu 2026-08-03 15:38:42 +00:00
admin
95e66f21c8 fix: sequential menu numbers 2026-08-03 15:37:04 +00:00
admin
1748371b34 feat: add remote tunnel to menu 2026-08-03 15:35:49 +00:00
admin
aba98108b0 fix: indentation, docker_bin absolute path check, rename to main.py 2026-08-03 15:31:29 +00:00
admin
d6fdbbe8db fix: check for docker at absolute path /opt/fw/docker/bin/docker 2026-08-03 15:27:54 +00:00
admin
84612e4c7e fix: deploy.sh exports docker PATH, link to /usr/bin 2026-08-03 15:27:23 +00:00
admin
d1c3f69f99 fix: symlink docker binaries to /usr/local/bin/ for PATH 2026-08-03 15:22:47 +00:00
admin
676202f625 add remove.sh for uninstalling running nodes 2026-08-03 15:11:35 +00:00
admin
d7a1d790be Revert "remove docker binaries from git, add remove.sh"
This reverts commit e9e9b2976e.
2026-08-03 15:11:13 +00:00
admin
e9e9b2976e remove docker binaries from git, add remove.sh 2026-08-03 15:10:02 +00:00
admin
ebde840b28 refactor: guided UI — no fw.conf needed, add node deploys immediately
- No fw.conf required on first run — only option is Add Node
- Adding a node deploys it immediately (saves to fw.conf)
- Shows pending deploys when fw.conf has undeployed nodes
- Status/Remove shown only when nodes are deployed
2026-08-03 15:07:58 +00:00
admin
3ba8e6e6c2 refactor: clean layout + docker binary 2026-08-03 14:50:01 +00:00
admin
128e59c0e4 fix: download Docker from URL, not apt (works offline/air-gapped)
- DOCKER_DOWNLOAD_URL and WALLARM_IMAGE_URL env vars for custom sources
- Falls back to offline/ dir if present
- Docker installed under /opt/fw/docker/bin/
2026-08-03 14:38:59 +00:00
admin
587aa70554 feat: self-contained /opt/fw/ — Docker runtime, offline dir, connectivity check
- setup.sh checks git.sechpoint.app reachability first
- Docker installed under /opt/fw/docker/ (binaries + data)
- /opt/fw/offline/ for pre-downloaded binaries (air-gapped)
- /opt/fw/fw.conf — user-editable config
- All self-contained, no system-wide installs
2026-08-03 14:33:20 +00:00
admin
e25ea0560b feat: offline mode — drop binaries in /opt/fw/offline/
Offline deployment for air-gapped corporate environments:
- /opt/fw/offline/docker-29.2.1.tgz → Docker static binary
- /opt/fw/offline/wallarm-node.tar → Wallarm Docker image
- Setup auto-detects and uses offline files
- Falls back to apt/docker.io if no offline files
2026-08-03 14:29:14 +00:00
admin
fd641905e0 feat: offline mode — pre-download Docker + Wallarm image to /opt/fw/offline/ 2026-08-03 14:25:19 +00:00
admin
d75a9a34c9 docs: update README for Docker deployment 2026-08-03 14:23:23 +00:00
admin
c747e9da63 fix: add Docker named volume for persistent data 2026-08-03 14:21:57 +00:00
admin
156c81f230 feat: add/edit/remove individual nodes, create fw.conf interactively 2026-08-03 14:20:10 +00:00
admin
3037f5f8ef fix: remove volume mount — it overwrites container /opt/wallarm 2026-08-03 13:32:03 +00:00
admin
a906b21b99 fix: install Docker in setup.sh 2026-08-03 13:25:47 +00:00
admin
b3cf1c8250 fix: handle nested and flat fw.conf formats 2026-08-03 13:24:44 +00:00
admin
350434e690 pivot: Docker-based deployment — one container per node
- Clean Docker approach: pull wallarm/node image, run containers
- No symlinks, no path patching, no filesystem tricks
- Works on any Linux with Docker (VMs, LXC, bare metal)
- Multi-node: each container gets unique port + volume
- Simplified to one Python file
2026-08-03 13:24:20 +00:00
admin
10fa528e77 feat: unshare -m bind mount for per-instance /opt/wallarm isolation 2026-08-03 13:08:23 +00:00
admin
b8b46daf00 revert: back to symlink approach, PrivateMounts incompatible 2026-08-03 13:03:16 +00:00
admin
d277d976d0 fix: mkdir -p /opt/wallarm before bind mount, fix status parsing 2026-08-03 13:02:07 +00:00
admin
06c1ecfe85 feat: systemd PrivateMounts bind-mount isolation, port offsets kept 2026-08-03 12:58:35 +00:00
admin
c8fd8bc37e fix: status parsing from systemd list-units 2026-08-03 12:47:32 +00:00
admin
1974fd0293 fix: save state in deploy_all, discover nodes from systemd 2026-08-03 12:28:01 +00:00
admin
d28d77661d fix: remove -f flag from cloud check (Wallarm API returns 404 on root) 2026-08-03 12:14:16 +00:00
admin
112bffb742 refactor: /opt/fw/deploy.sh wrapper, app/ subdir for main.py + config
- /opt/fw/deploy.sh — simple wrapper, user entry point
- /opt/fw/app/main.py — deploy script
- /opt/fw/app/fw.conf — node config
- /opt/fw/app/state.json — deployment state
- Instances stay at /opt/fw/{name}/wallarm/
2026-08-03 12:06:33 +00:00
admin
8ea81cdb17 fix: remove /opt/wallarm symlink after deploy 2026-08-03 11:57:57 +00:00
admin
944df2d394 chore: fix comment 2026-08-03 11:55:32 +00:00
admin
2049bcf617 fix: wcli uses -cfg not --config-file 2026-08-03 11:53:47 +00:00
admin
a2a658caaa fix: nginx ExecStartPre non-fatal (-) so deploy nginx doesn't block 2026-08-03 11:49:17 +00:00
admin
278d82441d fix: distro package fallback before pyenv for Python install 2026-08-03 11:47:03 +00:00
admin
31013b3e82 fix: message when fw.conf is empty 2026-08-03 11:34:53 +00:00
admin
076b801eda fix: proper error handling for access denied, kill deploy nginx before systemd 2026-08-03 11:26:49 +00:00
admin
03f56964b9 fix: cloud check is warning not failure, --help works 2026-08-03 11:24:47 +00:00
admin
43dfc6ec06 pivot: Python rewrite replacing Go — single deploy.py
- Single deploy.py (~400 lines) replaces all Go code
- setup.sh installs Python 3.12 via pyenv (cross-distro)
- Same functionality: menu, deploy, edit, remove, status, tunnel
- No build step — script runs directly
- Path patching and port offset in native Python
2026-08-03 11:09:08 +00:00
admin
31cbb56629 fix: curl or wget fallback for AIO download, preflight tweak 2026-08-03 10:40:14 +00:00
admin
e62efa5534 fix: setup.sh installs curl + git + go for all package managers 2026-08-03 10:38:36 +00:00
admin
0e781b157e chore: remove old bash/docker files, update README 2026-08-03 10:35:38 +00:00
admin
1924d5b1df fix: cross-filesystem friendly move with cp+rm fallback, kill deploy nginx 2026-08-02 09:45:51 +00:00
admin
8f9c0ac10c feat: --deploy-all flag, fix menu numbering, update clean.sh 2026-08-02 09:39:53 +00:00
admin
216b24b9ac fix: deploy menu numbers only undeployed nodes 2026-08-02 09:39:12 +00:00
admin
c49ee64b0b revert: symlink is the reliable approach for /opt/wallarm
Bind mounts and unshare namespace approaches are fragile across
systemd versions.  Symlink + --config-file for wcli provides
adequate isolation for multi-instance.
2026-08-02 09:29:07 +00:00
admin
11dade0c5e refactor: fw.conf JSON config replacing flat .env 2026-08-02 09:17:49 +00:00
admin
1c2dbb9540 fix: add missing mode declaration 2026-08-02 09:14:28 +00:00
admin
3d076221e2 feat: block/monitoring mode — ask during deploy, configurable in .env 2026-08-02 09:12:27 +00:00
admin
e5e1697402 reorder: deploy, edit, status, remove, remote, dash, quit 2026-08-02 09:09:16 +00:00
admin
c96fb88fbd feat: edit node — reconfigure port, upstream IP/port 2026-08-02 09:07:53 +00:00
admin
221b1968b0 feat: interactive menu, multi-node .env, numbered remove
- No flags needed — interactive menu on startup
- [1] Deploy: shows .env nodes, ✓ marks deployed, pick by number
- [2] Remove: numbered list of deployed nodes from state.json
- [3] Status: shows all nodes with systemd state
- [4] Tunnel, [5] Dashboard (TUI)
- .env supports WALLARM_NODES=srv1,srv2 with per-node keys
- Backward compatible with legacy single-node .env
2026-08-02 09:04:39 +00:00
admin
86a172a4ed fix: add --config-file to wcli in patchPaths for multi-instance 2026-08-02 08:41:20 +00:00
admin
f4e931bbd4 fix: use bash -c for find+sed, kill temp nginx, simplify systemd
- patchPaths uses bash -c 'find ... -exec sed {} +' instead of Go exec
- Port offset applied to ALL yaml/yml/conf files across instance
- Kill deploy-time nginx before systemd starts
- Simplified systemd template (removed broken fuser line)
2026-08-02 08:18:06 +00:00
admin
6a9337553a fix: per-instance port offset, improved systemd nginx start
- Port offset from instance name hash avoids supervisord conflicts
- ExecStartPre kills old nginx before starting new one
- Removed broken patchelf approach
2026-08-02 07:57:05 +00:00
admin
3884f094fd fix: dynamic symlink in ExecStartPre for multi-instance support
Each instance creates /opt/wallarm -> its own directory before starting.
Removed patchelf approach (broke library paths).
Systemd template updated with ExecStartPre ln -sf.
2026-08-02 07:45:48 +00:00
admin
aeb8be3ebc feat: patchelf ELF binaries per instance for true multi-node 2026-08-02 07:31:12 +00:00
admin
6c8b9dc166 fix: systemd ExecStartPre with - prefix, remove symlink dependency
- ExecStartPre=- allows nginx start failure without blocking service
- Removed shell constructs (|| true) incompatible with systemd
- Removed env.list background goroutine (setup.sh no longer used)
- Multi-instance needs per-instance ELF patching (WIP)
2026-08-02 07:20:43 +00:00
admin
56468d3366 feat: nginx with Wallarm module in systemd, wallarm-status endpoint 2026-08-02 06:33:07 +00:00
admin
50ba28c2f9 fix: always register node, remove broken skip logic and stray brace 2026-08-02 06:00:01 +00:00
admin
6f5c281089 fix: add Symlink constant, create /opt/wallarm → instance after move 2026-08-02 05:56:53 +00:00
admin
08653eca9d fix: patch all config files (*.sh, *.list, *.conf, *.yaml, *.yml, *.json) 2026-08-01 22:06:07 +00:00
admin
310ee0a8a4 fix: patchPaths adds wstore.yaml, avoid corrupting binaries 2026-08-01 22:00:05 +00:00
admin
8b49978ea1 fix: use bundled python3.10 in systemd, patch paths after move 2026-08-01 21:52:36 +00:00
admin
a6359beb61 fix: remove supervisord start (blocks forever with -n), use state for skip check 2026-08-01 21:15:21 +00:00
admin
938aa168b3 fix: skip registration if node.yaml exists, fix else block brace 2026-08-01 21:12:18 +00:00
admin
7f7bff15ca fix: remove duplicate Registering print, wait for registration 2026-08-01 20:48:02 +00:00
admin
e4b1021c52 fix: remove goroutine timeout, registration takes 30-60s normally 2026-08-01 20:29:47 +00:00
admin
da56eaf164 fix: explicit goroutine timeout for registration 2026-08-01 20:12:37 +00:00
admin
842024a6ea fix: source env.list before register, 60s timeout 2026-08-01 19:52:29 +00:00
admin
452976ab43 fix: help text, node status running after deploy 2026-08-01 19:39:26 +00:00
admin
d5252b3be0 feat: --remove and --status flags 2026-08-01 19:32:39 +00:00
admin
ddf0dd9884 fix: proper systemd template, auto-enable after deploy 2026-08-01 19:30:42 +00:00
admin
bf3ba41179 feat: start Wallarm via supervisord after registration 2026-08-01 19:26:52 +00:00
admin
b01f5d6a52 fix: register-node needs job:register arg, add --deploy flag for .env mode 2026-08-01 19:24:10 +00:00
admin
e9dff30d84 feat: .env file support — skip prompts on repeat deploys
- /opt/fw/.env stores token, cloud, node, port, upstream
- If .env exists with WALLARM_TOKEN, all prompts are skipped
- Saved automatically after successful deployment
- Perfect for testing: edit .env once, deploy repeatedly
2026-08-01 19:20:51 +00:00
admin
cbe6d51ac6 fix: bypass setup.sh, register node directly + pick-module 2026-08-01 19:18:39 +00:00
admin
57e0c8798a fix: patch set -e out of setup.sh before running 2026-08-01 19:00:30 +00:00
admin
f7689b4d6a fix: check node.yaml for success, ignore env.list exit code 2026-08-01 18:57:24 +00:00
admin
8484ca46a7 fix: keep env.list alive with background goroutine during setup.sh 2026-08-01 18:37:17 +00:00
admin
0922102226 fix: patch setup.sh env.list sed lines to be non-fatal, kill port with ss 2026-08-01 18:36:11 +00:00
admin
bc66018af3 fix: kill existing process on target port before starting nginx 2026-08-01 18:29:03 +00:00
admin
4432855a2d fix: trap DEBUG to ensure env.list always exists during setup.sh 2026-08-01 18:27:20 +00:00
admin
8638f198ff fix: basic nginx.conf (no wallarm directives before module loads) 2026-08-01 17:35:26 +00:00
admin
d405c98ff8 fix: deploy to real /opt/wallarm, then move to /opt/fw/{name}/wallarm
No symlinks, no path patching. Setup.sh runs in its native environment.
After success, os.Rename moves everything to the instance directory.
2026-08-01 17:30:05 +00:00
admin
9cf6a2a66b fix: patch setup.sh /opt/wallarm paths instead of symlink 2026-08-01 17:28:04 +00:00
admin
bb45f8606d fix: robust symlink creation, handle /opt/wallarm existing as directory 2026-08-01 17:22:13 +00:00
admin
a1d71b3eac feat: start per-instance NGINX with status endpoint before setup.sh 2026-08-01 17:18:19 +00:00
admin
c38535398d refactor: per-instance NGINX, clean native.go 2026-08-01 17:16:02 +00:00
admin
91b8b0e980 fix: set cmd.Dir to instance dir so setup.sh finds modules/ 2026-08-01 17:14:45 +00:00
admin
fd3a005fe8 fix: use Wallarm v6.12.5 (matching AIO_BASE from ingress repo) 2026-08-01 17:11:19 +00:00
admin
02e9fd2793 fix: add --custom-ngx-build back, fix preflight URL 2026-08-01 17:10:51 +00:00
admin
83f73c0e9c simplify: setup.sh just builds and drops binary in /opt/fw/
No directory structure creation, no confusing messages.
Deploy binary handles everything else.
2026-08-01 17:00:43 +00:00
admin
b44da10ae7 fix: preflight URL, /opt/fw/ refactor complete 2026-08-01 16:55:53 +00:00
admin
02f3368fa6 fix: remove cmd.Dir — setup.sh cp -a . /opt/wallarm self-destructs when cwd is /opt/wallarm 2026-08-01 16:53:19 +00:00
admin
af8df1a659 fix: add --keep flag to prevent Makeself from deleting /opt/wallarm on failure 2026-08-01 16:52:40 +00:00
admin
7875dbca13 fix: remove --force flag — caused setup.sh to hang 2026-08-01 16:46:23 +00:00
admin
d3db8ce4c0 fix: extract AIO to /opt/wallarm/ (setup.sh hardcodes this path)
setup.sh uses /opt/wallarm/ for env.list and all paths.
Extract once to base dir, each node registers with --force.
Simplified InstallNode to 4 steps: nginx → download → extract → setup.
2026-08-01 16:38:58 +00:00
admin
5a0cfe92e1 fix: flush log file, show last lines in error output 2026-08-01 16:34:48 +00:00
admin
b86d9d3604 fix: systemd unit starts per-instance NGINX before Wallarm services 2026-08-01 16:27:52 +00:00
admin
d8390eab2a feat: per-instance NGINX — each node gets its own nginx binary and config
- Shared NGINX binary at /opt/wallarm/nginx/sbin/nginx (installed once)
- Each node gets /opt/wallarm/nodes/{name}/nginx/ with:
  - sbin/nginx (copied from shared)
  - conf/nginx.conf (per-instance, unique port + upstream)
  - logs/ (per-instance access/error logs)
- setup.sh runs with --custom-ngx-build (uses our nginx)
2026-08-01 16:26:41 +00:00
admin
0a301fbd05 refactor: install NGINX under /opt/wallarm/nginx/ 2026-08-01 16:23:24 +00:00
admin
0ac2fc82cf fix: try all package managers for NGINX, clear error if all fail 2026-08-01 16:21:51 +00:00
admin
f299f7f895 feat: show version on startup and in setup.sh output 2026-08-01 16:11:57 +00:00
admin
82a28c6101 fix: remove --host flag, --cloud and --host are mutually exclusive 2026-08-01 16:04:22 +00:00
admin
2e6eca019d fix: use setup.sh batch mode with --batch --token --cloud
- Uses AIO installer's built-in batch mode instead of interactive prompts
- --batch --token TOKEN --cloud US|EU --host HOST for non-interactive install
- cloudFromHost() maps api.wallarm.com → EU, us1.api.wallarm.com → US
- No more stdin piping, proper CLI flags
2026-08-01 15:59:54 +00:00
admin
0ce12f4742 fix: use correct installer URL from storage.googleapis.com
Source: github.com/wallarm/ingress build/fetch-module.sh
URL: storage.googleapis.com/meganode_storage/6.13/wallarm-6.13.0.x86_64-glibc.sh
Preflight checks meganode_storage reachability instead of meganode.wallarm.com
2026-08-01 15:58:49 +00:00
admin
5923f684ec fix: auto-install NGINX before Wallarm node setup
Wallarm node (native deployment) requires NGINX as a runtime
dependency.  installNginx() runs apt-get install nginx if not
already present, before extracting/running the Wallarm installer.

Also fixed repo.wallarm.com → meganode.wallarm.com in preflight.
2026-08-01 15:53:02 +00:00
admin
157373ea16 refactor: rename wallarm → deploy, deploy.sh → setup.sh
- setup.sh bootstraps /opt/wallarm/ structure, builds deploy binary
- deploy binary at /opt/wallarm/deploy (renamed from wallarm)
- /opt/wallarm/source/ — Go source for local rebuilds
- /opt/wallarm/nodes/{instance}/ — per-node directories
- cmd/deploy/main.go replaces cmd/wallarm/main.go
2026-08-01 15:39:56 +00:00
admin
c1e1be2ab2 fix: deploy.sh copies source to /opt/wallarm/source/ for local rebuilds
- Binary + source installed to /opt/wallarm/
- Clients can rebuild: cd /opt/wallarm/source && go build ./cmd/wallarm/
- Source stays at repo root, not in a subdirectory
2026-08-01 15:34:02 +00:00
admin
5dc449b7af Revert "refactor: move Go source into source/ directory"
This reverts commit 78e316af64.
2026-08-01 15:33:35 +00:00
admin
78e316af64 refactor: move Go source into source/ directory
- source/cmd/wallarm/main.go — binary entrypoint
- source/internal/ — all packages
- source/go.mod, source/go.sum
- deploy.sh builds from source/ subdirectory
- Clean separation: source code vs deployment configs
2026-08-01 15:33:17 +00:00
admin
e2c0f5af08 refactor: move everything under /opt/wallarm/
- deploy.sh installs to /opt/wallarm/wallarm (not ~/deploy/)
- state.json at /opt/wallarm/state.json (not ~/.wallarm/)
- instances under /opt/wallarm/{name}/
- .env will live at /opt/wallarm/.env for global defaults
- Single tree, no home directory clutter
2026-08-01 15:30:37 +00:00
admin
f5919b4c85 fix: extract Makeself archive with --noexec, then run setup.sh
- Uses Makeself --noexec --target to extract without running
- Then runs the embedded setup.sh with WALLARM_API_TOKEN and
  WALLARM_API_HOST env vars
- Interactive prompts handled via stdin pipe
2026-08-01 15:25:44 +00:00
admin
b1c596ccf2 fix: use correct meganode.wallarm.com installer URL
- Updated from deprecated repo.wallarm.com to meganode.wallarm.com/6.12/
- Installer URL: wallarm-6.12.7.x86_64-glibc.sh
- Override with WALLARM_INSTALLER_URL env var
- Pipes token+region via stdin to skip interactive prompts
- Preflight checks meganode.wallarm.com reachability
2026-08-01 15:24:03 +00:00
admin
d0ad1c5243 fix: installer URL overridable, simplified deploy form
- WALLARM_INSTALLER_URL env var overrides the native node installer URL
- Address simplified: default IP 0.0.0.0, only ask for port
- Added upstream IP/port prompts
- Per-instance .env file saved to /opt/wallarm/{name}/.env
  (allows resuming/reinstalling from saved state)
- Confirmation summary before deploying
2026-08-01 15:21:04 +00:00
admin
ed9655ee79 fix: move deploy form out of TUI into plain terminal
Bubbletea and huh cannot coexist in the same process.  Fixed by:
- TUI handles only display: deploy choice + dashboard
- Deploy wizard runs as plain terminal prompts in main.go
- No more terminal conflicts, keys work correctly
- Same flow: wallarm → choice → form → deploy → state saved
2026-08-01 15:12:42 +00:00
admin
e378c2a436 fix: remove alt screen to prevent huh/bubbletea terminal conflict
tea.WithAltScreen() caused the alternate buffer to conflict with
huh form rendering.  Using normal screen lets huh render inline.
Also reverted broken tea.Suspend/tea.Resume (not valid commands).
2026-08-01 15:08:25 +00:00
admin
f5ddb145cb fix: prevent recursive form launch — set deploying before huh runs
m.deploying is now set synchronously before the goroutine starts,
preventing bubbletea from re-capturing number keys meant for the
huh form.  Removed duplicate '1' key handler from wizard view.
2026-08-01 15:06:33 +00:00
admin
a6e12d54cc fix: numbered cloud region options [1] US [2] EU for quick selection 2026-08-01 15:04:11 +00:00
admin
91add96b7d fix: Enter key now triggers deploy form from wizard view 2026-08-01 15:03:36 +00:00
admin
56a22756f8 docs: jump server setup guide for remote assistance
Step-by-step instructions for configuring a Linux VM as a jump server:
- SSH server config (AllowTcpForwarding, GatewayPorts, keepalive)
- Tunnel user creation (password or key auth, no shell)
- Zoraxy/Nginx reverse proxy for TLS:443
- Testing and troubleshooting
2026-08-01 15:01:37 +00:00
admin
8b610b1bdb fix: tunnel credentials now use plain terminal prompts
Bubbletea and huh were fighting over terminal input. Fixed by:
- TUI exits when user selects 'Remote Assistance'
- Plain terminal prompts in main.go collect jumphost URL, user, pass
- Starts tunnel directly, shows connection string for admin
- No more conflicting rendering between TUI and forms
2026-08-01 14:54:08 +00:00
admin
c169887fed feat: remote assistance — deploy locally or open tunnel for admin
Startup flow now offers:
- [1] Deploy locally — config wizard, deploys on this server
- [2] Remote assistance — prompts for jumphost URL, user, password
  (or SSH key path), opens reverse TLS:443 tunnel so a remote admin
  can connect and run deployment through their terminal.

Tunnel now supports password auth in addition to key auth.
2026-08-01 14:47:49 +00:00
admin
58faccbb27 fix: source-based deploy.sh — clones repo, builds binary
Gitea server has strict HTTP body limits (~1MB) preventing binary
uploads.  deploy.sh now clones the repo and builds from source using
Go (auto-installed if missing).

Build takes ~30 seconds.  Binary is ~7MB stripped, ~2MB with UPX.
2026-08-01 14:37:28 +00:00
36 changed files with 1212 additions and 5949 deletions

15
.env
View file

@ -1,15 +0,0 @@
# 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,3 +11,6 @@ notes/
wallarm wallarm
bin/ bin/
wallarm-upx wallarm-upx
bin/
wallarm-linux-amd64
deploy

212
JUMP_SERVER.md Normal file
View file

@ -0,0 +1,212 @@
# 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 |

View file

@ -1,36 +0,0 @@
.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,90 +1,78 @@
# Wallarm Native Node Manager # Wallarm Docker Node Manager
Single-binary deployment and management for Wallarm Native Nodes (connector mode, no Docker). Single-command Docker-based deployment for Wallarm filtering nodes.
One command to get started, one TUI to manage everything. Multiple nodes on one host, no conflicts, zero native dependencies.
## Quick Start ## Quick Start
```bash ```bash
curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/deploy.sh" | bash curl -fsSL "https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main/setup.sh" | bash
sudo ./deploy/wallarm sudo /opt/fw/deploy.sh
``` ```
That's it. The binary runs preflight checks, then opens an interactive TUI: Setup installs Python + Docker, downloads deploy script. Menu handles everything.
- **First run**: configuration wizard (cloud region, API token, node name, listen address)
- **Subsequent runs**: dashboard with node list, add/remove/configure actions
## Features ## Menu
- **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
``` ```
wallarm Interactive TUI (wizard or dashboard) [1] Deploy all Deploy all nodes from fw.conf
wallarm --tunnel Start reverse SSH tunnel to sechpoint.app [2] Add a node Interactive config + deploy
wallarm --version Show version [3] Status Show running containers
wallarm --help Show help [4] Remove a node Stop + delete container
[5] Edit a node Update port, upstream, mode in fw.conf
``` ```
## Dashboard ## Configuration
``` `/opt/fw/app/fw.conf` — JSON with node configs:
📊 Wallarm Dashboard
Type: native | Cloud: EU (api.wallarm.com)
──────────────────────────────────────────────────
Nodes: ```json
● srv1 — running (0.0.0.0:8081) {
● srv2 — running (0.0.0.0:8082) "srv1": {
"token": "your-wallarm-token",
Actions: "cloud": "EU",
[a] Add node "port": "8081",
[c] Configure "upstream_ip": "10.1.0.10",
[r] Remove node "upstream_port": "8081",
[t] Start tunnel "mode": "monitoring"
[q] Quit }
}
``` ```
## Architecture ## Architecture
``` ```
wallarm/ deploy.py Single Python file, ~200 lines
├── cmd/wallarm/main.go Entrypoint: preflight → TUI ├── Docker image: wallarm/node:6.13.0
├── internal/ ├── Named volumes for persistence
│ ├── shared/ System detection, validation, connectivity └── Unique ports per node
│ ├── preflight/ Mandatory checks on every start
│ ├── state/ ~/.wallarm/state.json persistence /opt/fw/
│ ├── native/ Systemd units, installer, node management ├── deploy.sh Entry wrapper
│ ├── tunnel/ Reverse SSH over TLS:443 via Zoraxy ├── app/
│ └── ui/ Bubbletea TUI (wizard + dashboard) │ ├── main.py Deploy script
├── bin/ │ ├── fw.conf Node config
│ └── wallarm-linux-amd64 Pre-built binary │ └── state.json Deployment state
├── deploy.sh One-command bootstrap └── wallarm-aio.sh Cached installer (native fallback)
├── go.mod / go.sum
└── Makefile Cross-compile targets
``` ```
## Building from Source ## Requirements
```bash - Linux with systemd
go build -ldflags "-s -w -X main.version=$(git describe --tags)" -o wallarm ./cmd/wallarm/ - Docker (auto-installed by setup.sh)
make linux-amd64 # Cross-compile - Python 3.10+ (auto-installed by setup.sh)
make all # All targets - Outbound to api.wallarm.com (EU) or us1.api.wallarm.com (US)
```
## Prerequisites ## Remote Assistance
- Linux (systemd required) See [JUMP_SERVER.md](JUMP_SERVER.md) for jump server setup.
- x86_64 or aarch64
- 2GB+ RAM, 10GB+ disk
- Outbound connectivity to Wallarm cloud (US/EU)
## License ## Agent Rules (for AI assistants working on this project)
Proprietary — see repository for terms. **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.

View file

@ -1,86 +0,0 @@
# 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

View file

@ -1,102 +0,0 @@
// 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)
}
}

View file

@ -1,530 +0,0 @@
#!/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 Normal file → Executable file
View file

@ -1,103 +1,5 @@
#!/bin/bash #!/bin/bash
# ============================================================================== export DOCKER_HOST=unix:///opt/fw/docker/docker.sock
# Wallarm Deployment — Single Binary Bootstrap # Wallarm Node Manager — wrapper
# ============================================================================== export PATH="/opt/fw/docker/bin:$PATH"
# Downloads the wallarm binary from the Git repository and places it in /usr/bin/env python3 /opt/fw/app/main.py "$@"
# ~/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"

View file

@ -1,16 +0,0 @@
# 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/
```

View file

@ -1,15 +0,0 @@
# 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
```

View file

@ -1,549 +0,0 @@
#!/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 "$@"

File diff suppressed because it is too large Load diff

View file

@ -1,268 +0,0 @@
#!/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 "$@"

View file

@ -1,512 +0,0 @@
#!/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 "$@"

View file

@ -1,91 +0,0 @@
#!/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
View file

@ -1,38 +0,0 @@
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
View file

@ -1,77 +0,0 @@
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=

View file

@ -1,218 +0,0 @@
// 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

@ -1,169 +0,0 @@
// 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, ""
}

View file

@ -1,174 +0,0 @@
// 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
}

View file

@ -1,84 +0,0 @@
// 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
}

View file

@ -1,146 +0,0 @@
// 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)
}

View file

@ -1,339 +0,0 @@
// 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
}

View file

@ -1,498 +0,0 @@
#!/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

26
remove.sh Executable file
View file

@ -0,0 +1,26 @@
#!/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

272
setup.sh Executable file → Normal file
View file

@ -1,191 +1,129 @@
#!/bin/bash #!/bin/bash
# ============================================================================== # ==============================================================================
# Wallarm Deployment Setup Script # Wallarm Docker Deployment Bootstrap
# ============================================================================== # ==============================================================================
# Downloads Wallarm deployment scripts from the Git repository and places # Flow: check connectivity → install Docker → download deploy script
# them in a single deploy/ directory. # All under /opt/fw/ — self-contained, user-editable config.
# #
# When run interactively (or piped via curl|bash to a terminal), asks which # curl -fsSL ".../setup.sh" | bash
# deployment type to download: # sudo /opt/fw/deploy.sh
# 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 set -euo pipefail
# Color definitions BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
RED='\033[0;31m' REPO="https://git.sechpoint.app/customer-engineering/wallarm/raw/branch/main"
GREEN='\033[0;32m' WALLARM_DIR="/opt/fw"
YELLOW='\033[1;33m' OFFLINE_DIR="${WALLARM_DIR}/offline"
BLUE='\033[1;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# Git repository archive URL (Gitea-style) echo -e "${BOLD}Wallarm Bootstrap${NC}"
REPO_ARCHIVE="https://git.sechpoint.app/customer-engineering/wallarm/archive/main.tar.gz"
# Temp directory for extracted archive # ── 1. Connectivity check ─────────────────────────────────────────────
TEMP_DIR=$(mktemp -d /tmp/wallarm-setup.XXXXXX) echo -e "${YELLOW}Checking connectivity...${NC}"
trap 'rm -rf "$TEMP_DIR"' EXIT 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}"
# 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 else
echo -e "${RED}${BOLD}ERROR:${NC} Neither curl nor wget is installed." echo -e "${RED} Cannot reach git.sechpoint.app${NC}"
echo -e "Please install one of them and run this script again." echo -e "${RED} Check network/firewall. Aborting.${NC}"
exit 1 exit 1
fi fi
# --- Archive download and extraction --- # ── 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
download_archive() { # ── 3. Ensure Python 3.10+ ────────────────────────────────────────────
echo -e "${YELLOW}Downloading deployment scripts from repository...${NC}" PYTHON_BIN=""
if [ "$DOWNLOAD_NAME" = "curl" ]; then 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
curl -fsSL "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null PYTHON_BIN="python3"
else
wget -qO- "$REPO_ARCHIVE" | tar -xz --strip-components=1 -C "$TEMP_DIR" 2>/dev/null
fi
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
echo -e "${GREEN} Success: archive downloaded and extracted.${NC}"
}
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 else
# Determine where we can read user input from echo -e "${YELLOW}Installing Python 3...${NC}"
INTERACTIVE="" apt-get update -qq 2>/dev/null && apt-get install -y -qq python3 2>/dev/null || \
if [[ -t 0 ]]; then yum install -y -q python3 2>/dev/null || \
INTERACTIVE="/dev/stdin" dnf install -y -q python3 2>/dev/null || \
elif [[ -c /dev/tty ]]; then apk add --no-cache python3 2>/dev/null || true
INTERACTIVE="/dev/tty" PYTHON_BIN="python3"
fi fi
echo -e "${GREEN}Python: $($PYTHON_BIN --version)${NC}"
if [[ -n "$INTERACTIVE" ]]; then # ── 4. Install Docker under /opt/fw/docker/ ───────────────────────────
echo -e "${CYAN}Which deployment type do you need?${NC}" DOCKER_DIR="${WALLARM_DIR}/docker"
echo -e " ${YELLOW}1${NC}) Docker only — Wallarm node as a container" DOCKER_URL="${DOCKER_DOWNLOAD_URL:-https://download.docker.com/linux/static/stable/x86_64/docker-29.2.1.tgz}"
echo -e " ${YELLOW}2${NC}) Native only — Wallarm node directly on this OS (no Docker)" 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}"
echo -e " ${YELLOW}3${NC}) Both — docker + native scripts"
echo if ! command -v docker >/dev/null 2>&1; then
while true; do echo -e "${YELLOW}Downloading Docker...${NC}"
read -r -p "$(echo -e "${YELLOW}Enter choice [1/2/3]: ${NC}")" choice <"$INTERACTIVE" 2>/dev/null || { mkdir -p "${DOCKER_DIR}/bin" "${DOCKER_DIR}/data"
echo -e "${YELLOW}Input unavailable, defaulting to BOTH.${NC}"
DEPLOY_TYPES+=("docker" "native") if [ -f "${OFFLINE_DIR}/docker-29.2.1.tgz" ]; then
break echo -e "${GREEN} Using offline binary${NC}"
} tar -xzf "${OFFLINE_DIR}/docker-29.2.1.tgz" -C "${DOCKER_DIR}/bin/"
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 else
# Fully headless — default to both curl -fsSL "${DOCKER_URL}" -o /tmp/docker.tgz 2>/dev/null && \
DEPLOY_TYPES+=("docker" "native") tar --strip-components=1 -xzf /tmp/docker.tgz -C "${DOCKER_DIR}/bin/" && \
echo -e "${CYAN}No terminal available: downloading BOTH deployment types.${NC}" rm -f /tmp/docker.tgz
echo -e "${YELLOW}To download only one type, set DEPLOYMENT_TYPE=docker or DEPLOYMENT_TYPE=native${NC}" 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"
fi fi
fi fi
echo
# Download archive once, then copy only the selected folders # ── 5. Pre-load Wallarm image if available ────────────────────────────
echo if command -v docker >/dev/null 2>&1; then
mkdir -p "deploy" if [ -f "${OFFLINE_DIR}/wallarm-node.tar" ]; then
download_archive echo -e "${YELLOW}Loading offline Wallarm image...${NC}"
docker load -i "${OFFLINE_DIR}/wallarm-node.tar" 2>/dev/null || true
fi
fi
for type in "${DEPLOY_TYPES[@]}"; do # ── 6. Download deploy script ─────────────────────────────────────────
deploy_type "$type" APP_DIR="${WALLARM_DIR}/app"
done mkdir -p "${APP_DIR}"
echo -e "${YELLOW}Downloading deploy script...${NC}"
# Clean exit trap removes temp dir 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 || {
# Patch library sourcing path for flat deploy/ structure # Fallback: create wrapper
# (repo scripts still reference ../common/ — fix until they're updated) cat > "${WALLARM_DIR}/deploy.sh" << 'WRAPPER'
for script in deploy/*.sh; do #!/bin/bash
[[ "$script" == "deploy/wallarm-lib.sh" ]] && continue exec python3 /opt/fw/app/main.py "$@"
sed -i 's|source "\$SCRIPT_DIR/\.\./common/wallarm-lib\.sh"|source "$SCRIPT_DIR/wallarm-lib.sh"|' "$script" WRAPPER
sed -i 's|# shellcheck source=\.\./common/wallarm-lib\.sh|# shellcheck source=./wallarm-lib.sh|' "$script" }
done chmod +x "${APP_DIR}/main.py" "${WALLARM_DIR}/deploy.sh"
echo echo
echo -e "${GREEN}${BOLD}Setup complete - requested scripts downloaded!${NC}" echo -e "${GREEN}${BOLD}Ready!${NC}"
echo -e " ${GREEN}sudo /opt/fw/deploy.sh${NC}"
echo echo
echo -e " Config: /opt/fw/app/fw.conf"
# Show next steps only for the deployment types that were downloaded echo -e " Docker: /opt/fw/docker/"
for deploy_type in "${DEPLOY_TYPES[@]}"; do echo -e " Offline: /opt/fw/offline/ (drop binaries here for air-gapped)"
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

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

1
sources/images/README.md Normal file
View file

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

252
sources/python/deploy.py Normal file
View file

@ -0,0 +1,252 @@
#!/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()

471
sources/python/main.py Normal file
View file

@ -0,0 +1,471 @@
#!/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

@ -0,0 +1,85 @@
#!/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()