Root cause of a real production failure on the first live install
(wap-proxy, 2026-07-25): DB_PASSWORD was generated with
`openssl rand -base64 24`, which can produce '/', '+', or '=' --
docker-compose.yml then naively interpolated the raw password into
postgres://postgres:${DB_PASSWORD}@db:5432/..., and a generated
password containing '/' broke the connection string outright. The API
never became healthy; log ingestion failed completely.
- install.sh now generates with `openssl rand -hex 32` (always
[0-9a-f], can't produce this class of character). Same fix applied
everywhere else openssl-rand-base64-24 was referenced.
- docker-compose.yml no longer builds DATABASE_URL by string
interpolation -- DB_PASSWORD is passed as its own var and the API
assembles the connection string safely internally using
net/url.UserPassword (proper percent-encoding), a second,
independent layer so the installer doesn't rely on the password
generator alone. See the matching argus-appliance commit for the
Go-side change and its regression test.
- backup.sh/healthcheck.sh/restore.sh/uninstall.sh still defaulted
ARGUS_INSTALL_DIR to $HOME/argus, inconsistent with install.sh/
update.sh's own /opt/argus default (changed in an earlier commit
this session) -- confirmed live on the same install: healthcheck.sh
and uninstall.sh reported "no installation found" when run from the
real, correct directory. All five scripts now agree on /opt/argus.
Corresponding argus-appliance fix (config.go's buildDatabaseURL, v3.73.1)
already built and pushed to git-cloud.weboria.eu/weboria/argus-api.
178 lines
8.9 KiB
Bash
Executable File
178 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# ARGUS Enterprise Web Security Platform — installer. Fetches the production
|
|
# compose stack, generates secrets, starts the platform, and prints your
|
|
# admin credentials.
|
|
#
|
|
# curl -fsSL https://git-cloud.weboria.eu/weboria/argus-installer/raw/branch/main/install.sh | bash
|
|
#
|
|
set -euo pipefail
|
|
|
|
REPO_RAW_BASE="https://git-cloud.weboria.eu/weboria/argus-installer/raw/branch/main"
|
|
INSTALL_DIR="${ARGUS_INSTALL_DIR:-/opt/argus}"
|
|
NETWORK_NAME="argus-network"
|
|
# Production images are pulled from Weboria's own registry, built into
|
|
# git-cloud.weboria.eu (Gitea's container registry) — docker-compose.yml's
|
|
# image: lines reference git-cloud.weboria.eu/weboria/argus-{api,ui,proxy}
|
|
# directly. Anonymous docker pull works with no login required.
|
|
|
|
# ── Output helpers ────────────────────────────────────────────────────────────
|
|
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
|
|
info() { printf ' %s\n' "$1"; }
|
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; }
|
|
warn() { printf ' \033[33m!\033[0m %s\n' "$1"; }
|
|
fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; }
|
|
|
|
bold "ARGUS Installer"
|
|
echo
|
|
|
|
# ── 1. OS detection (informational — Linux is the supported target) ──────────
|
|
OS="$(uname -s)"
|
|
case "$OS" in
|
|
Linux) ok "Detected Linux" ;;
|
|
Darwin) warn "Detected macOS — supported for local evaluation via Docker Desktop, not for production deployment." ;;
|
|
*) warn "Unrecognized OS ($OS) — proceeding, but only Linux is officially supported." ;;
|
|
esac
|
|
|
|
# ── 2. Docker / Compose availability ──────────────────────────────────────────
|
|
command -v docker >/dev/null 2>&1 || fail "Docker is not installed. Install Docker first: https://docs.docker.com/engine/install/"
|
|
docker info >/dev/null 2>&1 || fail "Docker is installed but not running (or this user lacks permission). Start Docker / add your user to the docker group, then re-run."
|
|
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 (the 'docker compose' plugin) is required. See https://docs.docker.com/compose/install/"
|
|
ok "Docker and Docker Compose are available"
|
|
|
|
# ── 3. Fetch deployment files ──────────────────────────────────────────────────
|
|
mkdir -p "$INSTALL_DIR/docker-socket-proxy"
|
|
cd "$INSTALL_DIR"
|
|
|
|
fetch() {
|
|
curl -fsSL "$REPO_RAW_BASE/$1" -o "$2" || fail "Failed to download $1"
|
|
}
|
|
|
|
fetch "docker-compose.yml" "docker-compose.yml"
|
|
fetch "docker-socket-proxy/haproxy.cfg.template" "docker-socket-proxy/haproxy.cfg.template"
|
|
for script in update.sh uninstall.sh healthcheck.sh backup.sh restore.sh; do
|
|
fetch "$script" "$script"
|
|
chmod +x "$script"
|
|
done
|
|
# The repo stores this as env.example (no leading dot) rather than .env.example
|
|
# -- Weboria's own WAF blocks any request path matching .env* as a standard
|
|
# credential-harvesting-probe rule, which also caught this legitimate static
|
|
# file when served from Gitea. Renaming sidesteps it without needing a WAF
|
|
# exception. Saved locally as .env either way.
|
|
if [ ! -f .env ]; then
|
|
fetch "env.example" ".env"
|
|
ok "Fetched deployment files into $INSTALL_DIR"
|
|
else
|
|
ok "Fetched compose file (kept your existing .env)"
|
|
fi
|
|
|
|
# ── 4. Generate secrets (first install only — never overwrite an existing .env) ──
|
|
# hex, not base64: base64 can produce '/', '+', '=', any of which breaks the
|
|
# raw postgres://user:PASSWORD@host connection string if it ever ends up
|
|
# interpolated unescaped (confirmed live on the first real production
|
|
# install — see config.go's buildDatabaseURL for the second, independent
|
|
# defensive layer this pairs with). Hex output is always [0-9a-f], which
|
|
# can never produce that class of bug, at 4 bits/char vs base64's ~6 —
|
|
# 32 hex chars (128 bits) is used in place of 24 base64 chars (~144 bits)
|
|
# to keep entropy comparable rather than matching digit-count cosmetically.
|
|
if grep -q '^DB_PASSWORD=change-me-in-production' .env 2>/dev/null; then
|
|
DB_PASSWORD="$(openssl rand -hex 32)"
|
|
sed -i.bak "s#^DB_PASSWORD=.*#DB_PASSWORD=${DB_PASSWORD}#" .env && rm -f .env.bak
|
|
ok "Generated a secure database password"
|
|
fi
|
|
|
|
# ── 5. Docker network ─────────────────────────────────────────────────────────
|
|
if docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then
|
|
ok "Docker network '$NETWORK_NAME' already exists"
|
|
else
|
|
docker network create "$NETWORK_NAME" >/dev/null
|
|
ok "Created Docker network '$NETWORK_NAME'"
|
|
fi
|
|
|
|
# ── 6. Pull and start ──────────────────────────────────────────────────────────
|
|
bold "Pulling images and starting ARGUS..."
|
|
docker compose pull
|
|
docker compose up -d
|
|
echo
|
|
|
|
# ── 7. Wait for the API to become healthy ─────────────────────────────────────
|
|
API_HOST_PORT="$(grep -oP '^API_HOST_PORT=\K.*' .env 2>/dev/null || true)"
|
|
API_HOST_PORT="${API_HOST_PORT:-9080}"
|
|
bold "Waiting for ARGUS to finish starting (this can take a few minutes on first boot)..."
|
|
READY=false
|
|
for _ in $(seq 1 90); do
|
|
if curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" >/dev/null 2>&1; then
|
|
READY=true
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
if [ "$READY" != "true" ]; then
|
|
warn "ARGUS did not report healthy within 7.5 minutes."
|
|
warn "Check container status with: docker compose -f $INSTALL_DIR/docker-compose.yml ps"
|
|
warn "and logs with: docker compose -f $INSTALL_DIR/docker-compose.yml logs api"
|
|
exit 1
|
|
fi
|
|
ok "ARGUS is up"
|
|
|
|
# ── 8. Rotate the default admin credentials ───────────────────────────────────
|
|
ADMIN_USER="admin"
|
|
ADMIN_PASSWORD="Ax9!$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 20)"
|
|
API_BASE="http://127.0.0.1:${API_HOST_PORT}/api/v1"
|
|
|
|
LOGIN_RESP="$(curl -fsS -X POST "${API_BASE}/auth/login" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"username":"admin","password":"admin"}' 2>/dev/null || true)"
|
|
TOKEN="$(printf '%s' "${LOGIN_RESP}" | grep -oP '"token"\s*:\s*"\K[^"]+' || true)"
|
|
|
|
CREDS_ROTATED=false
|
|
if [ -n "$TOKEN" ]; then
|
|
CHANGE_RESP="$(curl -fsS -o /dev/null -w '%{http_code}' -X POST "${API_BASE}/auth/change-credentials" \
|
|
-H 'Content-Type: application/json' \
|
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
-d "{\"current_password\":\"admin\",\"new_username\":\"${ADMIN_USER}\",\"new_password\":\"${ADMIN_PASSWORD}\",\"new_password_confirm\":\"${ADMIN_PASSWORD}\"}" 2>/dev/null || echo "000")"
|
|
if [ "$CHANGE_RESP" = "200" ]; then
|
|
CREDS_ROTATED=true
|
|
ok "Rotated the default admin password"
|
|
fi
|
|
fi
|
|
if [ "$CREDS_ROTATED" != "true" ]; then
|
|
warn "Could not auto-rotate admin credentials (already changed on a prior run?). Log in with your existing credentials, or admin/admin on a genuinely fresh install."
|
|
fi
|
|
|
|
# ── 9. Summary ─────────────────────────────────────────────────────────────────
|
|
UI_PORT="$(grep -oP '^UI_PORT=\K.*' .env 2>/dev/null || true)"
|
|
UI_PORT="${UI_PORT:-81}"
|
|
|
|
echo
|
|
bold "ARGUS Enterprise Web Security Platform has been successfully installed."
|
|
echo
|
|
info "Access URL: https://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${UI_PORT} (accept the self-signed certificate)"
|
|
if [ "$CREDS_ROTATED" = "true" ]; then
|
|
info "Username: ${ADMIN_USER}"
|
|
info "Password: ${ADMIN_PASSWORD}"
|
|
warn "Save this password now — it is not stored anywhere and cannot be recovered."
|
|
else
|
|
info "Log in with your existing admin credentials."
|
|
fi
|
|
echo
|
|
bold "Next steps:"
|
|
info "1. Open the administration panel at the Access URL above."
|
|
if [ "$CREDS_ROTATED" != "true" ]; then
|
|
info "2. Complete the first-run setup wizard (regional settings, guided protection checklist)."
|
|
info "3. Change the initial administrator password if you haven't already."
|
|
else
|
|
info "2. Complete the first-run setup wizard — regional settings and a guided review of every"
|
|
info " protection module (your admin password was already rotated above)."
|
|
fi
|
|
info "4. Configure domains and reverse proxy hosts under Proxy Hosts."
|
|
info "5. Review the ANIS threat intelligence connection under Threat Intel — enabled by"
|
|
info " default against the public Community Edition hub, no license key required."
|
|
info "6. Review security policies in the Security Policy Center."
|
|
echo
|
|
info "Configuration: ${INSTALL_DIR}/.env and ${INSTALL_DIR}/docker-compose.yml"
|
|
info "Upgrade: cd ${INSTALL_DIR} && docker compose pull && docker compose up -d"
|
|
info "Backup: cd ${INSTALL_DIR} && ./backup.sh"
|
|
info "Docs/support: see README.md in this directory"
|
|
echo
|