Data Masking với PostgreSQL Anonymizer: Làm sạch dữ liệu nhạy cảm cho môi trường Staging

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Bài toán nan giải khi đồng bộ dữ liệu Production

Sáu tháng trước, đội ngũ của chúng tôi đã không vượt qua được đợt đánh giá tuân thủ (compliance audit) do quy trình staging pipeline. Giống như nhiều hệ thống kỹ thuật khác, chúng tôi chạy một cron job hằng đêm để dump cơ sở dữ liệu production dung lượng 45 GB và restore thẳng vào staging. Các lập trình viên cần khối lượng dữ liệu thực tế, query planner profile chuẩn xác và những trường hợp edge case phức tạp mà các công cụ sinh mock data tổng hợp không bao giờ bao quát hết được.

Vấn đề nằm ở những thứ đi kèm cùng khối dữ liệu đó. Thông tin thanh toán thô của khách hàng, địa chỉ nhà riêng, mật khẩu đã hash và email cá nhân đều được đưa vào một môi trường mà hai mươi kỹ sư có quyền truy cập. Tệ hơn, các công cụ giám sát lỗi của bên thứ ba như Sentry đang âm thầm thu thập danh tính người dùng thật từ các unhandled exception trên staging. Theo các tiêu chuẩn GDPR và SOC 2, quy trình này bắt buộc phải dừng lại ngay lập tức.

Chúng tôi từng cân nhắc viết một script Python tạm thời để làm sạch (scrub) các bản ghi sau khi restore. Nhưng ý tưởng đó nhanh chóng bị loại bỏ. Việc viết các bộ làm sạch bằng Python đồng nghĩa với việc phải duy trì các schema dictionary rất dễ gãy (brittle) và liên tục lỗi mỗi khi có ai đó thêm migration mới. PostgreSQL hỗ trợ xử lý việc chuyển đổi dữ liệu riêng tư trực tiếp ngay trong engine thông qua hệ sinh thái extension, biến nó thành nơi lý tưởng nhất để áp dụng các quy tắc masking.

Các khái niệm cốt lõi của PostgreSQL Anonymizer

PostgreSQL Anonymizer (anon) là một extension mã nguồn mở được phát triển bởi Dalibo. Thay vì phải viết lại các file SQL dump bằng regex hoặc chạy các script xóa dữ liệu sau khi restore, nó hoạt động trực tiếp bên trong PostgreSQL bằng cách sử dụng security label và các chính sách bảo mật mang tính khai báo (declarative).

Ba phương pháp Masking dữ liệu

Extension này hỗ trợ ba chiến lược vận hành chính:

  • Anonymous Dumps: Stream dữ liệu đã được làm sạch thông qua pg_dump_anon. Database nguồn vẫn được giữ nguyên vẹn hoàn toàn, trong khi dữ liệu đầu ra được stream dưới dạng đã mask trực tiếp vào file dump hoặc Unix pipe.
  • In-Place Permanent Masking: Chỉnh sửa phá hủy (destructively modify) trực tiếp trên một instance chuyên dụng đã restore trước khi cấp quyền truy cập cho lập trình viên.
  • Dynamic Masking: Mask kết quả truy vấn tại runtime dựa trên role database của người gọi. Phương pháp này phù hợp nhất cho các read replica phục vụ phân tích (analytics) hoặc bảng điều khiển hỗ trợ khách hàng.

Đối với pipeline đồng bộ của mình, chúng tôi đã chọn mô hình anonymous dump. Nó đảm bảo dữ liệu PII chưa mask không bao giờ vượt ra khỏi ranh giới bộ nhớ của máy chủ production.

Các hàm Masking tích hợp sẵn

Extension này đi kèm với các bộ sinh dữ liệu thực tế được hỗ trợ bởi engine Faker nội bộ của nó:

  • anon.fake_first_name() và anon.fake_last_name(): Tạo tên người trông như thật.
  • anon.fake_email(): Thay thế email thật của khách hàng bằng các địa chỉ tổng hợp thực tế như [email protected].
  • anon.pseudo_iban() và anon.partial(): Cắt bớt hoặc chèn ký tự đệm vào định danh tài chính trong khi vẫn giữ nguyên độ dài chuỗi.
  • anon.noise(): Thêm một tỷ lệ phương sai ngẫu nhiên trong khoảng giới hạn vào các cột số mà không làm méo mó các đường cong phân phối tổng thể.

Các bước triển khai thực tế

Dưới đây là pipeline chính xác đang chạy trong job đồng bộ GitHub Actions của chúng tôi, đã được thử nghiệm thực tế qua nhiều tháng export với dung lượng hàng chục gigabyte.

Bước 1: Cài đặt và khởi tạo Extension

Cài đặt package tương ứng với phiên bản PostgreSQL chính của bạn (PostgreSQL 14, 15, hoặc 16). Trên các hệ thống Debian và Ubuntu:

sudo apt-get install postgresql-16-anon

Thêm anon vào shared libraries của server trong postgresql.conf, sau đó restart lại cluster:

shared_preload_libraries = 'anon'

Kết nối tới database nguồn của bạn với quyền superuser và khởi tạo extension:

CREATE EXTENSION IF NOT EXISTS anon CASCADE;
SELECT anon.init();

Bước 2: Khai báo quy tắc Masking với Security Labels

Thay vì phải quản lý một file mapping YAML bên ngoài rất dễ bị lỗi thời, PostgreSQL Anonymizer sử dụng các câu lệnh SECURITY LABEL có sẵn của PostgreSQL. Xét ví dụ về một bảng khách hàng trong hệ thống e-commerce tiêu chuẩn:

CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100) UNIQUE,
    phone_number VARCHAR(20),
    balance_cents INT
);

