Claude — C2B-PRE: Mã hoá toàn bộ Directus database backup + gỡ admin token khỏi shell auto-export (2026-07-19)
Claude — C2B-PRE: Mã hoá toàn bộ Directus database backup + gỡ admin token khỏi shell auto-export (2026-07-19)
Thực thi 2026-07-19 04:00–08:30 CEST. Tên file báo cáo giữ nguyên theo mission (
…-2026-07-18.md). PAYLOAD=REDACTED — không một giá trị password/token/khoá riêng nào xuất hiện trong tài liệu này.
1. Executive verdict
OVERALL = PASS_PENDING_FIRST_NATURAL_PRODUCTION_RUN_VERIFICATION
Hai đường phát tán plaintext đã bị chặn ở nguồn tạo mới:
- A. Directus DB → pg_dump → Google Drive: nightly artifact giờ được mã hoá client-side bằng OpenPGP public-key trước khi vào upload staging.
pg_dumpđược stream thẳng (pg_dump | gzip | gpg) nên bản dump plaintext không bao giờ chạm filesystem. - B.
directus_users.token→/root/.bashrc→ mọi interactive root shell: dòng auto-export đã bị gỡ; shell mới không còn mang token.
| Verdict | Giá trị |
|---|---|
DIRECTUS_DATABASE_SECRET_CLASSES |
PARTIALLY_MAPPED |
ROOT_SHELL_ADMIN_TOKEN_AUTO_EXPORT |
REMOVED |
BACKUP_CREDENTIAL_SELF_EXPOSURE |
NOT_FOUND |
DIRECTUS_BACKUP_ENCRYPTION |
PASS_CLIENT_SIDE_PUBLIC_KEY |
OFFVPS_DECRYPTION_PATH |
VERIFIED_OWNER_PRINCIPAL |
SECOND_INDEPENDENT_RECIPIENT |
OWNER_ACTION_REQUIRED |
FAIL_CLOSED_BACKUP_BEHAVIOR |
PASS |
BACKUP_FAILURE_VISIBILITY |
PASS_STATUS_AND_LOG |
LOCAL_RECOVERY_TEST |
PASS |
REMOTE_RECOVERY_TEST |
PASS_ISOLATED_ARTIFACT |
OFFVPS_RESTORE_RUNBOOK |
VERIFIED |
FIRST_NATURAL_PRODUCTION_RUN |
PENDING (còn 12h11m tại thời điểm chốt) |
HISTORICAL_DIRECTUS_DUMP_EXPOSURE |
REMAINS_CREDENTIALS_STILL_VALID |
VPS_SSOT_GIT_SAFETY |
PASS |
DISK_HEADROOM_GATE |
PASS |
RETENTION_FORMAT_COMPATIBILITY |
PASS_EXISTING_LOGIC (legacy loop giữ nguyên byte, chứng minh không thể khớp tên artifact mới) |
RETENTION_TRANSITION_MODE |
APPEND_ONLY_ENCRYPTED_PREFIX |
ENCRYPTED_RETENTION_POLICY |
TRANSITION_APPEND_ONLY_PENDING_SEPARATE_POLICY |
CONSUMERS_OF_SHELL_EXPORT |
IDENTIFIED |
OWNER_DECRYPT_DRILL |
RECOMMENDED_NOT_YET_DONE |
DIRECTUS_OFFSITE_BACKUP_SCOPE |
DATABASE_DIRECTUS_ONLY |
🔴 Đọc đúng verdict. Việc này chỉ ngăn tạo thêm bản plaintext. Nó không thu hồi, không vô hiệu hoá bất cứ credential nào. 14 artifact
vps-backup-*.tar.gzđang nằm trên Google Drive vẫn chứa toàn bộ 13 static token ở dạng đọc được, và các token đó vẫn còn hiệu lực.
2. Nguồn đã đọc
Đọc toàn văn revision mới nhất trên KB:
claude-post-migration-service-secret-controlplane-reconciliation-survey-2026-07-17.md(rev 8, 58.569 ký tự)claude-wave-a1-agent-data-image-compose-env-hardening-2026-07-17.md(rev 1, 41.831)claude-c0-stop-git-history-drive-propagation-2026-07-17.md(rev 1, 18.870)claude-c1-remove-directus-admin-hardcode-current-source-2026-07-17.md(rev 1, 15.480)claude-c2a-directus-bootstrap-admin-rotation-readiness-2026-07-18.md(rev 1, 42.843)
Mọi con số dưới đây đo lại live, không lấy từ memory. Chỗ nào live khác report, live thắng (xem §32).
3. Bằng chứng C2A được áp dụng / đính chính
| C2A khẳng định | Đo lại live 2026-07-19 | Kết luận |
|---|---|---|
| Directus 11.5.1 | image directus/directus:11.5 |
✅ khớp |
admin@example.com là full-admin duy nhất |
6abdec55-d911-44df-af96-3cf60b9654af, role Administrator, token len 64, active |
✅ khớp |
Static token lưu plaintext trong directus_users.token |
13/13 user có token; 13 fingerprint khác nhau | ✅ khớp |
| tac-agent token (32B) do claude-mcp dùng | 10 user có token 32B (3 active + 7 suspended), 2 user 36B, 1 user 64B | ✅ khớp |
/root/.bashrc auto-export admin token |
dòng 107, export DIRECTUS_ADMIN_TOKEN= |
✅ khớp |
Non-interactive shell không nhận token (guard [ -z "$PS1" ] && return ở L6) |
đo: non-interactive UNSET, bash -lc UNSET, bash -i SET len=64 |
✅ khớp |
| Dump chứa token | đo trực tiếp: stream pg_dump khớp 13 dòng token |
✅ xác nhận bằng giá trị |
Owner nmhuyen@gmail.com có roles/owner, đường truy cập độc lập |
xem §19 | ✅ khớp (+ bằng chứng phủ định mới) |
Số 13/13 là đóng góp mới: C2A không nêu tổng số user giữ static token; mission này đo được users_with_token = 13/13.
4. Quyết định Owner đã tuân thủ
Không rotate password · không rotate/revoke token · không thu hồi session · không sửa user/email/role/policy/TFA · không sửa nmhuyen@gmail.com · không direct DB WRITE · không restart/recreate service · không xoá Drive artifact lịch sử · không rewrite Git history · không xoá dormant copy · giữ nguyên khả năng recovery off-site · backup mới không rời staging dưới dạng plaintext · encryption hoàn tất trước upload · không encryption tự chế · private key không ở cùng nơi với ciphertext · không auto-export token vào shell · không tạo protected token cache mới (không có consumer nào bắt buộc) · không chạy C2B rotation.
5. Git / Config Safety Gate — PASS
Checkpoint: /root/c2bpre-checkpoint-20260719T040200Z (root:root 0700, nằm ngoài mọi Drive/GitHub source).
backup/ backup-to-gdrive.sh.PRE ebdf3be1445ba3aea7be95c06e54057aa3082a6498a3669119d5462e918d11a9
root-bashrc.PRE a454e538306bb27ef19c242ab832f36722876bc85f44c20ed8325b7a4f1de78a
code-backup-to-gdrive.sh.PRE b47c5e36995582939ef55e261543747f3e4e8022893b20b81ac869e269d3ee27 (== C0 POST ✅)
cron-env.sh.PRE 6839b71745ecddf0d4d499322ebaac8dbf440d3acd2b7fad99e4c201efb39a44
root-crontab.PRE 547e8151f95c0cc86ed48abd3401e8af1b04b1ce0f111a14bd2d9b13bc07b039 (== C0 ✅)
rclone.conf.METADATA (metadata only — xem ghi chú)
bundle/ opt-incomex-PRE-20260719T040200Z.bundle dcf0dfd21fa997556ce4cdec23022b0e174277308e9984498554db4b4553d3f3
opt-incomex-POST-20260719T062452Z.bundle 098f9abde2b20aa951e2849739aa91b317ec04dee369123813712d8b10b310c6
→ cả hai: "The bundle records a complete history." PASS (chạy TỪ TRONG repo — bài học C0)
evidence/ containers.{PRE,POST} · containers-detail.PRE · drive-root-{files,dirs}.PRE · git.PRE
disk-avail-bytes.PRE · encryption-key.META
rollback/ ROLLBACK.sh (0700, 0 external dependency)
Ghi chú rclone.conf: file chứa OAuth token Drive còn sống. Mission không sửa nó, nên cố ý KHÔNG copy — chỉ ghi metadata (path, owner/mode, sha256, type/scope, PAYLOAD=REDACTED). Tạo thêm một bản sao credential mà không cần dùng là phản tác dụng.
Phân loại:
| Đối tượng | Repo | Phân loại |
|---|---|---|
scripts/backup-to-gdrive.sh |
/opt/incomex |
MISSION_CHANGE — TRACKED, live == HEAD trước khi sửa |
docs/recovery/directus-backup-restore-runbook.md |
/opt/incomex |
MISSION_CHANGE — file mới |
/root/.bashrc |
ngoài Git | MISSION_CHANGE — có byte-exact backup |
| 15 modified + 89 untracked khác | /opt/incomex |
UNRELATED_WORK — giữ nguyên tuyệt đối |
/opt/incomex: branch feat/s177-sprint1-round-a, HEAD PRE 2b086472… (== C2A POST), 0 remote ⇒ push bất khả thi về mặt cấu trúc.
6. Directus database — kiểm kê lớp bí mật
Quét toàn bộ DB directus theo tên cột (chỉ tên, không giá trị):
| Cột | Non-null | Phân loại |
|---|---|---|
directus_users.token |
13 | 🔴 plaintext — CONFIRMED |
directus_users.password |
6 | argon2id hash |
directus_users.tfa_secret |
0 | — |
directus_sessions.token |
10 | 🔴 session token sống |
directus_sessions.next_token |
— | 🔴 cùng lớp |
directus_settings.ai_anthropic_api_key |
0 | trống |
directus_settings.ai_openai_api_key |
0 | trống |
directus_shares.password |
0 (0 row) | — |
dot_iu_runtime_lease.lease_token |
0 | — |
Flows / Operations:
flows 128 (active/event 104 · inactive/event 17 · active/schedule 5 · active/webhook 2)
operations 320 (log 108 · item-create 78 · request 50 · item-read 44 · exec 15 · item-update 14
· condition 7 · trigger 2 · transform 1 · item-delete 1)
operations có options khớp mẫu bí mật 43
request-op mang header xác thực 43
HTTP header NAMES thực tế dùng: Authorization · x-api-key · X-API-Key · Content-Type · X-Directus-Event
⇒ 43 operation gần như chắc chắn mang bearer token / API key của bên thứ ba trong options. Mission không in, không liệt kê, không giải mã nội dung đó — đúng yêu cầu "phân loại có hệ thống nhưng không in payload".
Ngoài ra DB còn 1010 bảng nghiệp vụ ngoài directus_* — không thể audit hết trong phạm vi này.
DIRECTUS_DATABASE_SECRET_CLASSES = PARTIALLY_MAPPED
Chính vì inventory không thể đầy đủ, biện pháp đúng là mã hoá toàn bộ dump chứ không phải lọc từng trường.
7. 13 static token — xác nhận bằng giá trị
users_total = 13 · users_with_token = 13 (13/13)
phân bố: len 64 × 1 (admin, active)
len 36 × 2 (active)
len 32 × 3 (active) + len 32 × 7 (suspended)
13 fingerprint HMAC KHÁC NHAU ⇒ 13 giá trị độc lập, không dùng lại
🔴 7 user suspended vẫn giữ static token — suspend không vô hiệu hoá static token trong Directus 11.5. Đây là bề mặt tấn công thật, chuyển thẳng sang backlog C2B3+.
Bằng chứng quyết định (đo trên stream, không ghi đĩa):
PLAINTEXT_DUMP_TOKEN_MATCH_LINES = 13 ← 13/13 token nằm trong bản dump ban đêm
NEW_CIPHERTEXT_KNOWN_SECRET_MATCH = 0 ← 0 trong ciphertext
DECRYPTED_TOKEN_MATCH_LINES = 13 ← giải mã ra lại đủ 13 ⇒ mã hoá không làm mất dữ liệu
8. tac-agent / claude-mcp
claude-mcp dùng token của user tac-agent (32B), không phải admin token (64B). Token tac-agent nằm trong cùng bảng directus_users ⇒ cũng bị đẩy lên Drive mỗi đêm trong bản dump. Mã hoá bảo vệ nó ngang với admin token. Mission không đụng tac-agent; rotate nó là C2B2.
9. Rủi ro credential trong flow/operation
FLOW_OPERATION_CREDENTIAL_RISK = PARTIALLY_MAPPED — 43/320 operation có options khớp mẫu bí mật và dùng header Authorization / x-api-key. Chưa map được: mỗi operation gọi dịch vụ nào, credential nào còn hiệu lực, ai sở hữu. Đó là FLOW_CREDENTIAL_WAVES trong backlog. Không operation nào bị sửa (320 PRE == 320 POST).
10. Pipeline plaintext trước khi sửa
37 20 * * * /opt/incomex/scripts/backup-to-gdrive.sh (20:37 Europe/Berlin)
[1/5] docker exec postgres pg_dump -U directus -d directus --no-owner --no-acl | gzip
→ ${WORK_DIR}/postgresql-directus.sql.gz ← PLAINTEXT trên đĩa
[2/5] Qdrant snapshots
[3/5] docker configs (.env đã bị loại từ Wave A1 1f36dbb ✅ — xác nhận còn nguyên)
[4/5] nginx conf + cp -rL /etc/letsencrypt/live/ ← 🔴 TLS private key thật
[5/5] cp -r /opt/incomex/scripts/*
tar -czf vps-backup-<TS>.tar.gz → rclone copy → Drive ROOT ← PLAINTEXT rời VPS
retention: giữ 14, XOÁ 1 artifact cũ mỗi đêm
Đo thật đêm 2026-07-18: Archive 259M · PG 115M · Qdrant 210M · Remote backups 14 · Deleting: vps-backup-20260704_203701.tar.gz.
11. Bản đồ propagation (đo theo GIÁ TRỊ, 13 token, value không bao giờ vào argv)
| Vị trí | Số file khớp | Phân loại |
|---|---|---|
directus_users.token (DB sống) |
13 | AUTHORIZED_DATABASE_STORAGE |
/root/.bashrc |
1 | ACTIVE_SHELL_AUTO_EXPORT → đã gỡ |
pg_dump stream ban đêm |
13 dòng | ACTIVE_BACKUP_PLAINTEXT_PROPAGATION → đã mã hoá |
/opt/incomex/backups |
14 | HISTORICAL_LOCAL_COPY (giữ nguyên) |
/etc/incomex/agent-data.env |
1 | AUTHORIZED (protected, Wave A1) |
| process env (agent-data ×4, nuxt ×1, api-executor) | 6 pid | AUTHORIZED runtime consumer |
/opt/incomex/scripts |
0 | — |
/etc/letsencrypt |
0 | — |
/opt/incomex/docker/nuxt-repo |
0 | — |
/opt/incomex/docker/agent-data-repo |
0 | — |
/root/.config (rclone + gcloud) |
0 | — |
| Drive artifacts (14 nightly) | không quét | HISTORICAL_DRIVE_COPY — tải về sẽ tạo thêm bản plaintext |
🔴 Phát hiện then chốt, thu hẹp phạm vi rất nhiều: trong toàn bộ nội dung archive ban đêm, chỉ duy nhất pg_dump mang static token. scripts/, TLS, docker configs đều 0. Nghĩa là mã hoá bản dump + bọc mã hoá cả archive đóng trọn đường phát tán token mới — không còn ngách nào khác.
12. Credential của chính backup pipeline
BACKUP_CREDENTIAL_SELF_EXPOSURE = NOT_FOUND
| Credential | Vị trí | Có trong backup source? |
|---|---|---|
| rclone Drive OAuth | /root/.config/rclone/rclone.conf (644, nhưng /root = 0700 ⇒ thực tế root-only) |
KHÔNG — không job nào chạm /root |
| gcloud ADC | /root/.config/gcloud/ |
KHÔNG |
| GCP SA key | /opt/incomex/docker/credentials/google-credentials.json |
KHÔNG — glob [3/5] khớp 21 file, 0 file credentials |
Không exclude nào bị thêm (không cần), không credential nào bị xoay, không đổi cơ chế xác thực Drive.
13. Consumer của shell export
CONSUMERS_OF_SHELL_EXPORT = IDENTIFIED
| Consumer | Thứ tự lấy token | Cron? |
|---|---|---|
dot-collection-track-readonly |
env → DOT_TOKEN → auto-login từ container env ✅ |
0 |
dot-schema-apply |
env → SM DIRECTUS_ADMIN_TOKEN_test → SM → DOT_TOKEN → báo lỗi |
0 |
dot-knowledge-sync-agentdata |
SM DIRECTUS_ADMIN_TOKEN_test |
0 |
test-all-connections.sh |
đọc từ docker/.env, không đọc shell env |
0 |
🔴 Phát hiện mới: secret DIRECTUS_ADMIN_TOKEN_test KHÔNG TỒN TẠI trong Secret Manager (NOT_FOUND); secret có thật tên là DIRECTUS_ADMIN_TOKEN. ⇒ Đường fallback SM của 2 DOT tool đã hỏng từ trước mission này. Ghi vào backlog, không sửa (ngoài phạm vi).
8 cron job dùng . /opt/incomex/scripts/cron-env.sh && export DIRECTUS_ADMIN_TOKEN — đọc docker/.env, hoàn toàn độc lập với .bashrc ⇒ không bị ảnh hưởng.
14. Thay đổi trên root shell
/root/.bashrc a454e538… → 5c390ba0… (644 root:root, không đổi mode)
diff PRE→POST: ĐÚNG 1 dòng (107) bị thay bằng 5 dòng comment. Không dòng nào khác đổi.
Dòng comment chỉ rõ cách dùng thay thế:
. /opt/incomex/scripts/cron-env.sh && export DIRECTUS_ADMIN_TOKEN && <command>
Chứng minh tương đương (fingerprint, PAYLOAD=REDACTED):
live DB admin token len=64 fp=e34b53cc21f2dd92
cron-env.sh len=64 fp=e34b53cc21f2dd92 ← giống hệt
.bashrc (trước khi gỡ) len=64 fp=e34b53cc21f2dd92
Xác minh sau khi gỡ:
NEW_INTERACTIVE_SHELL_TOKEN = UNSET (len=0)
NEW_NONINTERACTIVE_SHELL_TOKEN= UNSET
NEW_LOGIN_SHELL_TOKEN = UNSET
SHELL_STARTUP_ADMIN_TOKEN_COPY= 0
cron environment = KHÔNG đổi (crontab sha256 byte-identical)
Không tạo protected token cache mới — vì authority hiện hành (cron-env.sh → docker/.env, root:root 0600) đã đủ và không auto-source.
Lưu ý mức độ phơi nhiễm cũ, nói cho đúng: .bashrc là mode 0644 nhưng /root là 0700, nên user thường không đọc được. Rủi ro thật là token vào environment của mọi tiến trình con sinh ra từ shell tương tác, không phải quyền đọc file.
EXISTING_ROOT_SHELLS_MAY_REQUIRE_REOPEN = YES
Shell đang mở vẫn giữ giá trị cũ trong process environment. Không force-kill session nào. Owner chỉ cần đóng/mở lại terminal.
15. Cơ chế mã hoá đã chọn
Chọn: OpenPGP public-key (GnuPG 2.4.4, RSA 4096/4096), theo §13 ưu tiên #1 — "cơ chế mã hoá backup public-key đã có và đã được kiểm chứng".
Trên chính VPS này đã có sẵn cơ chế đó, đang chạy production:
systemd s177-lark-backup.timer ACTIVE, 3 ngày/lần
module /opt/incomex/lark-client/lark_client/gpg_backup.py
GSM LARK_BACKUP_GPG_PUBKEY + LARK_BACKUP_GPG_PRIVATEKEY_RECOVERY
invariant (trích nguyên văn): "VPS NEVER holds the private key. Encrypt-only.
VPS NEVER decrypts. There is no decrypt method here."
Mission tái dùng đúng khuôn mẫu này nhưng với key pair riêng biệt cho mục đích DIRECTUS_DATABASE_BACKUP_ENCRYPTION (không dùng chung khoá Lark).
Vì sao không dùng age: age chưa cài (có sẵn trong repo Ubuntu noble 1.1.1-1ubuntu0.24.04.3), sẽ phải thêm 1 package mới. GPG đã có sẵn, và quan trọng hơn: nó là cơ chế đã được Owner chấp nhận và đang chạy trên chính host này ⇒ đúng thứ tự ưu tiên §13, không thêm bề mặt mới.
Không dùng: ZIP password · passphrase đối xứng · tar/gzip giả danh mã hoá · crypto tự chế · mã hoá không xác thực · rclone crypt.
--recipient-file (gpg ≥ 2.1.14) cho phép mã hoá thẳng từ file khoá công khai, không cần import vào keyring ⇒ VPS không giữ trạng thái khoá nào.
16. Recipient công khai / fingerprint
RECIPIENT_FPR 31799F7AC051EBA68FB3F13BDCC1369B371837BA
pub keyid DCC1369B371837BA RSA 4096
sub keyid E800896AF06E9B61 RSA 4096 ← khoá con thực sự mã hoá
uid INCOMEX Directus DB Backup (C2B-PRE encrypt-only; private key held in GCP Secret Manager)
<directus-backup@incomexsaigoncorp.vn> Expire-Date: 0 (không hết hạn)
path /etc/incomex/backup/directus-backup-recipient.pub root:root 0600 (dir 0700)
sha256 5e6c0864df2f8f4c94a8477db0d60bc8cd0cbf237f0adf53c87ea7562cb24f9e
Khoá công khai không phải bí mật; nó chỉ mã hoá được, không giải mã được.
17. Nơi giữ khoá riêng (primary authority)
projects/812872501910/secrets/DIRECTUS_BACKUP_GPG_PRIVATEKEY_RECOVERY
project github-chatgpt-ggcloud
replication user-managed, europe-west3 (khớp convention 31 secret hiện có)
labels purpose=directus-db-backup-encryption, created-by=c2b-pre, keytype=gpg-private-key
versions 1 · enabled · createTime 2026-07-19T04:08:23Z
Quy trình sinh khoá — khoá riêng chưa từng chạm đĩa thường:
GNUPGHOMEtạo trong tmpfs/dev/shm, mode 0700,umask 077.- Sinh RSA4096/4096,
%no-protection(bảo vệ = IAM của Secret Manager, không phải passphrase — đúng khuôn mẫu Lark; mission cấm hardcode passphrase). - Public key →
/etc/incomex/backup/. - Private key → file tmpfs →
gcloud secrets create --data-file=-qua STDIN (không bao giờ vào argv). trap EXIT→shred -utoàn bộ vật liệu khoá + xoá GNUPGHOME tạm.
Bằng chứng vòng tròn khép kín (§14.8–14.12):
VPS_PRIVATE_KEY_PRESENT = NO (gpg --list-secret-keys trống)
đọc lại version 1 từ SM = 7532 bytes
imported_fpr = 31799F7AC051EBA68FB3F13BDCC1369B371837BA
FINGERPRINT_MATCH = PASS
DECRYPT_PROOF = PASS (sha256 plaintext == sha256 sau vòng mã hoá/giải mã)
STREAMING_FORM = PASS (stdin → stdout, đúng dạng pipeline dùng)
PRIVATE_KEY_MATERIAL_ON_VPS = 0 (quét lại sau khi dọn: không còn vật liệu khoá riêng nào)
Không disable, không destroy version nào. Không đụng bất kỳ secret nào khác.
18. 🔴 Giới hạn phải nói thẳng — VPS SA đọc được khoá riêng
Đo trực tiếp bằng testIamPermissions:
PROJECT-level granted cho cursor-ci-builder@github-chatgpt-ggcloud.iam.gserviceaccount.com:
secretmanager.secrets.create · secretmanager.secrets.list
secretmanager.versions.add · secretmanager.versions.access ← 🔴
SECRET-level trên một secret bất kỳ:
secretmanager.secrets.get · secretmanager.versions.access ← 🔴
⇒ Service account trên VPS có quyền đọc payload secret trên toàn project, kể cả khoá riêng backup.
Hệ quả trung thực:
- ✅ Mất VPS (hỏng, mất máy) không làm mất khả năng giải mã — Owner vẫn lấy được khoá. Đây là yêu cầu §15 và nó đạt.
- ❌ Chiếm được quyền root trên VPS thì vẫn lấy được khoá riêng ⇒ tính chất "compromise VPS không tự động trao khả năng giải mã historical artifact" KHÔNG thành lập. Tôi không tuyên bố tính chất mà tôi không có.
Vì sao vẫn đáng làm: kẻ chiếm root trên VPS đã có DB sống, toàn bộ token, và OAuth Drive với scope=drive. Giá trị thật của mã hoá ở đây là chặn đường rò phía Google Drive — nơi tài khoản Drive chưa xác định được danh tính, có 13.9 GiB Trash, và là kho ngoài tầm kiểm soát của VPS.
Không sửa IAM (ngoài phạm vi cho phép; hơn nữa quyền này kế thừa ở mức project, binding ở mức resource không thể thu hồi nó — chỉ Deny policy mới làm được).
Giảm thiểu → backlog: (a) siết IAM để tách khoá backup khỏi cursor-ci-builder@; (b) hoàn tất recipient thứ hai Owner giữ offline (§20).
19. Đường giải mã off-VPS do Owner kiểm soát
OFFVPS_DECRYPTION_PATH = VERIFIED_OWNER_PRINCIPAL
- Bằng chứng C2A (đo từ laptop Owner, không phải từ VPS): active account
nmhuyen@gmail.com— user principal, không phải service account —roles/ownertrêngithub-chatgpt-ggcloud;describeOK,secrets listOK (32), IAM read OK. - Bằng chứng phủ định mới, đo hôm nay từ VPS: VPS SA KHÔNG đọc được project IAM policy —
ERROR: cursor-ci-builder@… does not have permission … projects instance … getIamPolicy. Chính việc VPS không làm được điều Owner làm được là bằng chứng hai principal thực sự tách biệt. roles/ownerbao trùmsecretmanager.versions.access⇒ Owner đọc được khoá riêng mà không cần gì từ VPS.
⚠️ Cảnh báo trung thực (kế thừa C2A): đây là credential đã cache trên laptop. Nó chứng minh đường truy cập đang hoạt động; nó không chứng minh Owner đăng nhập lại được nếu mất laptop. Việc Owner tự đăng nhập bằng trình duyệt một lần để xác nhận là khuyến nghị mạnh (§20).
20. Recipient độc lập thứ hai
SECOND_INDEPENDENT_RECIPIENT = OWNER_ACTION_REQUIRED
Chưa có kênh bàn giao khoá an toàn nào được Owner phê duyệt. Mission cố tình không tự đặt khoá riêng vào một nơi "trông có vẻ off-site" — đúng chỉ thị §16.
Theo §16, containment vẫn PASS vì OFFVPS_DECRYPTION_PATH = VERIFIED_OWNER_PRINCIPAL. Nhưng khuyến nghị mạnh hoàn tất recipient thứ hai, đặc biệt vì §18: hiện tại chỉ có một nơi giữ khoá, và VPS SA đọc được nơi đó.
21. Bản vá pipeline
Script mới /opt/incomex/scripts/backup-to-gdrive.sh — 35f1095a8c70b91bc08a5d3a8f4a7284e58c7f222fe38ad009568056d3d922b0 (root:root 755, 140 → 265 dòng).
Hai artifact, đều là ciphertext, thay cho một tarball plaintext:
incomex-prod-directus-db-<UTC>.sql.gz.gpg ← pg_dump | gzip | gpg (STREAMING)
incomex-prod-vps-config-<UTC>.tar.gz.gpg ← tar -czf - | gpg (STREAMING)
<mỗi artifact>.meta.json ← sidecar KHÔNG bí mật
Tách đôi là cố ý: DB đi thẳng từ pg_dump ra ciphertext nên không cần lớp mã hoá lồng nhau, và Owner khôi phục DB chỉ cần một lệnh gpg -d — không phải giải mã → giải nén tar → giải mã tiếp. Đây là điểm mấu chốt để runbook dùng được với người không chuyên (H4).
Tên artifact chứa environment (prod), database (directus-db), UTC timestamp, và định dạng mã hoá (.gpg + format_version: incomex-encrypted-v1). Cố ý không bắt đầu bằng vps-backup- ⇒ glob retention cũ không thể khớp, kể cả nếu sau này ai đó liệt kê đệ quy.
Vòng đời plaintext:
| Giai đoạn | Plaintext ở đâu |
|---|---|
| PG dump | không đâu cả — chỉ trong pipe RAM |
| Thành phần config (qdrant/nginx/TLS/scripts) | WORK_DIR root-only 0700 trong /var/lib/incomex/backup-staging, ngoài git root / Drive source / web / KB / build context |
| Sau khi tạo ciphertext | rm -rf "$WORK_DIR" ngay lập tức |
| Mọi lối thoát (thành công/lỗi/tín hiệu) | trap cleanup EXIT xoá WORK_DIR + mọi .partial |
.partial + atomic rename (H0.5–0.7): ciphertext ghi ra <tên>.partial, chỉ được mv thành tên thật sau khi qua size floor + kiểm cấu trúc OpenPGP + đúng recipient keyid, sau sync.
Giữ nguyên: lịch 37 20 * * * · MAX_REMOTE_BACKUPS=14 · toàn bộ nội dung [2/5]–[5/5] · .env vẫn bị loại (Wave A1 1f36dbb còn nguyên).
22. Fail-closed / fail-visible
Status marker /var/lib/incomex/backup-status/directus-db-backup.status (root 0600), ghi atomic temp+rename, trường: run_id · pid · started_utc · last_attempt_utc · last_success_utc · status · stage · artifact_* · artifact_*_sha256 · recipient_fpr · error_class · payload=REDACTED.
Singleton lock: flock -n trên fd 9; trùng lặp → log ALREADY_RUNNING, exit 75, không tính là backup mới.
Phát hiện STALE_RUNNING: status RUNNING + không tiến trình nào giữ lock ⇒ lần chạy trước đã chết. Lệnh kiểm cho Owner nằm ở §5 runbook. Không dùng PID đơn lẻ làm bằng chứng.
Stage: PRECHECK → PG_DUMP → COMPONENTS → ENCRYPT → VERIFY → UPLOAD → RETENTION → COMPLETE.
BACKUP_FAILURE_VISIBILITY = PASS_STATUS_AND_LOG
Có reader thật: monitor Uptime Kuma push pg-backup-gdrive đang sống (push_endpoint_http=200). Script gửi status=up khi PASS và status=down kèm stage + error_class khi FAIL (bản cũ chỉ im lặng chết ⇒ chỉ báo động khi thiếu heartbeat). Không xây hệ thống monitoring mới.
23. Kiểm thử fail-closed (§20) — harness cô lập
Harness dựng từ chính candidate bằng sed: giữ nguyên toàn bộ fail() / status_write() / verify_ct() / trap / xử lý PIPESTATUS; chỉ vô hiệu nguồn dữ liệu và đích. Không phá recipient production.
| # | Kịch bản | exit | status | stage | error_class | upload | .partial |
|---|---|---|---|---|---|---|---|
| S0 | Đường xanh | 0 | PASS | COMPLETE | — | 4 file | 0 |
| S1 | Thiếu recipient | 1 | FAIL | PRECHECK | RECIPIENT_MISSING |
0 | 0 |
| S2 | Recipient sai fingerprint | 1 | FAIL | PRECHECK | RECIPIENT_FINGERPRINT_MISMATCH |
0 | 0 |
| S3 | gpg --encrypt exit 9 |
1 | FAIL | ENCRYPT | ENCRYPTION_FAILED |
0 | 0 |
| S4 | Upload hỏng | 1 | FAIL | UPLOAD | UPLOAD_FAILED |
0 | 0 |
| S5 | SIGTERM giữa chừng | 1 | FAIL | PG_DUMP | SIGNAL_INTERRUPT |
0 | 0 |
| S6 | Hạ tầng status hỏng | ≠0 | — | — | — | không đổi | 0 |
Mọi kịch bản: không upload plaintext · không fallback · không retention · exit ≠ 0 · last_success_utc KHÔNG bị cập nhật giả (S1 giữ nguyên mốc của S0).
🔴 Harness bắt được 2 lỗi thật trước khi deploy
Lỗi 1 — đường xanh FAIL oan (CIPHERTEXT_WRONG_RECIPIENT_DB).
gpg --batch --list-packets luôn exit 2 trên host này ("No secret key" — đúng như thiết kế encrypt-only) dù nó in ra đúng keyid E800896AF06E9B61. Dưới set -o pipefail, exit code 2 của gpg đè lên exit 0 của grep -q ⇒ pipeline trả 2 ⇒ || fail nổ. Nếu không có harness, đây là lỗi sẽ làm hỏng backup mỗi đêm, fail-closed nên sẽ mất backup âm thầm cho tới khi ai đó nhìn Kuma.
→ Sửa: capture output trước, so bằng case, không để rc của gpg quyết định.
Lỗi 2 — phân loại lỗi sai stage.
Đo được PIPESTATUS=[0 141 9]: khi gpg chết, gzip nhận SIGPIPE (141). Kiểm tra theo thứ tự PS[0]→PS[1]→PS[2] ⇒ báo nhầm FORMAT_OR_COMPRESSION_FAILED trong khi thủ phạm là mã hoá — vi phạm H0.4.
→ Sửa: kiểm stage cuối trước (PS[2] → PS[0] → PS[1]); lỗi upstream chỉ là hệ quả.
Lỗi 3 (tự phát hiện, sửa chủ động): run nào fail trước RETENTION thì không bao giờ prune ⇒ chuỗi đêm lỗi sẽ tích luỹ ciphertext trên đĩa đang ở 88%. → Thêm prune giới hạn ở PRECHECK (giữ 3 bản local mới nhất mỗi loại). Chỉ xoá local, không bao giờ chạm remote.
24. Test khôi phục local (§19.1) — PASS
DISK_HEADROOM_GATE = PASS:
DISK_FREE_PRE 12.348.854.272 bytes (11,5 GiB)
DIRECTUS_DB_BYTES 1.782.029.335 (1699 MB)
EXPECTED_ARTIFACT_BYTES 285.000.000
EXPECTED_PEAK_LOCAL_BYTES 515.000.000
SAFETY_RESERVE_BYTES 2.147.483.648 (2 GiB)
REQUIRED 2.662.483.648 ⇒ PASS (dư ~4,6×)
Không prune Docker, không xoá backup/log, không vacuum, không dọn cache để vượt gate.
TEST A PLAINTEXT_DUMP_TOKEN_MATCH_LINES = 13 (stream, không ghi đĩa)
TEST B PIPESTATUS = [0 0 0] · CIPHERTEXT_BYTES = 120.505.714 · PLAINTEXT_FILES_ON_DISK = 0
TEST C NEW_CIPHERTEXT_KNOWN_SECRET_MATCH = 0
TEST D giải mã qua đúng đường Owner (Secret Manager):
DECRYPTED_BYTES = 1.442.080.081
VALID_PG_DUMP_HEADER = PASS
CREATE_TABLE_COUNT = 385
DIRECTUS_SCHEMA_INVENTORY = directus_users/flows/operations/sessions/permissions : PRESENT ×5
đuôi file = "PostgreSQL database dump complete"
DECRYPTED_TOKEN_MATCH_LINES = 13 ⇒ khôi phục trung thực, không mất dữ liệu
KHÔNG restore vào production. Vật liệu giải mã cleanup bằng trap; PLAINTEXT_ON_DISK = 0.
25. Test remote cô lập (§19.2) — PASS_ISOLATED_ARTIFACT
Prefix riêng _c2bpre-isolated-test/ — không phải prefix production, không phải Drive root.
upload c2bpre-isolated-20260719T055642Z.sql.gz.gpg 120.505.714 bytes
src sha256 e5e7e4b6ee14c4da59d291fdd348ff3dac225f1848f182c4ff216e3a9a1d919b
lsjson remote_bytes = 120.505.714 (khớp)
download sha256 = e5e7e4b6… ⇒ ROUNDTRIP_INTEGRITY = PASS
scan DOWNLOADED_CIPHERTEXT_KNOWN_SECRET_MATCH = 0
decrypt 1.442.080.081 bytes · header PASS · 385 bảng · directus_users PRESENT · dump complete
cleanup rclone deletefile ĐÚNG 1 file → rmdir thư mục rỗng → prefix biến mất (0)
Không upload plaintext, không upload khoá riêng. Xoá bằng deletefile trên đường dẫn tuyệt đối chính xác, không bao giờ purge/delete phạm vi rộng — đặc biệt quan trọng vì remote gdrive-backup: có scope=drive và trỏ vào Drive cá nhân của Owner (thư mục gốc chứa "A soạn thảo/", "For Gemini /"…).
Production sau test: legacy_plaintext_root_count 14 → 14 · root_files_total 16 → 16 ⇒ PRODUCTION_RETENTION_UNCHANGED = PASS.
26. Retention: tương thích định dạng & chế độ chuyển tiếp
Logic cũ (đọc nguyên văn từ script live):
REMOTE_FILES=$(rclone lsf "$RCLONE_REMOTE" --files-only | grep "^vps-backup-" | sort -r)
COUNT>14 → rclone deletefile
Trả lời đúng các câu hỏi H3:
- Nhận dạng theo tiền tố tên file
^vps-backup-, không lọc phần mở rộng;sort -r= mới nhất trước (timestampYYYYMMDD_HHMMSSsắp xếp đúng). .gpgkhông được nhận dạng.- Nếu đổi glob để nhận
.age/.gpg⇒ artifact plaintext lịch sử sẽ vào chung tập ứng viên xoá. - 🔴 Có nguy cơ xoá nhầm nếu trộn định dạng: đếm hiện tại đúng bằng 14 ==
MAX_REMOTE_BACKUPS⇒ mọi artifact mới thêm vào Drive root sẽ xoá ngay 1 bản lịch sử. - Đang đúng ở biên retention.
Vì vậy: KHÔNG patch glob. Thay vào đó:
RETENTION_FORMAT_COMPATIBILITY = PASS_EXISTING_LOGIC
RETENTION_TRANSITION_MODE = APPEND_ONLY_ENCRYPTED_PREFIX
- Prefix mã hoá mới
incomex-encrypted-v1/— append-only, KHÔNG xoá gì trong pha này. - Vòng lặp retention cũ giữ nguyên từng byte; vì script không còn upload lên Drive root, số lượng không bao giờ vượt 14 ⇒ không xoá gì nữa. Tác dụng phụ tích cực: 14 bản plaintext lịch sử ngừng bị xoay vòng, được đóng băng nguyên vẹn.
- Tên artifact mới không bắt đầu bằng
vps-backup-⇒ lớp bảo vệ thứ hai.
ENCRYPTED_RETENTION_POLICY = TRANSITION_APPEND_ONLY_PENDING_SEPARATE_POLICY
🔴 Định lượng tăng trưởng lưu trữ — Owner cần biết:
db ~115 MiB (đo thật 120.505.714 B) + config ~144 MiB ≈ 259 MiB/đêm
≈ 7,6 GiB/tháng ≈ 92 GiB/năm KHÔNG xoá gì
Trước đây Drive giữ trạng thái ổn định ~3,6 GiB (14 × 259 MiB). Từ nay nó tăng tuyến tính. Cần một mission riêng đặt retention cho prefix mã hoá, sau khi đã có vài bản encrypted được xác minh. Dung lượng Drive còn trống chưa đo (cần quyền/quota API).
27. Lần chạy production tự nhiên đầu tiên
FIRST_NATURAL_PRODUCTION_RUN = PENDING
Chốt lúc 2026-07-19 08:25 CEST; lần cron tự nhiên kế tiếp 20:37 CEST hôm nay, còn 12h11m. Không manual-trigger production run. Không chạy background waiter vô hạn. Không ép lịch.
status_marker_exists = NO_NEVER_RAN (đúng: chưa chạy lần nào)
encrypted_prefix_on_drive = 0 (sẽ do lần chạy đầu tạo)
Điều kiện tiền đề đã thoả: prefix mã hoá không xoá gì · prefix plaintext cũ không bị đụng retention · disk headroom PASS · lock/status checker PASS · deploy nguyên tử PASS.
Việc cần kiểm ngay sau lần chạy tự nhiên đầu tiên (read-only): cron gọi đúng script đã vá · lock hoạt động · status RUNNING → PASS/COMPLETE · artifact mới chỉ ở prefix mã hoá · không có sibling plaintext · không sót .partial · checksum khớp sidecar · quét token trên ciphertext = 0 · staging không còn plaintext · không artifact lịch sử nào bị xoá · plaintext root count không giảm (vẫn 14) · encrypted prefix tăng đúng · download/decrypt bounded PASS · lịch không đổi · không container nào restart do backup.
28. Retention production không đổi — bằng chứng
Drive root vps-backup-* PRE 14 → POST 14
Drive root tổng số file PRE 16 → POST 16
MAX_REMOTE_BACKUPS 14 → 14 (không đổi)
crontab sha256 547e8151f95c0cc86ed48abd3401e8af1b04b1ce0f111a14bd2d9b13bc07b039 PRE == POST
lịch 37 20 * * * không đổi
HISTORICAL_BACKUP_DELETE = NO
29. Restore runbook
OFFVPS_RESTORE_RUNBOOK = VERIFIED
- A. Bản canonical trên VPS (tracked trong Git):
/opt/incomex/docs/recovery/directus-backup-restore-runbook.md - B. Bản off-VPS Owner truy cập được: Google Drive
incomex-recovery-docs/directus-backup-restore-runbook.md
vps sha256 8928cb580e68629e0e5fe1a8aac67fb872cff8a310ac7b90f3f2be72b5489212
drive read-back 8928cb580e68629e0e5fe1a8aac67fb872cff8a310ac7b90f3f2be72b5489212
OFFVPS_RUNBOOK_CHECKSUM = PASS readback_token_hits = 0
Viết cho Owner không chuyên kỹ thuật (H4): mỗi bước ghi rõ gõ lệnh gì · gõ ở đâu (Terminal MacBook / Google Cloud Console / trình duyệt Drive) · đăng nhập tài khoản nào · mong đợi thấy gì · nếu không thấy thì dừng ở đâu · không dán bí mật vào chat. Có mục riêng "NẾU VPS MẤT HOÀN TOÀN" giả định Owner chỉ còn MacBook + tài khoản Google, kèm lệnh copy-paste thật cho gpg 2.x trên macOS. Có mục kiểm status và phát hiện STALE_RUNNING. Có mục cảnh báo bản cũ chưa mã hoá. Có mục §8 nói rõ phạm vi DR.
Runbook không chứa khoá riêng hay bất kỳ credential nào — chỉ định danh (project, tên secret, fingerprint, tên thư mục). Đã sửa lại câu mô tả để không còn khớp mẫu dò khoá PGP tự động (tránh false-positive cho secret scanner).
OWNER_DECRYPT_DRILL = RECOMMENDED_NOT_YET_DONE
Agent đã giải mã thành công qua đúng đường authority tương đương Owner (Secret Manager). Diễn tập do chính Owner bấm tay nên làm trong một phiên ngắn có Owner tham gia, sau containment.
30. Quét sau mutation (§23)
SHELL_STARTUP_ADMIN_TOKEN_COPY = 0 (.bashrc / .profile / /etc/profile.d / /etc/bash.bashrc)
BACKUP_SCRIPT_SECRET_COPY = 0
UPLOAD_STAGING_PLAINTEXT_COPY = 0
STATUS_FILE_SECRET_COPY = 0
RESTORE_RUNBOOK_SECRET_COPY = 0
RECIPIENT_CONFIG_SECRET_COPY = 0
NEW_CIPHERTEXT_KNOWN_SECRET_MATCH= 0
MANIFEST_SECRET_COPY = 0
REPORT_EVIDENCE_SECRET_COPY = 0
PROCESS_ARGV_SECRET = 0
HEAD_TREE_SECRET_COPY = 0 (git grep trên HEAD: scripts + docs)
STAGED_DIFF_TOKEN_HITS = 0
PRIVATE_KEY_MATERIAL_ON_VPS = 0
MISSION_CHECKPOINT_SECRET_COPY = 1 ← CÓ CHỦ ĐÍCH, xem dưới
Giải thích 1 hit duy nhất: /root/c2bpre-checkpoint-…/backup/root-bashrc.PRE (root:root 0600). Đây là bản backup byte-exact bắt buộc để rollback — nó phải chứa dòng export cũ. Nằm dưới /root (0700), ngoài mọi Drive/GitHub/KB/web source; cả hai job backup đều có 0 tham chiếu tới /root. Cùng khuôn mẫu C1/C2A đã dùng cho các file *.PRE.
Còn tồn tại hợp lệ (không xoá trong mission này): DB Directus sống · /opt/incomex/docker/.env (nguồn injection hợp pháp) · /etc/incomex/agent-data.env · 14 dump local lịch sử · 14 artifact Drive lịch sử · environment của agent-data/nuxt đang chạy.
Vệ sinh phiên: khoá HMAC 32B sinh trong tmpfs, dùng chung cho PRE/POST, shred sau khi dùng, không lưu ở đâu — fingerprint 16-hex trong báo cáo này vô dụng ngoài phiên. Pattern file chứa giá trị token đặt trong /dev/shm 0600, giá trị không bao giờ vào argv (dùng grep -F -f <file>), cleanup bằng trap. Vật liệu test (116 MiB ciphertext + khoá GPG dùng-một-lần của harness) đã xoá sạch.
31. Hiện tại vs lịch sử
| Trạng thái | |
|---|---|
| Bản plaintext MỚI rời VPS | 🛑 ĐÃ CHẶN |
| Token trong shell tương tác mới | 🛑 ĐÃ CHẶN |
| 14 artifact Drive lịch sử | 🔴 VẪN CÒN, token vẫn hiệu lực |
| Drive Trash 13.9 GiB | 🔴 có thể còn bản cũ hơn — chưa đo |
14 dump local /opt/incomex/backups |
🔴 vẫn còn (cố ý giữ) |
Password trong git history + GitHub vps-daily-* |
🔴 vẫn còn (C3) |
| 7 user suspended vẫn giữ static token | 🔴 vẫn còn (C2B3+) |
Câu phải ghi nguyên nghĩa: Các Directus dump cũ trên Google Drive vẫn chứa static token và có thể chứa credential khác còn hiệu lực. Encryption chỉ ngăn tạo thêm plaintext dump. Nó không thu hồi hoặc vô hiệu hoá credential đã phát tán.
HISTORICAL_DIRECTUS_DUMP_EXPOSURE = REMAINS_CREDENTIALS_STILL_VALID
32. Zero-runtime-mutation
CONTAINER_IDS_PRE = POST SAME_IDS 11/11 (không container nào bị recreate)
10/11 container PRE == POST từng byte (ID + StartedAt + RestartCount + Status)
DIRECTUS_USER_MUTATION = 0 users_total 13 → 13
DIRECTUS_PASSWORD_CHANGE = 0
DIRECTUS_STATIC_TOKEN_CHANGE= 0 13/13 fingerprint HMAC identical PRE == POST
DIRECTUS_SESSION_REVOCATION = 0 sessions 8 → 10 (TĂNG do cron login tự nhiên; không xoá gì)
FLOW_OPERATION_CHANGE = 0 flows 128 → 128 · operations 320 → 320
OTHER_SERVICE_MUTATION = 0
Ngoại lệ duy nhất — incomex-nuxt (bracket riêng, không dùng để phủ nhận zero-touch của 10 container còn lại):
container ID 42079c55bf9c… KHÔNG đổi ⇒ không recreate, chỉ restart trong chỗ
StartedAt 2026-07-19T03:58:32Z → 06:10:34Z (cách nhau ~2h12m)
RestartCount 48 → 49 ExitCode = 0 OOMKilled = false RestartPolicy = unless-stopped
Khớp chính xác hiện tượng có sẵn từ trước mà C2A đã ghi (nuxt tự thoát ExitCode=0 ~1–2h/lần). Mission không chạy bất kỳ lệnh docker restart/recreate/exec nào nhắm vào incomex-nuxt. Không điều tra, không sửa — backlog.
33. Git commit & bundle
commit e2aa7cf0cc0b142d04aaf0fca2be693dd0a989fb
fix(backup): encrypt Directus database before off-site upload
2 files changed, 506 insertions(+), 90 deletions(-)
A docs/recovery/directus-backup-restore-runbook.md
M scripts/backup-to-gdrive.sh
Cổng trước commit: git add đúng 2 đường dẫn (không -A, không .) · git diff --cached --check rc=0 · quét giá trị token trên staged diff = 0 · quét khoá riêng = 0 · 0 file .env/rclone/credentials/.bashrc được stage.
Unrelated work bảo toàn tuyệt đối: 15 modified + 89 untracked trước == 15 modified + 89 untracked sau (mốc PRE của C2A: 15 M + 89 ?? = 104).
bundle POST opt-incomex-POST-20260719T062452Z.bundle
098f9abde2b20aa951e2849739aa91b317ec04dee369123813712d8b10b310c6
"The bundle records a complete history." PASS (chạy TỪ TRONG repo)
GITHUB_PUSH = NO (repo có 0 remote ⇒ push bất khả thi về cấu trúc)
Bundle chứa git history có secret lịch sử ⇒ root-only, chỉ nằm local trên VPS, ngoài Drive/GitHub/KB/web.
/root/.bashrc nằm ngoài Git ⇒ không stage; bảo vệ bằng backup byte-exact + ROLLBACK.sh.
34. Lệnh rollback
# Toàn bộ (script backup + .bashrc), 0 phụ thuộc ngoài:
ssh contabo '/root/c2bpre-checkpoint-20260719T040200Z/rollback/ROLLBACK.sh'
# Thủ công:
CK=/root/c2bpre-checkpoint-20260719T040200Z
install -m 0755 -o root -g root "$CK/backup/backup-to-gdrive.sh.PRE" /opt/incomex/scripts/backup-to-gdrive.sh
install -m 0644 -o root -g root "$CK/backup/root-bashrc.PRE" /root/.bashrc
sha256sum /opt/incomex/scripts/backup-to-gdrive.sh # phải == ebdf3be1…
sha256sum /root/.bashrc # phải == a454e538…
bash -n /opt/incomex/scripts/backup-to-gdrive.sh
# Git:
cd /opt/incomex && git revert e2aa7cf0cc0b142d04aaf0fca2be693dd0a989fb
⚠️ Cảnh báo rollback: khôi phục script cũ = quay lại upload plaintext lên Drive ngay đêm kế tiếp, và retention cũ sẽ xoá tiếp 1 artifact lịch sử mỗi đêm. Prefix mã hoá và secret trong Secret Manager cố ý giữ nguyên khi rollback.
35. Nếu Owner muốn dừng — các lựa chọn
Mission không rơi vào HOLD_DIRECTUS_BACKUP_ENCRYPTION_UNPROVEN: mã hoá đã chứng minh đầy đủ. Không cần Owner chọn giữa OPTION_A / OPTION_B.
Ba quyết định đang chờ Owner, không cái nào chặn containment:
- Recipient độc lập thứ hai (§20) — cần một kênh bàn giao an toàn.
- Retention cho prefix mã hoá (§26) — hiện append-only, +7,6 GiB/tháng.
- Siết IAM để
cursor-ci-builder@không đọc được khoá riêng backup (§18).
36. Backlog khắc phục — CHỈ KHOÁ, KHÔNG CHẠY
C2B1 Rotate bootstrap admin password + admin static token như MỘT bộ credential
C2B2 Map consumer, rotate tac-agent token, cập nhật claude-mcp nguyên tử, verify connector
C2B3+ Map & rotate từng static token còn lại — ƯU TIÊN 7 user suspended vẫn giữ token
FLOW_CREDENTIAL_WAVES 43 operation có header xác thực → xử lý theo từng integration
HISTORICAL_CLEANUP git/GitHub password history · 14 Drive dump plaintext · Drive Trash 13.9 GiB
· dump local · quyết định retention/pháp lý/khôi phục
Phát sinh thêm từ mission này (đều chưa đụng tới):
BL-1 Secret Manager KHÔNG có `DIRECTUS_ADMIN_TOKEN_test` ⇒ fallback SM của dot-schema-apply
và dot-knowledge-sync-agentdata đã hỏng sẵn. Nên trỏ về `DIRECTUS_ADMIN_TOKEN`.
BL-2 C6 — backup-to-gdrive.sh: QDRANT_LOCAL_API_KEY vẫn vào argv của curl mỗi đêm
(CỐ Ý giữ nguyên: sửa 4 lệnh curl có rủi ro làm hỏng bước Qdrant; "mỗi lần một việc").
BL-3 Retention cho prefix mã hoá (+7,6 GiB/tháng, hiện append-only).
BL-4 Siết IAM Secret Manager (§18).
BL-5 Diễn tập giải mã do chính Owner thực hiện (§29).
BL-6 /root/.bashrc dòng 108/112 có `export PATH="\$HOME/..."` — dấu `\` làm PATH nhận chuỗi
literal `$HOME`. Lỗi CÓ TỪ TRƯỚC (giống hệt trong bản PRE), không sửa.
BL-7 A3 — TLS private key vẫn nằm trong gói config ban đêm; nay ĐÃ ĐƯỢC MÃ HOÁ,
nhưng quyết định có nên backup nó hay không vẫn thuộc về Owner.
Không rotate credential nào trong mission này. Không tự soạn prompt cho các mission sau.
37. Phạm vi disaster recovery — nói cho đúng
DIRECTUS_OFFSITE_BACKUP_SCOPE = DATABASE_DIRECTUS_ONLY
Không được đọc thành "toàn bộ PostgreSQL đã được backup" hay "toàn hệ thống đã có off-site recovery".
DR_COVERAGE_GAP — đo lại live hôm nay (5 database, tổng 3306 MB):
directus 1699 MB ✅ CÓ off-site (đã mã hoá)
directus_gov_test_20260602 1202 MB ❌ CHƯA có off-site được chứng minh
incomex_metadata 390 MB ❌ CHƯA
workflow 7591 kB ❌ CHƯA
postgres 7519 kB ❌ chưa phân loại có cần backup hay không
Qdrant vector store ❌ KHÔNG nằm trong pg_dump (có snapshot trong gói config;
~19.9k vectors theo Wave A1 — CHƯA đo lại trong mission này)
object/file storage khác ❌ cần khảo sát riêng
Verdict đầy đủ chỉ có nghĩa: đường phát tán plaintext của database directus đã bị chặn — không có nghĩa DR toàn hệ thống đã hoàn chỉnh.
38. Ghi chú nuxt
Xem §32. incomex-nuxt restart 48→49, cùng container ID, ExitCode=0. Hiện tượng có sẵn, backlog only, không điều tra, không sửa trong mission này.
39. Đường dẫn bằng chứng
/root/c2bpre-checkpoint-20260719T040200Z/ (root:root 0700)
backup/{backup-to-gdrive.sh,root-bashrc,code-backup-to-gdrive.sh,cron-env.sh}.PRE
backup/root-crontab.PRE · backup/rclone.conf.METADATA · backup/backup.sha256
bundle/opt-incomex-{PRE,POST}-*.bundle · bundle/bundles.sha256
evidence/containers.{PRE,POST} · containers-detail.PRE
evidence/drive-root-{files,dirs}.PRE · git.PRE · disk-avail-bytes.PRE
evidence/encryption-key.META
rollback/ROLLBACK.sh (0700)
/root/c2bpre-work/ (root:root 0700)
backup-to-gdrive.sh.candidate (== bản live) · harness.sh
patch.py · patch2.py · patch-bashrc.py · c2b-fp.py
run-failtests.sh · remote-test.sh · runbook.md
Live:
/opt/incomex/scripts/backup-to-gdrive.sh 35f1095a…
/etc/incomex/backup/directus-backup-recipient.pub 5e6c0864… (0600)
/var/lib/incomex/backup-status/directus-db-backup.status (tạo ở lần chạy đầu)
/var/lib/incomex/backup-staging/ (0700, hiện rỗng)
/opt/incomex/docs/recovery/directus-backup-restore-runbook.md 8928cb58…
Drive:
gdrive-backup:incomex-encrypted-v1/ (tạo ở lần chạy đầu)
gdrive-backup:incomex-recovery-docs/directus-backup-restore-runbook.md
Secret Manager:
projects/812872501910/secrets/DIRECTUS_BACKUP_GPG_PRIVATEKEY_RECOVERY (v1, enabled)
Đã shred/không lưu: khoá HMAC phiên · pattern file token · toàn bộ vật liệu khoá riêng tạm · ciphertext test.
40. PAYLOAD=REDACTED — bằng chứng vệ sinh bí mật
Không echo password/static token/khoá riêng · không đưa secret vào argv (dùng grep -F -f <file tmpfs>, gcloud --data-file=- qua stdin, docker exec -e chỉ tên biến) · không ghi raw dump/directus_users row/session token/flow options/SM payload/rclone credential · không bật xtrace quanh xử lý bí mật · không bật verbose HTTP/rclone.
Báo cáo này chỉ chứa: lớp bí mật · tham chiếu bảng/cột · số đếm · fingerprint HMAC (vô dụng ngoài phiên) · kết quả mã hoá/giải mã · tham chiếu resource/version · metadata artifact · PAYLOAD=REDACTED.
Hai tự-khai báo trung thực:
- Ở bước khảo sát tôi đã
curlvào endpoint push production của Uptime Kuma (pg-backup-gdrive?status=up&msg=probe) để xác minh có reader cảnh báo thật hay không. Nó đã ghi một heartbeat "up" lúc ~04:10 UTC dù không có backup nào chạy. Không ảnh hưởng backup, dữ liệu hay credential; nhưng nó đặt lại đồng hồ heartbeat của monitor đó một lần. Lẽ ra tôi nên kiểm gián tiếp. Không lặp lại. - Backup ban đêm không tự giải mã để kiểm chứng — cố ý. Nếu bắt nó giải mã mỗi đêm thì VPS phải lấy khoá riêng hằng đêm, phá vỡ đúng tính chất "encrypt-only" mà thiết kế này dựa vào. Vì vậy mỗi đêm chỉ kiểm cấu trúc OpenPGP + đúng recipient keyid + ngưỡng kích thước; chứng minh giải mã đầy đủ do mission này thực hiện (§24, §25) và runbook hướng dẫn Owner làm lại bất cứ lúc nào. Đây là sai lệch có chủ đích so với H0.7, nêu rõ để Owner biết.
Kết thúc bắt buộc
DIRECTUS_PASSWORD_CHANGE = NO
DIRECTUS_STATIC_TOKEN_CHANGE = NO
DIRECTUS_STATIC_TOKEN_REVOKE = NO
DIRECTUS_SESSION_REVOCATION = NO
DIRECTUS_USER_CHANGE = NO
FLOW_OPERATION_CHANGE = NO
SECRET_MANAGER_DIRECTUS_CREDENTIAL_MUTATION = NO (chỉ TẠO MỚI 1 secret khoá backup, không đụng credential Directus)
DIRECT_DB_WRITE = NO (chỉ SELECT read-only)
CONTAINER_MUTATION = NO
SERVICE_RESTART = NO
MANUAL_PRODUCTION_DIRECTUS_BACKUP_RUN = NO
PRODUCTION_RETENTION_CHANGE = NO
HISTORICAL_BACKUP_DELETE = NO
GITHUB_PUSH = NO
GITHUB_WORKFLOW_MUTATION = NO
GIT_HISTORY_REWRITE = NO
DOCKER_CLEANUP = NO
C2B_ROTATION_EXECUTED = NO
DỪNG sau báo cáo. Không tự soạn, không tự chạy C2B1 / C2B2 hay mission nào khác.