From 3213ab1435acd686550ed5512aaad237a317b112 Mon Sep 17 00:00:00 2001 From: Kirill Shore Date: Mon, 28 Sep 2026 23:53:48 +0000 Subject: [PATCH] Add lab-02-security-as-a-system --- lab-02-security-as-a-system/README.md | 11 ++ .../scenario/admin_service.py | 72 ++++++++ .../scenario/config.sh | 40 +++++ .../scenario/identity.py | 42 +++++ .../scenario/init_state.sh | 63 +++++++ lab-02-security-as-a-system/scenario/reset.sh | 36 ++++ .../scenario/service_common.py | 47 +++++ lab-02-security-as-a-system/scenario/setup.sh | 162 ++++++++++++++++++ .../scenario/status_service.py | 108 ++++++++++++ .../scenario/web_service.py | 71 ++++++++ 10 files changed, 652 insertions(+) create mode 100644 lab-02-security-as-a-system/README.md create mode 100644 lab-02-security-as-a-system/scenario/admin_service.py create mode 100644 lab-02-security-as-a-system/scenario/config.sh create mode 100755 lab-02-security-as-a-system/scenario/identity.py create mode 100755 lab-02-security-as-a-system/scenario/init_state.sh create mode 100755 lab-02-security-as-a-system/scenario/reset.sh create mode 100644 lab-02-security-as-a-system/scenario/service_common.py create mode 100755 lab-02-security-as-a-system/scenario/setup.sh create mode 100644 lab-02-security-as-a-system/scenario/status_service.py create mode 100644 lab-02-security-as-a-system/scenario/web_service.py diff --git a/lab-02-security-as-a-system/README.md b/lab-02-security-as-a-system/README.md new file mode 100644 index 0000000..648b44e --- /dev/null +++ b/lab-02-security-as-a-system/README.md @@ -0,0 +1,11 @@ +# Lab 2 — Security as a System + +Lab 2 extends the MiniCorp client-router-server environment introduced in Lab 1. + +Students investigate the environment to determine what the system is actually doing and use observed evidence to support their conclusions. + +## Scenario + +The lab reuses the shared MiniCorp client-router-server topology. + +Scenario-specific services and assessment behavior are defined within this lab directory. diff --git a/lab-02-security-as-a-system/scenario/admin_service.py b/lab-02-security-as-a-system/scenario/admin_service.py new file mode 100644 index 0000000..647fdde --- /dev/null +++ b/lab-02-security-as-a-system/scenario/admin_service.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +from service_common import record_event + + +HOST = os.environ["MINICORP_SERVICE_HOST"] +PORT = int(os.environ["MINICORP_SERVICE_PORT"]) + +SERVICE_ID = os.environ["MINICORP_SERVICE_ID"] +SERVICE_TOKEN = os.environ["MINICORP_SERVICE_TOKEN"] + +EVIDENCE_LOG = os.environ["MINICORP_EVIDENCE_LOG"] + + +class AdminHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path != "/": + self.send_error(404) + return + + source = self.client_address[0] + + record_event( + EVIDENCE_LOG, + service="admin", + service_token=SERVICE_TOKEN, + source=source, + port=PORT, + ) + + body = ( + "MiniCorp Administration Service\n" + f"service_id: {SERVICE_ID}\n" + "environment: production\n" + "debug: enabled\n" + "backend: inventory-api\n" + ).encode("utf-8") + + self.send_response(200) + self.send_header( + "Content-Type", + "text/plain; charset=utf-8", + ) + self.send_header( + "Content-Length", + str(len(body)), + ) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format, *args): + pass + + +def main(): + server = ThreadingHTTPServer( + (HOST, PORT), + AdminHandler, + ) + + print( + f"MiniCorp admin service listening on {HOST}:{PORT}" + ) + + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/lab-02-security-as-a-system/scenario/config.sh b/lab-02-security-as-a-system/scenario/config.sh new file mode 100644 index 0000000..3d028f5 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/config.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Lab 2: Security as a System +# Scenario-specific configuration. +# +# Reusable network topology and addressing are provided by shared/minicorp. + +LAB_ID="lab-02-security-as-a-system" +LAB_NAME="Security as a System" + +# MiniCorp topology used by this scenario. +LAB_TOPOLOGY="client-router-server" + +# +# Persistent environment state +# + +LAB_STATE_DIR="/var/lib/minicorp/$LAB_ID" +LAB_IDENTITY_FILE="$LAB_STATE_DIR/service-identities" + +# +# Investigation evidence +# + +# Evidence must be available to Codio's secure assessment environment, +# which receives the student's workspace. +LAB_EVIDENCE_DIR="/home/codio/workspace/.minicorp-lab2" +LAB_EVIDENCE_LOG="$LAB_EVIDENCE_DIR/events.jsonl" + +# +# MiniCorp services +# + +LAB_WEB_PORT="8000" +LAB_STATUS_PORT="8081" +LAB_ADMIN_PORT="9090" + +LAB_WEB_STDOUT="/tmp/minicorp-lab2-web.stdout" +LAB_STATUS_STDOUT="/tmp/minicorp-lab2-status.stdout" +LAB_ADMIN_STDOUT="/tmp/minicorp-lab2-admin.stdout" diff --git a/lab-02-security-as-a-system/scenario/identity.py b/lab-02-security-as-a-system/scenario/identity.py new file mode 100755 index 0000000..e1373a1 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/identity.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import sys + +MODULUS = 1 << 24 +FACTOR = 5 +BIAS = 0x456789 +FACTOR_INVERSE = pow(FACTOR, -1, MODULUS) + + +def pack(value): + number = int(value, 16) + result = (number * FACTOR + BIAS) % MODULUS + return f"{result:06x}" + + +def unpack(value): + number = int(value, 16) + result = ((number - BIAS) * FACTOR_INVERSE) % MODULUS + return f"{result:06x}" + + +def main(): + if len(sys.argv) != 3: + raise SystemExit(2) + + operation = sys.argv[1] + value = sys.argv[2].lower() + + if len(value) != 6: + raise SystemExit(2) + + if operation == "pack": + print(pack(value)) + elif operation == "unpack": + print(unpack(value)) + else: + raise SystemExit(2) + + +if __name__ == "__main__": + main() diff --git a/lab-02-security-as-a-system/scenario/init_state.sh b/lab-02-security-as-a-system/scenario/init_state.sh new file mode 100755 index 0000000..99caf90 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/init_state.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "$SCRIPT_DIR/config.sh" + +if [[ "$EUID" -ne 0 ]]; then + printf 'ERROR: init_state.sh must run as root.\n' >&2 + exit 1 +fi + +# +# Persistent environment identity +# + +mkdir -p "$LAB_STATE_DIR" +chmod 700 "$LAB_STATE_DIR" + +if [[ ! -f "$LAB_IDENTITY_FILE" ]]; then + web_value="$(openssl rand -hex 3)" + api_value="$(openssl rand -hex 3)" + admin_value="$(openssl rand -hex 3)" + + web_token="$( + python3 "$SCRIPT_DIR/identity.py" pack "$web_value" + )" + + api_token="$( + python3 "$SCRIPT_DIR/identity.py" pack "$api_value" + )" + + admin_token="$( + python3 "$SCRIPT_DIR/identity.py" pack "$admin_value" + )" + + cat > "$LAB_IDENTITY_FILE" < "$LAB_EVIDENCE_LOG" +fi + +printf '\n' +mc_log "$LAB_NAME has been reset." diff --git a/lab-02-security-as-a-system/scenario/service_common.py b/lab-02-security-as-a-system/scenario/service_common.py new file mode 100644 index 0000000..ee4bb05 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/service_common.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import json +import os +from datetime import datetime, timezone + + +def record_event( + evidence_log, + *, + service, + service_token, + source, + port, + result="ok", + **extra, +): + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "service": service, + "service_token": service_token, + "source": source, + "port": port, + "result": result, + } + + entry.update(extra) + + line = ( + json.dumps( + entry, + sort_keys=True, + separators=(",", ":"), + ) + + "\n" + ) + + fd = os.open( + evidence_log, + os.O_WRONLY | os.O_CREAT | os.O_APPEND, + 0o600, + ) + + try: + os.write(fd, line.encode("utf-8")) + finally: + os.close(fd) diff --git a/lab-02-security-as-a-system/scenario/setup.sh b/lab-02-security-as-a-system/scenario/setup.sh new file mode 100755 index 0000000..a989df3 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/setup.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LAB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$LAB_DIR/../.." && pwd)" + +source "$SCRIPT_DIR/config.sh" +source "$REPO_ROOT/shared/minicorp/lib/minicorp.sh" +source "$REPO_ROOT/shared/minicorp/topologies/client-router-server.sh" + +printf '\n' +mc_log "Setting up $LAB_NAME" + +# +# Configuration +# + +if [[ "$LAB_TOPOLOGY" != "client-router-server" ]]; then + mc_error "Unsupported topology: $LAB_TOPOLOGY" + exit 1 +fi + +# +# Required tools +# + +for command_name in \ + ip \ + sysctl \ + openssl \ + python3 \ + ss +do + mc_require_command "$command_name" +done + +# +# Persistent state +# + +mc_log "Initializing MiniCorp lab state" + +"$SCRIPT_DIR/init_state.sh" + +source "$LAB_IDENTITY_FILE" + +# +# Runtime service identities +# + +LAB_WEB_SERVICE_ID="web-$( + python3 "$SCRIPT_DIR/identity.py" \ + unpack "${LAB_WEB_SERVICE_TOKEN#web-}" +)" + +LAB_API_SERVICE_ID="api-$( + python3 "$SCRIPT_DIR/identity.py" \ + unpack "${LAB_API_SERVICE_TOKEN#api-}" +)" + +LAB_ADMIN_SERVICE_ID="adm-$( + python3 "$SCRIPT_DIR/identity.py" \ + unpack "${LAB_ADMIN_SERVICE_TOKEN#adm-}" +)" + +# +# Clean stale runtime +# + +mc_log "Clearing stale MiniCorp runtime" + +mc_topology_client_router_server_down \ + >/dev/null 2>&1 || true + +rm -f \ + "$LAB_WEB_STDOUT" \ + "$LAB_STATUS_STDOUT" \ + "$LAB_ADMIN_STDOUT" + +# +# Build network +# + +mc_log "Building MiniCorp virtual network" + +mc_topology_client_router_server_up \ + >/dev/null + +if ! mc_topology_client_router_server_check \ + >/dev/null +then + mc_error "MiniCorp topology validation failed." + exit 1 +fi + +# +# Start services +# + +mc_log "Starting MiniCorp services" + +sudo ip netns exec "$MC_SERVER_NS" env \ + MINICORP_SERVICE_HOST="$MC_SERVER_ADDR" \ + MINICORP_SERVICE_PORT="$LAB_WEB_PORT" \ + MINICORP_SERVICE_ID="$LAB_WEB_SERVICE_ID" \ + MINICORP_SERVICE_TOKEN="$LAB_WEB_SERVICE_TOKEN" \ + MINICORP_EVIDENCE_LOG="$LAB_EVIDENCE_LOG" \ + python3 -u "$SCRIPT_DIR/web_service.py" \ + >"$LAB_WEB_STDOUT" 2>&1 & + +sudo ip netns exec "$MC_SERVER_NS" env \ + MINICORP_SERVICE_HOST="$MC_SERVER_ADDR" \ + MINICORP_SERVICE_PORT="$LAB_STATUS_PORT" \ + MINICORP_SERVICE_ID="$LAB_API_SERVICE_ID" \ + MINICORP_SERVICE_TOKEN="$LAB_API_SERVICE_TOKEN" \ + MINICORP_EVIDENCE_LOG="$LAB_EVIDENCE_LOG" \ + python3 -u "$SCRIPT_DIR/status_service.py" \ + >"$LAB_STATUS_STDOUT" 2>&1 & + +sudo ip netns exec "$MC_SERVER_NS" env \ + MINICORP_SERVICE_HOST="$MC_SERVER_ADDR" \ + MINICORP_SERVICE_PORT="$LAB_ADMIN_PORT" \ + MINICORP_SERVICE_ID="$LAB_ADMIN_SERVICE_ID" \ + MINICORP_SERVICE_TOKEN="$LAB_ADMIN_SERVICE_TOKEN" \ + MINICORP_EVIDENCE_LOG="$LAB_EVIDENCE_LOG" \ + python3 -u "$SCRIPT_DIR/admin_service.py" \ + >"$LAB_ADMIN_STDOUT" 2>&1 & + +# +# Verify listeners +# + +for port in \ + "$LAB_WEB_PORT" \ + "$LAB_STATUS_PORT" \ + "$LAB_ADMIN_PORT" +do + ready=0 + + for attempt in {1..20}; do + if mc_exec "$MC_SERVER_NS" ss -ltn | + grep -F "$MC_SERVER_ADDR:$port" \ + >/dev/null + then + ready=1 + break + fi + + sleep 0.1 + done + + if [[ "$ready" -ne 1 ]]; then + mc_error \ + "A required MiniCorp service failed to start." + exit 1 + fi +done + +printf '\n' +mc_log "$LAB_NAME is ready." diff --git a/lab-02-security-as-a-system/scenario/status_service.py b/lab-02-security-as-a-system/scenario/status_service.py new file mode 100644 index 0000000..a40de22 --- /dev/null +++ b/lab-02-security-as-a-system/scenario/status_service.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +import json +import os +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +from service_common import record_event + + +HOST = os.environ["MINICORP_SERVICE_HOST"] +PORT = int(os.environ["MINICORP_SERVICE_PORT"]) + +SERVICE_ID = os.environ["MINICORP_SERVICE_ID"] +SERVICE_TOKEN = os.environ["MINICORP_SERVICE_TOKEN"] + +EVIDENCE_LOG = os.environ["MINICORP_EVIDENCE_LOG"] + +SEQUENCE = ( + "ok", + "ok", + "error", + "ok", + "error", +) + +lock = threading.Lock() +request_count = 0 + + +def next_result(): + global request_count + + with lock: + request_count += 1 + number = request_count + + result = SEQUENCE[ + (number - 1) % len(SEQUENCE) + ] + + return number, result + + +class StatusHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path != "/": + self.send_error(404) + return + + number, result = next_result() + source = self.client_address[0] + + record_event( + EVIDENCE_LOG, + service="status", + service_token=SERVICE_TOKEN, + source=source, + port=PORT, + result=result, + request=number, + ) + + response = { + "status": result, + "service": "minicorp-api", + "service_id": SERVICE_ID, + } + + body = ( + json.dumps( + response, + sort_keys=True, + ) + + "\n" + ).encode("utf-8") + + self.send_response(200) + self.send_header( + "Content-Type", + "application/json", + ) + self.send_header( + "Content-Length", + str(len(body)), + ) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format, *args): + pass + + +def main(): + server = ThreadingHTTPServer( + (HOST, PORT), + StatusHandler, + ) + + print( + f"MiniCorp status service listening on {HOST}:{PORT}" + ) + + server.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/lab-02-security-as-a-system/scenario/web_service.py b/lab-02-security-as-a-system/scenario/web_service.py new file mode 100644 index 0000000..a7c874a --- /dev/null +++ b/lab-02-security-as-a-system/scenario/web_service.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +from service_common import record_event + + +HOST = os.environ["MINICORP_SERVICE_HOST"] +PORT = int(os.environ["MINICORP_SERVICE_PORT"]) + +SERVICE_ID = os.environ["MINICORP_SERVICE_ID"] +SERVICE_TOKEN = os.environ["MINICORP_SERVICE_TOKEN"] + +EVIDENCE_LOG = os.environ["MINICORP_EVIDENCE_LOG"] + + +class WebHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path != "/": + self.send_error(404) + return + + source = self.client_address[0] + + record_event( + EVIDENCE_LOG, + service="web", + service_token=SERVICE_TOKEN, + source=source, + port=PORT, + ) + + body = ( + "MiniCorp Employee Web Portal\n" + f"service_id: {SERVICE_ID}\n" + "environment: production\n" + "status: healthy\n" + ).encode("utf-8") + + self.send_response(200) + self.send_header( + "Content-Type", + "text/plain; charset=utf-8", + ) + self.send_header( + "Content-Length", + str(len(body)), + ) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format, *args): + pass + + +def main(): + server = ThreadingHTTPServer( + (HOST, PORT), + WebHandler, + ) + + print( + f"MiniCorp web service listening on {HOST}:{PORT}" + ) + + server.serve_forever() + + +if __name__ == "__main__": + main()