# Backup, restore và tính liên tục

**NCV DCIMS — NCV Dental Clinical Image Management System**

## 1. Mục tiêu và giới hạn

RAID/snapshot không phải backup. Một NAS duy nhất không thể bảo đảm zero data loss hoặc zero downtime. Chủ dự án phải duyệt RPO/RTO; giả định thiết kế tạm thời:

- RPO: 24 giờ.
- RTO: 8 giờ cho sự cố có phần cứng thay thế và backup đọc được.

Nếu nghiệp vụ yêu cầu thấp hơn, phải bổ sung backup/WAL/snapshot/standby và diễn tập tương ứng.

## 2. Phạm vi backup

| Thành phần | Bắt buộc | Phương pháp |
|---|---|---|
| PostgreSQL logical data | Có | `pg_dump` custom format, non-superuser backup role |
| Media original | Có | Backup file + checksum/inventory |
| Media preview | Tùy | Có thể tái tạo; backup nếu RTO yêu cầu |
| Confirmation/audit | Có | Nằm trong DB dump; kiểm tra riêng |
| Config non-secret/compose | Có | Version control/export |
| Secrets/keys | Có, tách biệt | Secret vault/offline escrow, không nằm chung plaintext |
| Logs cần điều tra | Theo retention | Export có kiểm soát |
| PostgreSQL live data directory | Không là backup chính | Không copy khi đang chạy |

## 3. Chiến lược 3-2-1

- Bản chạy trên NAS.
- Bản backup local khác volume/share, quyền chỉ backup service ghi.
- Ít nhất một bản sao offsite/ngoài NAS, mã hóa và tách credential.
- Snapshot Btrfs (nếu model hỗ trợ) giúp phục hồi nhanh nhưng không thay offsite backup.
- Hyper Backup sao chép backup set đã hoàn chỉnh/verify, không dựa riêng vào thư mục application live.

## 4. Tính nhất quán DB–media

`pg_dump` và copy media độc lập có thể lệch thời điểm. Mỗi backup run cần:

1. Tạo `backupRunId` và high-water mark.
2. Ngăn purge trong thời gian backup.
3. Ưu tiên maintenance/read-only window cho full consistent backup ban đầu.
4. Dump DB ở snapshot nhất quán.
5. Backup media được DB dump tham chiếu; file mới hơn có thể là orphan an toàn, nhưng file thiếu là lỗi.
6. Tạo inventory `{storageKey, sha256, size}` và đối chiếu.
7. Manifest ghi phiên bản app/schema, thời gian, DB checksum, media inventory hash, tool versions.
8. Chỉ đánh dấu success sau verify.

Nếu không có read-only window, thiết kế phải giữ version/file immutable đủ lâu để backup đọc được và reconciliation hiểu high-water mark.

Purge worker chỉ được bật execution khi receipt của backup gần nhất còn mới, có chữ ký Ed25519 hợp lệ và không có `backup-in-progress.lock`. API chỉ mount volume receipt ở chế độ read-only; API không được đọc kho backup hoặc private signing key. `NCV_DCIMS_BACKUP_RECEIPT_PATH`, `NCV_DCIMS_BACKUP_RECEIPT_PUBLIC_KEY_PATH` và hạn mặc định 36 giờ thay thế hoàn toàn cờ xác nhận backup thủ công. Receipt ghi checksum DB dump, media manifest, deletion-ledger manifest, app version và key ID; sửa nội dung, receipt quá hạn, lock đang tồn tại hoặc lỗi đọc đều chặn purge trước khi queue.

Backup runner chạy `pg_dump` custom format mà không đưa DSN vào argv, snapshot originals/previews/deletion ledger, xác minh SHA-256 từng file, ký receipt rồi mới publish điểm khôi phục. Scheduler chạy 02:00 theo `Asia/Ho_Chi_Minh`; rotation chỉ xóa điểm đã verify quá 60 ngày sau khi điểm mới thành công và không xóa điểm hợp lệ cuối cùng. Compose profile `backup` mount media/ledger read-only, repository và receipt thành hai volume riêng; các xác nhận policy/offsite/recovery-key vẫn mặc định `false`.