Gắn trực tiếp các khai báo quyền riêng tư vào các cột bên trong các file migration của bạn:

-- Mask tên bằng các tên giả thực tế
SECURITY LABEL FOR anon ON COLUMN customers.first_name
    IS 'MASKED WITH FUNCTION anon.fake_first_name()';

SECURITY LABEL FOR anon ON COLUMN customers.last_name
    IS 'MASKED WITH FUNCTION anon.fake_last_name()';

-- Thay thế email nhưng vẫn đảm bảo tính duy nhất (unique)
SECURITY LABEL FOR anon ON COLUMN customers.email
    IS 'MASKED WITH FUNCTION anon.pseudo_email(id::TEXT)';

-- Che bớt một phần số điện thoại (chỉ hiển thị vài số đầu và số cuối)
SECURITY LABEL FOR anon ON COLUMN customers.phone_number
    IS 'MASKED WITH FUNCTION anon.partial(phone_number, 2, ''******'', 2)';

-- Thêm biên độ dao động ngẫu nhiên +/- 15% cho số dư tài khoản
SECURITY LABEL FOR anon ON COLUMN customers.balance_cents
    IS 'MASKED WITH FUNCTION anon.noise(balance_cents, 0.15)';

Các quy tắc này được lưu trữ trong system catalog mặc định của PostgreSQL (pg_seclabel). Chúng luôn đi liền với schema qua các lần migration và được commit gọn gàng vào hệ thống quản lý phiên bản (version control).

Bước 3: Export tập dữ liệu đã được Anonymize

Export dữ liệu của bạn bằng pg_dump_anon. Lệnh này bọc lại pg_dump tiêu chuẩn và áp dụng các phép biến đổi masking trực tiếp (on-the-fly) khi các dòng dữ liệu được stream qua mạng:

pg_dump_anon \
    -h prod-replica.internal \
    -U backup_agent \
    -d app_production \
    -F c \
    -b \
    -v \
    -f /tmp/sanitized_export.dump

Restore file dump đã được làm sạch trực tiếp vào database staging đích:

pg_restore \
    -h staging-db.internal \
    -U postgres \
    -d app_staging \
    --clean \
    /tmp/sanitized_export.dump

Khi các lập trình viên kết nối tới app_staging, tính toàn vẹn quan hệ (relational integrity) vẫn được giữ nguyên. Các foreign key liên kết chính xác, nhưng mọi email, họ tên và số điện thoại của khách hàng đều đã được thay thế hoàn toàn bằng dữ liệu giả.

Bước 4: Bảo toàn tính toàn vẹn của Foreign Key

Các hàm sinh dữ liệu ngẫu nhiên thuần túy sẽ phá vỡ mối quan hệ foreign key. Nếu orders.customer_email tham chiếu tới customers.email, việc gán các chuỗi ngẫu nhiên độc lập cho cả hai bảng sẽ làm hỏng phép join.

Hãy sử dụng các hàm pseudonymous tất định (deterministic) với một giá trị salt nhất quán để bảo vệ các liên kết quan hệ:

-- Thiết lập salt chung cho phiên export
SELECT anon.init('a8f9c2d1e0b4a7f6c3d2e1b0');

SECURITY LABEL FOR anon ON COLUMN users.login_handle
    IS 'MASKED WITH FUNCTION anon.pseudo_first_name(login_handle)';

SECURITY LABEL FOR anon ON COLUMN activity_logs.user_handle
    IS 'MASKED WITH FUNCTION anon.pseudo_first_name(user_handle)';

Với một salt cố định, cùng một chuỗi đầu vào sẽ luôn cho ra một chuỗi thay thế giống hệt nhau. Phép join vẫn hoạt động chính xác, trong khi danh tính thực tế bên dưới vẫn được che giấu an toàn.

Kinh nghiệm thực tế sau sáu tháng triển khai Production

Có hai trở ngại vận hành đã phát sinh trong quá trình chúng tôi triển khai.

Thứ nhất là tải CPU (CPU overhead). Vì pg_dump_anon áp dụng hàm băm mật mã và định dạng chuỗi theo từng dòng, nên nó hoàn toàn bị giới hạn bởi CPU (CPU-bound). Trên tập dữ liệu 45 GB của chúng tôi, lệnh pg_dump tiêu chuẩn mất 7 phút. Nhưng lệnh dump có mask mất tới 19 phút—tăng gấp 2,7 lần. Việc trỏ backup agent vào một read replica độc lập đã giúp ngăn ngừa tình trạng quá tải CPU trên write node chính.

Thách thức thứ hai là sự lệch pha schema (schema drift). Khi một lập trình viên thêm cột mới shipping_notes chứa địa chỉ tự do, database theo mặc định sẽ export mà không mask cột này. Chúng tôi đã khắc phục lỗ hổng đó bằng cách thêm một bước kiểm tra tùy biến vào pipeline CI của mình:

-- Báo lỗi nếu tồn tại cột chưa được phân loại trong các bảng nhạy cảm
SELECT c.table_name, c.column_name
FROM information_schema.columns c
LEFT JOIN pg_seclabel s 
    ON s.objname = c.table_name || '.' || c.column_name
WHERE c.table_schema = 'public'
  AND c.table_name IN ('customers', 'orders', 'users')
  AND s.label IS NULL;

Nếu một migration thêm một cột chưa được gắn nhãn trong bảng được bảo vệ, build sẽ fail ngay trước khi code được đưa lên production.

Việc xử lý data masking ngay bên trong PostgreSQL đã loại bỏ hàng chục script bash hậu restore mỏng manh, dễ lỗi. Môi trường staging nhận được các tập dữ liệu mới, chuẩn quy mô production hằng đêm, mà dữ liệu khách hàng không bao giờ phải rời khỏi ranh giới an toàn của production.

Share: