Files
Cyrene ae50acfe40 fix(ssh): per-challenge SSH login + phew buffering/leak/timeout
Passwords failed on 10/16 challenges while state.json looked correct:
- only the 6 native GEMASTIK XVIII images provision 'ctfuser'; every imported
  XVI/XVII image does 'echo root:${PASSWORD} | chpasswd' and logs in as root.
  set_ssh_passwords() hardcoded ctfuser, so chpasswd set a password on an
  account nobody uses -> 'Permission denied' everywhere.
  Registry gains a per-challenge 'ssh_user'; chpasswd now targets the real
  login (and ctfuser/ctf when present) and reports failures loudly.
- phew checker: chall.py block-buffers stdout through the docker exec pipe
  (PYTHONUNBUFFERED now set) and leaks chall.py inside the container on
  timeout (26 orphans, container saturated) -> reaps the whole exec process
  group. Startup does a fresh Pailier keygen (~12 s) so crypto reads need
  _CRYPTO_TIMEOUT, not the 5 s prompt default.

Adds panel/verify_ssh_creds.py (proves the state->container binding from
inside via a real login), audit_ssh_users.sh, reset_runtime.sh.
2026-09-26 14:40:10 +08:00

70 lines
2.9 KiB
Bash

#!/usr/bin/env bash
# FULL reset of the runtime — keeps ONLY the shared challenge images.
#
# Removes: every team container, every team docker network, every anonymous/
# named volume, every per-team receiver systemd unit, and all team directories
# (state, flags, credentials, compose). KEEPS: services-* images (the
# challenges themselves), the panel, the global receiver, the challenge sources
# in services/, and the registry.
#
# This is the "purge everything except the challenges" path the organiser asked
# for after passwords drifted: fresh containers get fresh /etc/shadow state, so
# no stale credential can survive.
set -uo pipefail
BASE=/opt/gemastik18-final
TEAMS=$BASE/teams
echo "=== [1/6] stop per-team receivers + remove units ==="
for u in $(systemctl list-unit-files 'gemastik-receiver-team*.service' 2>/dev/null \
| awk '/gemastik-receiver-team/{print $1}'); do
systemctl disable --now "$u" >/dev/null 2>&1
rm -f "/etc/systemd/system/$u"
echo " removed $u"
done
systemctl daemon-reload
echo "=== [2/6] compose down for every team (before deleting dirs) ==="
for d in "$TEAMS"/team*/services; do
[ -d "$d" ] || continue
idx=$(basename "$(dirname "$d")")
echo " compose down $idx"
(cd "$d" && docker compose -p "$idx" -f docker-compose.yml down -v --remove-orphans 2>&1 | tail -1)
done
echo "=== [3/6] force-remove any surviving team containers ==="
left=$(docker ps -aq --filter 'name=_container_team' | wc -l)
echo " found $left"
[ "$left" -gt 0 ] && docker rm -f $(docker ps -aq --filter 'name=_container_team') >/dev/null 2>&1
echo "=== [4/6] remove team networks + stray volumes ==="
for n in $(docker network ls --format '{{.Name}}' | grep -E '^team[0-9]+_default$' || true); do
docker network rm "$n" >/dev/null 2>&1 && echo " network $n"
done
# anonymous + team-scoped volumes (challenge DB state lives here)
anon=$(docker volume ls -q --filter dangling=true | wc -l)
echo " dangling volumes: $anon"
[ "$anon" -gt 0 ] && docker volume prune -f >/dev/null 2>&1 && echo " pruned"
for v in $(docker volume ls --format '{{.Name}}' | grep -E '^team[0-9]+' || true); do
docker volume rm "$v" >/dev/null 2>&1 && echo " volume $v"
done
echo "=== [5/6] delete team dirs + ledgers ==="
rm -rf "$TEAMS"/team*
rm -f "$TEAMS"/leaderboard.json "$TEAMS"/points.json "$TEAMS"/attacks.json
echo " team dirs now: $(ls -d "$TEAMS"/team* 2>/dev/null | wc -l)"
echo "=== [6/6] drop team domains from Traefik ==="
cd "$BASE/panel" && python3 -c "
import sys; sys.path.insert(0,'.')
import teams
print(' ', teams.ensure_team_domains())
"
# the platform-level receiver is separate and must keep running
systemctl restart gemastik-panel 2>/dev/null
echo " panel: $(systemctl is-active gemastik-panel)"
echo "=== done ==="
docker ps --format '{{.Names}}' | grep -c '_container_team' || echo " team containers: 0"
docker images --format '{{.Repository}}' | grep -c '^services-' || true
df -h / | tail -1