External tombstone writer ghi create-only vào `NCV_DCIMS_MEDIA_DELETION_LEDGER_ROOT`, bắt buộc là đường dẫn absolute nằm ngoài PostgreSQL và media roots. Reader đối chiếu read-only vẫn có thể chạy bằng:

```text
NCV_DCIMS_RESTORE_RECONCILIATION_INSPECT=true
NCV_DCIMS_MEDIA_DELETION_LEDGER_ROOT=<absolute-private-ledger-root>
pnpm --filter ncv-dcims-api media:inspect-deletion-reconciliation
```

Restore apply engine chỉ chạy trên database tên `ncv_dcims_restore` ở host allowlist, với câu xác nhận chính xác và restore target được phê duyệt. Engine xóa/kiểm lại original và preview trước, sau đó mới áp deletion state, hoàn tất purge task, ghi `MediaDeletionApplyReceipt` và audit; tombstone không có record trong DB vẫn có receipt mà không đoán storage key. `compose.restore-drill.yml` thực hiện verify point → materialize media → `pg_restore` → migrate forward → apply **external deletion ledger mới nhất** → kiểm DB và file vật lý. Ledger này được mount read-only từ `NCV_DCIMS_RESTORE_DELETION_LEDGER_HOST_PATH`, phải nằm ngoài điểm backup và workspace; bản snapshot ledger trong điểm backup chỉ dùng kiểm tra tính toàn vẹn, không đủ để chống hồi sinh dữ liệu đã bị purge sau thời điểm backup. Harness đã qua drill PostgreSQL 17.10/filesystem cô lập; Docker/NAS/SMB thật vẫn chờ chứng nhận hạ tầng.
## 5. Lịch và retention

Quyết định D-02 đã chốt ngày 2026-07-30:

- backup toàn hệ thống tự động một lần mỗi ngày lúc 02:00 theo múi giờ `Asia/Ho_Chi_Minh`; mỗi bản là một điểm khôi phục độc lập tại thời điểm hoàn tất;
- một lượt lỗi được retry bằng cùng `backupRunId`; lock ngăn chạy chồng và trong một ngày chỉ công bố tối đa một điểm khôi phục đã verify;
- giữ mỗi bản backup tối đa 60 ngày kể từ thời điểm hoàn tất thành công;
- từ ngày thứ 61, bản hết hạn bị loại khỏi danh sách điểm khôi phục và được xóa khỏi kho local cùng bản sao offsite;
- không đưa thư mục/kho backup vào nguồn backup, vì vậy bản mới không chứa file backup cũ;
- chỉ chạy dọn retention sau khi có bản backup mới verify thành công và không bao giờ xóa bản hợp lệ cuối cùng;
- tạm hoãn dọn bản đang được dùng cho restore drill, phục hồi sự cố hoặc hold được phê duyệt; hết hold phải tiếp tục áp chính sách 60 ngày.

Với kho chống trùng lặp, xóa snapshot hết hạn làm mất điểm khôi phục đó ngay. Khối dữ liệu vật lý chỉ được garbage-collect khi không còn snapshot hợp lệ nào tham chiếu. Mọi thao tác dọn phải có manifest/audit tổng hợp, không ghi tên file bệnh nhân, storage key hoặc secret.

## 6. Mã hóa và quyền

- Backup mã hóa trước/ở đích bằng thuật toán/công cụ được tổ chức duyệt.
- Khóa không lưu chỉ trên cùng NAS; có ít nhất hai custodian và recovery test.
- Backup container: read DB/media, write backup staging; không có quyền phục vụ HTTP hoặc purge application media.
- Restore credential tách khỏi runtime và chỉ cấp khi diễn tập/sự cố.
- Log không chứa passphrase/connection URL.

## 7. Readiness và scripts

### 7.1 Preflight chỉ đọc đã triển khai

```text
pnpm backup:preflight
pnpm backup:preflight -- --require-backup
pnpm backup:preflight -- --require-restore
```

Lệnh mặc định luôn chỉ đọc metadata. Nó kiểm tra sự hiện diện của `pg_dump`/`pg_restore`, credential file có thể đọc, originals có thể đọc, staging/restore target có quyền phù hợp, các vùng không chồng lấn và restore target đang rỗng. Nội dung credential, đường dẫn, key ID và tên file trong restore target không được trả ra báo cáo.

`--require-backup` trả lỗi nếu chưa đủ điều kiện kiểm tra backup kỹ thuật, bao gồm lịch `DAILY` lúc `02:00` theo `Asia/Ho_Chi_Minh` và `NCV_DCIMS_BACKUP_RETENTION_DAYS=60`. `--require-restore` còn yêu cầu bốn xác nhận rõ ràng: policy đã duyệt, offsite đã cấu hình, recovery key đã được thử và restore target cô lập đã được duyệt. Các cờ này mặc định `false`; preflight không tự xác minh rằng lời xác nhận là đúng, không thực hiện xóa bản backup và không thay thế restore drill.

### 7.2 Công cụ tạo khóa ký receipt

```text
set NCV_DCIMS_BACKUP_KEY_OUTPUT_ROOT=<thư mục tuyệt đối mới hoặc rỗng>
pnpm backup:generate-receipt-keys
```

Lệnh yêu cầu cờ `--generate` được khai báo ngay trong package script, chỉ ghi vào thư mục tuyệt đối mới hoặc rỗng và không ghi đè. Kết quả gồm `receipt-private.pem` và `receipt-public.pem`; stdout chỉ trả thuật toán, trạng thái và fingerprint không bí mật. Private key chỉ mount vào backup service, public key mount read-only vào API và restore drill. Không đặt private key trong repository, kho backup hoặc cùng nơi duy nhất với NAS.

Ed25519 chỉ chứng minh receipt không bị sửa; nó **không mã hóa** database dump hay media. Mã hóa backup, custody recovery key và bản sao offsite vẫn phải cấu hình riêng tại NAS/đích backup và kiểm chứng bằng restore thật.

### 7.3 Preflight restore drill cô lập

```text
pnpm backup:restore-drill-preflight
pnpm container:drill:restore
```

Preflight là read-only và fail-closed. Nó yêu cầu Docker Engine/Compose, điểm backup nằm ngoài workspace, public key nằm ngoài workspace, external deletion ledger mới nhất nằm ngoài cả workspace lẫn điểm backup, không mount private signing key, secret URL chỉ trỏ tới host `ncv-dcims-postgres-restore` và database `ncv_dcims_restore`. Trước khi báo sẵn sàng, lệnh xác minh chữ ký receipt, checksum database dump, hai manifest và mọi file media; báo cáo không trả đường dẫn, credential, backup run ID hay key material.

Chỉ chạy `container:drill:restore` sau khi preflight đạt. Drill materialize media vào volume rỗng, `pg_restore` vào PostgreSQL tạm, chạy migration tiến về trước, áp external deletion ledger mới nhất được mount read-only rồi đối chiếu database và file vật lý. Không đổi URL secret sang database live để “thử nhanh”.

### 7.4 Công cụ đã triển khai

- `scripts/run-backup.mjs`: tạo một điểm backup đầy đủ và receipt đã ký.
- `scripts/run-backup-scheduler.mjs`: lịch 02:00 giờ Việt Nam.
- `scripts/verify-backup-point.mjs`: kiểm chữ ký/checksum và tùy chọn materialize media.
- `scripts/check-restore-drill-readiness.mjs`: cổng sẵn sàng read-only trước drill.
- `compose.restore-drill.yml`: drill end-to-end trên môi trường cô lập.

Các công cụ khóa absolute roots, chống chạy chồng, dùng file tạm/atomic publication, structured exit code và không in secret. Bằng chứng drill cô lập ngày 2026-08-01 nằm tại `docs/evidence/d02-restore-drill-2026-08-01.md`; kết quả này chưa thay thế việc cấu hình mã hóa/offsite và diễn tập trên NAS staging thật.
## 8. Manifest đề xuất

```json
{
  "schemaVersion": 1,
  "backupRunId": "uuid",
  "createdAt": "2026-01-01T00:00:00Z",
  "applicationVersion": "semver-or-commit",
  "databaseSchemaVersion": "migration-id",
  "database": {
    "file": "database.dump",
    "sha256": "hex",
    "format": "pg_dump-custom"
  },
  "media": {
    "inventoryFile": "media-inventory.jsonl",
    "inventorySha256": "hex",
    "fileCount": 0,
    "totalBytes": 0
  },
  "encryption": {
    "scheme": "approved-scheme",
    "keyId": "non-secret-id"
  }
}
```

