Provision and operate a full RedAmon instance on any Linux server (EC2, DigitalOcean, Hetzner, bare metal) from your laptop, given only its IP + an SSH credential + a domain.
deploy.sh is a thin remote driver around RedAmon's own redamon.sh control script. It
prepares a bare host, clones the repo onto it, drives redamon.sh over SSH, and wraps the
whole thing in an internet-facing security layer that redamon.sh deliberately does not
provide (RedAmon is designed local-only): nginx + TLS + firewall + host hardening.
The security promise: one public origin over HTTPS. From the internet only the webapp UI (443, plus 80 for the ACME challenge and an HTTP->HTTPS redirect) is reachable. The agent API, the outbound Kali MCP servers, databases, orchestrator, and the reverse-shell catcher stay bound to loopback. nginx proxies the webapp and (same-origin) exactly the four agent WebSocket paths; nothing else on the agent is exposed.
One exception, off by default: the INBOUND MCP server. With
MCP_SERVER_ENABLED=true the same origin also serves /api/mcp-server, a credentialed
machine-facing endpoint an external AI agent connects to with a bearer token. It is a
different shape of surface from the UI, so it has its own section below.
cd tooling/deploy/single-host
cp .env.example .env
# edit .env: HOST_IP, DOMAIN, SSH_KEY_PATH, OPERATOR_ALLOW_CIDRS, LETSENCRYPT_EMAIL,
# ADMIN_NAME / ADMIN_EMAIL / ADMIN_PASSWORD
./deploy.sh initFirst build takes 30-60 minutes (Kali + agent images). When it finishes, log in at
https://<your-domain>/ with the admin you set in .env.
Upgrading from before 6.9? This directory moved from
deploy/single-hosttotooling/deploy/single-host.git pullmoves only tracked files, so your.envand any provided TLS material stay at the old path. Run./redamon.sh migrate-layoutfrom the repo root once to move them (./redamon.sh updateandupdo it automatically). Until thendeploy.shfalls back to the old location and warns.
Local machine: bash, ssh, scp, curl. Only for password auth you also need
sshpass (apt install sshpass / brew install hudochenkov/sshpass/sshpass). Key auth is
recommended and needs nothing extra.
The server: a fresh Ubuntu 22.04/24.04 box. You install nothing on it by hand -
deploy.sh detects and installs every prerequisite (Docker Engine + Compose v2, nginx,
certbot, ufw, fail2ban, swap, ...) idempotently. See "How the host is bootstrapped" below.
RedAmon is heavy. redamon.sh enforces a hard 8 GB Docker-visible RAM floor at bring-up
(effective ~7.5 GB after slack), aborting below it unless REDAMON_SKIP_RAM_GATE=true.
| Profile | vCPU | RAM | Disk | Example | Notes |
|---|---|---|---|---|---|
| Bare minimum (core only, no GVM/KB) | 2 | 8 GB | 100 GB | t3.large | Fits the ~91 GB platform footprint with almost no operational headroom. Cap the build cache (DOCKER_BUILD_CACHE_MAX_GB) or the parallel build can hit 100%. Slow/swappy; keep swap on. Supply-chain scans will be admission-rejected here (see below). |
| Recommended (core + real scans, no GVM) | 4 | 16 GB | 200 GB | m5.xlarge | ~91 GB platform + ~100 GB free for operational data. Sane default, and the floor for supply-chain SCA. |
| Full (GVM + KB + Ollama judge + parallel scans) | 8 | 32 GB | 250 GB | m5.2xlarge | ~121 GB platform + ~100 GB operational. Ollama alone needs ~5-6 GB RAM while scanning. |
Disk sizing. The fixed platform footprint is ~91 GB without GVM (OS ~9 GB + the 15 built
images ~61 GB + build cache ~20 GB + the ~0.3 GB offline OSV database volume) and ~121 GB with
GVM (its images plus the vulnerability-feed volumes/DB add ~25-30 GB, and those grow on every
feed update). On top of that, budget ~100 GB of free space for operational data - the Neo4j
attack graph, scan artifacts/outputs, Postgres, and container logs all grow with use. That lands
at 200 GB without GVM, 250 GB with GVM, each leaving roughly 100 GB free for operations. gp3
EBS resizes online, so you can start smaller and grow in place. install and update now drop the
cache each build orphans, which holds that ~20 GB line item near its floor instead of letting it
creep; DOCKER_BUILD_CACHE_MAX_GB remains the belt-and-suspenders daemon-level ceiling.
RAM and supply-chain SCA. Supply-chain is the only feature that spawns a second heavy container per job: the scan/partial books a 1.75 GB envelope and the network-isolated "dirty" analyzer another ~1.5 GB alongside it. The memory governor reserves both, so a starved host rejects the scan rather than OOM-killing the stack - but on the 8 GB bare-minimum profile that rejection is the normal outcome once anything else is running. Plan on the 16 GB profile if you intend to use supply-chain SCA.
cp .env.example .env, then fill it in. Every key, grouped as in the file:
| Key | Purpose | Local-only |
|---|---|---|
HOST_IP |
Public IP/DNS of the target. Required. | yes |
REMOTE_USER |
SSH sudoer (default ubuntu). |
yes |
SSH_KEY_PATH |
Path to .pem key (recommended). ~ is expanded. |
yes (never copied to host) |
SSH_PASSWORD |
Password-auth fallback. Needs local sshpass. Discouraged. |
yes |
SSH_PORT |
SSH port (default 22). Also drives the host ufw SSH-allow rule and the fail2ban [sshd] jail, so set it to the port the host's sshd actually listens on. |
no (shipped to host) |
| Key | Purpose |
|---|---|
REPO_URL |
Public clone URL. No token needed. |
REPO_BRANCH |
Branch to deploy (default master). |
APP_DIR |
Checkout dir name. Keep stable - it fixes COMPOSE_PROJECT_NAME, which redamon.sh uses for DB-volume detection. Do not change between init and update. |
REDAMON_VERSION |
Optional version stamp baked into images. Blank -> redamon.sh default. |
| Key | Purpose |
|---|---|
ACCESS_MODE |
https-domain (default) | https-ip | http-domain | http-ip. See "Access modes" below. |
DOMAIN |
Required for *-domain modes; ignored for *-ip. |
HTTP_PORT / HTTPS_PORT |
Ports nginx listens on (and ufw opens). Default 80/443. Keep HTTP_PORT=80 if using Let's Encrypt (http-01 always validates on 80). Change only when fronted by another LB/proxy that presents 80/443 to clients. |
| Key | Purpose |
|---|---|
TLS_MODE |
letsencrypt (domain only) | provided | self-signed (ip only). |
LETSENCRYPT_EMAIL |
Required for letsencrypt. |
LETSENCRYPT_STAGING |
true issues against LE staging (not browser-trusted) while testing, to avoid the 5-certs/week rate limit. |
SSL_CERT_LOCAL / SSL_KEY_LOCAL |
provided mode: local paths (relative to this dir) SCP'd to the host. Drop files in cert/. |
SSL_KEY_PASSWORD |
Passphrase if the provided key is encrypted. |
HSTS_ENABLE |
Emit HSTS on https (default true). Auto-absent for http-*. |
| Key | Purpose |
|---|---|
OPERATOR_ALLOW_CIDRS |
Comma list. nginx allow-gate + ufw 443 source. Strongly recommended. |
SSH_ALLOW_CIDRS |
Comma list for ufw :22. Blank -> falls back to OPERATOR_ALLOW_CIDRS. |
GATE_MODE |
ip_allowlist (recommended) | basic_auth | none. |
BASIC_AUTH_USER / BASIC_AUTH_PASS |
Required for basic_auth. |
WS_REQUIRE_SESSION |
Default true. nginx auth_request requires a valid webapp session cookie before ANY /ws/* upgrade. Keep it on as defense-in-depth: all four agent WS paths now enforce a signed ws-ticket + a server-side same-origin check at the app layer (fail-closed when AGENT_WS_TICKET_SECRET is unset), and this cookie gate sits in front of that as a second layer. |
CSP_ENFORCE |
Default false (Content-Security-Policy-Report-Only). Set true to enforce the CSP after confirming the WebGL graph and xterm terminal still render. |
ENABLE_UFW |
Host firewall (default true). |
ENABLE_SSH_HARDENING |
Key-only + no-root login. Skips disabling password login if you deployed over a password (so you don't lock yourself out). |
ENABLE_FAIL2BAN |
sshd + nginx jails. |
ENABLE_UNATTENDED_UPGRADES |
Auto security patches. |
| Key | Maps to | Notes |
|---|---|---|
ENABLE_GVM |
--gvm |
Heavy; 10-20 min feed sync; the install generates a strong GVM_PASSWORD (pinned in the server .env) and applies it to gvmd's admin. |
ENABLE_KB |
--kbase |
Bakes ML embedding models (+~4.4 GB image). The real KB switch. |
ENABLE_KB_REFRESH |
kb-refresh sidecar | |
ENABLE_ZRAM |
REDAMON_ENABLE_ZRAM=1 |
Compressed-RAM cushion. |
SKIP_KB is intentionally not an input: redamon.sh derives it from the .kbase-enabled
flag it manages. Control KB with ENABLE_KB only.
| Key | Purpose |
|---|---|
SWAP_PCT |
Swapfile as a percentage of host RAM, at ANY size (default 25, 0 skips). 25% is what unlocks the memory governor's higher burst factor. |
SWAP_SIZE_GB |
DEPRECATED explicit override in GB; blank uses SWAP_PCT. |
OS_RESERVE_PCT / SERVICES_PCT / BURST_FACTOR / BURST_SWAP_MIN_PCT / BLAST_PCT / DISK_RESERVE_PCT / REDAMON_WEIGHT_* |
Memory-governor shares. Percentages only, so the same file is correct on an 8 GB and a 64 GB instance: redamon.sh computes the actual sizes from the target's own RAM. |
REDAMON_SKIP_RAM_GATE |
true bypasses the 8 GB floor on tiny boxes (risky). |
REDAMON_BUILD_PARALLEL |
Cap concurrent image builds. Blank -> redamon.sh auto-sizes. |
DOCKER_DNS |
e.g. 8.8.8.8,8.8.4.4 merged into /etc/docker/daemon.json if container DNS breaks. |
DOCKER_BUILD_CACHE_MAX_GB |
Cap the BuildKit build cache (GB) via daemon.json auto-GC. Blank -> Docker's default. install/update already drop the cache each build orphans, so this is a second-line ceiling for cache the daemon accumulates outside RedAmon's builds; set e.g. 30 on a shared or long-lived host. |
<SERVICE>_CPUS |
Per-service CPU limit (NEO4J_CPUS, KALI_CPUS, AGENT_CPUS, ...). Auto-capped at init to min(compose default, host nproc) so up never fails on a box with fewer CPUs than compose's generous defaults (neo4j 8 / kali 10 / agent 8). Set one to pin it; blank -> auto. |
| Key | Purpose |
|---|---|
REVSHELL_TARGET_CIDRS |
Scopes the ufw 4444 rule to your RoE targets. 4444 is loopback-bound by default; see the Security posture section for how to actually catch reverse shells (tunnel, or republish + DOCKER-USER rule). |
TUNNELS_ENABLED |
Keep false on an internet-exposed host. |
| Key | Purpose |
|---|---|
ADMIN_NAME / ADMIN_EMAIL / ADMIN_PASSWORD |
The first admin user, created automatically during init. There is no app-layer login lockout, so use a strong password (nginx limit_req is the only brute-force brake). |
| Key | Purpose |
|---|---|
NVD_API_KEY |
Faster NVD lookups. |
KB_EMBEDDING_USE_API / KB_EMBEDDING_API_BASE_URL / KB_EMBEDDING_API_KEY |
Use a remote embedding API instead of the baked local model. |
OSV_DB_AUTO_REFRESH |
false disables the lazy-on-scan refresh of the offline OSV database. Set it for an air-gapped host, otherwise every supply-chain scan burns OSV_DB_REFRESH_TIMEOUT on a download that cannot succeed. Blank -> true. |
OSV_DB_ECOSYSTEMS |
Comma-separated ecosystems to keep synced. Blank -> all eight (~280 MB). npm alone is ~208 MB and a noticeably shorter first install. An ecosystem you drop here is never refreshed, and a scan against it reports a missing ecosystem rather than a clean result. |
OSV_DB_TTL_SECONDS |
Freshness window before a re-sync (blank -> 86400). |
OSV_DB_REFRESH_TIMEOUT |
Ceiling on a single refresh, seconds (blank -> 900). |
SCA_INTEL_AUTO_REFRESH |
false stops contacting the supply-chain incident catalog feed (supplychainattack.org, ~5 MB) on an air-gapped host; an empty catalog still receives the bundled offline copy, installed with no network. Blank -> true. Unlike the OSV database this one is also seeded at install/update, because captured traffic is matched against it without any scan having run. |
SCA_INTEL_TTL_SECONDS |
Freshness window before a re-sync (blank -> 86400). |
SCA_INTEL_RETRY_SECONDS |
Retry floor after a failed or rejected fetch (blank -> 3600), so a broken feed is not re-fetched on every scan. |
SCA_INTEL_REFRESH_TIMEOUT |
Ceiling on a single refresh, seconds (blank -> 120). Lower than the OSV one because this feed is ~5 MB, not ~208 MB. |
SCA_INTEL_BOOTSTRAP_ON_SCAN |
false stops a scan populating a cold catalog volume (blank -> true). |
SCA_INTEL_MATCH_ENABLED |
false turns off matching captured requests against the catalog (blank -> true). |
CAPTURE_IOC_IGNORE_SUFFIXES |
Hosts excluded from that match. Blank -> the five OAST providers the catalog lists as indicators, so your own Burp Collaborator callbacks are not flagged. |
API_USAGE_CHECK_ENABLED |
false (or 0, no, off, any case) turns off Settings > API Keys > Check API usage on an air-gapped host: the check makes the webapp call each provider's account/usage endpoint with the saved keys, on demand. The last saved report stays readable. Blank -> true. |
LLM provider keys are configured in the UI, not here.
Supply-chain SCA / offline OSV database. init and update call redamon.sh's
ensure_osv_db, which populates the redamon-osv-db volume (~280 MB for all eight ecosystems)
after the tool images are built. It is best-effort: a network failure warns and lets the
stack come up, so verify re-checks the volume and warns if it is empty. To fix one by hand:
ssh in and run ./redamon.sh supply-chain-sync (optionally ... npm PyPI Go). The download
is plain HTTPS egress to the OSV GCS bucket, which the default ufw policy (allow outgoing)
already permits.
| Key | Purpose |
|---|---|
INIT_FORCE |
true skips the typed INIT wipe confirmation (CI only - dangerous). |
BACKUP_BEFORE_UPDATE |
Reserved (pre-update DB snapshot). |
DRY_RUN |
Print the resolved plan and exit without touching the host. |
VERBOSE |
Stream full remote output. |
ALLOW_INSECURE |
Must be 1 to permit http-* modes. |
| Pick | When |
|---|---|
https-domain |
You have a real DNS name pointing at the host. The default and recommended. Use TLS_MODE=letsencrypt. |
https-ip |
Bare IP, no DNS, but you still want TLS. TLS_MODE=self-signed (browser warning) or provided. |
http-domain / http-ip |
Lab/test only. No TLS. Gated behind ALLOW_INSECURE=1 and a loud banner - they invert the whole security posture (plaintext cookies/creds, no HSTS, agent WS unencrypted). |
Derived automatically from ACCESS_MODE (you never set these):
| ACCESS_MODE | Public host | nginx | WS scheme | Secure cookie / HSTS |
|---|---|---|---|---|
https-domain |
$DOMAIN |
443 + 80->443 | wss://$DOMAIN/ws/... |
on |
https-ip |
$HOST_IP |
443 + 80->443 | wss://$HOST_IP/ws/... |
on (self-signed warning) |
http-domain |
$DOMAIN |
80 only | ws://$DOMAIN/ws/... |
off |
http-ip |
$HOST_IP |
80 only | ws://$HOST_IP/ws/... |
off |
The agent WebSocket URL is baked into the webapp image at build time
(NEXT_PUBLIC_AGENT_WS_URL). Changing the scheme or host later requires a webapp rebuild
(re-run update), not just an nginx reload.
- letsencrypt (recommended,
https-domain): needsDOMAINresolving to the host and port 80 reachable. The deploy stands up an ACME webroot on 80, runscertbot certonly --webroot, then installs the hardened 443 vhost pointing at/etc/letsencrypt/live/$DOMAIN/. Auto-renew is wired via certbot's timer + an nginx reload hook. SetLETSENCRYPT_STAGING=truewhile testing. - provided (
https-domainorhttps-ip): dropfullchain.pem+privkey.pemincert/(or pointSSL_CERT_LOCAL/SSL_KEY_LOCALat them). They are SCP'd to/etc/ssl/redamon/(key600), md5-idempotent. The only way to a trusted cert on a bare IP. - self-signed (
https-ipescape hatch): generated on the host with the IP in a SAN. Browser warning expected. Not for production. - No TLS (
http-*): certbot is skipped entirely.
| Command | What it does | Destructive |
|---|---|---|
./deploy.sh init |
Wipe ALL Docker state + the checkout, then build from zero. Auto-creates the first admin from .env. |
yes (typed INIT confirm) |
./deploy.sh update |
Pull latest REPO_BRANCH HEAD and apply (diff-driven rebuild via redamon.sh). Preserves all volumes/data. |
no |
./deploy.sh status |
redamon.sh status + docker compose ps + ufw + nginx -t + cert expiry. |
no |
./deploy.sh harden |
Re-apply host hardening + nginx/TLS only (idempotent), no rebuild. | no |
./deploy.sh ssl-renew |
Renew (certbot) or re-install (provided) the cert + reload nginx. | no |
./deploy.sh down |
Stop the stack, keep volumes/images. | no |
./deploy.sh logs [service] |
Tail docker compose logs -f for a service (default agent). |
no |
./deploy.sh revshell-open |
Per-engagement: expose 4444 to REVSHELL_TARGET_CIDRS via a host socat forwarder (fails closed on reboot). |
no |
./deploy.sh revshell-close |
Tear the 4444 forwarder down and remove its ufw rules. |
no |
Connection fields can be overridden positionally, e.g.
./deploy.sh init 1.2.3.4 ~/.ssh/redamon.pem ubuntu. Use --env NAME to select
.env.NAME for per-instance configs (prod, staging, a client name).
MCP_SERVER_ENABLED=true exposes /api/mcp-server so your own AI agent can start recon
scans, read the attack-surface graph and adjust recon tuning, acting as ONE RedAmon user
inside that user's own projects. Tokens are minted per user in Global Settings ->
MCP Server; the full model is in
docs/readmes/README.MCP.SERVER.md.
Three gates sit in front of it, and all three must admit the agent. This is the part that surprises people, because two of them are invisible from the app:
| Layer | Controlled by | If it refuses |
|---|---|---|
| Cloud Security Group | your provider | connection times out |
| ufw on the app port | OPERATOR_ALLOW_CIDRS + MCP_CLIENT_CIDRS |
connection refused/dropped, nginx never sees it |
nginx location = /api/mcp-server |
MCP_CLIENT_CIDRS, MCP_EDGE_ALLOW_BEARER |
403 |
The firewall filters by PORT and cannot see the URL path. So when
OPERATOR_ALLOW_CIDRS is set (the recommended posture), an agent connecting from anywhere
else is dropped before nginx, and MCP_EDGE_ALLOW_BEARER=true on its own changes
nothing. Set MCP_CLIENT_CIDRS to your agent's egress range: it is admitted to the port,
then narrowed by the nginx location to /api/mcp-server only. It does not gain the UI, the
login page, or the agent WebSocket paths.
Two hard rules the deploy enforces for you:
http-*modes are refused outright. The credential is a bearer token in a header; over plaintext it is broadcast on every call and, unlike a session cookie, it outlives the session.ALLOW_INSECURE=1does not override this.GATE_MODE=basic_authreturns 403 untilMCP_EDGE_ALLOW_BEARER=true, because Basic and Bearer cannot share oneAuthorizationheader.
deploy.sh verify probes the endpoint and distinguishes the failure modes: 404 (disabled or
the flag never reached the container), 401 (working), 403 (the edge gate ate the header).
Repeated 401s are banned by the redamon-mcp-auth fail2ban jail.
In https-ip with a self-signed certificate most MCP clients reject the connection. Use a
real certificate (TLS_MODE=provided) or a domain.
RedAmon's threat model assumes a local-only deployment with no anonymous internet attacker. Putting it on a public IP invalidates that, so the deploy closes the gap:
- Parts of the agent REST API remain unauthenticated (the whole
/workspace/*family,/sessions/*,/files, FastAPI/docs+/openapi.json);/graph/execand/emergency-stop-allnow requirerequire_internal_auth(wave 2). All four WS paths now enforce a signed ws-ticket + a server-side same-origin check and fail closed whenAGENT_WS_TICKET_SECRETis unset - including/ws/kali-terminal(the root PTY) and the two cypherfix sockets that clone repos / edit code. The deploy still treats the agent as fully untrusted from the edge:- nginx proxies only the four
/ws/*paths to the agent - never any agent REST route. auth_request(WS_REQUIRE_SESSION=true) makes nginx validate the webapp session cookie against/api/auth/mebefore allowing any WS upgrade, so reaching those agent sockets requires a logged-in user - not just a network position - on top of the app-layer ws-ticket.- the operator IP allowlist / basic-auth gate sits in front of everything.
- nginx proxies only the four
- The webapp trusts
X-Internal-Keyas a service-to-service auth bypass and injectsX-User-Id/X-User-Roledownstream after auth. nginx strips these inbound headers on every request (plusX-Powered-By) so a client can never spoof identity or bypass auth. - There is no login rate-limit or lockout in the app. nginx supplies
limit_reqon/api/auth/login(5r/m) and a general/api/limit (30r/s), a per-IP connection cap, and slowloris timeouts. - Headers: the webapp sets none of its own, so nginx is authoritative - HSTS, a tightened
CSP (no
unsafe-eval;object-src 'none';base-uri/form-action 'self'),X-Frame-Options DENY,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, and COOP/CORP. - TLS: TLS 1.2/1.3 only, modern ECDHE ciphers,
ssl_session_tickets off, OCSP stapling. - Every secret must be strong: the deploy runs a secrets gate that fails the build on any
unset/default/short secret (
changeme,redamon_secret, ...). redamon.sh generates them; the deploy only verifies. ip_allowlist(defaultGATE_MODE) is strongly recommended given the powerful agent WS surface + root PTY - defense-in-depth on top of the app-layer ws-ticket and login.- Reverse shell (4444) is bound to loopback by the prod overlay and closed at the
firewall. To catch a direct reverse shell during an engagement, set
REVSHELL_TARGET_CIDRSto your RoE target scope and run./deploy.sh revshell-open; run./deploy.sh revshell-closethe moment you are done. How it works:revshell-openstarts a transientsocatforwarder on the host's public IP (<public-ip>:4444 -> 127.0.0.1:4444) and opens4444in ufw to your target CIDRs only. Becausesocatis a host listener (not a Docker-published port), ufw's source scoping actually applies - a plain ufw rule on a Docker-published port would be bypassed by Docker's own iptables chains. A host reboot drops the forwarder, so 4444 fails closed. The msf handler on 4444 is unauthenticated, so it is exposed only toREVSHELL_TARGET_CIDRS, never the world; set your payload's LHOST to the host's public IP and start the msf handler in RedAmon. (Alternative: a tunnel ngrok/chisel to127.0.0.1:4444withTUNNELS_ENABLED=true; never run a world-open 4444 and a tunnel at once.) - If
ENABLE_GVM=true, the install generates a strongGVM_PASSWORD, pins it in the server.env, and applies it to gvmd'sadminuser (so the app and gvmd never disagree). The deploy printsadmin / <password>once; it is always recoverable withgrep '^GVM_PASSWORD=' ~/redamon/.envon the host.
deploy.sh configures the host ufw, but on a cloud host your provider firewall (AWS
Security Group / GCP firewall / Azure NSG) is the reliable outer boundary — Docker's own
iptables chains can bypass ufw, so the cloud firewall is what still protects you if a
loopback re-bind is ever wrong (e.g. a stray docker compose up that republishes
3000/8090/4444 on 0.0.0.0). Open only:
| Port | Source | Purpose |
|---|---|---|
| 443/tcp | your OPERATOR_ALLOW_CIDRS (or 0.0.0.0/0 if you rely on the nginx gate) |
the app (HTTPS) |
| 80/tcp | 0.0.0.0/0 |
Let's Encrypt ACME challenge + redirect to 443 (required for TLS_MODE=letsencrypt) |
| 22/tcp | your IP only | SSH management |
Open nothing else. In particular do not open 8090 (agent), 3000 (webapp),
5432/7687/7474 (PostgreSQL/Neo4j), or 8000-8016/4444/8015 (Kali MCP / progress /
tunnel manager / PTY / reverse-shell) — all are loopback-bound and reached only through nginx
on 443. Opening 8090 would expose the agent's unauthenticated WebSockets (including the
anonymous root PTY /ws/kali-terminal) and its REST surface directly, bypassing nginx and
its session gate. The browser reaches the agent's WebSockets at wss://<domain>/ws/… on 443
(nginx → 127.0.0.1:8090), so 8090 never needs to be publicly reachable.
nginx runs on the host, not in a container. It sits only at the edge (80/443), reverse-proxying to two loopback backends — the webapp (
127.0.0.1:3000, for/and/api/*) and the agent (127.0.0.1:8090, for the four/ws/*paths only). It is not a middlebox between containers: all internal service-to-service traffic (webapp ↔ agent ↔ orchestrator ↔ DBs ↔ Kali) flows directly over the Docker bridge networks and never passes through nginx.
cp .env.example .env; setHOST_IP,DOMAIN,SSH_KEY_PATH,OPERATOR_ALLOW_CIDRS,LETSENCRYPT_EMAIL, andADMIN_NAME/ADMIN_EMAIL/ADMIN_PASSWORD. PointDOMAIN's A-record atHOST_IPfirst (letsencrypt needs it)../deploy.sh init-> typeINITto confirm the wipe.- Watch: teardown -> host bootstrap -> hardening -> clone + overlay ->
redamon.sh install(30-60 min build) -> secrets gate -> admin bootstrap -> nginx + TLS -> verify. - Verification asserts loopback binds (3000/8090 on 127.0.0.1 only), datastores never
off-loopback, container health, an admin exists, and
https://<domain>/api/health-> 200. - Log in at
https://<domain>/.
DRY_RUN=true ./deploy.sh init prints the resolved plan without touching the host.
init (and harden) run a dynamic detect-and-install matrix - each item is probed and
installed only if missing or too old, idempotently: base apt packages (git, openssl, curl,
jq, expect, ...), Docker Engine + Compose v2 from Docker's official repo (purging any old
docker.io/v1 compose first), the docker service + group membership, nginx, certbot (only
when letsencrypt is used), ufw, fail2ban, unattended-upgrades, a swapfile sized at SWAP_PCT of RAM
hosts, optional Docker DNS, and inotify limits. Nothing is assumed pre-present.
update is a thin wrapper around redamon.sh update: git pull --ff-only + diff-driven
selective rebuild + secret regen (all via redamon.sh, which preserves DB passwords by volume
detection), then re-renders nginx. No source patches are applied: the single-origin agent WS
URL is a first-class ARG NEXT_PUBLIC_AGENT_WS_URL in the base webapp/Dockerfile, baked from
the prod overlay's build-arg, so redamon.sh's own webapp rebuild bakes the right value.
All engagement data (Postgres, Neo4j, reports, GVM feeds, models) lives in named volumes
that survive update.
The prod compose overlay (compose/docker-compose.prod.yml) is SCP'd to the host and installed
at $HOME/.redamon-deploy (outside the checkout, so it never blocks git pull --ff-only). It
sets NEXT_PUBLIC_AGENT_WS_URL=wss://<host>/ws/agent as the webapp build-arg. The deploy works
even if tooling/deploy/single-host/ is not committed to the cloned branch (committing it is still fine
and makes the overlay part of the repo history).
Rollback is manual: git reset --hard <prev> on the host checkout, then ./deploy.sh update.
- RAM gate abort ("need ~8 GB"): provision more RAM, or set
REDAMON_SKIP_RAM_GATE=true(risky) and keep swap on (SWAP_PCT). - certbot fails:
DOMAINmust resolve to the host and port 80 must be reachable (open it in your cloud Security Group /OPERATOR_ALLOW_CIDRSmust not block the ACME check - LE validates from arbitrary IPs, so 80 must be world-open). UseLETSENCRYPT_STAGING=trueto iterate without burning the rate limit. - Secrets-gate failure: a secret is unset/default/short. redamon.sh should have generated
them; check the server
~/<APP_DIR>/.env. - GVM feed-sync wait: with
ENABLE_GVM=true, scans don't work until feeds finish syncing (10-20 min after first boot). - Never run raw
docker compose upon the host. The loopback re-binds hold only while the prod overlay is active (COMPOSE_FILE). A baredocker compose upreloads the base file and republishes 3000/8090/4444 on 0.0.0.0 and starts the profile-less GVM stack. Always go throughredamon.sh(which the deploy points at the overlay).redamon.sh up devis likewise forbidden here (it hardcodes its own compose files and defeats the overlay). - Locked out over SSH: if you deployed over a password,
ENABLE_SSH_HARDENINGwill not disable password login. Install a key, then re-runhardenwith key auth to close it.
deploy.sh never edits redamon.sh. It drives it through four channels: the subcommand
(MODE), CLI flags on install, exported env vars, and keys appended to the server-side
application .env.
.env key |
Drives |
|---|---|
ENABLE_GVM |
install --gvm |
ENABLE_KB |
install --kbase |
ENABLE_ZRAM |
REDAMON_ENABLE_ZRAM=1 |
REDAMON_BUILD_PARALLEL |
REDAMON_BUILD_PARALLEL (blank -> auto) |
DOCKER_BUILD_CACHE_MAX_GB |
builder.gc.defaultKeepStorage in /etc/docker/daemon.json (blank -> Docker default) |
REDAMON_SKIP_RAM_GATE |
REDAMON_SKIP_RAM_GATE=1 |
REDAMON_VERSION |
image build-arg stamp |
| (fixed) | COMPOSE_FILE = base + prod overlay, made sticky |
APP_DIR |
COMPOSE_PROJECT_NAME (do not change between init/update) |
ADMIN_* |
first-admin creation on init |
NVD_API_KEY, KB_EMBEDDING_*, TUNNELS_ENABLED, OSV_DB_* |
appended to the server .env |
Intentionally hands-off (redamon.sh owns these; the deploy only verifies): AUTH_SECRET,
INTERNAL_API_KEY, SCANNER_API_KEY, ORCHESTRATOR_API_KEY, MCP_AUTH_TOKEN,
AGENT_WS_TICKET_SECRET, TUNNEL_AUTH_TOKEN, POSTGRES_PASSWORD, NEO4J_PASSWORD, and
SKIP_KB (derived from the .kbase-enabled flag).