PKI và quản lý key: keystore, KMS, rotation, secret không nằm trong code
Tiếp đi: quản lý key. shop-app giờ có quá nhiều key: AES key cho PII, secret webhook, private key ký webhook, key ký JWT, certificate TLS. Hiện tất cả đang nằm trong application.yml commit lên Git. Nên tổ chức lại thế nào?
Câu hỏi đúng lúc. Thuật toán tốt mấy cũng vô ích nếu key bị lộ. Phần TLS handshake và CA chain đã có ở bài HTTPS/TLS; bài này nói về key nằm ở đâu, ai được dùng, đổi thế nào.
1. Bài toán
# application.yml (đang trên Git)
shop:
crypto:
pii-key: 3q2+7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
webhook:
secret: whsec_live_...
2. Vấn đề
- Ai clone repo (nhân viên cũ, CI log, fork public nhầm) đều có key. Xóa commit cũng không đủ, lịch sử Git còn.
- Không biết ai đã dùng key, khi nào: không có audit.
- Không đổi được key mà không deploy lại, và không có cách giải mã dữ liệu cũ khi đã đổi.
- Cùng một key cho dev, staging, prod.
3. Giải pháp
Recap PKI: certificate = public key + danh tính (domain, tổ chức) + hạn dùng, được CA ký. Client tin Root CA trong truststore, kiểm chữ ký từng cấp: Root → Intermediate → Leaf.
Trong Java:
| Keystore | Truststore | |
|---|---|---|
| Chứa | Private key + certificate chain của mình | Certificate CA mình tin |
| Dùng khi | Server TLS, client mTLS, ký JWT/webhook | Gọi HTTPS ra ngoài, verify cert phía kia |
| Mặc định | Không có | $JAVA_HOME/lib/security/cacerts |
| Định dạng | PKCS12 (.p12, mặc định từ Java 9); JKS là định dạng cũ | |
Phân tầng quản lý secret:
| Thành phần | Vai trò |
|---|---|
| Secret manager (Vault, AWS Secrets Manager, Azure Key Vault secrets) | Lưu secret dạng giá trị: password DB, webhook secret. App đọc lúc khởi động hoặc định kỳ |
| KMS (AWS KMS, GCP KMS, Azure Key Vault keys) | Giữ key, không trả key ra; app gửi dữ liệu vào để encrypt/decrypt/sign. IAM + audit log |
| HSM | Phần cứng chống trích xuất key (FIPS 140-2/3). KMS cloud thường chạy trên HSM; CloudHSM khi cần HSM riêng |
4. Code từng bước
Bước 1: Tạo keystore / truststore
# Keystore PKCS12 từ cert + key PEM (ví dụ cert mTLS của shop-app)
openssl pkcs12 -export -in shop.crt -inkey shop.key -certfile ca-chain.crt \
-name shop -out shop.p12 -passout env:KEYSTORE_PASSWORD
# Truststore chỉ chứa CA nội bộ
keytool -importcert -alias internal-ca -file internal-ca.crt \
-keystore truststore.p12 -storetype PKCS12 -storepass:env TRUSTSTORE_PASSWORD -noprompt
keytool -list -v -keystore shop.p12 -storepass:env KEYSTORE_PASSWORD | grep -E "Alias|Valid"
Bước 2: Config. Spring Boot 3.1+ có SSL bundle, gom keystore/truststore một chỗ, có thể dùng PEM trực tiếp và tự reload khi file đổi:
spring:
ssl:
bundle:
pem:
payment-mtls:
reload-on-update: true
keystore:
certificate: file:/etc/shop/tls/shop.crt
private-key: file:/etc/shop/tls/shop.key
truststore:
certificate: file:/etc/shop/tls/internal-ca.crt
config:
import: "optional:aws-secretsmanager:prod/shop-app" # hoặc vault://
shop:
crypto:
kms-key-arn: ${PII_KMS_KEY_ARN}
Bước 3: Code Java. Dùng bundle cho RestClient gọi payment qua mTLS:
@Bean
RestClient paymentClient(RestClient.Builder builder, SslBundles bundles) {
var settings = ClientHttpRequestFactorySettings.defaults()
.withSslBundle(bundles.getBundle("payment-mtls"));
return builder.baseUrl("https://payment.internal")
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
Envelope encryption với KMS (nối tiếp bài 13.4), AWS SDK v2:
@Component
public class KmsEnvelope {
private final KmsClient kms;
private final String keyArn;
private final AesGcmCipher aes; // hàm encrypt/decrypt nhận SecretKey, như bài 13.4
public KmsEnvelope(KmsClient kms, @Value("${shop.crypto.kms-key-arn}") String keyArn, AesGcmCipher aes) {
this.kms = kms; this.keyArn = keyArn; this.aes = aes;
}
public EncryptedBlob encrypt(byte[] data, String aad) {
GenerateDataKeyResponse dk = kms.generateDataKey(r -> r.keyId(keyArn).keySpec(DataKeySpec.AES_256)
.encryptionContext(Map.of("aad", aad)));
byte[] plainKey = dk.plaintext().asByteArray();
try {
byte[] ct = aes.encrypt(new SecretKeySpec(plainKey, "AES"), data, aad);
return new EncryptedBlob(dk.ciphertextBlob().asByteArray(), ct); // lưu cả hai
} finally {
Arrays.fill(plainKey, (byte) 0); // xóa key khỏi heap sớm
}
}
}
Bước 4: Test
curl -v https://payment.internal/health --cert shop.crt --key shop.key --cacert internal-ca.crt
# Kiểm hạn cert còn bao lâu
openssl s_client -connect api.shop.example:443 -servername api.shop.example </dev/null 2>/dev/null \
| openssl x509 -noout -enddate
# Tìm secret lọt vào Git
gitleaks detect --source .
5. Cách hoạt động: rotation không downtime
Key ký JWT / webhook (asymmetric) - dùng kid:
t0: JWKS = {k1} ký bằng k1
t1: JWKS = {k1, k2} vẫn ký bằng k1 (publish trước, chờ cache JWKS của client hết hạn)
t2: JWKS = {k1, k2} ký bằng k2 (token cũ k1 vẫn verify được)
t3: JWKS = {k2} sau khi token k1 hết hạn, gỡ k1
Key AES dữ liệu (symmetric) - ciphertext mang key id:
decrypt: đọc kid trong blob -> chọn key tương ứng
encrypt: luôn dùng key mới nhất
job nền: re-encrypt dần bản ghi kid cũ, xong thì hủy key cũ
(envelope: chỉ cần re-wrap DEK)
Webhook HMAC secret:
thời gian chuyển tiếp chấp nhận chữ ký của cả secret cũ và mới
6. So sánh: secret để ở đâu
| Cách | Ưu | Nhược |
|---|---|---|
| Hardcode / Git | Không | Lộ vĩnh viễn trong lịch sử, không audit, không rotate |
| Biến môi trường | Đơn giản, 12-factor | Lộ qua /proc, crash dump, docker inspect; rotate cần restart |
| K8s Secret (file mount) | Tách khỏi image | Mặc định chỉ Base64 trong etcd; cần bật encryption at rest + RBAC |
| Secret manager / Vault | Audit, rotate, dynamic secret (DB credentials ngắn hạn) | Thêm phụ thuộc hạ tầng |
| KMS / HSM | Key không bao giờ rời hệ thống, IAM chi tiết | Độ trễ mạng, chi phí theo request; dùng envelope để giảm |
7. Khi nào nên dùng / không nên dùng
- Luôn: không có secret trong code, image, log. Mỗi môi trường một key. Quét Git bằng gitleaks/trufflehog trong CI.
- KMS + envelope: key mã hóa PII, card token. Secret manager: password DB, API key bên thứ ba.
- HSM riêng: khi có yêu cầu PCI DSS/FIPS chặt, CA nội bộ, ký giao dịch tài chính.
- Đừng tự viết hệ thống quản lý key nếu cloud đã có.
Webhook secret production đã nằm trong Git 6 tháng. Mình xóa khỏi file, rewrite history bằng git filter-repo là xong đúng không?
Chưa xong. Rewrite history chỉ dọn dẹp, không thu hồi được bản sao đã tồn tại (máy dev, CI cache, fork, backup). Coi secret đã lộ. Thứ tự đúng:
- Rotate ngay: tạo secret mới ở provider, cấu hình shop-app chấp nhận cả hai trong thời gian ngắn, rồi thu hồi secret cũ.
- Điều tra: xem log webhook trong 6 tháng có request nào hợp lệ chữ ký nhưng không có giao dịch tương ứng phía provider không.
- Dọn: chuyển secret sang secret manager, sau đó mới rewrite history nếu muốn.
- Ngăn tái phát: pre-commit hook + gitleaks trong CI, bật secret scanning của GitHub/GitLab.
Mình thấy kỳ: AES key thì mã hóa bằng master key, master key thì phải có cái gì đó bảo vệ nữa. Cuối cùng vẫn phải có một secret nằm đâu đó, vậy KMS giải quyết được gì?
Bạn nhìn đúng: luôn có "secret số 0" (bootstrap secret). KMS không xóa được nó, mà đổi loại của nó:
- Master key nằm trong HSM của KMS, không export được. Không ai copy được, kể cả admin.
- "Secret số 0" của app không còn là một chuỗi key, mà là danh tính workload: IAM role của EC2/ECS, Kubernetes ServiceAccount với IRSA / Workload Identity, Azure Managed Identity. Credentials tạm thời, tự xoay, gắn với máy/pod.
- Mọi lần dùng key đều qua API, có audit log và có thể thu hồi quyền tức thì.
Trước: app ── đọc ──> AES key trong env (copy được, dùng ở đâu cũng được)
Sau: pod (ServiceAccount) ── IAM ──> KMS.Decrypt(wrappedDEK)
lộ wrappedDEK một mình: vô dụng
lộ pod: chỉ dùng được khi còn quyền, có log, thu hồi được
Phỏng vấn hỏi về quản lý key và certificate thì trả lời sao?
Q1. Keystore và truststore khác nhau thế nào?
🇻🇳 Tiếng Việt:
Keystore chứa private key và certificate chain của chính ứng dụng, dùng khi làm server TLS, client mTLS hoặc ký dữ liệu. Truststore chứa certificate của các CA mà ứng dụng tin, dùng để verify certificate phía bên kia. Trong Spring Boot 3 em cấu hình cả hai qua SSL bundle.
🇬🇧 English:
A keystore holds the application's own private key and certificate chain, used when acting as a TLS server, an mTLS client or for signing. A truststore holds the CA certificates the application trusts, used to verify the other side's certificate. In Spring Boot 3 I configure both through SSL bundles.
Q2. Em quản lý secret và key trong production thế nào?
🇻🇳 Tiếng Việt:
Không bao giờ để secret trong code hay image. Secret dạng giá trị như password DB để trong secret manager, app truy cập bằng danh tính workload như IAM role. Key mã hóa dữ liệu dùng KMS với envelope encryption, master key không rời HSM, mọi thao tác có audit. CI có quét secret, và mỗi môi trường dùng key riêng.
🇬🇧 English:
Secrets never go into code or images. Value secrets such as database passwords live in a secret manager, accessed through a workload identity like an IAM role. Data encryption keys use a KMS with envelope encryption, so the master key never leaves the HSM and every operation is audited. CI scans for leaked secrets, and each environment has its own keys.
Q3. Xoay key ký JWT mà không làm logout mọi người thế nào?
🇻🇳 Tiếng Việt:
Mỗi key có
kid. Em publish public key mới lên JWKS trước, chờ cache phía resource server cập nhật, rồi mới chuyển sang ký bằng key mới. Key cũ giữ trong JWKS đến khi token cuối cùng ký bằng nó hết hạn, sau đó gỡ.
🇬🇧 English:
Every key has a
kid. I publish the new public key in the JWKS first, wait for resource servers to refresh their cache, and only then start signing with the new key. The old key stays in the JWKS until the last token signed with it expires, and then it is removed.
Cách nhớ ngắn gọn
- Keystore = của mình; truststore = mình tin ai.
- Secret manager cho giá trị, KMS cho key, HSM khi key không được phép rời phần cứng.
- Rotation: có kid / key id, chạy song song cũ và mới, gỡ cũ sau.
- Lộ secret: rotate trước, dọn sau.
Tổng kết chương 13
| Bài | Chủ đề | Ý chính | shop-app dùng ở đâu |
|---|---|---|---|
| 13.1 | Encoding, hash, encryption | Base64 không bảo mật; SecureRandom cho mọi giá trị bí mật | Token reset password |
| 13.2 | Hash & MAC | SHA-256, HMAC, constant-time compare, chống replay | Verify webhook từ payment provider |
| 13.3 | Hash password | Chậm + salt + memory-hard (Argon2id), pepper, migrate hash cũ | Bảng users |
| 13.4 | Symmetric & AEAD | AES-GCM, không ECB, không lặp nonce, envelope encryption | Card token, PII |
| 13.5 | RSA & chữ ký số | Public/private key, hybrid, RS256/ES256/Ed25519, key size | Ký webhook gửi đối tác, JWT |
| 13.6 | PKI & quản lý key | Keystore/truststore, KMS/HSM, rotation, không secret trong code | mTLS, KMS, xoay key |
Tự kiểm tra
Lỗi PKIX khi gọi HTTPS nội bộ thường do thiếu gì?
Truststore không chứa CA đã ký cert server (hoặc server không gửi intermediate). Thêm CA vào truststore/SSL bundle, không tắt verify.
KMS khác secret manager chỗ nào?
Secret manager trả giá trị secret cho app. KMS giữ key bên trong, app gửi dữ liệu vào để encrypt/decrypt/sign.
Vì sao biến môi trường chưa phải là nơi lý tưởng nhất cho secret?
Lộ qua process listing, crash dump, docker inspect, log debug; không audit, rotate cần restart.
Các bước xoay key ký JWT không downtime?
Publish public key mới vào JWKS, chờ cache, chuyển sang ký bằng key mới, gỡ key cũ khi token cũ hết hạn.
Secret đã lộ trên Git, việc đầu tiên là gì?
Rotate (thu hồi) secret, rồi điều tra và dọn lịch sử.