KB-642D

Claude — C2B-PRE: Mã hoá toàn bộ Directus database backup + gỡ admin token khỏi shell auto-export (2026-07-19)

48 min read Revision 1
current-statec2b-predirectusbackup-encryptiongpg-public-keyopenpgpstreaming-encryptionfail-closedfail-visiblesecret-containmentstatic-tokenpg-dumpgdriveappend-only-prefixsecret-manageroff-vps-recoveryrestore-runbookzero-touchgit-safetyrollback-ready2026-07-19PASS_PENDING_FIRST_NATURAL_PRODUCTION_RUN_VERIFICATION

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.comroles/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_userscũ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_TOKENauto-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.shdocker/.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:

  1. GNUPGHOME tạo trong tmpfs /dev/shm, mode 0700, umask 077.
  2. 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).
  3. Public key → /etc/incomex/backup/.
  4. Private key → file tmpfs → gcloud secrets create --data-file=- qua STDIN (không bao giờ vào argv).
  5. trap EXITshred -u toà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.comuser principal, không phải service account — roles/owner trên github-chatgpt-ggcloud; describe OK, secrets list OK (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/owner bao trùm secretmanager.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 PASSOFFVPS_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.sh35f1095a8c70b91bc08a5d3a8f4a7284e58c7f222fe38ad009568056d3d922b0 (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

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: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 → 16PRODUCTION_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:

  1. 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 (timestamp YYYYMMDD_HHMMSS sắp xếp đúng).
  2. .gpg không được nhận dạng.
  3. Nếu đổi glob để nhận .age/.gpg ⇒ artifact plaintext lịch sử sẽ vào chung tập ứng viên xoá.
  4. 🔴 Có nguy cơ xoá nhầm nếu trộn định dạng: đếm hiện tại đúng bằng 14 == MAX_REMOTE_BACKUPSmọi artifact mới thêm vào Drive root sẽ xoá ngay 1 bản lịch sử.
  5. Đ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 14khô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:

  1. Recipient độc lập thứ hai (§20) — cần một kênh bàn giao an toàn.
  2. Retention cho prefix mã hoá (§26) — hiện append-only, +7,6 GiB/tháng.
  3. 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:

  1. Ở bước khảo sát tôi đã curl và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.
  2. 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.

Back to Knowledge Hub knowledge/current-state/reports/claude-c2b-pre-encrypt-directus-database-backup-2026-07-18.md