Skip to content

Deploying to a Server

Samuele Giampieri edited this page Jul 13, 2026 · 3 revisions

Deploying RedAmon to a Server

RedAmon normally runs on your own machine at http://localhost:3000. This guide is for the other case: putting a full RedAmon instance on a Linux server on the internet - an AWS EC2 box, a DigitalOcean droplet, a Hetzner server, or bare metal - so you (and your teammates) can reach it from a browser over HTTPS.

You drive the whole thing from your laptop with one command. You never install anything on the server by hand.

Local vs. server - which do you want?

  • Just trying RedAmon out, or working solo on your own laptop? Follow Getting Started instead. It runs on localhost and needs none of this.
  • Want a shared, always-on instance reachable over the internet? You're in the right place.

Security first. RedAmon was designed to run local-only and trusts everything on its own network. Exposing it raw on a public IP would hand anonymous attackers an unauthenticated root shell (the Kali sandbox terminal), among other things. The deploy tool exists precisely to close that gap: it wraps RedAmon in nginx + TLS + a firewall + host hardening so that only the login page is reachable from the internet. Do not shortcut it by opening extra ports "to make things work" - see What the deploy protects you from.


The short version

If you already know your way around a cloud server, here's the whole flow:

# on your laptop, in the repo
cd 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 init

Type INIT when prompted, wait 30-60 minutes for the first build, then log in at https://<your-domain>/. The rest of this page explains each step.


Before you begin

You need three things.

1. A server

A fresh Ubuntu 22.04 or 24.04 box with a public IP. You install nothing on it manually - deploy.sh detects and installs every prerequisite for you (Docker, nginx, certbot, the firewall, swap, and so on), and it's safe to re-run.

RedAmon is heavy. Pick a size:

Profile vCPU RAM Disk Example (AWS) Notes
Bare minimum (core only) 2 8 GB 100 GB t3.large Fits the ~90 GB platform with almost no operational headroom. Slow and swappy.
Recommended (core + real scans, no GVM) 4 16 GB 200 GB m5.xlarge ~90 GB platform + ~100 GB free for your data. The sane default.
Full (GVM + Knowledge Base + local AI judge) 8 32 GB 250 GB m5.2xlarge ~120 GB platform + ~100 GB operational. Needed for the heavy features.

Disk sizing. The fixed platform footprint is ~90 GB without GVM (OS ~9 GB + the 13 built images ~60 GB + build cache ~20 GB) and ~120 GB with GVM (its images plus the vulnerability-feed volumes/DB add ~25-30 GB and keep growing on feed updates). On top of that, plan for ~100 GB of free space for operational data - the Neo4j attack graph, scan artifacts/outputs, Postgres, and container logs all grow as you use it. So: 200 GB without GVM, 250 GB with GVM, each leaving about 100 GB free for operations. gp3-style volumes resize online, so you can start smaller and grow in place; set DOCKER_BUILD_CACHE_MAX_GB so the build cache can't slowly eat that headroom.

8 GB RAM is a hard floor. RedAmon refuses to start below it. If you must run on something smaller, you can override the check (see Troubleshooting), but expect it to be painful.

2. A domain name (strongly recommended)

A DNS name like redamon.example.com with its A record pointing at your server's IP. This gets you a real, browser-trusted HTTPS certificate automatically (via Let's Encrypt), which is the default and by far the smoothest path.

No domain? You can still deploy on a bare IP, but you'll get a browser certificate warning (self-signed TLS). See Access modes.

Set up the DNS record before you deploy - Let's Encrypt validates the certificate by looking up your domain, so it has to resolve first.

3. A few tools on your laptop

bash, ssh, scp, and curl. Almost every Linux and macOS machine already has these. (Only if you insist on password-based SSH do you also need sshpass - but key-based SSH is recommended and needs nothing extra.)

You'll also need the SSH key (.pem file) that lets you log into the server, and the repository checked out on your laptop.


Step 1: Open the right ports on your cloud firewall

Before deploying, configure your cloud provider's firewall (AWS Security Group, GCP firewall, Azure NSG). This is your reliable outer wall. Open exactly three ports and nothing else:

