Secrets management, audit logging và PII masking
Bài cuối chương: secrets và audit. Mình phát hiện file application-prod.yml có password DB và API key của payment gateway nằm trong git. Giờ làm sao?
1. Bài toán
application-prod.yml (đã commit 8 tháng trước)
spring.datasource.password: S3cr3t!
payment.api-key: sk_live_...
Ai có quyền đọc repo (dev cũ, CI, fork) đều có key production.
2. Vấn đề
- Xóa ở commit mới không đủ: lịch sử git vẫn còn.
- Secret dùng chung mọi môi trường, không ai biết ai đang dùng.
- Không rotate được vì đổi là phải deploy lại mọi service.
- Log request in nguyên header
Authorizationvà số điện thoại khách.
3. Giải pháp
Xử lý sự cố ngay: coi secret đã lộ. Rotate trước (đổi password DB, thu hồi API key), rồi mới dọn lịch sử git (git filter-repo), kiểm tra log truy cập xem có bị dùng chưa.
Lâu dài: secret không bao giờ nằm trong code.
| Thành phần | Vai trò |
|---|---|
| Secret manager (Vault, AWS Secrets Manager, Azure Key Vault) | Lưu secret, phân quyền, audit ai đọc, rotate |
| KMS | Giữ master key, mã hóa key khác (envelope encryption); key không rời KMS |
| Workload identity | App xác thực với vault bằng danh tính của pod/VM, không cần "secret để lấy secret" |
| Dynamic secret | Vault cấp user DB riêng, sống vài giờ, tự thu hồi |
| Secret scanning (gitleaks, GitHub push protection) | Chặn commit chứa key |
4. Code từng bước
Bước 1: config chỉ tham chiếu
spring:
datasource:
url: jdbc:postgresql://db:5432/shop
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
config:
import: optional:vault:// # spring-cloud-starter-vault-config
payment:
api-key: ${PAYMENT_API_KEY}
Bước 2: chặn ở CI
# .pre-commit / CI step
gitleaks detect --source . --redact --exit-code 1
Bước 3: Code Java, audit log có cấu trúc và mask PII:
public record AuditEvent(
Instant at, String actorId, String actorType, // USER / SELLER / ADMIN / SYSTEM
String action, String resource, String resourceId, // ORDER_REFUND, order, ord_1
String outcome, String ip, String traceId,
Map<String, Object> details, String prevHash, String hash) {}
@Service
public class AuditService {
private final AuditRepository repo; // bảng append-only, app chỉ có quyền INSERT
@Transactional
public void record(String actor, String action, String resId, Map<String, Object> details) {
String prev = repo.lastHash();
Map<String, Object> safe = PiiMasker.mask(details);
String hash = sha256(prev + actor + action + resId + canonicalJson(safe));
repo.insert(new AuditEvent(Instant.now(), actor, "ADMIN", action, "order", resId,
"SUCCESS", RequestContext.ip(), RequestContext.traceId(), safe, prev, hash));
}
}
final class PiiMasker {
static String phone(String p) { return p == null ? null : "*****" + p.substring(p.length() - 3); }
static String email(String e) { int i = e.indexOf('@'); return e.charAt(0) + "***" + e.substring(i); }
}
Bước 4: kiểm tra
git log -p | gitleaks detect --pipe # còn secret trong lịch sử?
curl -s .../admin/orders/ord_1/refunds -X POST ... # rồi xem bảng audit có bản ghi, phone đã mask
grep -E "Bearer |sk_live_" app.log # phải rỗng
5. Cách hoạt động
Secret:
Pod (service account) ──auth──> Vault ──> DB creds (TTL 1h)
└── audit: ai đọc gì, lúc nào
Envelope encryption:
data ──mã hóa bằng DEK──> ciphertext
DEK ──mã hóa bằng KEK (trong KMS)──> encrypted DEK (lưu cạnh data)
Rotate KEK: chỉ mã hóa lại DEK, không mã hóa lại toàn bộ data
Audit hash chain (tamper-evident):
e1.hash = H(e1) e2.hash = H(e1.hash + e2) e3.hash = H(e2.hash + e3)
Sửa/xóa e2 => e3 không khớp => phát hiện được
+ đẩy sang storage WORM / SIEM ngoài tầm tay admin app
6. So sánh: application log vs audit log
| Application log | Audit log | |
|---|---|---|
| Mục đích | Debug, vận hành | Trách nhiệm, điều tra, tuân thủ |
| Nội dung | Kỹ thuật, có thể ồn | Ai, làm gì, với cái gì, khi nào, kết quả, từ đâu |
| Sửa/xóa | Rotate, xóa sau vài ngày | Append-only, giữ theo quy định (1-7 năm) |
| Quyền | Dev đọc được | Hạn chế, chính việc đọc cũng được audit |
| Mất bản ghi | Chấp nhận được | Không chấp nhận (ghi cùng transaction hoặc outbox) |
7. Khi nào nên dùng / không nên dùng
- Audit bắt buộc: login, đổi quyền, refund, đổi giá, export dữ liệu khách, xem PII, đổi config.
- Không audit mọi GET: quá nhiều, không ai đọc.
- Không bao giờ log: password, token, API key, số thẻ đầy đủ, OTP.
- Env var đủ cho dự án nhỏ; khi nhiều service, nhiều môi trường, cần rotation thì dùng secret manager.
Rotate API key của payment gateway thì lúc đổi, các instance cũ vẫn dùng key cũ, instance mới dùng key mới. Làm sao không bị lỗi giữa chừng?
Đúng, đổi "một phát" sẽ có khoảng lỗi. Dùng hai key song song:
1. Tạo key B ở payment gateway (key A vẫn sống)
2. Ghi B vào Vault
3. App reload B (rolling restart hoặc refresh định kỳ)
4. Theo dõi: còn request nào dùng A? -> không còn
5. Thu hồi A
Chiều ngược lại cũng vậy: khi mình phát API key cho partner, cho phép mỗi partner có 2 key active, để họ tự rotate. Lưu key partner dạng hash (như password), chỉ hiện một lần lúc tạo, có prefix để nhận dạng (shp_live_...) giúp secret scanning tìm được.
Có 20 service, mỗi dev log một kiểu. Mình không thể review từng dòng log.info. Có cách nào chặn PII lọt vào log một cách hệ thống không?
Có, kết hợp nhiều lớp, đừng trông vào kỷ luật từng người:
- Kiểu dữ liệu: bọc PII trong type riêng có
toString()đã mask, nhưrecord Phone(String value) { public String toString() { return "***" + last3; } }. Log nhầm object vẫn an toàn. - Không log body/header thô: filter log request chỉ in method, path, status, latency, traceId; header
Authorization,Cookiebị bỏ. - Masking ở logback: pattern converter / JSON encoder thay regex email, phone, số thẻ trước khi ghi.
- Ở pipeline log: Fluent Bit/Vector scrub lần nữa, quét cảnh báo nếu phát hiện.
- Tokenization cho dữ liệu cần tra cứu: log
customerIdthay vì email.
Phỏng vấn hỏi quản lý secret và audit logging thì trả lời sao? Tổng kết luôn chương 11 giúp mình.
Q1. Bạn quản lý secrets thế nào?
🇻🇳 Tiếng Việt:
Secret không nằm trong code, git hay image. App đọc từ secret manager như Vault hoặc Key Vault, xác thực bằng workload identity. Ưu tiên secret ngắn hạn, dynamic credential, có rotation tự động và hai key song song khi đổi. CI có secret scanning. Lộ secret thì rotate ngay trước, rồi mới dọn lịch sử.
🇬🇧 English:
Secrets never live in code, git or images. Applications read them from a secret manager like Vault or Key Vault using workload identity. I prefer short-lived or dynamic credentials with automated rotation and two overlapping keys during a switch. CI runs secret scanning. If a secret leaks, rotate it first, then clean the history.
Q2. Audit log khác application log thế nào, làm sao chống sửa?
🇻🇳 Tiếng Việt:
Audit log ghi ai làm gì, với resource nào, khi nào, kết quả và từ đâu, phục vụ điều tra và tuân thủ. Nó append-only, giữ lâu, quyền đọc hạn chế. Để tamper-evident em dùng hash chain, mỗi bản ghi chứa hash của bản trước, và đẩy sang storage WORM hoặc SIEM ngoài quyền admin ứng dụng.
🇬🇧 English:
An audit log records who did what to which resource, when, with what outcome and from where, for investigations and compliance. It is append-only, retained long-term and access-restricted. To make it tamper-evident I chain hashes so each entry includes the previous entry's hash, and ship it to WORM storage or a SIEM outside the application admins' control.
Q3. Bảo vệ PII trong log thế nào?
🇻🇳 Tiếng Việt:
Không log body và header nhạy cảm, bọc PII trong type có toString đã mask, mask bằng regex ở logger và pipeline, log ID thay cho dữ liệu cá nhân. Tuyệt đối không log password, token, số thẻ.
🇬🇧 English:
Do not log raw bodies or sensitive headers, wrap PII in types whose toString is masked, apply regex masking in the logger and the log pipeline, and log identifiers instead of personal data. Never log passwords, tokens or card numbers.
Cách nhớ ngắn gọn
- Secret: không trong git, đọc từ vault, ngắn hạn, rotate bằng 2 key.
- Lộ: rotate trước, dọn sau.
- Audit: who/what/when/where/outcome, append-only, hash chain, WORM.
- PII: mask bằng type + logger + pipeline.
Tổng kết chương 11
| Bài | Chủ đề | Ý chính |
|---|---|---|
| 11.1 | Resource và contract | URL danh từ, action là sub-resource, cursor pagination, OpenAPI contract-first + lint |
| 11.2 | Method, PATCH, status | Safe/idempotent, PUT thay hết, Merge Patch, cây quyết định 400/409/412/422 |
| 11.3 | Lỗi và idempotency | RFC 9457 + code ổn định, Idempotency-Key (khác body → 422), retry có jitter |
| 11.4 | Versioning | Expand-contract, URL major version, Deprecation/Sunset, tolerant reader |
| 11.5 | Browser security | SOP, CORS không phải khiên, CSRF + SameSite, escape trước CSP sau |
| 11.6 | OWASP API Top 10 | BOLA/BFLA/BOPLA, giới hạn tài nguyên, SSRF, inventory |
| 11.7 | Secrets và audit | Vault/KMS, rotation, audit append-only tamper-evident, PII masking |
Tiếp theo: xác thực tập trung với Keycloak.
Tự kiểm tra
Phát hiện API key trong git, bước đầu tiên là gì?
Rotate/thu hồi key. Dọn lịch sử git là bước sau, vì key có thể đã bị copy.
Envelope encryption giúp gì khi rotate key?
Chỉ cần mã hóa lại DEK bằng KEK mới, không phải mã hóa lại toàn bộ dữ liệu.
Hash chain trong audit log phát hiện được gì?
Sửa hoặc xóa một bản ghi làm hash các bản sau không khớp.
Một audit event tối thiểu cần những gì?
Thời điểm, actor, action, resource + ID, kết quả, nguồn (IP), traceId.
API key cấp cho partner nên lưu thế nào?
Lưu hash, chỉ hiện một lần, có prefix nhận dạng, cho phép 2 key active để rotate.