DataOps nâng cao 15 — Bảo mật & tuân thủ trong pipeline

13 thg 7, 2026 2 lượt xem
#security
#data-engineering
#pii
#dataops
#access-control

Vì sao bảo mật phải là "sinh ra cùng" pipeline, không phải "vá sau"

Trong ngân hàng, một pipeline dữ liệu không chỉ là mã ETL — nó là một luồng dữ liệu khách hàng có ràng buộc pháp lý. Mỗi lần dữ liệu di chuyển giữa các lớp (nguồn → staging → mart → báo cáo) là một lần rủi ro rò rỉ, lộ PII (Personally Identifiable Information — thông tin định danh cá nhân), hoặc cấp quyền sai. Cách làm sai kinh điển: xây pipeline chạy được trước, rồi cuối dự án mới "gắn bảo mật vào". Kết quả là credential đã nằm trong Git history, PII đã trôi xuống môi trường dev, và không ai biết ai đã truy cập cái gì.

Security-by-design nghĩa là bảo mật là một thuộc tính của kiến trúc pipeline ngay từ commit đầu tiên, không phải một task cuối sprint. Bài này đi qua bảy lớp phòng thủ nhúng thẳng vào DataOps: quản lý secrets, least-privilege, xử lý PII trong transform, kiểm soát truy cập ở lớp phục vụ, audit & lineage, tuân thủ NĐ13/NHNN, và shift-left security. Bài nối tiếp trực tiếp DataOps nâng cao 11 (phân tách môi trường) và DataOps nâng cao 4 (CI/CD).

Lớp 1 — Secrets: đừng bao giờ để credential lọt vào code

Nguyên tắc tuyệt đối: không hardcode username/password, connection string, API key, khoá mã hoá vào code, repo, file cấu hình, hay Docker image. Lý do trong ngân hàng cực kỳ nghiêm trọng:

  • Credential commit vào Git thì tồn tại vĩnh viễn trong history — xoá dòng đó ở commit sau không đủ, phải rewrite history và xoay (rotate) khoá ngay.
  • Image chứa secret sẽ rò rỉ tới bất kỳ ai docker pull được, kể cả registry nội bộ bị lộ.
  • File .env commit nhầm là nguyên nhân rò rỉ phổ biến nhất trong thực tế.

Cách đúng là tách secret ra khỏi mã, nạp lúc chạy qua secret manager (Vault, cloud KMS/Secrets Manager) hoặc biến môi trường do orchestrator inject:

# Airflow: KHÔNG hardcode. Lấy connection từ Connections/secret backend.
from airflow.hooks.base import BaseHook

conn = BaseHook.get_connection("ncb_dwh")   # user/pass nằm ở secret backend
engine_url = f"postgresql://{conn.login}:{conn.password}@{conn.host}/{conn.schema}"
# minh hoạ — không đánh dấu chạy được (Python, không phải SQL sandbox)

Quan trọng không kém: secret scanning trong CI. Mỗi commit/PR phải qua bước quét (gitleaks, detect-secrets, trufflehog) để chặn merge nếu phát hiện chuỗi giống key. Đây là chốt chặn cuối trước khi secret vào repo — xem chi tiết Sec 5 — Secrets & supply chain.

Nơi lưu secretĐánh giáGhi chú
Hardcode trong .py/.sqlCấm tuyệt đốiRò rỉ qua Git, image
config.yml commit vào repoCấmKể cả repo private
Biến môi trường (CI/orchestrator inject)Chấp nhậnKhông log ra stdout
Secret manager / KMSKhuyến nghịCó audit, rotation, RBAC

Lớp 2 — Least-privilege cho tài khoản kết nối

Tài khoản mà dbt, Airflow, hay job ETL dùng để kết nối warehouse chỉ được cấp đúng quyền cần thiết, không hơn. Sai lầm phổ biến: một service account "quyền admin cho tiện". Nếu credential đó rò rỉ, kẻ tấn công có toàn quyền warehouse.

Chuẩn least-privilege trong ngân hàng:

  • Tài khoản transform (dbt) chỉ cần SELECT trên nguồn + CREATE/DROP trên schema build riêng của nó — không cần đọc PII ở schema báo cáo cuối.
  • Tài khoản ingest (Airflow load) chỉ INSERT vào staging, không SELECT mart.
  • Tài khoản BI/serving chỉ SELECT trên các view/mart đã mask, không chạm bảng thô.
  • Tách account theo môi trường: credential prod không bao giờ có mặt ở non-prod (DataOps nâng cao 11).