Không ghi absolute NAS path, password hoặc encryption key vào manifest.

## 9. Verify

Mỗi run:

- checksum backup artifacts và manifest;
- test `pg_restore --list`;
- sample/full media hash tùy tier;
- inventory không có missing file;
- kiểm encryption decrypt bằng recovery credential trong môi trường kiểm soát;
- ghi `BackupRun.verificationStatus`.

Định kỳ restore vào môi trường cô lập mới là bằng chứng backup dùng được; checksum riêng không đủ.

## 10. Restore runbook

1. Khai báo incident/change ticket, chỉ định restore point và owner.
2. Bật maintenance, chặn mutation/CRM grants/upload; lưu audit marker.
3. Cô lập hệ thống lỗi, bảo toàn bằng chứng nếu là security incident.
4. Xác minh manifest/checksum/key/tool/schema compatibility.
5. Tạo database/media target sạch; không ghi đè production trước khi xác nhận.
6. Restore PostgreSQL bằng role chuyên dụng.
7. Restore media original, preview hoặc đánh dấu cần tái tạo.
8. Chạy migration tương thích theo runbook của version backup.
9. Chạy integrity: DB→file, file→DB, hashes, version/current, confirmation manifest.
10. Chạy smoke/auth/authorization/upload-read tests bằng dữ liệu giả.
11. Đối chiếu RPO, sample business records và owner sign-off.
12. Chuyển traffic, mở read trước rồi mutation; theo dõi.
13. Audit restore và post-restore report; giữ hệ thống cũ theo legal hold.

Không “restore thử” trực tiếp lên production.

## 11. Disaster scenarios

| Sự cố | Phục hồi |
|---|---|
| Xóa mềm nhầm | Restore app workflow từ trash, không cần full restore |
| Purge/file corruption đơn lẻ | Lấy artifact đúng hash từ backup, reconcile DB |
| DB corruption | Maintenance, restore DB + media point tương thích |
| Ransomware NAS | Rebuild sạch, rotate credentials, restore offsite |
| Mất toàn NAS | NAS/phần cứng thay thế, config, DB, media, keys từ offsite |
| Release lỗi | Rollback app compatible hoặc forward-fix; không down migration phá dữ liệu |

## 12. Diễn tập và bằng chứng

- Hằng ngày: job/manifest/checksum alert.
- Hằng tuần: inventory/sample restore tự động.
- Hằng quý: full restore drill cô lập và đo RPO/RTO.
- Hằng năm hoặc sau thay đổi lớn: disaster exercise gồm offsite/key recovery.

Biên bản gồm backup ID, thời gian, người thực hiện, kết quả, missing/corrupt count, RPO/RTO thực tế, lỗi và action owner.

Tham chiếu: [NIST contingency planning](https://csrc.nist.gov/topics/security-and-privacy/security-programs-and-operations/contingency-planning) nhấn mạnh kế hoạch, thủ tục và biện pháp kỹ thuật phối hợp để phục hồi hệ thống, vận hành và dữ liệu.

## Bản sửa backup fail-closed ngày 2026-08-12

- Backup production chỉ chạy khi đủ bốn storage marker: media, deletion ledger, backup repository và backup receipts. Nếu NAS/share chưa mount sau restart hoặc mount sai thư mục, tiến trình dừng trước khi tạo `points`, lock hoặc file backup.
- Compose production truyền đầy đủ lịch `02:00 Asia/Ho_Chi_Minh` và bắt buộc `NCV_DCIMS_BACKUP_ENCRYPTION_KEY_ID`.
- Ba xác nhận vận hành (`policy approved`, `offsite configured`, `recovery key tested`) mặc định `false`; operator phải khai báo đúng trạng thái thực tế. Không còn tự mặc định là đã đạt.
- Retention vẫn là 60 ngày và backup container vẫn là one-shot (`restart: no`).
- Kiểm thử cục bộ: 34/34 test backup/readiness/restore/NAS đạt; kiểm tra container tĩnh đạt. Smoke test Docker thật chưa chạy vì Docker Desktop engine đang tắt trên máy phát triển.