Aaron Brown commited on
Commit
58f7026
Β·
1 Parent(s): 8691b5f

Snapshot-driven service lifecycle, fix Bookworm build

Browse files

- Dockerfile: remove nikto/hydra (not in Debian repos), install sqlmap
via pip, start only uvicorn at boot (no hardcoded services)
- start.sh: now snapshot-driven β€” reads spec.json topology.hosts to
determine which services to start per episode. Called by reset(),
not at container boot.
- Removes hardcoded infra assumptions β€” services scale with manifest

Files changed (2) hide show
  1. Dockerfile +22 -14
  2. start.sh +116 -149
Dockerfile CHANGED
@@ -1,15 +1,20 @@
1
  # =============================================================================
2
  # OpenRange β€” Production All-in-One Dockerfile
3
  # =============================================================================
4
- # Python 3.11 base + all range services installed via apt.
5
- # No PPA needed β€” python:3.11-slim-bookworm ships Python 3.11 natively.
 
 
 
6
  # =============================================================================
7
 
8
  FROM python:3.11-slim-bookworm
9
 
10
  ENV DEBIAN_FRONTEND=noninteractive
11
 
12
- # ── 1. System packages: services + security tools ────────────────────────────
 
 
13
 
14
  RUN apt-get update && apt-get install -y --no-install-recommends \
15
  # Web
@@ -26,26 +31,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
26
  postfix \
27
  # SSH
28
  openssh-server \
29
- # Security tools (agent toolkit β€” no artificial allowlists)
30
- nmap sqlmap hydra nikto \
31
  netcat-openbsd dnsutils tcpdump curl wget sshpass \
32
  iputils-ping whois \
33
  # Utilities
34
  jq procps iproute2 git ca-certificates bash \
