Runbook phục hồi thảm họa (Phase S)
Phạm vi và an toàn
Phần tiêu đề “Phạm vi và an toàn”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.
Drill nhanh sau khi kéo backup
Phần tiêu đề “Drill nhanh sau khi kéo backup”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:
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ống sự cố
Phần tiêu đề “Tình huống sự cố”| Tình huống | Dấu hiệu | Quyết định và hành động |
|---|---|---|
| Xóa nhầm hoặc dữ liệu sai, VPS còn hoạt động | Mất dữ liệu/logic sai sau một thời điểm; dịch vụ còn truy cập được | Dừ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 migration | Backend 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 được | Chọ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ền | VPS không truy cập được, dữ liệu/backup có thể bị sửa hoặc xóa | Cô 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 age | Không mở được backup bằng khóa chính | Lấ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ại | Khô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 đủ. |
Khôi phục dữ liệu khi VPS vẫn còn
Phần tiêu đề “Khôi phục dữ liệu khi VPS vẫn còn”-
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,
SHA256SUMShợp lệ và identity giải mã được. -
Lưu log và thông tin cần phân tích. Đừng xóa hoặc ghi đè backup nguồn.
-
Không chép private identity lên VPS. Chạy
restore.shtrên operator đã mã hóa ổ đĩa, giữ backup và identity tại chỗ, và chọn Docker SSH context (hoặcDOCKER_HOST=ssh://user@alias) trỏ tới máy đích. Tạo alias OpenSSH trong~/.ssh/config; đặtPort/IdentityFiletrong 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_PATHphải chính xác là${BACKUP_DIR}/.lockcủ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ơnDOCKER_HOST; chỉ endpointunix:///...cục bộ hoặcssh://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.envvà Compose đã giải mã bằngRESTORE_ENV_FILE/RESTORE_COMPOSE_FILE. Script parse một whitelist giá trị dotenv literal, khôngsourcefile. 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=20261009export 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 077WORK_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-restorevà 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 userbackup-pull.Terminal window export DOCKER_CONTEXT=portal-prod-restoreunset DOCKER_HOSTexport RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lockexport 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-replaceCó thể dùng thay thế
DOCKER_CONTEXTbằngexport DOCKER_HOST=ssh://<user>@<alias>. Không cấu hìnhDOCKER_CONTEXTvàDOCKER_HOSTmâu thuẫn. Không dùngRESTORE_TRUSTED_ENV=1vớ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. -
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_USERvàPOSTGRES_DBrõ 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ênPOSTGRES_DB. Không dùng--no-erasure-replaytrong 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.
-
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ố.
Deploy lỗi sau migration
Phần tiêu đề “Deploy lỗi sau migration”-
Lấy chính thư mục
YYYYMMDD/predeploy/<timestamp>/được log deploy in ra. Đối chiếu manifesttarget_imagesvới image dự định triển khai và kiểm checksum.restore.shphả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. -
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=20261009PREDEPLOY_TIMESTAMP=20261009_123456 # lấy đúng timestamp deploy log đã ghiPREDEPLOY_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 077PREDEPLOY_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-restoreunset DOCKER_HOSTexport RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lockexport 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" -
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. -
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.
Mất volume ảnh
Phần tiêu đề “Mất volume ảnh”Chọn thư mục backup của ngày mong muốn, rồi chạy:
export DOCKER_CONTEXT=portal-prod-restoreunset DOCKER_HOSTexport RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lockexport 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.
Dựng lại trên VM trắng
Phần tiêu đề “Dựng lại trên VM trắng”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.
-
Cài Docker + Compose,
age,rsyncvà Python 3. Trên máy operator đã có bản backup kéo về, chạyrestore_drill.shtrước để kiểm integrity. -
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 taglatest. -
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.envvào git/log. Máy đích không được nhận age identity. Ví dụ trên operator:Terminal window umask 077WORK_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_CONFIGtrên operator tới khi hoàn tất restore và hậu kiểm ở bước 6. -
Đặt
.envquyền600; để trốngSMTP_*, webhook alert và các biếnGOOGLE_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. -
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ớiBACKUP_DIR/.locktrên VM và dùng config trongWORK_CONFIGcò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.shdocker 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-vmunset DOCKER_HOSTexport RESTORE_REMOTE_LOCK_PATH=/opt/backups/.lockexport 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. -
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. -
Chạy
./scripts/install_cron.sh --dry-runvà./scripts/check_backups.shvớ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. -
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.
Sau khi phục hồi
Phần tiêu đề “Sau khi phục hồi”- 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ặcreconcile_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