# ============================================================================= # Attack/Defence stack - all services, one file. # # Deployed by .gitea/workflows/deploy.yaml to $STACK_DIR (/opt/stacks/myapp). # Build contexts are relative to this file, i.e. the repo root. # # TWO RULES THIS FILE MUST OBEY (the workflow enforces both and will refuse # to deploy otherwise): # # 1. Every published port is pinned to ${WG_IP}. A bare "1338:8080" binds # 0.0.0.0, and Docker's iptables chain is evaluated before ufw's INPUT # chain -- so `ufw deny 1338` would NOT close it. Never omit the host_ip. # # 2. Anything that must survive a deploy lives either in a named volume or # under a `data/` directory. The workflow rsyncs the repo over the host # with --delete and only `data/` and `.env` are excluded from that. # A bind mount anywhere else gets wiped on every push. # # Required in /opt/stacks/myapp/.env (host-only, never in git) - see # .env.example: # WG_IP=10.x.x.x # the WireGuard address to publish on # SECRET_KEY=... # puhososik JWT signing key # ============================================================================= name: myapp x-common: &common restart: unless-stopped security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: "10m" max-file: "3" services: # ------------------------------------------------------------ chessstars # Go + cgo engine. Listens on :8080 (PORT), state in /data. chessstars: <<: *common build: ./chessstars pids_limit: 256 mem_limit: 256M cpus: 1.0 networks: [chessstars_net] ports: - "${WG_IP}:1338:8080" volumes: - ./chessstars/data:/data environment: AUTH_CLEANUP_TTL: 60m AUTH_CLEANUP_INTERVAL: 5m GAME_CLEANUP_FINISHED_TTL: 30m GAME_CLEANUP_OPEN_TTL: 60m GAME_CLEANUP_INTERVAL: 5m # The cleaners below only rm files on a mounted volume; none of them needs a # socket. network_mode: none removes them as a pivot if a service falls. chessstars_course_cleaner: <<: *common image: c4tbuts4d/dedcleaner:latest network_mode: none pids_limit: 32 mem_limit: 32M cpus: 0.1 volumes: - ./chessstars/data/courses:/courses environment: DELETE_AFTER: 60m SLEEP: 5m DIRS: /courses # --------------------------------------------------------------- example # Flask, :5000. Notes moved from ./notes to ./example/data/notes so the # rsync --delete in the deploy stops wiping them on every push (rule 2). example: <<: *common build: ./example pids_limit: 256 mem_limit: 64M cpus: 0.5 networks: [example_net] ports: - "${WG_IP}:1337:5000" volumes: - ./example/data/notes:/app/notes example_cleaner: <<: *common image: c4tbuts4d/dedcleaner:latest network_mode: none pids_limit: 32 mem_limit: 32M cpus: 0.1 volumes: - ./example/data/notes:/notes environment: DELETE_AFTER: 30m SLEEP: 30m DIRS: /notes # ------------------------------------------------------------- hard_exam # Flask, :1337. src/db.py resolves the DB to /data/data.db -> the # container path is /app/data/data.db, so /app/data is what gets persisted. # The old `./src/data.db:/app/data.db` mount pointed at a path the app never # opens, and Docker would have created it as a directory. hard_exam: <<: *common build: ./hard_exam pids_limit: 128 mem_limit: 256M cpus: 0.5 networks: [hard_exam_net] ports: - "${WG_IP}:51234:1337" volumes: - ./hard_exam/data:/app/data # ------------------------------------------------------------- puhososik # Flask, :5000. SECRET_KEY signs JWTs - app.py falls back to # 'default_secret_for_local_test' if unset, which would let anyone forge a # token, so fail the deploy loudly instead of booting insecure. puhososik: <<: *common build: ./puhososik pids_limit: 256 mem_limit: 128M cpus: 0.5 networks: [puhososik_net] ports: - "${WG_IP}:5252:5000" volumes: - ./puhososik/data:/app/data environment: SECRET_KEY: ${SECRET_KEY:?set SECRET_KEY in /opt/stacks/myapp/.env} puhososik_cleaner: <<: *common build: ./puhososik/cleaner network_mode: none pids_limit: 32 mem_limit: 32M cpus: 0.1 depends_on: - puhososik volumes: - ./puhososik/data:/data environment: DB_PATH: /data/puhososik.db DELETE_AFTER: 1800 SLEEP: 300 BATCH_SIZE: 400 # ----------------------------------------------- reverse_and_polish (pwn) # socat -> patchelf'd binary as uid 1337. entrypoint.sh runs its own cleaner # loop over /data/ctf.db, so no sidecar and nothing to persist here. reverse_and_polish: <<: *common build: ./reverse_and_polish pids_limit: 256 mem_limit: 256M cpus: 1.0 networks: [reverse_and_polish_net] ports: - "${WG_IP}:7845:1337" # --------------------------------------------------------- skibidimonitor # Rust/axum, :12345. BIND_ADDR is 0.0.0.0 *inside* the container - that is # correct and unrelated to rule 1; the host_ip on the port mapping is what # keeps it off the public internet. skibidimonitor: <<: *common build: ./skibidimonitor pids_limit: 256 mem_limit: 256M cpus: 1.0 networks: [skibidimonitor_net] ports: - "${WG_IP}:12345:12345" volumes: - skibidi-data:/app/data environment: BIND_ADDR: 0.0.0.0:12345 DATABASE_URL: sqlite:///app/data/skibidi.db?mode=rwc SESSION_TTL_SECONDS: 300 SNAPSHOT_CACHE_TTL_SECONDS: 300 # One network per service instead of the shared default bridge: without this # every container resolves and reaches every other one by name, so a single RCE # hands the attacker the whole stack. networks: chessstars_net: example_net: hard_exam_net: puhososik_net: reverse_and_polish_net: skibidimonitor_net: volumes: skibidi-data: