diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index ee371c1..fca7967 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -7,39 +7,92 @@ on: jobs: test-and-lint: - name: Code Quality & Testing + name: Code Quality, Security Gate & Testing runs-on: ewoooc-dedicated-runner steps: - name: Checkout Code uses: actions/checkout@v4 + - name: Security policy gate + run: | + set -euo pipefail + echo "== 檢查禁止進入正式部署的臨時維運腳本 ==" + forbidden_files=" + iwooos_javae_monitor.sh + iwooos_autopatch.py + fix_guardian.py + fix_register.sh + fix_wazuh.sh + ops/harden-host.sh + " + for file in $forbidden_files; do + if [ -e "$file" ]; then + echo "禁止部署:$file 不得存在於正式產品 repo。" + exit 1 + fi + done + + echo "== 檢查硬編碼密碼、手工 SSH 修補與挖礦 IOC ==" + if grep -RInE '(sshpass|sudo -S|WAZUH_PASS\s*=|YOUR_BOT_TOKEN|xmrig|kinsing|kdevtmpfsi|stratum|pool\.supportxmr\.com|221\.156\.167\.200|0936223270|Wooo-0936223270)' \ + --exclude-dir=.git \ + --exclude-dir=.gitea \ + --exclude-dir=node_modules \ + --exclude-dir=.next \ + --exclude=package-lock.json \ + --exclude='*.md' \ + .; then + echo "禁止部署:偵測到硬編碼密碼、挖礦 IOC 或手工 SSH 修補痕跡。" + exit 1 + fi + - name: Setup Python Environment run: | apt-get update -qq - apt-get install -y -qq python3-pip python3-venv curl + apt-get install -y -qq python3-pip python3-venv python3 -m venv venv echo "PATH=$PWD/venv/bin:$PATH" >> $GITHUB_ENV - name: Install Backend Dependencies run: | - pip install -r platform/backend/requirements.txt pytest + pip install -r platform/backend/requirements.txt pytest pip-audit + + - name: Python dependency audit + run: pip-audit -r platform/backend/requirements.txt - name: Run Backend Quant Engine Tests - run: pytest platform/backend/app/analytics/ || true + run: pytest platform/backend/app/analytics/ - name: Setup Node.js Environment + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: platform/web/package-lock.json + + - name: Install Frontend Dependencies run: | - curl -fsSL https://deb.nodesource.com/setup_18.x | bash - - apt-get install -y nodejs + cd platform/web + npm ci --legacy-peer-deps + + - name: Frontend dependency audit + run: | + cd platform/web + npm audit --audit-level=high - name: Run Frontend Linting run: | cd platform/web - npm install --legacy-peer-deps - npm run lint || true + npm run lint + + - name: Validate Docker Compose + env: + DB_PASSWORD: ci-placeholder-db-password + REDIS_PASSWORD: ci-placeholder-redis-password + NEXTAUTH_SECRET: ci-placeholder-nextauth-secret + run: docker compose -f docker-compose.prod.yml config -q deploy-docker: - name: Deploy to Production VM via Rsync + name: Deploy to Production VM via Gitea CD needs: test-and-lint runs-on: ewoooc-dedicated-runner if: github.ref == 'refs/heads/main' @@ -60,7 +113,8 @@ jobs: - name: Sync Files to Production run: | - rsync -avz --ignore-errors --inplace -e "ssh -i ~/.ssh/id_deploy" \ + printf "%s\n" "${{ github.sha }}" > REVISION + rsync -az --delete --delay-updates -e "ssh -i ~/.ssh/id_deploy" \ --exclude='.git/' \ --exclude='.gitea/' \ --exclude='node_modules/' \ @@ -77,8 +131,19 @@ jobs: username: ${{ secrets.PROD_SERVER_USER }} key: ${{ secrets.PROD_SSH_PRIVATE_KEY }} script: | + set -euo pipefail echo "🚀 [Deploy] Starting deployment for 2026fifa.wooo.work" cd /opt/fifa2026/current - docker compose -f docker-compose.prod.yml up --build -d + + for file in iwooos_javae_monitor.sh iwooos_autopatch.py fix_guardian.py fix_register.sh fix_wazuh.sh ops/harden-host.sh; do + if [ -e "$file" ]; then + echo "❌ [Deploy] Forbidden emergency script still exists on production: $file" + exit 1 + fi + done + + docker compose -f docker-compose.prod.yml config -q + docker compose -f docker-compose.prod.yml build --pull + docker compose -f docker-compose.prod.yml up -d --remove-orphans docker image prune -f echo "✅ [Deploy] Deployment completed successfully!" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index bde1f54..e66a626 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -29,7 +29,7 @@ services: - DAC_OVERRIDE # postgres needs these minimal caps environment: POSTGRES_USER: fifa_user - POSTGRES_PASSWORD: ${DB_PASSWORD:-change_me} + POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required} POSTGRES_DB: fifa2026 volumes: - pg-data:/var/lib/postgresql/data @@ -56,13 +56,13 @@ services: --appendonly yes --protected-mode yes --bind 0.0.0.0 - --requirepass ${REDIS_PASSWORD:-change_me_redis} + --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required} volumes: - redis-data:/data networks: - wc2026-net healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-change_me_redis}", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}", "ping"] interval: 10s timeout: 5s retries: 5 @@ -77,8 +77,8 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - THE_ODDS_API_KEY=${THE_ODDS_API_KEY:-} - THE_ODDS_BASE=${THE_ODDS_BASE:-https://api.the-odds-api.com} - THE_ODDS_SPORT_KEY=${THE_ODDS_SPORT_KEY:-soccer_fifa_world_cup} @@ -121,7 +121,7 @@ services: <<: *backend-security command: ["python", "-m", "app.analytics.worldcup_seed"] environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 - TZ=Asia/Taipei depends_on: fifa2026-postgres: @@ -136,8 +136,8 @@ services: <<: *security-defaults command: ["python", "-m", "app.analytics.crawler"] environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - THE_ODDS_API_KEY=${THE_ODDS_API_KEY:-} - THE_ODDS_BASE=${THE_ODDS_BASE:-https://api.the-odds-api.com} - THE_ODDS_SPORT_KEY=${THE_ODDS_SPORT_KEY:-soccer_fifa_world_cup} @@ -166,7 +166,7 @@ services: <<: *security-defaults command: ["python", "-m", "app.analytics.news_worker"] environment: - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - NEWS_POLL_INTERVAL_SECONDS=${NEWS_POLL_INTERVAL_SECONDS:-900} - NEWS_QUERY=${NEWS_QUERY:-2026 FIFA World Cup OR 2026 世界盃 OR 世界盃 2026} - GEMINI_API_KEY=${GEMINI_API_KEY:-} @@ -188,8 +188,8 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - GEMINI_API_KEY=${GEMINI_API_KEY:-} - GEMINI_MODEL=${GEMINI_MODEL:-gemini-3-flash-preview} - GEMINI_REVIEW_MODEL=${GEMINI_REVIEW_MODEL:-gemini-2.5-flash} @@ -226,8 +226,8 @@ services: <<: *security-defaults command: ["python", "-m", "app.analytics.daily_card_calendar_worker"] environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - DAILY_CARD_CALENDAR_POLL_INTERVAL_SECONDS=${DAILY_CARD_CALENDAR_POLL_INTERVAL_SECONDS:-300} - DAILY_CARD_CALENDAR_CACHE_TTL_SECONDS=${DAILY_CARD_CALENDAR_CACHE_TTL_SECONDS:-420} - DAILY_CARD_CALENDAR_START_DATE=${DAILY_CARD_CALENDAR_START_DATE:-2026-06-11} @@ -250,8 +250,8 @@ services: <<: *security-defaults command: ["python", "-m", "app.analytics.fixtures_worker"] environment: - - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - DATABASE_URL=postgresql+asyncpg://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - FIXTURES_JSON_URL=${FIXTURES_JSON_URL:-https://www.thestatsapi.com/world-cup/data/fixtures.json} - FIXTURES_POLL_INTERVAL_SECONDS=${FIXTURES_POLL_INTERVAL_SECONDS:-21600} - TZ=Asia/Taipei @@ -277,8 +277,8 @@ services: - NEXT_PUBLIC_WS_URL=wss://2026fifa.wooo.work/ws/matches - ANALYTICS_BACKEND_URL=http://fifa2026-backend:8000 - NEXTAUTH_URL=https://2026fifa.wooo.work - - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-replace-me-in-production} - - DATABASE_URL=postgresql://fifa_user:${DB_PASSWORD:-change_me}@fifa2026-postgres:5432/fifa2026 + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?NEXTAUTH_SECRET is required} + - DATABASE_URL=postgresql://fifa_user:${DB_PASSWORD:?DB_PASSWORD is required}@fifa2026-postgres:5432/fifa2026 - TZ=Asia/Taipei depends_on: fifa2026-backend: @@ -292,7 +292,7 @@ services: restart: always <<: *security-defaults environment: - - REDIS_URL=redis://:${REDIS_PASSWORD:-change_me_redis}@fifa2026-redis:6379/0 + - REDIS_URL=redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@fifa2026-redis:6379/0 - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} depends_on: diff --git a/docs/security/incident-2026-06-18-188-miner-triage.md b/docs/security/incident-2026-06-18-188-miner-triage.md new file mode 100644 index 0000000..cedfd60 --- /dev/null +++ b/docs/security/incident-2026-06-18-188-miner-triage.md @@ -0,0 +1,54 @@ +# 2026-06-18 188 主機挖礦木馬疑慮清查紀錄 + +## 結論摘要 + +截至 2026-06-18 11:20(Asia/Taipei)的第一輪清查,尚未在 `current-fifa2026-web-1` 或 FIFA 相關容器內確認正在執行的挖礦程序。 + +實際確認到的高風險問題是:正式產品 repo 與正式部署目錄曾混入多個手工維運與緊急防禦腳本。這些腳本包含硬編碼密碼、跨主機 `sshpass`、自動修改正式機檔案、手動重建容器、直接刪除暫存 ELF、以及非 CI/CD 管控的自動 kill 流程。這類做法不可再進入正式環境。 + +## 188 現場觀察 + +- CPU 熱點主要是 `signoz-clickhouse`、`falco`、VibeWork/Momo 相關服務與監控元件。 +- FIFA 相關容器 CPU 佔用很低,`current-fifa2026-web-1` 在抽樣時為 0% 左右。 +- `current-fifa2026-web-1` 容器內只看到 `next-server`,未看到 `javae`、`npm start`、`xmrig`、`kinsing`、`kdevtmpfsi` 或 `/tmp` 可疑執行檔。 +- `/tmp`、`/var/tmp`、`/dev/shm` 抽樣未看到典型挖礦 ELF。 +- `ss` 抽樣未看到連往常見挖礦連接埠或已封鎖 IOC 的現行連線。 +- Falco 主要警報來自 Wazuh 與 Momo/Postgres healthcheck 讀取敏感檔案造成的告警噪音,需另行調整規則與 healthcheck。 + +## 已確認高風險檔案 + +下列檔案不得存在於正式產品 repo,也不得由手工 SSH 推進到正式環境: + +- `iwooos_javae_monitor.sh` +- `iwooos_autopatch.py` +- `fix_guardian.py` +- `fix_register.sh` +- `fix_wazuh.sh` +- `ops/harden-host.sh` + +風險原因: + +- 含硬編碼密碼或手工 sudo 流程。 +- 含跨主機 sshpass 與自動修改遠端主機設定。 +- 含自動修改 `package.json`、重建 Docker、直接操作正式容器。 +- 含可能刪除證據的 `/tmp` ELF 清除流程。 +- 含與 Docker Compose 狀態不一致的手動 `docker run`。 + +## 已套用的 repo 修正 + +- 移除上述 6 個高風險腳本。 +- 將 Wazuh API route 改成只使用環境變數,不再硬編碼帳密。 +- Wazuh 未設定或讀取失敗時,明確回傳「未接入真實資料」,不再輸出假告警、假弱點統計或假事件。 +- Gitea CD 新增安全閘門,阻擋硬編碼密碼、手工 SSH 修補、挖礦 IOC、`sshpass` 與禁止檔案。 +- Gitea CD 移除會吞錯的 `|| true`,測試、lint、稽核失敗即停止部署。 +- Gitea CD 改用 `rsync --delete`,讓正式機同步清掉已從 repo 移除的高風險腳本。 +- Docker Compose 改成要求正式環境必須提供 `DB_PASSWORD`、`REDIS_PASSWORD`、`NEXTAUTH_SECRET`,不得再使用 `change_me` 類預設值。 + +## 後續必做事項 + +- 輪換所有曾出現在腳本、環境檔、shell 歷史或部署目錄中的密碼與 API key。 +- 由 Gitea Secrets 管控正式部署密鑰,不再把密鑰寫進 repo 或腳本。 +- 移除 188 上 `/home/ollama/scripts/iwooos_javae_monitor.sh` 的 cron。 +- 清查 192.168.0.112 guardian/Wazuh 來源,確認反覆 SSH 進 188 執行防火牆檢查的流程是否仍需要保留。 +- 調整 Falco/Wazuh 規則,降低 healthcheck 造成的敏感檔案誤報。 +- 收斂 188 對外服務埠,只保留必要入口。 diff --git a/iwooos_javae_monitor.sh b/iwooos_javae_monitor.sh deleted file mode 100644 index 67af7e3..0000000 --- a/iwooos_javae_monitor.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# AWOOOI - javae Mining Trojan Monitor -# This script monitors the current-fifa2026-web-1 container for suspicious processes. -# It looks for 'javae', 'npm start', and 'entrypoint.sh' running as unexpected users or with high CPU. - -CONTAINER_NAME="current-fifa2026-web-1" -LOG_FILE="/home/ollama/logs/iwooos_javae_monitor.log" -TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN" # Can be injected or ignored if Wazuh is used -CHAT_ID="YOUR_CHAT_ID" - -mkdir -p /home/ollama/logs - -# Check if container is running -if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - exit 0 -fi - -# Find suspicious PIDs -# nextjs user (uid 1001) should ONLY be running "node node_modules/.bin/next start" -# Any process containing "npm" or "javae" or "entrypoint" is suspicious -SUSPICIOUS_PIDS=$(docker exec ${CONTAINER_NAME} ps aux 2>/dev/null | awk 'NR>1 && $11~/npm|javae|entrypoint\.sh/ {print $2}') - -if [ ! -z "$SUSPICIOUS_PIDS" ]; then - DATE=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$DATE] 🚨 SUSPICIOUS PROCESS DETECTED in $CONTAINER_NAME!" >> $LOG_FILE - - for PID in $SUSPICIOUS_PIDS; do - CMD=$(docker exec ${CONTAINER_NAME} ps -p $PID -o cmd= 2>/dev/null) - echo "[$DATE] Killing PID $PID : $CMD" >> $LOG_FILE - - # Send alert to Wazuh / Telegram - # For now, just log and kill - docker exec -u 0 ${CONTAINER_NAME} kill -9 $PID 2>/dev/null - done - - # Send an alert to Telegram if script exists - if [ -f "/home/ollama/bin/send_telegram.py" ]; then - /usr/bin/python3 /home/ollama/bin/send_telegram.py "🚨 警告:在 $CONTAINER_NAME 容器內偵測到挖礦木馬 (javae/npm start)!已經自動將其阻斷。" - fi -fi