Trong dbt, việc cấp quyền được viết như code qua grants hoặc on-run-end hooks, để quyền có version control và review được:

# dbt model config — cấp quyền theo vai trò, không đánh dấu chạy được
models:
  ncb_marts:
    +grants:
      select: ['role_bi_reader', 'role_risk_analyst']

Lớp 3 — PII trong transform: mask, mã hoá, tối thiểu hoá

Đây là lớp trọng yếu nhất với ngân hàng. Dữ liệu nhạy cảm (số CMND/CCCD, số tài khoản, số thẻ, số điện thoại, địa chỉ) phải được phát hiện, phân loại và xử lý ngay trong pipeline, không để trôi nguyên bản xuống các đích không cần.

Bốn kỹ thuật cần phân biệt rõ:

Kỹ thuậtĐịnh nghĩaĐảo ngược được?Dùng khi
MaskingChe một phần/toàn bộ giá trị hiển thịKhôngLớp phục vụ, che theo vai trò
EncryptionMã hoá bằng khoá, giải bằng khoáCó (nếu có khoá)Lưu trữ tại rest, cột nhạy cảm
TokenizationThay bằng token vô nghĩa, tra bảng ánh xạ riêngCó (qua vault)Số thẻ, PCI-DSS
PseudonymizationThay định danh bằng bí danh ổn địnhCó (có bảng ánh xạ)Phân tích mà không lộ danh tính

Chi tiết về mã hoá & masking xem Gov 5 — Mã hoá & masking.

Tối thiểu hoá dữ liệu (data minimization) là nguyên tắc rẻ và mạnh nhất: nếu mart phân tích hành vi giao dịch không cần số CCCD, thì đừng SELECT cột đó ngay từ model staging. Cột nhạy cảm không tồn tại trong pipeline thì không thể rò rỉ. Trong dbt, hãy SELECT tường minh danh sách cột thay vì SELECT *, để việc kéo một cột nhạy cảm là hành động có chủ đích và review được.

Với môi trường non-prod, pseudonymization ngay trong pipeline cho phép dev test với dữ liệu "giống thật" mà không chạm danh tính thật — nối với kỹ thuật synthetic/masking ở DataOps nâng cao 11.

Lớp 4 — Kiểm soát truy cập ở lớp phục vụ: RBAC + RLS + CLS

Trên data warehouse và các mart phục vụ, ba cơ chế bổ trợ nhau:

  • RBAC (Role-Based Access Control): gán quyền theo vai trò, không theo từng người. Nhân viên vào role, role có quyền — quản lý được ở quy mô hàng nghìn user.
  • Column-Level Security (CLS): che/ẩn cột theo vai trò. Nhân viên chi nhánh thấy account_no bị che, cán bộ tuân thủ thấy đầy đủ.
  • Row-Level Security (RLS): lọc dòng theo thuộc tính người dùng. Nhân viên chi nhánh Hà Nội chỉ thấy khách của Hà Nội.

Chi tiết mô hình quyền xem Gov 6 — Kiểm soát truy cập. Warehouse hiện đại có RLS/masking policy khai báo (row access policy, dynamic masking), nhưng ta có thể mô phỏng logic tương đương bằng SQL thuần để hiểu bản chất — và block dưới đây chạy được trên sandbox PostgreSQL read-only.

SQL 1 — Column masking: che account_no cho vai trò không đủ quyền

Đây là bản chất của dynamic data masking: cùng một view, nhưng giá trị hiển thị phụ thuộc vai trò. Ta mô phỏng "vai trò teller (chỉ thấy 3 số cuối)" bằng regexp_replace để che phần đầu số tài khoản, đồng thời ghép tên khách (phải JOIN customersaccounts không có cột tên) và quy mô giao dịch để đội vận hành đối chiếu:

-- ▶ Chạy được
SELECT c.full_name,
       a.currency,
       regexp_replace(a.account_no, '.(?=.{3})', '*', 'g') AS account_masked,
       ROUND(a.balance::numeric, 2)                        AS so_du,
       COUNT(t.id)                                         AS so_gd,
       d.name                                              AS phong_ban_ref
FROM customers c
JOIN accounts a      ON a.customer_id = c.id
LEFT JOIN transactions t ON t.account_id = a.id
JOIN employees e     ON e.id = (c.id % 5) + 1
JOIN departments d   ON d.id = e.department_id
GROUP BY c.full_name, a.currency, a.account_no, a.balance, d.name
ORDER BY so_du DESC
LIMIT 20;