35
  && rm -rf /var/lib/apt/lists/*
36
 
37
- # ── 2. Install uv for Python dependency management ──────────────────────────
 
 
 
38
 
39
  RUN pip install --no-cache-dir uv
40
 
41
- # ── 3. Create directories and fix permissions ────────────────────────────────
42
 
43
- RUN mkdir -p /var/log/siem/consolidated /run/sshd /run/php \
44
  /var/run/mysqld /var/log/mysql /var/log/nginx \
45
  && chown mysql:mysql /var/run/mysqld /var/log/mysql 2>/dev/null || true \
46
  && chmod 755 /var/log/siem
47
 
48
- # ── 4. Copy application code and install Python deps ────────────────────────
49
 
50
  WORKDIR /app
51
  COPY . /app/env
@@ -59,19 +67,19 @@ RUN uv venv --python python3.11 /app/.venv \
59
  uv sync --no-editable; \
60
  fi
61
 
62
- RUN chmod +x /app/env/start.sh 2>/dev/null || true
63
-
64
- # ── 5. Environment ──────────────────────────────────────────────────────────
65
 
66
  ENV PATH="/app/.venv/bin:$PATH"
67
  ENV PYTHONPATH="/app/env/src:/app/env:$PYTHONPATH"
68
  ENV OPENRANGE_EXECUTION_MODE=subprocess
69
 
70
- # ── 6. Health check (60s start-period for service boot) ─────────────────────
71
 
72
  HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
73
  CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
74
 
75
  EXPOSE 8000
76
 
77
- CMD ["bash", "/app/env/start.sh"]
 
 
 
1
  # =============================================================================
2
  # OpenRange β€” Production All-in-One Dockerfile
3
  # =============================================================================
4
+ # Python 3.11 base image with system packages available for procedural
5
+ # service provisioning. The OpenEnv server (uvicorn) is the only process
6
+ # started at boot β€” individual services (mysql, nginx, slapd, …) are
7
+ # started/stopped dynamically by RangeEnvironment.reset() based on the
8
+ # active snapshot manifest. No services are hardcoded.
9
  # =============================================================================
10
 
11
  FROM python:3.11-slim-bookworm
12
 
13
  ENV DEBIAN_FRONTEND=noninteractive
14
 
15
+ # ── 1. System packages ───────────────────────────────────────────────────────
16
+ # Install the *superset* of packages that any tier might need.
17
+ # The Builder/manifest decides which ones actually run per episode.
18
 
19
  RUN apt-get update && apt-get install -y --no-install-recommends \
20
  # Web
 
31
  postfix \
32
  # SSH
33
  openssh-server \
34
+ # Recon & exploitation (available to agents via subprocess)
35
+ nmap \
36
  netcat-openbsd dnsutils tcpdump curl wget sshpass \
37
  iputils-ping whois \
38
  # Utilities
39
  jq procps iproute2 git ca-certificates bash \
40
  && rm -rf /var/lib/apt/lists/*
41
 
42
+ # Python-based security tools (not in Debian repos)
43
+ RUN pip install --no-cache-dir sqlmap
44
+
45
+ # ── 2. Install uv for dependency management ──────────────────────────────────
46
 
47
  RUN pip install --no-cache-dir uv
48
 
49
+ # ── 3. Create base directories ───────────────────────────────────────────────
50
 
51
+ RUN mkdir -p /var/log/siem/consolidated /run/sshd \
52
  /var/run/mysqld /var/log/mysql /var/log/nginx \
53
  && chown mysql:mysql /var/run/mysqld /var/log/mysql 2>/dev/null || true \
54
  && chmod 755 /var/log/siem
55
 
56
+ # ── 4. Copy application code and install Python deps ─────────────────────────
57
 
58
  WORKDIR /app
59
  COPY . /app/env
 
67
  uv sync --no-editable; \
68
  fi
69
 
70
+ # ── 5. Environment ───────────────────────────────────────────────────────────
 
 
71
 
72
  ENV PATH="/app/.venv/bin:$PATH"
73
  ENV PYTHONPATH="/app/env/src:/app/env:$PYTHONPATH"
74
  ENV OPENRANGE_EXECUTION_MODE=subprocess
75
 
76
+ # ── 6. Health check ──────────────────────────────────────────────────────────
77
 
78
  HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
79
  CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
80
 
81
  EXPOSE 8000
82
 
83
+ # ── 7. Start only the OpenEnv server β€” services are snapshot-driven ──────────
84
+
85
+ CMD ["python3", "-m", "uvicorn", "open_range.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
start.sh CHANGED
@@ -1,15 +1,20 @@
1
  #!/usr/bin/env bash
2
  # =============================================================================
3
- # OpenRange β€” All-in-One Service Startup Script
4
  # =============================================================================
5
- # Follows the OpenEnv openapp_env pattern:
6
- # 1. Create required directories
7
- # 2. Start background services with readiness polling
8
- # 3. exec uvicorn as PID 1
 
 
 
 
9
  # =============================================================================
10
 
11
  set -uo pipefail
12
 
 
13
  LOGDIR="/var/log/siem"
14
  CONSOLIDATED="${LOGDIR}/consolidated"
15
 
@@ -26,193 +31,155 @@ cleanup() {
26
  }
27
  trap cleanup EXIT INT TERM
28
 
29
- # ── 1. Create required directories ──────────────────────────────────────────
30
 
31
- echo "[start.sh] Creating required directories..."
32
  mkdir -p "${CONSOLIDATED}"
33
- mkdir -p /run/php
34
- mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld
35
- chown mysql:mysql "${LOGDIR}" /var/log/mysql 2>/dev/null || true
36
- mkdir -p /var/run/sshd
37
- mkdir -p /var/run/slapd
38
- mkdir -p /var/lib/samba/private
39
- mkdir -p /var/log/nginx
40
- mkdir -p /var/log/mysql
41
-
42
- # ── 2. MySQL / MariaDB ────────────────────────────────────────────────────
43
-
44
- echo "[start.sh] Starting MySQL/MariaDB..."
45
- # Detect which daemon is available (MariaDB on Bookworm, MySQL on Jammy)
46
- MYSQLD=$(command -v mariadbd || command -v mysqld || echo "")
47
- if [ -n "$MYSQLD" ]; then
 
 
 
 
 
 
 
 
 
 
 
48
  if [ ! -d /var/lib/mysql/mysql ]; then
49
- echo "[start.sh] Initializing database data directory..."
50
  if command -v mariadb-install-db >/dev/null 2>&1; then
51
  mariadb-install-db --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
52
  else
53
- mysqld --initialize-insecure --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
54
  fi
55
  fi
56
 
57
  $MYSQLD --user=mysql --log-error="${LOGDIR}/mysql.log" &
58
  PIDS+=($!)
59
 
60
- echo -n "[start.sh] Waiting for database readiness"
61
- ADMIN_CMD=$(command -v mariadb-admin || command -v mysqladmin || echo "")
62
  for i in $(seq 1 30); do
63
- if [ -n "$ADMIN_CMD" ] && $ADMIN_CMD ping --silent 2>/dev/null; then
64
- echo " ready (${i}s)"
65
- break
66
  fi
67
- echo -n "."
68
  sleep 1
69
- if [ "$i" -eq 30 ]; then
70
- echo " TIMEOUT"
71
- echo "[start.sh] WARNING: Database did not become ready in 30s"
72
- fi
73
  done
74
- else
75
- echo "[start.sh] MySQL/MariaDB not installed, skipping"
76
- fi
77
-
78
- # ── 3. PHP-FPM ──────────────────────────────────────────────────────────────
79
 
80
- echo "[start.sh] Starting PHP-FPM..."
81
- # Find the correct php-fpm binary (varies by distro)
82
- PHP_FPM=$(command -v php-fpm8.2 || command -v php-fpm8.1 || command -v php-fpm || echo "")
83
- if [ -n "$PHP_FPM" ]; then
84
- $PHP_FPM --nodaemonize --force-stderr \
85
- > "${LOGDIR}/php-fpm.log" 2>&1 &
86
  PIDS+=($!)
87
-
88
- # Poll for PHP-FPM socket (path varies)
89
- echo -n "[start.sh] Waiting for PHP-FPM readiness"
90
- for i in $(seq 1 15); do
91
- if ls /run/php/php*-fpm.sock >/dev/null 2>&1; then
92
- echo " ready (${i}s)"
93
- break
94
  fi
95
- echo -n "."
96
  sleep 1
97
- if [ "$i" -eq 15 ]; then
98
- echo " TIMEOUT"
99
- echo "[start.sh] WARNING: PHP-FPM socket not found after 15s"
100
- fi
101
  done
102
- else
103
- echo "[start.sh] PHP-FPM not installed, skipping"
104
- fi
105
-
106
- # ── 4. Nginx ────────────────────────────────────────────────────────────────
107
-
108
- echo "[start.sh] Starting Nginx..."
109
- nginx -g "daemon off;" \
110
- > "${LOGDIR}/nginx.log" 2>&1 &
111
- PIDS+=($!)
112
-
113
- echo -n "[start.sh] Waiting for Nginx readiness"
114
- for i in $(seq 1 10); do
115
- if curl -sf http://localhost:80/ >/dev/null 2>&1 || \
116
- curl -sf http://localhost:80/ 2>&1 | grep -q ""; then
117
- echo " ready (${i}s)"
118
- break
119
- fi
120
- echo -n "."
121
- sleep 1
122
- if [ "$i" -eq 10 ]; then
123
- echo " TIMEOUT"
124
- echo "[start.sh] WARNING: Nginx did not respond within 10s"
125
- fi
126
- done
127
-
128
- # ── 5. rsyslog ──────────────────────────────────────────────────────────────
129
-
130
- echo "[start.sh] Starting rsyslog..."
131
- rsyslogd -n \
132
- > "${LOGDIR}/rsyslog.log" 2>&1 &
133
- PIDS+=($!)
134
- echo "[start.sh] rsyslog started (PID $!)"
135
-
136
- # ── 6. slapd (OpenLDAP) ────────────────────────────────────────────────────
137
 
138
- echo "[start.sh] Starting slapd..."
139
- if command -v slapd >/dev/null 2>&1; then
140
- slapd -h "ldap:/// ldapi:///" -u openldap -g openldap \
141
- > "${LOGDIR}/slapd.log" 2>&1 &
142
  PIDS+=($!)
143
-
144
- echo -n "[start.sh] Waiting for slapd readiness"
145
  for i in $(seq 1 10); do
146
  if ldapsearch -x -H ldap://localhost -b "" -s base namingContexts >/dev/null 2>&1; then
147
- echo " ready (${i}s)"
148
- break
149
  fi
150
- echo -n "."
151
  sleep 1
152
- if [ "$i" -eq 10 ]; then
153
- echo " TIMEOUT"
154
- echo "[start.sh] WARNING: slapd did not respond within 10s"
155
- fi
156
  done
157
- else
158
- echo "[start.sh] slapd not installed, skipping"
159
- fi
160
-
161
- # ── 7. Samba (smbd) ─────────────────────────────────────────────────────────
162
 
163
- echo "[start.sh] Starting Samba..."
164
- if command -v smbd >/dev/null 2>&1; then
165
- smbd --foreground --no-process-group \
166
- > "${LOGDIR}/smbd.log" 2>&1 &
167
  PIDS+=($!)
 
 
168
 
169
- echo -n "[start.sh] Waiting for smbd readiness"
 
 
 
 
170
  for i in $(seq 1 10); do
171
  if smbclient -L localhost -N >/dev/null 2>&1; then
172
- echo " ready (${i}s)"
173
- break
174
  fi
175
- echo -n "."
176
  sleep 1
177
- if [ "$i" -eq 10 ]; then
178
- echo " TIMEOUT"
179
- echo "[start.sh] WARNING: smbd did not respond within 10s"
180
- fi
181
  done
182
- else
183
- echo "[start.sh] smbd not installed, skipping"
184
- fi
185
-
186
- # ── 8. Postfix ──────────────────────────────────────────────────────────────
187
 
188
- echo "[start.sh] Starting Postfix..."
189
- if command -v postfix >/dev/null 2>&1; then
190
  postfix start > "${LOGDIR}/postfix.log" 2>&1 || true
191
- echo "[start.sh] Postfix started"
192
- else
193
- echo "[start.sh] postfix not installed, skipping"
194
- fi
195
-
196
- # ── 9. SSH ──────────────────────────────────────────────────────────────────
197
 
198
- echo "[start.sh] Starting SSH..."
199
- if command -v sshd >/dev/null 2>&1; then
 
200
  /usr/sbin/sshd -E "${LOGDIR}/sshd.log" &
201
  PIDS+=($!)
202
- echo "[start.sh] sshd started (PID $!)"
203
- else
204
- echo "[start.sh] sshd not installed, skipping"
205
- fi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
- # ── Summary ─────────────────────────────────────────────────────────────────
 
 
 
208
 
209
  echo "============================================================"
210
- echo "[start.sh] All services started. PIDs: ${PIDS[*]}"
211
  echo "[start.sh] Logs at: ${LOGDIR}/"
212
- echo "[start.sh] Starting uvicorn on port 8000..."
213
  echo "============================================================"
214
-
215
- # ── 10. exec uvicorn as PID 1 ──────────────────────────────────────────────
216
-
217
- cd /app/env
218
- exec python3 -m uvicorn open_range.server.app:app --host 0.0.0.0 --port 8000
 
1
  #!/usr/bin/env bash
2
  # =============================================================================
3
+ # OpenRange β€” Snapshot-Driven Service Startup
4
  # =============================================================================
5
+ # Called by RangeEnvironment.reset() to start services defined in a snapshot.
6
+ # NOT called at container boot β€” the Dockerfile starts only uvicorn.
7
+ #
8
+ # Usage: start.sh <snapshot_dir>
9
+ # snapshot_dir must contain a spec.json with a topology.hosts list.
10
+ # Each host name maps to a known service (nginx, mysql, slapd, etc.).
11
+ #
12
+ # Services are started based on what the snapshot requires, not hardcoded.
13
  # =============================================================================
14
 
15
  set -uo pipefail
16
 
17
+ SNAPSHOT_DIR="${1:?Usage: start.sh <snapshot_dir>}"
18
  LOGDIR="/var/log/siem"
19
  CONSOLIDATED="${LOGDIR}/consolidated"
20
 
 
31
  }
32
  trap cleanup EXIT INT TERM
33
 
34
+ # ── Parse snapshot topology ───────────────────────────────────────────────────
35
 
 
36
  mkdir -p "${CONSOLIDATED}"
37
+
38
+ if [ ! -f "${SNAPSHOT_DIR}/spec.json" ]; then
39
+ echo "[start.sh] ERROR: No spec.json found in ${SNAPSHOT_DIR}"
40
+ exit 1
41
+ fi
42
+
43
+ # Extract host list from topology
44
+ HOSTS=$(python3 -c "
45
+ import json, sys
46
+ with open('${SNAPSHOT_DIR}/spec.json') as f:
47
+ spec = json.load(f)
48
+ hosts = spec.get('topology', {}).get('hosts', [])
49
+ print(' '.join(hosts))
50
+ " 2>/dev/null || echo "")
51
+
52
+ echo "[start.sh] Snapshot hosts: ${HOSTS:-none}"
53
+
54
+ # ── Service starters (called only if snapshot needs them) ─────────────────────
55
+
56
+ start_mysql() {
57
+ local MYSQLD=$(command -v mariadbd || command -v mysqld || echo "")
58
+ if [ -z "$MYSQLD" ]; then echo "[start.sh] mysql: not installed"; return; fi
59
+
60
+ mkdir -p /var/run/mysqld && chown mysql:mysql /var/run/mysqld 2>/dev/null || true
61
+ mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql 2>/dev/null || true
62
+
63
  if [ ! -d /var/lib/mysql/mysql ]; then
 
64
  if command -v mariadb-install-db >/dev/null 2>&1; then
65
  mariadb-install-db --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
66
  else
67
+ $MYSQLD --initialize-insecure --user=mysql 2>&1 | tee "${LOGDIR}/mysql.log"
68
  fi
69
  fi
70
 
71
  $MYSQLD --user=mysql --log-error="${LOGDIR}/mysql.log" &
72
  PIDS+=($!)
73
 
74
+ local ADMIN=$(command -v mariadb-admin || command -v mysqladmin || echo "")
 
75
  for i in $(seq 1 30); do
76
+ if [ -n "$ADMIN" ] && $ADMIN ping --silent 2>/dev/null; then
77
+ echo "[start.sh] mysql: ready (${i}s)"; return
 
78
  fi
 
79
  sleep 1
 
 
 
 
80
  done
81
+ echo "[start.sh] mysql: timeout"
82
+ }
 
 
 
83
 
84
+ start_nginx() {
85
+ if ! command -v nginx >/dev/null 2>&1; then echo "[start.sh] nginx: not installed"; return; fi
86
+ mkdir -p /var/log/nginx
87
+ nginx -g "daemon off;" > "${LOGDIR}/nginx.log" 2>&1 &
 
 
88
  PIDS+=($!)
89
+ for i in $(seq 1 10); do
90
+ if curl -sf http://localhost:80/ >/dev/null 2>&1; then
91
+ echo "[start.sh] nginx: ready (${i}s)"; return
 
 
 
 
92
  fi
 
93
  sleep 1
 
 
 
 
94
  done
95
+ echo "[start.sh] nginx: timeout"
96
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ start_slapd() {
99
+ if ! command -v slapd >/dev/null 2>&1; then echo "[start.sh] slapd: not installed"; return; fi
100
+ mkdir -p /var/run/slapd
101
+ slapd -h "ldap:/// ldapi:///" -u openldap -g openldap > "${LOGDIR}/slapd.log" 2>&1 &
102
  PIDS+=($!)
 
 
103
  for i in $(seq 1 10); do
104
  if ldapsearch -x -H ldap://localhost -b "" -s base namingContexts >/dev/null 2>&1; then
105
+ echo "[start.sh] slapd: ready (${i}s)"; return
 
106
  fi
 
107
  sleep 1
 
 
 
 
108
  done
109
+ echo "[start.sh] slapd: timeout"
110
+ }
 
 
 
111
 
112
+ start_rsyslog() {
113
+ if ! command -v rsyslogd >/dev/null 2>&1; then echo "[start.sh] rsyslog: not installed"; return; fi
114
+ rsyslogd -n > "${LOGDIR}/rsyslog.log" 2>&1 &
 
115
  PIDS+=($!)
116
+ echo "[start.sh] rsyslog: started"
117
+ }
118
 
119
+ start_samba() {
120
+ if ! command -v smbd >/dev/null 2>&1; then echo "[start.sh] samba: not installed"; return; fi
121
+ mkdir -p /var/lib/samba/private
122
+ smbd --foreground --no-process-group > "${LOGDIR}/smbd.log" 2>&1 &
123
+ PIDS+=($!)
124
  for i in $(seq 1 10); do
125
  if smbclient -L localhost -N >/dev/null 2>&1; then
126
+ echo "[start.sh] samba: ready (${i}s)"; return
 
127
  fi
 
128
  sleep 1
 
 
 
 
129
  done
130
+ echo "[start.sh] samba: timeout"
131
+ }
 
 
 
132
 
133
+ start_postfix() {
134
+ if ! command -v postfix >/dev/null 2>&1; then echo "[start.sh] postfix: not installed"; return; fi
135
  postfix start > "${LOGDIR}/postfix.log" 2>&1 || true
136
+ echo "[start.sh] postfix: started"
137
+ }
 
 
 
 
138
 
139
+ start_sshd() {
140
+ if ! command -v sshd >/dev/null 2>&1; then echo "[start.sh] sshd: not installed"; return; fi
141
+ mkdir -p /var/run/sshd
142
  /usr/sbin/sshd -E "${LOGDIR}/sshd.log" &
143
  PIDS+=($!)
144
+ echo "[start.sh] sshd: started"
145
+ }
146
+
147
+ # ── Map host names to services ────────────────────────────────────────────────
148
+ # The manifest topology uses logical host names. Map them to service starters.
149
+
150
+ declare -A HOST_SERVICE_MAP=(
151
+ [web]=start_nginx
152
+ [db]=start_mysql
153
+ [ldap]=start_slapd
154
+ [siem]=start_rsyslog
155
+ [files]=start_samba
156
+ [mail]=start_postfix
157
+ [firewall]=start_rsyslog # firewall host uses rsyslog for logging
158
+ )
159
+
160
+ # SSH is started if any host needs remote access
161
+ SSH_NEEDED=false
162
+
163
+ for host in $HOSTS; do
164
+ starter="${HOST_SERVICE_MAP[$host]:-}"
165
+ if [ -n "$starter" ]; then
166
+ echo "[start.sh] Starting service for host: $host"
167
+ $starter
168
+ else
169
+ echo "[start.sh] Host '$host' has no mapped service (may be agent-only)"
170
+ fi
171
+ # Any host beyond attacker/siem might need SSH
172
+ if [ "$host" != "attacker" ] && [ "$host" != "siem" ]; then
173
+ SSH_NEEDED=true
174
+ fi
175
+ done
176
 
177
+ if $SSH_NEEDED; then
178
+ echo "[start.sh] Starting SSH (needed for host access)"
179
+ start_sshd
180
+ fi
181
 
182
  echo "============================================================"
183
+ echo "[start.sh] Services started for snapshot. PIDs: ${PIDS[*]:-none}"
184
  echo "[start.sh] Logs at: ${LOGDIR}/"
 
185
  echo "============================================================"