Port Allow from Why
443/tcp Your IP (or 0.0.0.0/0 if you trust the built-in gate) The RedAmon app (HTTPS)
80/tcp 0.0.0.0/0 Required so Let's Encrypt can issue your certificate, and to redirect visitors to HTTPS
22/tcp Your IP only SSH management

Do not open anything else. In particular, never open 8090, 3000, 5432, 7687, 7474, or 4444. Those are internal services (including the anonymous root terminal) that stay locked to the server's own loopback interface. Your browser reaches everything it needs through port 443. Opening the others would defeat the entire security design.


Step 2: Configure your deployment

On your laptop, from the repository root:

cd deploy/single-host
cp .env.example .env

Now open .env in your editor. It's long, but for a standard HTTPS-on-a-domain deployment you only need to fill in a handful of fields. Here's a minimal, working example:

# --- how to reach the server ---
HOST_IP=203.0.113.50                    # your server's public IP
REMOTE_USER=ubuntu                      # the SSH user (ubuntu on most cloud images)
SSH_KEY_PATH=~/.ssh/redamon.pem         # your SSH key

# --- how the app is exposed ---
ACCESS_MODE=https-domain                # the recommended default
DOMAIN=redamon.example.com              # your domain (A record -> HOST_IP)
TLS_MODE=letsencrypt                    # free, browser-trusted certificate
LETSENCRYPT_EMAIL=you@example.com       # for certificate renewal notices

# --- who can reach it ---
OPERATOR_ALLOW_CIDRS=203.0.113.10/32    # YOUR IP; only these can open the app

# --- the first login ---
ADMIN_NAME=Your Name
ADMIN_EMAIL=you@example.com
ADMIN_PASSWORD=<a-long-strong-password>

That's enough for a secure, TLS-protected instance. A few notes on the important fields:

  • OPERATOR_ALLOW_CIDRS is your access list. Only these IP ranges can even see the login page. Set it to your office/home IP (e.g. 203.0.113.10/32 for a single address). You can list several, comma-separated. Leave it wide (0.0.0.0/0) only if you deliberately want the login page reachable from anywhere.
  • ADMIN_PASSWORD must be strong. There is no lockout after failed logins in the app itself (only a rate-limit at the edge), so a weak admin password is a real risk. Use a long, random one.
  • SSH_KEY_PATH stays on your laptop and is never copied to the server.

Every other variable has a sensible default. When you need to tune something - extra features, custom TLS, tighter hardening, a non-standard SSH port - the Complete .env reference further down this page explains every single variable in detail.

Turning on the heavy features (optional)

These are off by default because they add build time and RAM pressure. Flip them to true in .env only if you have the capacity for them:

Set To enable Cost
ENABLE_GVM=true The GVM / OpenVAS vulnerability scanner +10-20 min feed sync on first boot; needs the Full profile
ENABLE_KB=true The local Knowledge Base (semantic search) +~4.4 GB image, more RAM

LLM API keys are not set here. You configure your AI provider keys inside the app after logging in - see AI Model Providers and Global Settings.


Step 3: Deploy

Still in deploy/single-host, run:

./deploy.sh init

The tool asks you to type INIT to confirm. This is a from-scratch build, and the wipe is total: it erases every Docker container, image, volume, and network on the server - not just RedAmon's. On a fresh, dedicated box that's exactly what you want; do not point init at a server that already runs other Docker workloads you care about. Once confirmed, it works through:

  1. Host bootstrap - installs Docker, nginx, certbot, the firewall, swap, and so on.
  2. Hardening - locks down SSH and the host firewall.
  3. Clone & build - pulls the code onto the server and builds all the images. This is the slow part: 30-60 minutes the first time, because of the large Kali and agent images. Grab a coffee.
  4. First admin - creates the login account from your .env.
  5. nginx + TLS - issues your HTTPS certificate and stands up the reverse proxy.
  6. Verification - confirms internal services are locked to loopback, containers are healthy, and https://<domain>/api/health returns OK.

Preview without touching anything. Run DRY_RUN=true ./deploy.sh init first to see exactly what the tool would do, without changing the server.

Want to watch the details? Add VERBOSE=true in front of the command to stream the full remote output.


Step 4: Log in

When init finishes, open https://<your-domain>/ in a browser (from an IP that's in your OPERATOR_ALLOW_CIDRS) and sign in with the admin email and password you set.