regexp_replace(account_no, '.(?=.{3})', '*', 'g') thay mọi ký tự trừ 3 ký tự cuối bằng * — teller thấy ********123 thay vì số đầy đủ. Trong warehouse thật, cùng logic này nằm trong masking policy áp tự động theo current_role(); SQL trên minh hoạ đúng phép biến đổi để bạn hiểu điều policy làm bên dưới.

SQL 2 — Row filter: mô phỏng RLS theo chi nhánh (city) và loại tiền

Bây giờ mô phỏng row access policy: nhân viên chi nhánh chỉ được thấy khách thuộc thành phố của mình. Ta giả lập ràng buộc "vai trò = chi nhánh Hà Nội, chỉ tài khoản VND" bằng mệnh đề WHERE — chính là điều RLS làm khi bơm predicate theo thuộc tính người dùng vào mọi truy vấn:

-- ▶ Chạy được
SELECT c.city,
       c.full_name,
       a.account_no,
       a.currency,
       ROUND(SUM(t.amount)::numeric, 2) AS tong_gd,
       COUNT(t.id)                      AS so_gd
FROM customers c
JOIN accounts a       ON a.customer_id = c.id
JOIN transactions t   ON t.account_id = a.id
WHERE c.city = 'Hà Nội'          -- RLS: chỉ chi nhánh của user
  AND a.currency = 'VND'          -- thêm ràng buộc theo vai trò
GROUP BY c.city, c.full_name, a.account_no, a.currency
HAVING SUM(t.amount) > 0
ORDER BY tong_gd DESC
LIMIT 20;

Điểm cốt lõi: trong hệ thống thật, người dùng không tự viết mệnh đề WHERE c.city = ... — warehouse tự động thêm predicate theo thuộc tính (chi nhánh) của họ, nên họ không thể bỏ nó để xem dữ liệu chi nhánh khác. SQL trên cho thấy predicate đó trông thế nào.

Lớp 5 — Audit & lineage: giải trình "ai làm gì với dữ liệu"

Ngân hàng phải trả lời được câu hỏi của kiểm toán/NHNN: ai đã truy cập hoặc biến đổi dữ liệu nào, khi nào, để làm gì? Điều này cần hai thứ:

  • Access audit log: mọi truy vấn trên bảng nhạy cảm được ghi lại (user, timestamp, câu lệnh, dòng chạm). Warehouse hiện đại có access history/audit table sẵn.
  • Data lineage: theo dõi dữ liệu chảy từ nguồn nào, qua transform nào, tới mart/báo cáo nào. Khi lộ PII, lineage cho biết ngay cột đó xuất phát và lan tới đâu — xem Gov 2 — Catalog & lineage.

dbt tự sinh lineage (DAG model) và có thể log ngày build; kết hợp với audit của warehouse cho bức tranh đầy đủ. Nguyên tắc: audit là bất biến — không ai (kể cả admin) được sửa log, để nó có giá trị pháp lý.

Lớp 6 — Nhúng tuân thủ NĐ13/NHNN vào pipeline

Tuân thủ không phải tài liệu Word cuối dự án, mà là ràng buộc thực thi trong pipeline. Ba nghĩa vụ chính từ Nghị định 13/2023/NĐ-CP (bảo vệ dữ liệu cá nhân) cần "code hoá":

Nghĩa vụNhúng vào pipeline thế nào
Consent & mục đíchChỉ xử lý dữ liệu cho mục đích đã đồng ý; gắn cột consent_flag/purpose và lọc trong transform
Retention (lưu trữ)Job định kỳ xoá/ẩn dữ liệu quá hạn theo chính sách từng loại dữ liệu
Quyền xoá (right to erasure)Pipeline có luồng xử lý yêu cầu xoá — xoá/ẩn danh dữ liệu khách khi được yêu cầu hợp lệ

Chi tiết nghĩa vụ pháp lý xem Gov 7 — Quyền riêng tư & tuân thủ. Điểm mấu chốt: retention và erasure phải là job có lịch, có audit, không phải thao tác tay — vì phải chứng minh được đã thực hiện đúng hạn.

Lớp 7 — Shift-left & policy-as-code: chặn cấu hình sai trong CI

