Add lab-02-security-as-a-system

This commit is contained in:
kirillshore committed 2026-09-28 23:53:48 +00:00
1 parent dcf58ecf2a
commit 3213ab1435
10 files changed
+652

No files matched your search

@@ -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()
@@ -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"
+42
View File
@@ -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()
+63
View File
@@ -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" <<STATE
LAB_WEB_SERVICE_TOKEN="web-$web_token"
LAB_API_SERVICE_TOKEN="api-$api_token"
LAB_ADMIN_SERVICE_TOKEN="adm-$admin_token"
STATE
chown root:root "$LAB_IDENTITY_FILE"
chmod 600 "$LAB_IDENTITY_FILE"
fi
#
# Investigation evidence
#
mkdir -p "$LAB_EVIDENCE_DIR"
if [[ ! -f "$LAB_EVIDENCE_LOG" ]]; then
touch "$LAB_EVIDENCE_LOG"
fi
# Keep the evidence readable by the Codio user and secure grader.
chown codio:codio "$LAB_EVIDENCE_DIR"
chown codio:codio "$LAB_EVIDENCE_LOG"
chmod 700 "$LAB_EVIDENCE_DIR"
chmod 600 "$LAB_EVIDENCE_LOG"
+36
View File
@@ -0,0 +1,36 @@
#!/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 "Resetting $LAB_NAME"
#
# Remove runtime
#
mc_topology_client_router_server_down
rm -f \
"$LAB_WEB_STDOUT" \
"$LAB_STATUS_STDOUT" \
"$LAB_ADMIN_STDOUT"
#
# Clear investigation evidence
#
if [[ -f "$LAB_EVIDENCE_LOG" ]]; then
: > "$LAB_EVIDENCE_LOG"
fi
printf '\n'
mc_log "$LAB_NAME has been reset."
@@ -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)
+162
View File
@@ -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."
@@ -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()
@@ -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()