From here, the rest of the wiki takes over:

  1. User Management - add teammates
  2. Creating a Project - set up your first target
  3. Running Reconnaissance - run a scan
  4. AI Model Providers - plug in your LLM keys

Access modes

ACCESS_MODE is the master switch for how the app is exposed. In almost all cases you want the first one.

Mode When to use it TLS
https-domain You have a domain. The default and recommended choice. Free Let's Encrypt cert (TLS_MODE=letsencrypt)
https-ip Bare IP, no domain, but you still want encryption Self-signed (browser warning) or your own provided cert
http-domain / http-ip Lab/testing only - no encryption at all None

Avoid the http-* modes on any real server. They send passwords and session cookies in plaintext and turn off the security headers. They're gated behind ALLOW_INSECURE=1 and a loud warning for exactly that reason. Only use them on a throwaway lab box you control end-to-end.

Using your own certificate? Set TLS_MODE=provided, drop fullchain.pem and privkey.pem into the cert/ folder, and the tool ships them to the server for you. This is the way to get a trusted certificate on a bare IP.


Complete .env reference: every variable

This is the full configuration surface, grouped exactly as it appears in .env.example. For a first deployment you only touch the handful in Step 2; everything else has a working default. Two things apply across the whole file:

  • CLI positionals override the connection fields. ./deploy.sh init 1.2.3.4 ~/.ssh/key.pem ubuntu overrides HOST_IP, SSH_KEY_PATH, and REMOTE_USER for that run.
  • Multiple instances: keep one config per instance (.env.prod, .env.staging, .env.<client>) and select it with --env NAME.

Connection - how deploy.sh reaches the box