Shift-left security nghĩa là đẩy kiểm tra bảo mật về sớm nhất trong vòng đời — vào lúc PR, không phải lúc production sự cố. Với dữ liệu, các kiểm tra chạy trong CI (DataOps nâng cao 4):

  • Secret scanning (đã nói ở lớp 1).
  • Policy-as-code: viết chính sách thành quy tắc máy kiểm được, chặn merge nếu vi phạm. Ví dụ chặn: bảng để PUBLIC truy cập, cột PII không có masking policy, view expose cột nhạy cảm không cần, model SELECT * từ bảng chứa PII.
  • Dependency & image scanning: quét lỗ hổng thư viện và image cơ sở (supply chain), vá trước khi deploy.
  • Infra bảo mật: pipeline chạy trong mạng riêng, không expose port ra internet, image tối giản.

Ý tưởng cốt lõi: một cấu hình sai (bảng public, cột PII lộ) bị chặn ngay ở PR rẻ hơn ngàn lần so với phát hiện sau sự cố rò rỉ. Đây cũng là điểm nối với self-service platform (DataOps nâng cao 7): guardrail policy-as-code cho phép nhiều đội tự làm mà không phá vỡ chuẩn bảo mật.

Use case thực tế

Bối cảnh: NCB xây mart phân tích hành vi giao dịch phục vụ 40 chi nhánh và đội risk. Yêu cầu: che PII theo vai trò, quyền theo chi nhánh, audit đầy đủ, tuân thủ NĐ13.

Các bước triển khai:

  1. Secrets: chuyển toàn bộ 12 connection string từ config.yml (đang trong repo) sang Airflow Connections + Vault. Bật gitleaks trong CI — lần quét đầu phát hiện 3 API key còn sót trong Git history, rotate ngay và rewrite history.
  2. Least-privilege: tách 4 service account (ingest chỉ INSERT staging, dbt chỉ build schema analytics_build, BI chỉ SELECT view mask, admin tách riêng có MFA). Trước đó chỉ 1 account admin dùng chung.
  3. PII trong transform: model staging bỏ 6 cột nhạy cảm không dùng (data minimization); số CCCD được tokenize; số tài khoản chỉ giữ ở bảng gốc, mart hiển thị bản mask.
  4. RLS + CLS ở lớp phục vụ: áp row access policy theo chi nhánh (nhân viên Hà Nội chỉ thấy khách Hà Nội — như SQL 2) và masking policy account_no theo role (như SQL 1). Cán bộ tuân thủ thấy đầy đủ, teller thấy bản che.
  5. Audit & retention: bật access history trên 8 bảng nhạy cảm; job hàng đêm xoá dữ liệu quá hạn retention và xử lý hàng đợi yêu cầu erasure.
  6. Policy-as-code: CI chặn merge nếu model expose cột PII không mask hoặc bảng để PUBLIC.

Kết quả sau 1 quý: không còn secret trong repo (CI chặn tự động); một cuộc kiểm toán nội bộ truy được đầy đủ "ai xem dữ liệu chi nhánh nào" từ audit log; sự cố "báo cáo lộ số tài khoản đầy đủ" bị chặn ngay ở PR nhờ policy-as-code thay vì phát hiện sau khi phát hành.

Ghi nhớ

  • Security-by-design, không dán vá sau: bảo mật là thuộc tính kiến trúc từ commit đầu, đặc biệt với dữ liệu khách hàng ngân hàng.
  • Secrets: không hardcode credential/khoá vào code/repo/config/image (rò rỉ vĩnh viễn qua Git history); dùng secret manager + biến môi trường; bật secret scanning trong CI.
  • Least-privilege: tài khoản dbt/Airflow chỉ quyền cần thiết; tách account theo môi trường; cấp quyền như code qua dbt grants/hooks.
  • PII trong transform: phát hiện & phân loại; phân biệt masking / encryption / tokenization / pseudonymization; tối thiểu hoá — không kéo cột nhạy cảm nếu không cần.
  • Lớp phục vụ: RBAC + Row-Level + Column-Level security; user không tự bỏ được predicate RLS; SQL 1/2 minh hoạ đúng phép mask & row-filter.
  • Audit & lineage bất biến để giải trình ai truy cập/biến đổi gì; NĐ13/NHNN nhúng thành job có lịch: consent/mục đích, retention, quyền xoá.
  • Shift-left / policy-as-code chặn cấu hình sai (bảng public, PII không mask) ngay ở PR — rẻ hơn ngàn lần xử lý sau sự cố.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5