security: gate fifa2026 production deploys
This commit is contained in:
@@ -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!"
|
||||
|
||||
@@ -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:
|
||||
|
||||
54
docs/security/incident-2026-06-18-188-miner-triage.md
Normal file
54
docs/security/incident-2026-06-18-188-miner-triage.md
Normal file
@@ -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 對外服務埠,只保留必要入口。
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user