Bỏ qua để đến nội dung

Runbook phục hồi thảm họa (Phase S)

Runbook này dùng khi cần kiểm chứng một bộ backup hoặc khôi phục dịch vụ. restore.sh thay toàn bộ database đích bằng snapshot và có thể thay nội dung volume ảnh; chỉ dùng trên môi trường riêng trong diễn tập. Trên production chỉ chạy khi incident đã được xác nhận, đã chọn đúng bộ backup, người chịu trách nhiệm đã quyết định thời điểm và đã chấp nhận mất dữ liệu phát sinh sau thời điểm backup. Không dùng restore_drill.sh như một đường tắt để thử production.

Backup chứa PII và secret production. Private age identity không được để trên VPS hoặc cùng thư mục với backup; quy tắc custody/luân chuyển ở runbook khóa. Tên cá nhân giữ khóa, nhà cung cấp VPS và người có quyền deploy chưa được chỉ định; PO phải điền trước go-live.

Máy operator cần Docker, age, Python 3, sha256sum và image Postgres 16 (hoặc quyền tải image). Identity phải nằm ngoài thư mục backup; mở khóa ổ đĩa trước khi chạy. Có thể truyền thư mục ngày cụ thể hoặc thư mục gốc chứa nhiều ngày:

Terminal window
export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
scripts/restore_drill.sh "$HOME/admin_portal_backups/20261009"
# Hoặc để script chọn ngày/bộ hoàn chỉnh mới nhất:
scripts/restore_drill.sh "$HOME/admin_portal_backups"

Script xác minh checksum/manifest, chọn DB/media/config mới nhất cùng ngày, stream giải mã media/config để đối chiếu danh sách/hash, nạp DB vào Postgres container dùng một lần rồi so số dòng từng bảng và Alembic revision với manifest. Nó in thời gian từng bước, xóa container và dữ liệu tạm, không đụng Compose stack hay volume hiện có. Chạy sau các lần kéo quan trọng; tối thiểu hằng tháng. Bản kiểm tra này không chứng minh frontend, MFA, domain, TLS, API game hoặc việc dựng lại từ VM trắng hoạt động.