Variable Default Detail
HOST_IP (required) Public IP or DNS name of the target server. This is where deploy.sh connects over SSH, and - in the *-ip access modes - the public address browsers use. It stays on your laptop and is never written to the server. Overridable as the first CLI positional.
REMOTE_USER ubuntu The SSH login user. It must be able to run sudo (ideally passwordless). ubuntu is right for most AWS/cloud Ubuntu images; use admin, debian, or another sudoer as your image dictates.
SSH_KEY_PATH ~/.ssh/redamon.pem Path to your private SSH key. The recommended auth method. ~ is expanded. The key never leaves your laptop - it is not copied to the server. Overridable as the second CLI positional.
SSH_PASSWORD (empty) Password-auth fallback. Requires sshpass installed locally. Discouraged - and note that deploying over a password makes the hardening step leave password login enabled (so you can't lock yourself out), which is weaker. Prefer a key.
SSH_PORT 22 The port the server's sshd actually listens on. This value is shipped to the host: it drives both the ufw allow-rule for SSH and the fail2ban [sshd] jail. If your server moved SSH to a non-standard port, set this to match - otherwise the firewall could lock you out.

Repo - where the code comes from

Variable Default Detail
REPO_URL https://github.com/samugit83/redamon.git The public git URL cloned onto the server. No access token is needed. Point it at your own fork if you deploy custom code.
REPO_BRANCH master The branch to deploy. ./deploy.sh update pulls this branch's latest commit.
APP_DIR redamon The checkout directory name on the server (under the remote user's home). It also fixes COMPOSE_PROJECT_NAME, which RedAmon uses to recognize your existing database volumes and preserve their passwords. Never change it between init and update - doing so orphans your data volumes, and RedAmon would treat the instance as brand new.
REDAMON_VERSION (empty) An optional version string stamped into the built images for traceability. Blank uses RedAmon's own default. Cosmetic - it doesn't change behavior.

Public access mode & TLS - how the UI is exposed

Variable Default Detail
ACCESS_MODE https-domain The master switch: https-domain | https-ip | http-domain | http-ip. It derives the public host, the nginx listeners, the WebSocket scheme (wss/ws), and whether Secure cookies + HSTS are on. See Access modes. The two http-* values are lab-only and refuse to run without ALLOW_INSECURE=1.
DOMAIN redamon.example.com Your DNS name. Required for the *-domain modes, ignored for *-ip. Its A record must resolve to HOST_IP before you deploy, because Let's Encrypt validates it.
HTTP_PORT 80 The port nginx listens on for HTTP and that ufw opens. Keep it 80 when using Let's Encrypt - the ACME http-01 challenge always validates on port 80. Change it only when another load balancer/proxy sits in front and presents 80/443 to clients itself.
HTTPS_PORT 443 The HTTPS port nginx listens on and ufw opens. Same caveat as HTTP_PORT: only change it behind a fronting load balancer.
TLS_MODE letsencrypt How the certificate is obtained. letsencrypt (domain only; free, browser-trusted, auto-renewing) | provided (you supply the cert) | self-signed (IP only; generated on the host, browser warning expected). Consulted only for https-* modes.
LETSENCRYPT_EMAIL ops@example.com Required for letsencrypt. Used for certbot registration and expiry/renewal notices. Use a mailbox you actually read.
LETSENCRYPT_STAGING false Set true to issue against Let's Encrypt's staging CA while testing. Staging certs aren't browser-trusted, but they dodge the production limit of 5 certificates per week - invaluable while you iterate on DNS/firewall. Flip back to false for the real certificate.
SSL_CERT_LOCAL cert/fullchain.pem provided mode only: local path (relative to deploy/single-host) to your full-chain certificate. It's SCP'd to /etc/ssl/redamon/ on the host, idempotently (by md5). Drop your files in the cert/ folder.
SSL_KEY_LOCAL cert/privkey.pem provided mode only: local path to your private key. Copied to the host with 600 permissions.
SSL_KEY_PASSWORD (empty) The passphrase, if your provided private key is encrypted.
HSTS_ENABLE true Emit the Strict-Transport-Security header on HTTPS, telling browsers to only ever use HTTPS for your domain. Automatically absent in the http-* modes. Leave it on for any real deployment.

Access control & host hardening - defense in depth

Variable Default Detail
OPERATOR_ALLOW_CIDRS 203.0.113.10/32 A comma-separated CIDR list that does double duty: it's the nginx allow-gate (only these sources see the app) and the ufw source for port 443. Scope it to your own IP ranges. Using 0.0.0.0/0 means the login page is reachable from anywhere and you rely on the login + rate-limit alone.
SSH_ALLOW_CIDRS (empty) A comma-separated CIDR list for the ufw port-22 rule. If blank, it falls back to OPERATOR_ALLOW_CIDRS. Set it separately when you administer over SSH from a different location than you browse the app.
GATE_MODE ip_allowlist How nginx gates the app: ip_allowlist (recommended - uses OPERATOR_ALLOW_CIDRS) | basic_auth (an HTTP Basic auth prompt in front of everything) | none (no edge gate; rely on the app login only).
BASIC_AUTH_USER (empty) Required when GATE_MODE=basic_auth: the username nginx demands before the app loads.
BASIC_AUTH_PASS (empty) Required when GATE_MODE=basic_auth: the matching password.
WS_REQUIRE_SESSION true Makes nginx run an auth_request that validates a logged-in webapp session cookie before allowing any /ws/* WebSocket upgrade. This sits in front of the app-layer signed ws-ticket as a second layer. Keep it on - the agent WebSockets include a root PTY.
CSP_ENFORCE false false runs the Content-Security-Policy in Report-Only mode (computed and reported, but nothing is blocked - the safe default). Set true to actually enforce the CSP, but only after confirming the WebGL attack-surface graph and the xterm terminal still render correctly.
ENABLE_UFW true The host firewall (ufw). Leave on - it's a core part of the loopback-only posture.
ENABLE_SSH_HARDENING true Enforce key-only login and disable root SSH. Safety valve: if you deployed over a password, it will not disable password login (so you can't be locked out). Install a key and re-run harden over it to fully close password auth.
ENABLE_FAIL2BAN true Enable fail2ban jails for sshd and nginx that ban IPs after repeated failures.
ENABLE_UNATTENDED_UPGRADES true Enable automatic, unattended installation of host security updates.

Feature flags - map to redamon.sh install options

Variable Default Detail
ENABLE_GVM false Maps to install --gvm. Turns on the GVM/OpenVAS vulnerability scanner. Heavy: expect a 10-20 min feed sync on first boot before scans return anything, and plan for the Full sizing profile. The deploy rotates GVM's default admin/admin credentials and prints the new password once - capture it.
ENABLE_KB false Maps to install --kbase. The real Knowledge Base switch: it bakes ML embedding models into the image (+~4.4 GB) and enables semantic search over your KB.
ENABLE_KB_REFRESH false Enables the kb-refresh sidecar that periodically refreshes Knowledge Base content.
ENABLE_ZRAM true Maps to REDAMON_ENABLE_ZRAM=1. Enables a compressed-RAM (zram) cushion that helps memory-tight hosts survive spikes.

SKIP_KB is deliberately not a variable you set. RedAmon derives it from the .kbase-enabled flag it manages internally. Control the Knowledge Base with ENABLE_KB only.

Resources & host tuning

Variable Default Detail
SWAP_SIZE_GB 8 Size (GB) of the swapfile the deploy creates when host RAM is under 16 GB. 0 skips swap creation. On small boxes keep it on - RedAmon's 8 GB RAM floor is tight and swap prevents hard OOM kills.
REDAMON_SKIP_RAM_GATE false true bypasses RedAmon's hard 8 GB RAM floor so it will boot on an undersized box. Risky; if you use it, keep SWAP_SIZE_GB on and expect heavy swapping.
REDAMON_BUILD_PARALLEL (empty) Caps how many container images build concurrently. Blank lets RedAmon auto-size from the host's CPU/RAM. Lower it (e.g. 1 or 2) if the build itself runs out of memory.
DOCKER_DNS (empty) Comma-separated resolvers (e.g. 8.8.8.8,8.8.4.4) merged into /etc/docker/daemon.json. Set this only if container DNS resolution is broken on your host; otherwise leave it blank.
DOCKER_BUILD_CACHE_MAX_GB (empty) Ceiling (GB) for the Docker BuildKit build cache, applied as builder.gc.defaultKeepStorage in /etc/docker/daemon.json so the daemon auto-garbage-collects the cache to that size. update reuses the cache for fast incremental rebuilds but never prunes it, so over many updates it grows unbounded and slowly eats disk. Set e.g. 30 to bound it without slowing rebuilds (the cap only evicts the least-useful layers). Blank leaves Docker's default GC. Changing it triggers a one-time docker restart during bootstrap.

Engagement / offensive knobs

Variable Default Detail
REVSHELL_TARGET_CIDRS (empty) The Rules-of-Engagement target scope for ./deploy.sh revshell-open. It scopes the temporary ufw rule for port 4444 to just these CIDRs. Empty means port 4444 stays fully closed. See Catching reverse shells.
TUNNELS_ENABLED false Keep off on an internet-exposed host. It controls the outbound tunnel managers (ngrok/chisel-style); you don't want those available on a public box unless you're deliberately using one, and never alongside a world-open 4444.

First admin - required for init

Variable Default Detail
ADMIN_NAME (required) Display name of the first admin user, created automatically during init.
ADMIN_EMAIL (required) Login email of the first admin.
ADMIN_PASSWORD (required) The first admin's password. There is no app-layer login lockout - nginx's limit_req rate limit is the only brute-force brake - so this must be long and random.

Optional app config - appended to the server-side .env

Variable Default Detail
NVD_API_KEY (empty) An NVD API key. Grants a higher rate limit for CVE/NVD lookups, so vulnerability enrichment runs faster.
KB_EMBEDDING_USE_API (empty) Set to use a remote embedding API instead of the baked-in local model. Lets you skip the +4.4 GB model image and offload embeddings to a service.
KB_EMBEDDING_API_BASE_URL (empty) Base URL of that remote embedding API.
KB_EMBEDDING_API_KEY (empty) Auth key for the remote embedding API.

LLM provider keys are not set here. Configure your AI provider credentials inside the app after logging in - see AI Model Providers and Global Settings.

Deploy behavior / safety

Variable Default Detail
INIT_FORCE false true skips the typed INIT wipe confirmation. CI only, dangerous - it removes the last guard before a total Docker wipe. Never set it interactively.
BACKUP_BEFORE_UPDATE false Reserved for a pre-update database snapshot (Postgres dump + Neo4j snapshot). Currently a placeholder flag.
DRY_RUN false true prints the fully resolved plan and exits without touching the host. Run DRY_RUN=true ./deploy.sh init to preview exactly what will happen.
VERBOSE false true streams the full remote command output instead of the summarized progress view. Useful when a step fails and you need to see why.
ALLOW_INSECURE 0 Must be 1 to permit the plaintext http-* access modes. A guardrail: those modes invert the whole security posture (plaintext cookies/credentials, no HSTS), so they refuse to run without this explicit opt-in.

Day-2 operations

All of these run from your laptop, from the same deploy/single-host directory. None of them (except init) touch your data - all your projects, scans, reports, and graph data live in Docker volumes that survive updates.

Command What it does
./deploy.sh update Pull the latest code and rebuild only what changed. Keeps all your data.
./deploy.sh status Health check: containers, firewall, nginx config, certificate expiry.
./deploy.sh logs [service] Tail live logs (defaults to the agent service, e.g. ./deploy.sh logs webapp).
./deploy.sh ssl-renew Renew or re-install the TLS certificate and reload nginx.
./deploy.sh harden Re-apply host hardening + nginx/TLS only, without rebuilding.
./deploy.sh down Stop the stack (keeps all data and images).

Managing more than one instance? Keep separate config files like .env.prod and .env.staging, and select one with --env, e.g. ./deploy.sh status --env staging.

One rule that matters: never SSH into the server and run docker compose up by hand. That reloads the wrong configuration and would republish the internal ports on the public interface. Always go through ./deploy.sh. (The same applies to redamon.sh up dev on the host - it hardcodes its own compose files and defeats the production overlay.)


Updating, data safety, and rollback

./deploy.sh update is the routine you'll use most. It pulls the latest commit of your REPO_BRANCH, then rebuilds only the images that actually changed. It is non-destructive: all your engagement data - Postgres, Neo4j, reports, GVM feeds, embedding models - lives in named Docker volumes that survive the update untouched. Database passwords are preserved too, because RedAmon detects the existing volumes by the stable COMPOSE_PROJECT_NAME (which is why you must never change APP_DIR between runs).

What update does under the hood: the deploy applies three small code changes to the checkout on the server at deploy time (they aren't committed to the app source): the baked single-origin WebSocket URL, a WebSocket-origin fix for CypherFix, and the Secure-cookie flip for HTTPS. Before pulling, update restores those files so the tree fast-forwards cleanly, runs the pull + selective rebuild, then re-applies the patches and rebuilds the webapp image so the single-origin URL and Secure-cookie behavior survive. Finally it re-renders the nginx config.

Rollback is manual. There is no one-command rollback. To go back to a previous version, SSH to the server, git reset --hard <previous-commit> inside the ~/<APP_DIR> checkout, then run ./deploy.sh update from your laptop to rebuild against that commit. Because your data is in volumes, a code rollback doesn't lose engagement data - but a schema change that already migrated your database may not cleanly roll back, so treat major-version downgrades with care.

Back up before big changes. BACKUP_BEFORE_UPDATE is currently a reserved placeholder, so take your own snapshot before a risky update: a pg_dump of Postgres and a Neo4j dump from inside their containers, or a provider-level disk snapshot of the whole server.


Under the hood: how the deploy works

You don't need this to deploy, but it demystifies what the one command is doing.

deploy.sh is a thin remote driver around RedAmon's own redamon.sh. It doesn't reimplement RedAmon - it prepares a bare host, gets the repo onto it, and drives redamon.sh install/update over SSH, then wraps the result in the internet-facing security layer (nginx + TLS + firewall + hardening) that redamon.sh deliberately omits because RedAmon is designed to run local-only. It never edits redamon.sh; it steers it through four channels: the subcommand, CLI flags on install, exported environment variables, and keys appended to the server-side application .env.

The host is bootstrapped dynamically. On init (and harden), the deploy runs a detect-and-install matrix - each item is probed and installed only if missing or too old, idempotently: base 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 and group membership, nginx, certbot (only when Let's Encrypt is used), ufw, fail2ban, unattended-upgrades, an 8 GB swapfile on hosts with < 16 GB RAM, optional Docker DNS, and raised inotify limits. Nothing is assumed to be pre-installed, which is why you can point it at a stock Ubuntu image.

nginx runs on the host, not in a container, and only at the edge. It listens on 80/443 and reverse-proxies to two loopback backends: the webapp (127.0.0.1:3000, serving / and /api/*) and the agent (127.0.0.1:8090, serving only the four /ws/* WebSocket paths - never any agent REST route). It is not a middlebox between containers: all internal service-to-service traffic (webapp ↔ agent ↔ orchestrator ↔ databases ↔ Kali) flows directly over the Docker bridge networks and never passes through nginx. That's the whole reason only port 443 needs to be public - the browser reaches the agent's WebSockets at wss://<domain>/ws/… on 443, and nginx forwards them to 127.0.0.1:8090 internally.

The prod overlay makes the loopback binds stick. The deploy ships a production compose overlay and points redamon.sh at it via a sticky COMPOSE_FILE. That overlay is what rebinds 3000/8090/4444 to 127.0.0.1. It holds only while that overlay is active - which is exactly why a bare docker compose up (or redamon.sh up dev) on the host is forbidden: it would reload the base file and republish those ports on 0.0.0.0.


Catching reverse shells during an engagement

Port 4444 (the reverse-shell catcher) is closed by default. To catch a direct reverse shell during an authorized engagement, scope it to your target's IP range and open it only for as long as you need:

# in .env, set your Rules-of-Engagement target scope:
REVSHELL_TARGET_CIDRS=198.51.100.0/24

./deploy.sh revshell-open     # opens 4444 to those targets only
# ... run your engagement ...
./deploy.sh revshell-close    # close it the moment you're done

Set your payload's LHOST to the server's public IP and start the handler inside RedAmon. The opening is deliberately fragile - a server reboot closes it automatically - so it can never be left open by accident. See Reverse Shells for how to work with the sessions themselves.


What the deploy protects you from

You don't need to understand the internals, but it helps to know why the "open only three ports" rule is non-negotiable. In plain terms, the deploy gives you:

  • One public door. Only the webapp login (over HTTPS) is reachable from the internet. The agent API, the databases, the Kali sandbox, and the anonymous root terminal all stay bound to the server's loopback interface, unreachable from outside.
  • A login gate before the app even loads. Your OPERATOR_ALLOW_CIDRS allowlist (or optional basic-auth) sits in front of everything.
  • A logged-in-session requirement on the agent's live connections, on top of a signed-ticket check in the app itself.
  • A hardened edge - modern TLS only, strict security headers, brute-force rate-limiting on the login endpoint, and stripping of any headers a client might use to fake its identity.
  • A hardened host - firewall, fail2ban, automatic security updates, and key-only SSH.
  • A secrets check that refuses to build if any password or key is left at a default or weak value.

The one thing the tool can't do for you is your cloud provider's firewall. Docker can sometimes bypass the host firewall, so your Security Group / NSG is the reliable outer boundary. That's why Step 1 matters: open only 443, 80, and 22.


Troubleshooting

The build aborts saying it needs ~8 GB of RAM. Your server is too small. Move to a bigger instance, or (at your own risk) set REDAMON_SKIP_RAM_GATE=true and keep swap on with SWAP_SIZE_GB=8. Expect heavy swapping.

Certificate issuance (certbot) fails. Two things must be true: your DOMAIN must resolve to the server's IP, and port 80 must be open to the world in your cloud firewall (Let's Encrypt validates from unpredictable IPs). While you're sorting it out, set LETSENCRYPT_STAGING=true to test against Let's Encrypt's staging server without burning through the weekly rate limit - then flip it back to false for the real certificate.

GVM scans return nothing right after deploy. If you enabled ENABLE_GVM, its vulnerability feeds take 10-20 minutes to sync on first boot. Scans won't produce results until that finishes.

I locked myself out over SSH. If you deployed using a password (rather than a key), the hardening step deliberately leaves password login enabled so you can't be locked out. Install an SSH key, then re-run ./deploy.sh harden over the key connection to fully close password login.

A deploy step failed halfway. First look at what happened: ./deploy.sh status shows the current state, and ./deploy.sh logs <service> tails a specific container. The non-destructive commands (status, harden, ssl-renew, update) are idempotent and safe to re-run - they detect what's already done. Note that re-running init does not resume: it wipes everything and rebuilds from zero (another 30-60 min). If the build failed and your config was fine, ./deploy.sh update is usually the faster way to retry, since it rebuilds without the full wipe.

For a complete, variable-by-variable reference (every .env key, the exact security posture, the update/rollback mechanics, and the host bootstrap matrix), see the single-host deploy README in the repository.


Next steps

With RedAmon live on your server:

Clone this wiki locally