Tình huốngDấu hiệuQuyết định và hành động
Xóa nhầm hoặc dữ liệu sai, VPS còn hoạt độngMất dữ liệu/logic sai sau một thời điểm; dịch vụ còn truy cập đượcDừng thao tác ghi gây lỗi. Chọn bộ trước sự cố và khôi phục DB/media; restore.sh chụp danh sách người chơi đã erased từ DB hiện tại trước khi thay DB rồi replay sau restore.
Deploy hỏng sau migrationBackend crash, migration mới đã chạy, health check failƯu tiên fix-forward nếu an toàn. Nếu phải quay về, dùng bundle predeploy cùng ngày; khôi phục DB/media, sau đó đưa backend/frontend về tag có trong manifest bằng rollback_to_version.sh.
Volume ảnh bị mất/hỏngẢnh game/avatar trả 404 hoặc volume không mount đượcChọn backup media đúng ngày, xác minh checksum, rồi dùng restore.sh <day-dir> --media-only --media-replace.
Mất VPS hoặc nghi bị chiếm quyềnVPS không truy cập được, dữ liệu/backup có thể bị sửa hoặc xóaCô lập/revoke quyền truy cập VPS cũ. Dùng bản đã kéo về máy operator trước thời điểm nghi xâm nhập. Dựng máy mới theo mục “Dựng lại trên VM trắng”; sau restore rotate secrets và revoke credential bên thứ ba. Không tin backup được tạo sau thời điểm nghi compromise.
Mất identity vận hành ageKhông mở được backup bằng khóa chínhLấy bản sao private key đã cất riêng (password manager hoặc USB/giấy, S-D8/S-D13); nếu có khóa dự phòng thì dùng nó. Kiểm chứng một bản, tạo identity mới rồi luân chuyển recipient theo runbook khóa. Mất mọi bản sao của khóa duy nhất thì các backup cũ không mở được: tạo khóa mới và backup lại ngay.
Bản mới nhất lỗi checksum/không giải mãsha256sum -c, age hoặc restore drill thất bạiKhông sửa file để “làm checksum khớp”. Thử bộ ngày trước. Nếu bản mới hơn đọc được DB nhưng một phần còn lại hỏng, có thể dùng export_erased_players.sh để xuất sổ xóa mới hơn rồi truyền qua restore.sh --erased-from; nếu không đọc được thì không tuyên bố sổ xóa đã replay đầy đủ.
  1. Ghi incident, thời điểm cần quay về và các thay đổi sẽ mất; xác nhận bộ backup có đủ DB/media/config, SHA256SUMS hợp lệ và identity giải mã được.

  2. Lưu log và thông tin cần phân tích. Đừng xóa hoặc ghi đè backup nguồn.

  3. Không chép private identity lên VPS. Chạy restore.sh trên operator đã mã hóa ổ đĩa, giữ backup và identity tại chỗ, và chọn Docker SSH context (hoặc DOCKER_HOST=ssh://user@alias) trỏ tới máy đích. Tạo alias OpenSSH trong ~/.ssh/config; đặt Port/IdentityFile trong config, không gắn port vào URI. User SSH phải được Docker cho phép và có quyền ghi/mở khóa file backup lock trên VPS. RESTORE_REMOTE_LOCK_PATH phải chính xác là ${BACKUP_DIR}/.lock của job backup trên VPS (mặc định /opt/backups/.lock). Restore giữ flock đó đến khi hoàn tất và từ chối chạy nếu thiếu lock path, SSH không kết nối, lock đang bị giữ hoặc endpoint là loại không hỗ trợ. DOCKER_CONTEXT được ưu tiên hơn DOCKER_HOST; chỉ endpoint unix:///... cục bộ hoặc ssh://user@alias được phép, endpoint TCP bị từ chối.

    Giải mã config archive ở operator vào thư mục quyền 0700; truyền file .env và Compose đã giải mã bằng RESTORE_ENV_FILE/RESTORE_COMPOSE_FILE. Script parse một whitelist giá trị dotenv literal, không source file. Compose cũng đọc .env để nội suy cấu hình. Đặt project name đúng bằng project hiện tại trên VPS; xác minh media volume được in ra trước khi dùng --media-replace.

    Chuẩn bị Compose/.env ở operator từ config backup cùng bộ; không chạy source .env. Giữ thư mục giải mã riêng tư đến khi restore và hậu kiểm xong:

    Terminal window
    BACKUP_ROOT="$HOME/admin_portal_backups"
    BACKUP_DAY=20261009
    export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
    (cd "$BACKUP_ROOT/$BACKUP_DAY" && sha256sum -c SHA256SUMS)
    CONFIG_ARCHIVE="$(find "$BACKUP_ROOT/$BACKUP_DAY" -maxdepth 1 -type f \
    -name 'config_*.tar.age' -print | sort | tail -n 1)"
    umask 077
    WORK_CONFIG="$(mktemp -d)"
    chmod 700 "$WORK_CONFIG"
    age -d -i "$BACKUP_AGE_IDENTITY_FILE" "$CONFIG_ARCHIVE" \
    | tar -xpf - -C "$WORK_CONFIG"
    chmod 600 "$WORK_CONFIG/.env"

    Tạo Docker context portal-prod-restore và alias SSH theo ví dụ DEPLOY.md §9 trước khi dùng lệnh restore; user/alias phải là tài khoản restore được cấp quyền Docker và lock, không dùng restricted user backup-pull.

    Terminal window
    export DOCKER_CONTEXT=portal-prod-restore
    unset DOCKER_HOST
    export RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lock
    export RESTORE_ENV_FILE="$WORK_CONFIG/.env"
    export RESTORE_COMPOSE_FILE="$WORK_CONFIG/docker-compose.prod.yml"
    export RESTORE_PROJECT_NAME='<project Compose đang chạy trên VPS>'
    # BACKUP_ROOT, BACKUP_DAY, WORK_CONFIG và identity đã được đặt phía trên.
    # Media-only chỉ thay nội dung ảnh sau khi đã đối chiếu engine/project/volume:
    ./scripts/restore.sh "$BACKUP_ROOT/$BACKUP_DAY" --media-only --media-replace

    Có thể dùng thay thế DOCKER_CONTEXT bằng export DOCKER_HOST=ssh://<user>@<alias>. Không cấu hình DOCKER_CONTEXT và DOCKER_HOST mâu thuẫn. Không dùng RESTORE_TRUSTED_ENV=1 với backup đã giải mã; tùy chọn này chỉ dành cho dotenv shell tin cậy trên restore cục bộ và bị bỏ qua khi Docker là remote.

  4. Với database, trước khi chạy phải xác nhận manifest, database/project đích, image backend và thời điểm mất dữ liệu. Export POSTGRES_USER và POSTGRES_DB rõ ràng, chạy --db-only, kiểm tra toàn bộ thông tin ở prompt rồi chỉ nhập chính xác tên POSTGRES_DB. Không dùng --no-erasure-replay trong khôi phục thật:

    Terminal window
    export POSTGRES_USER='<user trong cấu hình đã kiểm tra>'
    export POSTGRES_DB='<database đích đã kiểm tra>'
    ./scripts/restore.sh "$BACKUP_ROOT/$BACKUP_DAY" --db-only
    # Tại prompt: chỉ nhập chính xác giá trị POSTGRES_DB sau khi soát summary.

    Kiểm thử thực tế trong workspace mới xác minh đường media-only qua Docker SSH bằng context dùng một lần; nó kiểm tra project/volume, identity/archive chỉ ở operator, lock cạnh tranh và giải phóng lock. Chưa có bằng chứng restore database/config trên VM trắng hay VPS thật; chỉ chạy full restore sau khi hoàn tất drill VM trắng và đối chiếu project/image/credentials.

  5. Chờ health check/summary. Nếu restore lỗi giữa chừng, backend/frontend được giữ dừng; sửa nguyên nhân rồi chạy lại cùng bộ backup. Không đưa private identity lên VPS để xử lý sự cố.

  1. Lấy chính thư mục YYYYMMDD/predeploy/<timestamp>/ được log deploy in ra. Đối chiếu manifest target_images với image dự định triển khai và kiểm checksum. restore.sh phải nhận chính thư mục bundle này, không phải thư mục ngày chứa các backup thường.

  2. Giải mã config archive từ đúng bundle vào workspace cục bộ riêng tư, rồi restore DB từ cùng bundle. Không xóa workspace trước hậu kiểm:

    Terminal window
    BACKUP_ROOT="$HOME/admin_portal_backups"
    BACKUP_DAY=20261009
    PREDEPLOY_TIMESTAMP=20261009_123456 # lấy đúng timestamp deploy log đã ghi
    PREDEPLOY_DIR="$BACKUP_ROOT/$BACKUP_DAY/predeploy/$PREDEPLOY_TIMESTAMP"
    (cd "$PREDEPLOY_DIR" && sha256sum -c SHA256SUMS)
    CONFIG_ARCHIVE="$(find "$PREDEPLOY_DIR" -maxdepth 1 -type f \
    -name 'config_*.tar.age' -print | sort | tail -n 1)"
    umask 077
    PREDEPLOY_WORK="$(mktemp -d)"
    chmod 700 "$PREDEPLOY_WORK"
    export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
    age -d -i "$BACKUP_AGE_IDENTITY_FILE" "$CONFIG_ARCHIVE" \
    | tar -xpf - -C "$PREDEPLOY_WORK"
    chmod 600 "$PREDEPLOY_WORK/.env"
    export DOCKER_CONTEXT=portal-prod-restore
    unset DOCKER_HOST
    export RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lock
    export RESTORE_ENV_FILE="$PREDEPLOY_WORK/.env"
    export RESTORE_COMPOSE_FILE="$PREDEPLOY_WORK/docker-compose.prod.yml"
    export RESTORE_PROJECT_NAME='<project Compose đang chạy trên VPS>'
    export POSTGRES_USER='<user đã xác minh>' POSTGRES_DB='<database đã xác minh>'
    ./scripts/restore.sh "$PREDEPLOY_DIR" --db-only
    # Đối chiếu summary rồi gõ chính xác POSTGRES_DB tại prompt xác nhận.
    # Chỉ xóa config giải mã sau các bước hậu kiểm:
    # rm -rf -- "$PREDEPLOY_WORK"
  3. Nếu còn lệch frontend/backend tag, đọc tag trong manifest predeploy và chạy ./scripts/rollback_to_version.sh <backend-version> <frontend-version>. Image phải còn được nạp trên máy (docker image inspect); nếu thiếu, gửi lại image đúng tag từ máy build trước khi rollback.

  4. Xác nhận migration, health, đăng nhập/MFA và API. Nếu không thể khôi phục an toàn, giữ traffic/app ở trạng thái dừng và tiếp tục theo incident.

Chọn thư mục backup của ngày mong muốn, rồi chạy:

Terminal window
export DOCKER_CONTEXT=portal-prod-restore
unset DOCKER_HOST
export RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lock
export RESTORE_ENV_FILE="$WORK_CONFIG/.env"
export RESTORE_COMPOSE_FILE="$WORK_CONFIG/docker-compose.prod.yml"
export RESTORE_PROJECT_NAME='<project Compose đang chạy trên VPS>'
export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
./scripts/restore.sh "$BACKUP_ROOT/$BACKUP_DAY" --media-only --media-replace

--media-replace xóa nội dung volume ảnh trước khi giải nén; kiểm tra đúng MEDIA_VOLUME và backup trước khi chạy. Xác nhận quyền sở hữu volume và kiểm icon/avatar trong portal/game sau khi phục hồi.

Diễn tập hằng quý và bắt buộc trước go-live. Dùng VM dùng một lần có mã hóa ổ đĩa nếu có dữ liệu thật; xác nhận chủ sở hữu sẽ hủy VM và disk sau khi drill. Không dùng domain thật nếu chưa chuẩn bị: xin chứng chỉ Let’s Encrypt trong drill có thể chạm rate limit. Với VM không có domain, dùng HTTP nội bộ hoặc certificate tự ký theo kế hoạch drill; đánh dấu TLS/public DNS chưa được xác minh.

  1. Cài Docker + Compose, age, rsync và Python 3. Trên máy operator đã có bản backup kéo về, chạy restore_drill.sh trước để kiểm integrity.

  2. Tạo thư mục cài đặt sạch, chép gói deploy/ và chuẩn bị image backend/ frontend theo tag trong DB manifest. Image không nằm trong backup cấu hình; build/export lại từ máy dev theo quy trình §2.2. Không lấy tag latest.

  3. Dùng identity trên operator để giải mã config vào thư mục làm việc quyền 0700, sau đó chuyển thư mục đó qua SSH/rsync tới VM mới. Không đưa plaintext .env vào git/log. Máy đích không được nhận age identity. Ví dụ trên operator:

    Terminal window
    umask 077
    WORK_CONFIG="$(mktemp -d)"
    chmod 700 "$WORK_CONFIG"
    export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
    BACKUP_ROOT="${BACKUP_ROOT:-$HOME/admin_portal_backups}"
    BACKUP_DAY="${BACKUP_DAY:-20261009}"
    CONFIG_ARCHIVE="$(find "$BACKUP_ROOT/$BACKUP_DAY" -maxdepth 1 -type f \
    -name 'config_*.tar.age' -print | sort | tail -n 1)"
    age -d -i "$BACKUP_AGE_IDENTITY_FILE" "$CONFIG_ARCHIVE" \
    | tar -xf - -C "$WORK_CONFIG"
    ssh admin@restore-host 'install -d -m 700 /opt/web_admin'
    rsync -a --chmod=Du=rwx,Dgo=,Fu=rw,Fgo= "$WORK_CONFIG/" \
    admin@restore-host:/opt/web_admin/

    Kiểm tra .env, credentials/, compose và nginx/ đã vào đúng vị trí. Chỉnh DOMAIN/CORS cho môi trường test nếu cần, nhưng giữ MFA_ENCRYPTION_KEY đúng giá trị backup để thử MFA. Giữ bản giải mã WORK_CONFIG trên operator tới khi hoàn tất restore và hậu kiểm ở bước 6.

  4. Đặt .env quyền 600; để trống SMTP_*, webhook alert và các biến GOOGLE_PLAY_*/AdMob trong môi trường diễn tập để không gửi mail/alert hay acknowledge/consume giao dịch production. Không cài crontab production.

  5. Giữ backup mã hóa ở operator; không cần chép archive hay age identity lên VM. Nạp image rồi khởi động chỉ Postgres trên VM. Từ operator, chọn Docker SSH context của VM trắng, đặt RESTORE_REMOTE_LOCK_PATH đúng với BACKUP_DIR/.lock trên VM và dùng config trong WORK_CONFIG còn đang giữ cục bộ. Media-only Docker SSH đã qua integration test; DB/config full restore trên VM trắng vẫn chưa được xác minh:

    Terminal window
    ./scripts/load_images.sh
    docker compose -f docker-compose.prod.yml up -d postgres
    # Chạy các lệnh sau từ operator sau khi cấu hình context/SSH alias:
    export DOCKER_CONTEXT=restore-vm
    unset DOCKER_HOST
    export RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lock
    export RESTORE_ENV_FILE="$WORK_CONFIG/.env"
    export RESTORE_COMPOSE_FILE="$WORK_CONFIG/docker-compose.prod.yml"
    export RESTORE_PROJECT_NAME='<project Compose đã chọn cho VM>'
    export BACKUP_AGE_IDENTITY_FILE="$HOME/keys/age-portal-ops.txt"
    export POSTGRES_USER='<user đã xác minh>' POSTGRES_DB='<database đã xác minh>'
    ./scripts/restore.sh "$BACKUP_ROOT/$BACKUP_DAY" --db-only
    # Đọc summary và chỉ nhập chính xác POSTGRES_DB ở prompt xác nhận.
  6. Kiểm tra /health, đăng nhập admin và xác nhận step-up MFA bằng app TOTP cũ, mở một game, kiểm tra Dashboard đến ngày backup, ảnh hiển thị, trạng thái erased của người chơi đã xóa và gọi một API game bằng key kiểm thử. Ghi từng thời điểm, không ghi secret/token vào biên bản.

  7. Chạy ./scripts/install_cron.sh --dry-run và ./scripts/check_backups.sh với alert ra ngoài đã tắt; nếu journal/pull-monitor không có trên VM tạm thì ghi rõ cảnh báo tương ứng, không đổi thành “pass”. Không cài cron thật trong VM drill.

  8. Ghi bước TLS/domain nào đã thử hoặc bỏ qua, vướng mắc, thời gian từng bước, tổng thời gian và so với RTO ≤ 4 giờ vào drill log. Xóa config giải mã cục bộ sau khi hậu kiểm xong, rồi hủy VM và xóa disk theo quy trình nhà cung cấp:

    Terminal window
    rm -rf -- "$WORK_CONFIG"

Không chạy lệnh phục hồi trên VPS production trong khuôn khổ drill. Bằng chứng hiện có gồm roundtrip tự động và integration media-only qua Docker SSH context trên Docker tạm; drill DB/config trên VM trắng, VPS checks, MFA/domain/API game, tên người giữ khóa và RTO production vẫn chờ operator thực hiện. Kết quả integration không chứng minh khôi phục toàn bộ VPS hoặc đạt RTO.

  • Thông báo incident và phạm vi ảnh hưởng cho stakeholder theo SLA đã chốt. Incident SLA trong decisions_phase_f.md §2 vẫn cần chủ sở hữu xác nhận.
  • Đối soát IAP trong khoảng từ backup tới incident bằng dữ liệu provider đã xác minh; dùng lịch sử Google Play/App Store và công cụ reconciliation phù hợp (reconcile_voided_purchases.py, hoặc reconcile_purchases.py --input <provider-export.json>). Không nhập dữ liệu client tự khai làm bằng chứng giao dịch.
  • Xoay secrets nếu có khả năng bị lộ; sau khi VPS bị chiếm, revoke cả service accounts/webhooks/API keys liên quan. MFA encryption key chỉ được rotate cùng migration mã hóa lại DB như Task F.3 mô tả.
  • Ghi thời điểm backup, RPO thực tế (tính từ lần operator kéo gần nhất), thời gian phục hồi, số liệu hậu kiểm, hành động còn mở và người chịu trách nhiệm.

Áp dụng cho Portal v1.2.2