KMS (envelope encryption, key policy), Secrets Manager vs Parameter Store, rotation, ACM
Tiếp đi: KMS và Secrets Manager. Ở bài ECS, task definition lấy password DB từ secret shop/db, nhưng thật ra mình đã tạo secret đó bằng tay và chưa hiểu bên dưới nó mã hóa bằng gì. Giờ security team yêu cầu: (1) mọi dữ liệu nhạy cảm của shop-app phải mã hóa bằng key do công ty kiểm soát, (2) số thẻ lưu trong RDS phải mã hóa thêm ở tầng ứng dụng, (3) password DB phải đổi định kỳ mà không restart app. Bắt đầu từ đâu?
Ba yêu cầu đó tương ứng với ba dịch vụ bạn sẽ dùng cùng nhau: KMS (key do bạn kiểm soát, mọi dịch vụ khác mã hóa bằng nó), envelope encryption bằng SDK (mã hóa tầng ứng dụng), và Secrets Manager rotation (đổi password tự động). Lý thuyết mật mã (AES-GCM, nonce, KDF, vì sao không tự viết crypto) đã có ở chương 13 và bài 13.6 về key management; ở đây ta xem AWS hiện thực chúng thế nào và shop-app dùng ra sao.
1. Bài toán: dữ liệu nhạy cảm của shop-app đang nằm ở đâu
Thứ nhạy cảm Đang ở Mã hóa hiện tại
────────────────────── ──────────────────────────── ───────────────────────────────
password RDS shop-db secret shop/db (tạo tay) key mặc định aws/secretsmanager
ảnh sản phẩm, hóa đơn S3 shop-app-assets SSE-S3 (key của AWS, tự động)
volume EC2, snapshot EBS chưa bật
log ứng dụng CloudWatch /shop/app mặc định (AWS-owned)
số thẻ (4 số cuối + token PSP), CMND RDS cột text chỉ có storage encryption RDS
feature flag, URL nội bộ hardcode trong application.yml
TLS shop.example.com chưa có cert
JWT signing key biến env trong task def ai xem được task def là đọc được
2. Vấn đề
- Không kiểm soát key: key mặc định (AWS owned/AWS managed) không có key policy của bạn, không tắt được, không audit được ai dùng, không chia sẻ cross-account được. Audit yêu cầu "customer managed key" là chuyện thật.
- Mã hóa storage không bảo vệ khỏi app: RDS encryption at rest chống mất đĩa, nhưng ai
SELECTđược là đọc số thẻ. Cần mã hóa trường trước khi ghi. - Password tĩnh: đổi password = sửa secret + restart mọi task; không ai dám làm, nên 2 năm không đổi.
- Config lẫn secret: feature flag để trong file, muốn bật tắt phải deploy; secret để trong env, lộ trong
describe-task-definition. - Gọi KMS cho mỗi bản ghi thì chậm (API ~ vài ms) và tốn (tính theo request); KMS cũng chỉ mã hóa trực tiếp tối đa 4 KB.
3. Giải pháp: KMS + envelope encryption + Secrets Manager + Parameter Store (+ ACM cho TLS)
AWS KMS (Key Management Service): dịch vụ giữ key trong HSM (FIPS 140-2 Level 3), key không bao giờ rời KMS ở dạng rõ. Bạn gửi dữ liệu nhỏ (≤ 4 KB) để KMS encrypt/decrypt, hoặc xin KMS sinh data key. Mọi lời gọi ghi vào CloudTrail.
Thuật ngữ đề thi:
| Thuật ngữ | Nghĩa | Ghi nhớ |
|---|---|---|
| AWS owned key | Key của AWS dùng chung nhiều account, bạn không thấy, không quản lý | Mặc định của DynamoDB, S3 SSE-S3 |
AWS managed key (aws/s3, aws/rds, aws/secretsmanager…) | Tạo trong account bạn, theo dịch vụ, chỉ dịch vụ đó dùng; không sửa key policy, không xóa, tự rotate mỗi năm; miễn phí lưu | Thấy trong console nhưng không kiểm soát |
| Customer managed key (CMK) | Bạn tạo, bạn viết key policy, bật/tắt rotation, disable, lên lịch xóa, alias, tag, dùng cross-account; khoảng 1 USD/tháng/key + request | alias/shop-key |
| Symmetric vs asymmetric key | Symmetric AES-256: encrypt/decrypt, phổ biến nhất. Asymmetric RSA/ECC: ký số (sign/verify) hoặc mã hóa bằng public key ngoài AWS. HMAC key để tạo/verify MAC | Envelope → symmetric; ký JWT → asymmetric |
| Key material origin | AWS_KMS (KMS sinh), EXTERNAL (BYOK: bạn import key material), AWS_CLOUDHSM (custom key store), EXTERNAL_KEY_STORE (XKS, key nằm ngoài AWS) | "must control key material / hold keys outside AWS" |
| Key policy | Resource policy gắn vào key; bắt buộc phải có. Không có key policy cho phép thì IAM policy vô dụng. Policy mặc định cho phép account root ủy quyền qua IAM ("kms:*" cho arn:aws:iam::ACCOUNT:root) | Key policy + IAM policy cả hai đều phải cho phép (trừ khi key policy cho principal trực tiếp) |
| Grant | Quyền tạm, có thể thu hồi, do dịch vụ AWS tạo thay bạn (ví dụ EBS tạo grant để decrypt volume) | Thấy trong CloudTrail CreateGrant |
| kms:ViaService condition | Chỉ cho dùng key qua một dịch vụ (ví dụ chỉ qua S3 ở ap-southeast-1) | Chặn dùng key trực tiếp |
| Envelope encryption | KMS sinh data key (plaintext + bản mã hóa bằng CMK). App mã hóa dữ liệu bằng data key plaintext (AES-GCM local), lưu encrypted data key cạnh ciphertext, xóa plaintext key khỏi RAM. Giải mã: gửi encrypted data key lên KMS Decrypt → data key → giải mã local | 1 lần gọi KMS cho N MB dữ liệu; key không rời KMS |
| Encryption context | Cặp key=value không bí mật (ví dụ orderId=123) gắn vào mã hóa; giải mã phải đưa đúng context; ghi trong CloudTrail (AAD) | Chống dùng ciphertext sai chỗ |
| Automatic key rotation | KMS tạo key material mới mỗi 1 năm (có thể 90–2560 ngày), giữ mọi bản cũ để giải mã dữ liệu cũ; ARN/alias không đổi, không cần re-encrypt | Khác với rotate secret |
| Multi-Region key | Key có cùng key ID/material ở nhiều region (primary + replica); mã hóa region A, giải mã region B; cho DR/Global Tables | Khác với key thường (region-bound) |
| Deletion | Phải lên lịch 7–30 ngày (pending deletion), hủy được; xóa key = mất vĩnh viễn mọi dữ liệu mã hóa bằng nó | Disable trước, chờ, rồi xóa |
S3 encryption (hay thi): SSE-S3 (AES-256, key của S3, mặc định từ 2023, miễn phí) · SSE-KMS (CMK hoặc aws/s3; audit CloudTrail, kiểm soát quyền qua key policy; tốn request KMS → bật S3 Bucket Keys để giảm) · DSSE-KMS (hai lớp) · SSE-C (bạn gửi key trong header mỗi request, S3 không lưu key) · Client-side (mã hóa trước khi upload, S3 Encryption Client). Bucket policy chặn upload không mã hóa bằng condition s3:x-amz-server-side-encryption.
Secrets Manager: lưu secret (password, API key, JSON), mã hóa bằng KMS, rotation tự động bằng Lambda (có template sẵn cho RDS/Redshift/DocumentDB, kể cả "managed rotation" không cần Lambda cho RDS), versioning (AWSCURRENT, AWSPENDING, AWSPREVIOUS), cross-account qua resource policy, replicate secret sang region khác. Khoảng 0,4 USD/secret/tháng + theo 10 000 API call.
SSM Parameter Store: cây tham số /shop/app/…, kiểu String, StringList, SecureString (mã hóa KMS). Standard tier miễn phí (10 000 tham số, 4 KB), Advanced tier có phí (8 KB, policy hết hạn, nhiều hơn). Không có rotation tự động; versioning có; hợp cho config và feature flag.
ACM (Certificate Manager): cấp và tự gia hạn cert TLS public miễn phí cho ALB, CloudFront, API Gateway (không cho EC2 trực tiếp, trừ Nitro Enclaves); validate bằng DNS (Route 53) hoặc email; ACM Private CA cho cert nội bộ (tốn tiền). Dùng ở bài 22.10.
CloudHSM: HSM riêng của bạn (single-tenant), bạn quản lý user và key, AWS không truy cập được; cho yêu cầu compliance FIPS 140-2 Level 3 với key do bạn hoàn toàn kiểm soát, Oracle TDE, SSL offload; tốn tiền theo giờ. Đề: "dedicated hardware", "single-tenant HSM" → CloudHSM; còn "managed, integrated with AWS services" → KMS.
4. Code và cấu hình từng bước
Bước 1: tạo CMK alias/shop-key với key policy
cat > shop-key-policy.json <<EOF
{ "Version": "2012-10-17", "Id": "shop-key-policy", "Statement": [
{ "Sid": "RootAndAdmin", "Effect": "Allow",
"Principal": { "AWS": ["arn:aws:iam::$ACCOUNT_ID:root", "arn:aws:iam::$ACCOUNT_ID:role/ShopAdminRole"] },
"Action": "kms:*", "Resource": "*" },
{ "Sid": "AppUse", "Effect": "Allow",
"Principal": { "AWS": [
"arn:aws:iam::$ACCOUNT_ID:role/shop-app-ec2-role",
"arn:aws:iam::$ACCOUNT_ID:role/shop-app-task-role",
"arn:aws:iam::$ACCOUNT_ID:role/shop-ecs-exec-role",
"arn:aws:iam::$ACCOUNT_ID:role/shop-lambda-role" ] },
"Action": ["kms:Encrypt","kms:Decrypt","kms:ReEncrypt*","kms:GenerateDataKey*","kms:DescribeKey"],
"Resource": "*" },
{ "Sid": "AllowServicesViaGrant", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::$ACCOUNT_ID:role/shop-app-task-role" },
"Action": ["kms:CreateGrant","kms:ListGrants","kms:RevokeGrant"], "Resource": "*",
"Condition": { "Bool": { "kms:GrantIsForAWSResource": "true" } } }
] }
EOF
KEY_ID=$(aws kms create-key --description "shop-app data key" \
--key-spec SYMMETRIC_DEFAULT --key-usage ENCRYPT_DECRYPT \
--policy file://shop-key-policy.json \
--tags TagKey=Project,TagValue=shop-app TagKey=Env,TagValue=lab \
--query KeyMetadata.KeyId --output text --profile shop-lab)
aws kms create-alias --alias-name alias/shop-key --target-key-id $KEY_ID --profile shop-lab
aws kms enable-key-rotation --key-id $KEY_ID --rotation-period-in-days 365 --profile shop-lab
Hai lưu ý quan trọng: (1) luôn giữ statement cho account root trong key policy, nếu không bạn có thể tự khóa mình ra khỏi key (KMS sẽ từ chối policy làm key không ai quản được, nhưng vẫn có cách tự bắn vào chân); (2) key policy cho role trực tiếp như trên đủ để role dùng key mà không cần IAM policy có kms:*, nhưng thực tế người ta vẫn thêm IAM policy để có hai lớp.
Bước 2: envelope encryption bằng SDK v2 cho cột số thẻ
Ý tưởng: mỗi bản ghi (hoặc mỗi nhóm bản ghi) một data key. KMS GenerateDataKey trả về plaintext key và encrypted key. Mã hóa local bằng AES-256-GCM với IV ngẫu nhiên 12 byte; lưu [encryptedDataKey | iv | ciphertext]. Dùng encryptionContext buộc ciphertext vào bản ghi. Khi giải mã, gọi Decrypt với encrypted key: không cần truyền key ID vì KMS đọc được từ ciphertext (với symmetric key).
// build.gradle / pom: software.amazon.awssdk:kms
package com.shop.security;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.DataKeySpec;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
@Component
public class EnvelopeCrypto {
private static final String KEY_ALIAS = "alias/shop-key";
private static final int IV_LEN = 12, TAG_BITS = 128;
private final KmsClient kms; // credential từ default chain (task role / IRSA / profile)
private final SecureRandom random = new SecureRandom();
public EnvelopeCrypto(KmsClient kms) { this.kms = kms; }
/** @return blob = [2 byte len][encrypted data key][12 byte iv][ciphertext+tag] */
public byte[] encrypt(byte[] plaintext, Map<String, String> context) throws Exception {
var dk = kms.generateDataKey(b -> b.keyId(KEY_ALIAS)
.keySpec(DataKeySpec.AES_256)
.encryptionContext(context));
byte[] plainKey = dk.plaintext().asByteArray();
byte[] encKey = dk.ciphertextBlob().asByteArray();
try {
byte[] iv = new byte[IV_LEN];
random.nextBytes(iv);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(plainKey, "AES"), new GCMParameterSpec(TAG_BITS, iv));
c.updateAAD(context.toString().getBytes()); // buộc context vào cả tầng local
byte[] ct = c.doFinal(plaintext);
return ByteBuffer.allocate(2 + encKey.length + IV_LEN + ct.length)
.putShort((short) encKey.length).put(encKey).put(iv).put(ct).array();
} finally {
Arrays.fill(plainKey, (byte) 0); // xóa data key khỏi RAM ngay
}
}
public byte[] decrypt(byte[] blob, Map<String, String> context) throws Exception {
var buf = ByteBuffer.wrap(blob);
byte[] encKey = new byte[buf.getShort()]; buf.get(encKey);
byte[] iv = new byte[IV_LEN]; buf.get(iv);
byte[] ct = new byte[buf.remaining()]; buf.get(ct);
// Không cần keyId: KMS đọc từ ciphertext. Context phải khớp, sai → InvalidCiphertextException
byte[] plainKey = kms.decrypt(b -> b.ciphertextBlob(SdkBytes.fromByteArray(encKey))
.encryptionContext(context)).plaintext().asByteArray();
try {
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(plainKey, "AES"), new GCMParameterSpec(TAG_BITS, iv));
c.updateAAD(context.toString().getBytes());
return c.doFinal(ct);
} finally {
Arrays.fill(plainKey, (byte) 0);
}
}
}
// Dùng trong JPA converter cho cột card_token
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, byte[]> { … }
// Entity
@Convert(converter = EncryptedStringConverter.class)
@Column(name = "card_token")
private String cardToken;
Trong thực tế hãy dùng AWS Encryption SDK for Java (aws-encryption-sdk-java) hoặc AWS Database Encryption SDK: nó làm đúng mọi thứ ở trên (format chuẩn, data key caching để giảm request KMS, multi-key, ký), ta tự viết ở đây để hiểu cơ chế. Đừng tự viết crypto cho production, xem lý do ở bài 13.1.
Data key caching: nếu mỗi bản ghi gọi GenerateDataKey thì 1 triệu đơn hàng = 1 triệu request KMS (tốn tiền và chạm quota ~5 500–30 000 req/s tùy region). Encryption SDK có CachingCryptoMaterialsManager: dùng lại một data key cho tối đa N message / T giây. Đánh đổi nhỏ về bán kính ảnh hưởng, đổi lấy giảm 100–1000 lần request.
Bước 3: S3 SSE-KMS bằng shop-key, chặn upload không mã hóa
aws s3api put-bucket-encryption --bucket shop-app-assets-$ACCOUNT_ID \
--server-side-encryption-configuration "{\"Rules\":[{
\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"alias/shop-key\"},
\"BucketKeyEnabled\":true}]}" --profile shop-lab
# Bucket policy: từ chối PutObject nếu không phải aws:kms với đúng key
cat > deny-unencrypted.json <<EOF
{ "Version":"2012-10-17", "Statement":[
{ "Sid":"DenyWrongEncryption", "Effect":"Deny", "Principal":"*",
"Action":"s3:PutObject", "Resource":"arn:aws:s3:::shop-app-assets-$ACCOUNT_ID/*",
"Condition":{ "StringNotEqualsIfExists":{ "s3:x-amz-server-side-encryption":"aws:kms" } } },
{ "Sid":"DenyWrongKey", "Effect":"Deny", "Principal":"*",
"Action":"s3:PutObject", "Resource":"arn:aws:s3:::shop-app-assets-$ACCOUNT_ID/*",
"Condition":{ "StringNotEqualsIfExists":{
"s3:x-amz-server-side-encryption-aws-kms-key-id":"arn:aws:kms:$AWS_REGION:$ACCOUNT_ID:key/$KEY_ID" } } } ] }
EOF
aws s3api put-bucket-policy --bucket shop-app-assets-$ACCOUNT_ID --policy file://deny-unencrypted.json --profile shop-lab
BucketKeyEnabled = S3 Bucket Keys: S3 xin KMS một bucket-level key rồi tự sinh data key cho object, giảm tới 99% request KMS (và tiền). Hệ quả cho code Java: role đọc object giờ cần kms:Decrypt trên shop-key, không chỉ s3:GetObject. Đây là lỗi AccessDenied khó hiểu nhất khi chuyển SSE-S3 → SSE-KMS. Presigned URL (bài 22.6) cũng vậy: người dùng URL mượn quyền của người ký, người ký phải có kms:Decrypt.
Bước 4: Secrets Manager secret shop/db với KMS key và rotation
# Tạo secret JSON (format mà rotation template của RDS hiểu)
aws secretsmanager create-secret --name shop/db \
--description "shop-db PostgreSQL credentials" --kms-key-id alias/shop-key \
--secret-string "{\"engine\":\"postgres\",\"host\":\"$RDS_EP\",\"port\":5432,\"dbname\":\"shop\",
\"username\":\"shop\",\"password\":\"$(openssl rand -base64 24 | tr -d '/+=')\"}" \
--tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
# Rotation: cách 1 "managed rotation" cho RDS (không cần Lambda của bạn) khi secret do RDS quản lý
# Cách 2 (secret tự tạo): Lambda từ template SecretsManagerRDSPostgreSQLRotationSingleUser qua SAR
aws serverlessrepo create-cloud-formation-change-set \
--application-id arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSPostgreSQLRotationSingleUser \
--stack-name shop-db-rotation --capabilities CAPABILITY_IAM CAPABILITY_RESOURCE_POLICY \
--parameter-overrides Name=endpoint,Value=https://secretsmanager.$AWS_REGION.amazonaws.com \
Name=functionName,Value=shop-db-rotation \
Name=vpcSubnetIds,Value=$PRIV_A,$PRIV_B Name=vpcSecurityGroupIds,Value=$APP_SG \
--profile shop-lab
# ... execute-change-set, chờ stack CREATE_COMPLETE, rồi:
ROT_ARN=$(aws lambda get-function --function-name shop-db-rotation --query Configuration.FunctionArn --output text --profile shop-lab)
aws secretsmanager rotate-secret --secret-id shop/db --rotation-lambda-arn $ROT_ARN \
--rotation-rules "{\"ScheduleExpression\":\"rate(30 days)\",\"Duration\":\"2h\"}" --profile shop-lab
Rotation Lambda chạy trong VPC (để tới RDS) qua SG shop-app-sg, và cần tới Secrets Manager (endpoint VPC hoặc NAT). Bốn bước của nó: createSecret (sinh password mới, lưu version AWSPENDING) → setSecret (ALTER USER trong DB) → testSecret (login bằng password mới) → finishSecret (chuyển nhãn AWSCURRENT). Single user: đổi password của chính user → có khoảnh khắc app giữ password cũ bị từ chối, app phải retry bằng cách đọc lại secret. Alternating users: hai user shop và shop_clone luân phiên, không có khoảnh khắc chết, là cách đề thi gọi "zero downtime rotation".
Bước 5: Spring Boot đọc secret và config qua Spring Cloud AWS 3
<!-- pom.xml -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>3.2.1</version><type>pom</type><scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency><groupId>io.awspring.cloud</groupId><artifactId>spring-cloud-aws-starter-secrets-manager</artifactId></dependency>
<dependency><groupId>io.awspring.cloud</groupId><artifactId>spring-cloud-aws-starter-parameter-store</artifactId></dependency>
# application-aws.yml
spring:
config:
import:
- "aws-secretsmanager:shop/db" # JSON → từng key thành property: username, password, host…
- "aws-parameterstore:/shop/app/" # mọi tham số dưới prefix → property, bỏ prefix
- "optional:aws-parameterstore:/shop/app/${spring.profiles.active}/"
datasource:
url: jdbc:postgresql://${host}:${port}/${dbname}
username: ${username}
password: ${password}
cloud:
aws:
region:
static: ${AWS_REGION:ap-southeast-1}
secretsmanager:
reload:
strategy: refresh # cần spring-cloud-context; đọc lại định kỳ, bắn RefreshEvent
period: 5m
parameterstore:
reload:
strategy: refresh
period: 1m
shop:
features:
new-checkout: ${feature-flags.new-checkout:false} # từ /shop/app/feature-flags (StringList/JSON)
// Feature flag đọc lại được không cần restart
@Component
@RefreshScope
public class FeatureFlags {
@Value("${feature-flags.new-checkout:false}") boolean newCheckout;
@Value("${feature-flags.free-shipping-threshold:500000}") long freeShippingThreshold;
// getter…
}
// Rotation password: HikariCP tạo kết nối mới bằng password mới
@Bean
HikariDataSource dataSource(SecretsManagerClient sm, DataSourceProperties props) {
var ds = new HikariDataSource();
ds.setJdbcUrl(props.getUrl());
ds.setUsername(props.getUsername());
ds.setPassword(props.getPassword());
return ds;
}
@EventListener(RefreshScopeRefreshedEvent.class) // bắn bởi reload strategy khi secret đổi
void onRefresh(RefreshScopeRefreshedEvent e) {
String newPwd = env.getProperty("password");
hikari.getHikariConfigMXBean().setPassword(newPwd); // kết nối MỚI dùng password mới
hikari.getHikariPoolMXBean().softEvictConnections(); // kết nối cũ rút dần, không cắt request đang chạy
}
Cách "chuẩn AWS" hơn nữa cho JDBC: dùng AWS Secrets Manager JDBC Library (aws-secretsmanager-jdbc): driver wrapper jdbc-secretsmanager:postgresql://… với username là tên secret; khi auth fail nó tự đọc secret mới và retry. Hoặc bỏ password hẳn: RDS IAM authentication (token 15 phút) qua RDS Proxy (bài 22.7).
Bước 6: Parameter Store cho feature flag
aws ssm put-parameter --name /shop/app/feature-flags/new-checkout --type String --value "false" \
--tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
aws ssm put-parameter --name /shop/app/feature-flags/free-shipping-threshold --type String --value "500000" \
--tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
# SecureString cho thứ bí mật nhưng không cần rotation (API key PSP)
aws ssm put-parameter --name /shop/app/psp-api-key --type SecureString --key-id alias/shop-key \
--value "${PSP_API_KEY}" --tags Key=Project,Value=shop-app Key=Env,Value=lab --profile shop-lab
# Role app cần: ssm:GetParameter(s)ByPath trên arn:aws:ssm:…:parameter/shop/app/* + kms:Decrypt shop-key
5. Cách hoạt động
Envelope encryption (ghi số thẻ):
shop-app ──GenerateDataKey(alias/shop-key, ctx{orderId})──► KMS (HSM)
◄── plaintext DK (32B) + encrypted DK (~180B) ──────┘
│
├─ AES-GCM(plaintext DK, iv, cardToken) ──► ciphertext
├─ zero plaintext DK
└─ INSERT card_token = [encDK | iv | ciphertext] ──► RDS (storage cũng mã hóa bằng aws/rds hoặc shop-key)
Đọc: SELECT ──► tách encDK ──Decrypt(encDK, ctx)──► KMS ──► plaintext DK ──► AES-GCM decrypt
CloudTrail ghi: ai (task role), key nào, context nào, lúc nào. Kẻ dump DB: chỉ có ciphertext + encDK vô dụng.
Key policy vs IAM policy:
┌────────────────────────────────────────┐
principal gọi KMS ──► │ key policy cho phép? ── không ──► Deny │
│ có (trực tiếp) ──────────► Allow │
│ có (ủy quyền root→IAM) ──► IAM policy cho phép? ──► Allow / Deny
└────────────────────────────────────────┘
+ grant (dịch vụ tạo) + SCP / permission boundary (bài 22.2) + VPC endpoint policy
Rotation KMS key (material): Rotation secret (giá trị):
alias/shop-key ─┬─ material v1 (2024) shop/db ─┬─ AWSPREVIOUS "p@ss-old"
├─ material v2 (2025) ├─ AWSCURRENT "p@ss-new" ◄── app đọc
└─ material v3 (2026) ◄── encrypt └─ AWSPENDING (đang rotate)
decrypt: KMS tự chọn material theo ciphertext app: đọc lại khi auth fail / theo chu kỳ
Secrets Manager rotation (alternating users):
shop (CURRENT) ──► Lambda: tạo pwd mới cho shop_clone ──► ALTER USER shop_clone ──► test login ──► shop_clone = CURRENT
app: refresh 5 phút sau dùng shop_clone; kết nối cũ của shop vẫn sống tới khi rút dần → zero downtime
6. So sánh
Secrets Manager vs Parameter Store (câu hỏi "chọn gì" phổ biến nhất của domain Secure):
| Secrets Manager | Parameter Store (SSM) | |
|---|---|---|
| Dành cho | Secret: password DB, API key, cert private key | Config: flag, URL, AMI ID; SecureString cho secret nhỏ |
| Rotation tự động | Có (Lambda, managed cho RDS/Redshift/DocDB) | Không (tự làm bằng EventBridge + Lambda) |
| Giá | ~0,4 USD/secret/tháng + 0,05 USD/10k call | Standard miễn phí; Advanced ~0,05 USD/tham số/tháng + call |
| Kích thước | 64 KB | 4 KB (Standard), 8 KB (Advanced) |
| Cross-account | Có (resource policy) | Có với Advanced (RAM share), hạn chế |
| Multi-region | Replica secret | Không tự động |
| Phân cấp / path | Tên có / nhưng không query theo path | GetParametersByPath, policy theo prefix |
| Tích hợp | RDS tạo secret tự động, ECS/Lambda/CloudFormation dynamic reference | ECS/Lambda/CloudFormation/CodeBuild, SSM Run Command |
| Chọn khi đề nói | "rotate credentials automatically", "RDS password", "cross-account secret" | "configuration data", "hierarchical", "lowest cost", "no rotation needed" |
S3 encryption options:
| SSE-S3 | SSE-KMS | SSE-C | Client-side | |
|---|---|---|---|---|
| Ai giữ key | S3 | KMS (CMK hoặc aws/s3) | Bạn, gửi mỗi request (HTTPS bắt buộc) | Bạn (hoặc KMS qua Encryption SDK) |
| Audit ai decrypt | Không | CloudTrail | Không | Tùy |
| Kiểm soát riêng theo key | Không | Key policy, ViaService, disable key = khóa toàn bucket | Bạn | Bạn |
| Chi phí thêm | 0 | Request KMS (giảm bằng Bucket Keys) | 0 | Compute |
| Header | AES256 | aws:kms + key id | …customer-algorithm/key/key-MD5 | Không |
| Đề thi | "encryption at rest, minimal effort" | "audit trail of key usage", "control who decrypts", "separate keys per tenant" | "company must manage keys, S3 must not store them" | "data encrypted before leaving the premises" |
KMS vs CloudHSM vs ACM:
| KMS | CloudHSM | ACM | |
|---|---|---|---|
| Là gì | Managed key service, multi-tenant HSM | HSM single-tenant của bạn trong VPC | Cert TLS X.509 |
| Tích hợp dịch vụ AWS | Hơn 100 dịch vụ | Qua custom key store của KMS; SDK PKCS#11/JCE | ALB, CloudFront, API Gateway |
| Ai quản lý user key | AWS (IAM/key policy) | Bạn hoàn toàn | AWS cấp & gia hạn |
| Giá | ~1 USD/key/tháng + request | Theo giờ mỗi HSM (đắt), không Free Tier | Public cert miễn phí; Private CA tốn tiền |
| Đề thi | Mặc định | "FIPS 140-2 Level 3 dedicated", "Oracle TDE", "keys never shared with AWS" | "TLS certificate", "auto renew" |
7. Khi nào dùng / không dùng
Customer managed key khi: cần audit, cần disable/thu hồi, cần cross-account (CloudFront OAC đọc bucket SSE-KMS, account khác đọc snapshot), compliance yêu cầu, cần tách key theo tenant/môi trường. AWS managed key đủ cho: lab, dữ liệu nội bộ ít nhạy cảm, khi không có yêu cầu kiểm soát. Đừng tạo 50 CMK không lý do (1 USD/tháng mỗi cái và quản policy mệt).
Envelope encryption tầng ứng dụng cho: PII, số thẻ, token PSP, dữ liệu phải "mã hóa trước khi rời app". Không cho: mọi cột (tìm kiếm, index, JOIN trên cột mã hóa không được; cân nhắc hash riêng để tìm, hoặc tokenization), log, dữ liệu phải sort/filter trong DB.
Secrets Manager cho password DB, API key cần rotation; Parameter Store cho config, feature flag, endpoint, và secret không cần rotation khi muốn tiết kiệm. Mẫu phổ biến: secret ở Secrets Manager, ARN của secret ở Parameter Store.
ACM cho mọi TLS ở ALB/CloudFront/API Gateway; không cho TLS trên EC2 trực tiếp (dùng Let's Encrypt hoặc import cert vào ACM rồi gắn ALB). CloudHSM chỉ khi compliance ép.
AccessDenied dù role có s3:GetObject; và hóa đơn KMS tháng này tăng đáng ngờ. Hai chuyện này liên quan gì với nhau?Mình đổi bucket assets sang SSE-KMS với alias/shop-key. Từ lúc đó task ECS đọc ảnh bị AccessDenied dù IAM policy của shop-app-task-role có s3:GetObject, mà CloudFront phía trước cũng trả 403. Và Cost Explorer thấy dòng KMS tăng từ 0 lên vài USD. Sao vậy?
Hai hiện tượng cùng một gốc: với SSE-KMS, mỗi lần đọc object S3 phải gọi kms:Decrypt thay bạn, và lời gọi đó được đánh giá bằng quyền của principal đang đọc. Cho nên:
1. AccessDenied của task role. s3:GetObject chỉ là một nửa. Nửa kia là kms:Decrypt trên shop-key, và phải được cho phép ở key policy (hoặc key policy ủy quyền cho IAM rồi IAM policy cho phép). Ở bước 1 mình đã đưa shop-app-task-role vào key policy; nếu bạn tạo key bằng Console mặc định thì key policy chỉ có root + người tạo. Kiểm tra bằng CloudTrail: event Decrypt với errorCode: AccessDenied và userIdentity.sessionContext.sessionIssuer.arn là task role.
aws kms get-key-policy --key-id alias/shop-key --policy-name default --output text --profile shop-lab | jq .Statement[].Principal
# Thêm role vào key policy (put-key-policy ghi đè toàn bộ → lấy về, sửa, đẩy lên)
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--max-results 5 --query 'Events[].CloudTrailEvent' --output text --profile shop-lab | jq -r 'select(.errorCode) | {errorCode, who: .userIdentity.arn}'
2. CloudFront 403. CloudFront dùng OAC (origin access control, bài 22.10) để đọc bucket, và OAC là principal cloudfront.amazonaws.com. Key policy phải cho phép nó decrypt, giới hạn bằng điều kiện distribution:
{ "Sid": "AllowCloudFrontOAC", "Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*"], "Resource": "*",
"Condition": { "StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::$ACCOUNT_ID:distribution/$DIST_ID" } } }
Cùng logic cho mọi "người đọc khác": account khác đọc bucket cross-account cần cả bucket policy và key policy cho account/role đó; Lambda shop-image-resize (bài 22.12) ghi thumbnail cần kms:GenerateDataKey; presigned URL hoạt động bằng quyền của người ký.
3. Hóa đơn KMS. Mỗi GetObject/PutObject trên object SSE-KMS = 1 request KMS (khoảng 0,03 USD/10 000 request, kiểm tra pricing). Bucket ảnh sản phẩm được đọc hàng trăm nghìn lần/ngày → vài USD/tháng. Ba cách giảm, theo thứ tự:
- Bật S3 Bucket Keys (
BucketKeyEnabled: truetrong bucket encryption). S3 dùng một bucket-level key ngắn hạn thay cho data key mỗi object; request KMS giảm tới 99%. Chỉ áp dụng cho object ghi sau khi bật (object cũ: copy lại chính nó để re-encrypt). - Để CloudFront cache: hit cache không chạm S3, không chạm KMS. TTL ảnh sản phẩm dài (immutable, versioned theo tên file).
- Hỏi lại yêu cầu: ảnh sản phẩm công khai có cần SSE-KMS không? Hóa đơn, CMND thì có; ảnh sản phẩm có thể để SSE-S3 ở bucket/prefix riêng. SSE-KMS mua được audit và kiểm soát; dữ liệu không cần hai thứ đó thì đừng trả.
aws s3api get-bucket-encryption --bucket shop-app-assets-$ACCOUNT_ID --profile shop-lab
# ... "BucketKeyEnabled": true ← nếu false, put lại như Bước 3
aws cloudwatch get-metric-statistics --namespace AWS/KMS --metric-name SecondsUntilKeyMaterialExpiration ... # không phải cái này
# Đếm request KMS: CloudTrail Lake hoặc Cost Explorer filter Service=KMS, usage type ...-KMS-Requests
Bài học chung cho đề thi và cho đời: SSE-KMS = quyền trên key + quyền trên bucket, và tốn request; Bucket Keys là câu trả lời cho "reduce KMS cost on S3".
Bật rotation 30 ngày cho shop/db rồi, nhưng mình lo: đúng lúc Lambda đổi password thì 2 task ECS đang chạy vẫn giữ password cũ trong HikariCP, request sẽ lỗi hàng loạt phải không? Và KMS key rotation có làm dữ liệu cũ trong S3 không giải mã được không?
Hai lo lắng, một đúng một phần, một không cần lo.
KMS key rotation: không cần lo. Khi KMS rotate, nó sinh key material mới cho các lần mã hóa sau, nhưng giữ mọi material cũ vĩnh viễn trong cùng key ID. Ciphertext chứa thông tin material nào đã dùng, nên Decrypt tự chọn đúng. Không có re-encrypt, không có downtime, ARN/alias không đổi, giá không đổi. Thứ duy nhất "rotate" làm thay đổi: giới hạn bán kính ảnh hưởng nếu một material bị lộ (về lý thuyết). Cũng vì vậy, rotation KMS không phải "đổi khóa" theo nghĩa thông thường; muốn ép dữ liệu cũ dùng material mới thì phải tự ReEncrypt hoặc copy object. Trường hợp cần nhớ: key có material EXTERNAL (BYOK) hoặc trong custom key store không tự rotate được, bạn phải rotate thủ công bằng cách tạo key mới và đổi alias trỏ sang (manual rotation: alias là thứ giúp làm được việc này mà không sửa code).
Secret rotation: đúng một phần. Hãy nhìn đúng điểm chết:
Single-user rotation, app không làm gì:
t0 Lambda ALTER USER shop PASSWORD 'new' ← từ đây login bằng 'old' bị từ chối
t0+ Hikari: 10 kết nối ĐANG MỞ vẫn sống (Postgres không cắt session khi đổi password)
→ request bình thường vẫn chạy!
t1 một kết nối idle quá maxLifetime (30 phút) → Hikari đóng, mở kết nối mới bằng 'old' → FATAL: password authentication failed
→ pool co dần, request bắt đầu lỗi khi pool cạn, thường 30–60 phút SAU rotation, không phải ngay lúc đó
Nên hiện tượng là "cháy chậm", và vì thế dễ bị bỏ qua cho tới đêm. Bốn mức xử lý:
| Mức | Làm gì | Kết quả |
|---|---|---|
| 1 | Alternating users (template …RotationMultiUser): hai user luân phiên, password của user đang dùng không bao giờ bị đổi dưới chân app; app chỉ cần đọc secret mới trong vòng 1 chu kỳ | Không có khoảnh khắc nào password cũ bị từ chối trước khi app kịp đọc mới |
| 2 | App đọc lại secret định kỳ (Spring Cloud AWS reload 5 phút) và cập nhật Hikari như code ở Bước 5 (setPassword + softEvictConnections) | Kết nối mới dùng password mới; cũ rút dần |
| 3 | Dùng aws-secretsmanager-jdbc: khi gặp lỗi auth, driver đọc lại secret (có cache) và retry một lần | Tự lành ngay cả khi reload chưa tới |
| 4 | Bỏ password: RDS IAM auth qua RDS Proxy, hoặc proxy giữ secret và app chỉ auth với proxy bằng IAM | Không còn gì để rotate ở phía app |
Với shop-app: mức 1 + 2 là đủ và chính là cách mô tả trong phỏng vấn. Và một chi tiết nhỏ hay quên: ECS bơm secret vào env lúc task khởi động; task sống 3 tuần thì env vẫn là password 3 tuần trước. Nếu bạn dùng cách bơm env (bài 22.13) thay cho đọc trực tiếp bằng SDK, rotation chỉ an toàn khi dùng alternating users và chu kỳ deploy ngắn hơn chu kỳ rotation, hoặc update-service --force-new-deployment sau mỗi lần rotate (EventBridge rule bắt event RotationSucceeded). Đó là lý do mục 4 Bước 5 chuyển sang spring.config.import đọc trực tiếp.
Kiểm tra lúc nào cũng được bằng cách rotate tay: aws secretsmanager rotate-secret --secret-id shop/db, rồi theo dõi log app 10 phút và đếm lỗi 28P01 (invalid_password) trong Logs Insights. Trong lab sẽ làm đúng việc này.
Cho mình bài lab để tự tay làm phần này nhé.
Lab 22.14: shop-key, envelope encryption, S3 SSE-KMS, secret shop/db có rotation, feature flag trong Parameter Store
Mục tiêu
- Tạo CMK
alias/shop-keyvới key policy cho các role shop-app, bật rotation;describe-keythấyKeyRotationEnabled: true. - Chạy chương trình Java envelope encryption: mã hóa chuỗi, lưu blob, giải mã lại đúng; đổi encryption context thì giải mã thất bại; CloudTrail thấy
GenerateDataKeyvàDecryptvới context. - Bucket assets dùng SSE-KMS + Bucket Keys; upload không mã hóa bị từ chối;
head-objectthấyaws:kmsvà key ID. - Secret
shop/dbmã hóa bằng shop-key, rotate tay thành công, versionAWSCURRENTđổi; Parameter Store/shop/app/feature-flags/*được Spring Boot đọc và đổi giá trị không restart.
Chi phí và thời gian
- KMS: CMK không nằm trong Free Tier, khoảng 1 USD/tháng tính theo giờ (≈ 0,0014 USD/giờ), và 20 000 request/tháng miễn phí; lab dùng vài trăm request. Xóa key phải chờ 7 ngày pending nên chi phí vài cent.
- Secrets Manager: 30 ngày thử miễn phí cho secret mới; sau đó ~0,4 USD/secret/tháng. Parameter Store Standard: miễn phí.
- Rotation Lambda: trong Free Tier. Nếu RDS
shop-dbchưa chạy, phần rotation thật bỏ qua (RDS db.t3.micro có 750 giờ Free Tier 12 tháng đầu). - Thời gian: 75–100 phút.
Chuẩn bị
- Profile
shop-lab; JDK 21, Maven; jq, openssl, psql (kiểm tra rotation). - Bucket
shop-app-assets-$ACCOUNT_ID(22.6); roleshop-app-task-role,shop-ecs-exec-role(22.13),shop-lambda-role(22.12),shop-app-ec2-role(22.4); role nào chưa có thì bỏ khỏi key policy (KMS từ chối policy có principal không tồn tại). - Tùy chọn: RDS
shop-dbđang chạy, VPC endpoint secretsmanager hoặc NAT (22.13) để rotation Lambda chạy trong VPC.
export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text --profile shop-lab)
export BUCKET=shop-app-assets-$ACCOUNT_ID
Các bước
Bước 1: tạo key theo mục 4 Bước 1 (policy, create-key, alias, rotation). Kiểm tra:
aws kms describe-key --key-id alias/shop-key \
--query 'KeyMetadata.{id:KeyId,state:KeyState,spec:KeySpec,origin:Origin,manager:KeyManager}' --profile shop-lab
# {"state":"Enabled","spec":"SYMMETRIC_DEFAULT","origin":"AWS_KMS","manager":"CUSTOMER"}
aws kms get-key-rotation-status --key-id alias/shop-key --profile shop-lab
# {"KeyRotationEnabled": true, "RotationPeriodInDays": 365, ...}
Console: KMS → Customer managed keys → Create key → Symmetric, Encrypt and decrypt → Alias shop-key → Key administrators: ShopAdminRole → Key users: các role app → Key rotation tab.
Bước 2: thử Encrypt/Decrypt trực tiếp và giới hạn 4 KB
echo -n "hello shop" > small.txt
aws kms encrypt --key-id alias/shop-key --plaintext fileb://small.txt \
--encryption-context app=shop,field=demo --query CiphertextBlob --output text --profile shop-lab | base64 -d > small.enc
aws kms decrypt --ciphertext-blob fileb://small.enc --encryption-context app=shop,field=demo \
--query Plaintext --output text --profile shop-lab | base64 -d; echo
# hello shop
aws kms decrypt --ciphertext-blob fileb://small.enc --encryption-context app=shop,field=OTHER --profile shop-lab
# An error occurred (InvalidCiphertextException) ← context sai
head -c 5000 /dev/urandom > big.bin
aws kms encrypt --key-id alias/shop-key --plaintext fileb://big.bin --profile shop-lab
# ValidationException: ... 4096 bytes ← vì vậy cần envelope encryption
Bước 3: chương trình Java envelope encryption
// EnvelopeDemo.java (dùng class EnvelopeCrypto ở mục 4, bước 2)
public static void main(String[] a) throws Exception {
var kms = KmsClient.builder().region(Region.AP_SOUTHEAST_1).build(); // profile: AWS_PROFILE=shop-lab
var crypto = new EnvelopeCrypto(kms);
var ctx = Map.of("table", "orders", "field", "card_token", "orderId", "ORD-1001");
byte[] blob = crypto.encrypt("tok_4242_9f3a".getBytes(), ctx);
System.out.println("blob bytes = " + blob.length); // ~ 2 + 184 + 12 + 13 + 16
Files.write(Path.of("card.blob"), blob);
System.out.println(new String(crypto.decrypt(blob, ctx))); // tok_4242_9f3a
try { crypto.decrypt(blob, Map.of("orderId", "ORD-9999")); }
catch (Exception e) { System.out.println("wrong ctx: " + e.getClass().getSimpleName()); }
}
AWS_PROFILE=shop-lab mvn -q compile exec:java -Dexec.mainClass=com.shop.security.EnvelopeDemo
# blob bytes = 227
# tok_4242_9f3a
# wrong ctx: InvalidCiphertextException
# CloudTrail (chờ 5–15 phút để event xuất hiện)
aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=$KEY_ID \
--max-results 10 --query 'Events[].{t:EventTime,e:EventName}' --output table --profile shop-lab
# GenerateDataKey, Decrypt, Decrypt (lần sai), Encrypt...
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=GenerateDataKey --max-results 1 \
--query 'Events[0].CloudTrailEvent' --output text --profile shop-lab | jq '.requestParameters.encryptionContext'
# {"table":"orders","field":"card_token","orderId":"ORD-1001"}
Console: CloudTrail → Event history → Resource type AWS::KMS::Key → bấm event → xem encryptionContext trong request.
Bước 4: S3 SSE-KMS theo mục 4 Bước 3 (put-bucket-encryption + bucket policy deny).
aws s3 cp small.txt s3://$BUCKET/docs/invoice-1.txt --profile shop-lab # SDK/CLI tự thêm header theo default encryption → OK
aws s3api head-object --bucket $BUCKET --key docs/invoice-1.txt \
--query '{sse:ServerSideEncryption,key:SSEKMSKeyId,bucketKey:BucketKeyEnabled}' --profile shop-lab
# {"sse":"aws:kms","key":"arn:aws:kms:...:key/...","bucketKey":true}
aws s3 cp small.txt s3://$BUCKET/docs/invoice-2.txt --sse AES256 --profile shop-lab
# upload failed: ... AccessDenied ← bucket policy từ chối SSE-S3
aws s3 cp s3://$BUCKET/docs/invoice-1.txt - --profile shop-lab # đọc được vì ShopAdminRole có kms:*
Console: S3 → bucket → Properties → Default encryption → SSE-KMS, chọn alias/shop-key, Bucket Key: Enable; Permissions → Bucket policy.
Bước 5: Secrets Manager theo mục 4 Bước 4 (create-secret với --kms-key-id alias/shop-key). Nếu có RDS: deploy rotation Lambda từ SAR và bật rotation. Nếu không có RDS: vẫn làm được phần versioning bằng rotate tay với Lambda rỗng không cần, dùng put-secret-value:
aws secretsmanager describe-secret --secret-id shop/db \
--query '{kms:KmsKeyId,rotation:RotationEnabled,versions:VersionIdsToStages}' --profile shop-lab
aws secretsmanager get-secret-value --secret-id shop/db --query SecretString --output text --profile shop-lab | jq 'del(.password)'
# Có RDS: rotate tay và theo dõi
aws secretsmanager rotate-secret --secret-id shop/db --profile shop-lab
sleep 60
aws secretsmanager describe-secret --secret-id shop/db --query 'VersionIdsToStages' --profile shop-lab
# hai version: một AWSCURRENT (mới), một AWSPREVIOUS (cũ)
PGPASSWORD=$(aws secretsmanager get-secret-value --secret-id shop/db --query SecretString --output text --profile shop-lab | jq -r .password) \
psql -h $RDS_EP -U shop -d shop -c 'select 1' # login bằng password mới OK (từ máy trong VPC/bastion)
# Không có RDS: tạo version mới bằng tay để thấy label dịch chuyển
aws secretsmanager put-secret-value --secret-id shop/db \
--secret-string "$(aws secretsmanager get-secret-value --secret-id shop/db --query SecretString --output text --profile shop-lab | jq -c '.password="'"$(openssl rand -base64 18 | tr -d /+=)"'"')" --profile shop-lab
aws secretsmanager list-secret-version-ids --secret-id shop/db --query 'Versions[].{stages:VersionStages,created:CreatedDate}' --profile shop-lab
Console: Secrets Manager → Store a new secret → Credentials for Amazon RDS database → chọn shop-db → Encryption key alias/shop-key → Rotation: Automatic, 30 days, create a new Lambda function.
Bước 6: Parameter Store + Spring Boot đọc theo mục 4 Bước 5–6. Chạy shop-app local với profile aws và AWS_PROFILE=shop-lab (default chain đọc profile), hoặc deploy lại task ECS (role đã có quyền).
aws ssm get-parameters-by-path --path /shop/app/ --recursive --with-decryption \
--query 'Parameters[].{name:Name,type:Type,value:Value,ver:Version}' --output table --profile shop-lab
AWS_PROFILE=shop-lab SPRING_PROFILES_ACTIVE=aws java -jar target/shop-app.jar &
curl -s localhost:8080/actuator/env/feature-flags.new-checkout | jq .property
# {"source":"aws-parameterstore:/shop/app/","value":"false"}
curl -s localhost:8080/actuator/env/password | jq .property.value
# "******" ← Actuator che giá trị; nguồn là aws-secretsmanager:shop/db
Kiểm tra kết quả
# 1. Key và policy
aws kms list-aliases --query "Aliases[?AliasName=='alias/shop-key']" --profile shop-lab
aws kms get-key-policy --key-id alias/shop-key --policy-name default --output text --profile shop-lab | jq '.Statement[].Sid'
# "RootAndAdmin" "AppUse" "AllowServicesViaGrant"
# 2. Object S3 mã hóa đúng key (xem bước 4), và role app đọc được (giả lập bằng assume role)
CREDS=$(aws sts assume-role --role-arn arn:aws:iam::$ACCOUNT_ID:role/shop-app-task-role --role-session-name t --profile shop-lab 2>/dev/null) \
|| echo "task role không cho ShopAdminRole assume → kiểm tra bằng ECS Exec trong task thay thế"
# 3. Secret mã hóa bằng shop-key
aws secretsmanager describe-secret --secret-id shop/db --query KmsKeyId --output text --profile shop-lab
# arn:aws:kms:ap-southeast-1:...:key/<KEY_ID>
# 4. Feature flag đổi không restart
aws ssm put-parameter --name /shop/app/feature-flags/new-checkout --value "true" --overwrite --profile shop-lab
sleep 70 # reload period 1m
curl -s localhost:8080/actuator/env/feature-flags.new-checkout | jq .property.value
# "true"
Thử thêm
1. Disable key và xem mọi thứ dừng. aws kms disable-key --key-id alias/shop-key; rồi aws s3 cp s3://$BUCKET/docs/invoice-1.txt - → KMS.DisabledException; get-secret-value shop/db → lỗi; app khởi động không được. Đây là "kill switch" của CMK: một lệnh khóa toàn bộ dữ liệu mã hóa bằng nó mà không xóa gì. enable-key để mở lại.
2. Thu hẹp key policy bằng kms:ViaService. Thêm condition vào statement AppUse: "StringEquals": {"kms:ViaService": ["s3.ap-southeast-1.amazonaws.com","secretsmanager.ap-southeast-1.amazonaws.com","ssm.ap-southeast-1.amazonaws.com"]}. Giờ EnvelopeDemo (gọi KMS trực tiếp) bị từ chối, nhưng S3/Secrets Manager vẫn chạy. Dùng khi muốn cấm app giữ data key trong tay.
3. Multi-Region key. aws kms create-key --multi-region rồi replicate-key --replica-region ap-northeast-1; mã hóa ở Singapore, giải mã ở Tokyo bằng replica (cùng key ID mrk-…). Hợp với DR và DynamoDB Global Tables (bài 22.18). Nhớ xóa cả replica.
4. Grant. aws kms list-grants --key-id alias/shop-key sau khi tạo EBS volume/RDS snapshot mã hóa bằng shop-key: thấy grant do ec2.amazonaws.com/rds tạo, với RetiringPrincipal. Đó là cách dịch vụ AWS dùng key thay bạn.
5. Secret cross-account. Gắn resource policy cho secret cho phép account thứ hai GetSecretValue; nhận ra rằng account kia vẫn bị từ chối cho đến khi key policy shop-key cũng cho phép nó kms:Decrypt. Hai lớp, luôn luôn.
Dọn tài nguyên
# 1. Tắt rotation, xóa secret (mặc định chờ 30 ngày; force để xóa ngay trong lab)
aws secretsmanager cancel-rotate-secret --secret-id shop/db --profile shop-lab 2>/dev/null
aws secretsmanager delete-secret --secret-id shop/db --force-delete-without-recovery --profile shop-lab
# Rotation stack (Lambda + role) nếu đã tạo
aws cloudformation delete-stack --stack-name serverlessrepo-shop-db-rotation --profile shop-lab
# 2. Parameter Store
aws ssm delete-parameters --names /shop/app/feature-flags/new-checkout /shop/app/feature-flags/free-shipping-threshold /shop/app/psp-api-key --profile shop-lab
# 3. S3: trả bucket về SSE-S3 (hoặc xóa bucket nếu kết thúc chương), bỏ bucket policy
aws s3api delete-bucket-policy --bucket $BUCKET --profile shop-lab
aws s3api put-bucket-encryption --bucket $BUCKET --server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' --profile shop-lab
aws s3 rm s3://$BUCKET/docs/ --recursive --profile shop-lab
# Object SSE-KMS còn lại sẽ KHÔNG đọc được sau khi key bị xóa → copy lại để re-encrypt bằng SSE-S3 trước:
aws s3 cp s3://$BUCKET/ s3://$BUCKET/ --recursive --sse AES256 --metadata-directive REPLACE --profile shop-lab
# 4. KMS: ECR repo (22.13), EBS, RDS snapshot... dùng shop-key phải xóa/re-encrypt trước. Rồi lên lịch xóa (tối thiểu 7 ngày; vẫn tính tiền key tới lúc xóa)
aws kms delete-alias --alias-name alias/shop-key --profile shop-lab
aws kms disable-key --key-id $KEY_ID --profile shop-lab
aws kms schedule-key-deletion --key-id $KEY_ID --pending-window-in-days 7 --profile shop-lab
# Đổi ý trong 7 ngày: aws kms cancel-key-deletion --key-id $KEY_ID
# 5. Dọn file local có bí mật
rm -f small.enc card.blob shop-key-policy.json deny-unencrypted.json
# Kiểm tra
aws kms list-keys --profile shop-lab; aws secretsmanager list-secrets --query 'SecretList[].Name' --profile shop-lab
Bạn giữ alias/shop-key và shop/db nếu còn làm bài 22.15–22.21 (các bài đó dùng lại); chỉ xóa khi kết thúc chương ở lab tổng hợp 22.21.
Phỏng vấn và đề thi SAA hỏi về KMS, Secrets Manager, mã hóa thì sao?
Q1. Envelope encryption là gì, vì sao KMS dùng nó thay vì mã hóa trực tiếp?
🇻🇳 Tiếng Việt:
KMS chỉ mã hóa trực tiếp tối đa 4 KB và mỗi lần phải gọi mạng tới HSM, nên không dùng cho dữ liệu lớn hay nhiều bản ghi. Envelope encryption: em xin KMS một data key bằng GenerateDataKey, nhận cả bản rõ và bản đã mã hóa bằng CMK. Em mã hóa dữ liệu local bằng AES-GCM với data key rõ, lưu data key đã mã hóa cạnh ciphertext, xóa bản rõ khỏi RAM. Giải mã thì gửi data key đã mã hóa lên KMS Decrypt rồi giải mã local. Lợi: CMK không bao giờ rời KMS, một lời gọi KMS cho cả tệp lớn, có audit trong CloudTrail, và encryption context buộc ciphertext vào đúng bản ghi. Thực tế em dùng AWS Encryption SDK với data key caching thay vì tự viết.
🇬🇧 English:
KMS encrypts at most 4 KB directly and every call is a network round trip to the HSM, so it is not for large data or many records. With envelope encryption I ask KMS for a data key via GenerateDataKey and receive both the plaintext key and a copy encrypted under the CMK. I encrypt the data locally with AES-GCM using the plaintext key, store the encrypted data key next to the ciphertext, and wipe the plaintext key from memory. To decrypt, I send the encrypted data key to KMS Decrypt and then decrypt locally. Benefits: the CMK never leaves KMS, one KMS call covers a large file, every key use is audited in CloudTrail, and the encryption context binds the ciphertext to the right record. In practice I use the AWS Encryption SDK with data key caching instead of hand-rolling it.
Q2. Key policy và IAM policy trong KMS quan hệ thế nào? Vì sao thêm quyền IAM rồi vẫn AccessDenied?
🇻🇳 Tiếng Việt:
Key policy là resource policy bắt buộc của mỗi key và là nguồn quyền gốc. IAM policy chỉ có hiệu lực nếu key policy có statement cho account root, tức ủy quyền cho IAM. Vì vậy một role có kms:Decrypt trong IAM vẫn bị từ chối nếu key policy không ủy quyền hoặc không nêu role đó trực tiếp. Ngoài hai lớp này còn grant do dịch vụ tạo, condition như kms:ViaService và kms:EncryptionContext, SCP và permission boundary. Với S3 SSE-KMS, người đọc cần cả s3:GetObject và kms:Decrypt; lỗi AccessDenied sau khi chuyển sang SSE-KMS gần như luôn là thiếu quyền trên key.
🇬🇧 English:
The key policy is the mandatory resource policy of each key and the root source of permissions. IAM policies only take effect if the key policy includes a statement for the account root, which delegates to IAM. So a role with kms:Decrypt in IAM is still denied if the key policy neither delegates nor names that role directly. Beyond those two layers there are grants created by services, conditions such as kms:ViaService and kms:EncryptionContext, SCPs and permission boundaries. With S3 SSE-KMS a reader needs both s3:GetObject and kms:Decrypt; an AccessDenied after switching to SSE-KMS is almost always a missing permission on the key.
Q3. Secrets Manager và Parameter Store chọn cái nào? Rotation password DB làm sao không gây lỗi?
🇻🇳 Tiếng Việt:
Secrets Manager cho secret cần rotation tự động, chia sẻ cross-account hoặc replicate region, nhất là password RDS; tốn khoảng 0,4 USD mỗi secret mỗi tháng. Parameter Store cho config, feature flag, dữ liệu phân cấp, và secret nhỏ không cần rotation, với Standard tier miễn phí. Rotation không gây lỗi khi em dùng alternating users để password của user đang dùng không bị đổi dưới chân app, app đọc lại secret định kỳ và cập nhật pool kết nối bằng cách rút dần kết nối cũ, hoặc dùng driver JDBC của Secrets Manager tự đọc lại khi auth fail. Nếu secret bơm qua env của ECS thì phải redeploy sau khi rotate, nên em ưu tiên đọc trực tiếp bằng SDK hoặc Spring Cloud AWS.
🇬🇧 English:
Secrets Manager is for secrets that need automatic rotation, cross-account sharing or regional replication, especially RDS passwords; it costs about 0.4 USD per secret per month. Parameter Store is for configuration, feature flags, hierarchical data and small secrets that do not need rotation, with the Standard tier free. Rotation does not cause errors when I use alternating users so the password of the user in use is never changed under the application, the app re-reads the secret periodically and updates the connection pool by softly evicting old connections, or I use the Secrets Manager JDBC driver that re-reads on authentication failure. If secrets are injected through ECS environment variables, a redeploy is required after rotation, so I prefer reading them directly through the SDK or Spring Cloud AWS.
Q4. Tự động rotate KMS key có làm dữ liệu cũ không đọc được không? Khi nào phải rotate thủ công?
🇻🇳 Tiếng Việt:
Không. KMS giữ mọi key material cũ trong cùng key ID, ciphertext cho biết material nào đã dùng, nên decrypt dữ liệu cũ vẫn chạy mà không cần re-encrypt và không đổi ARN. Material mới chỉ dùng cho mã hóa sau đó. Rotate thủ công, bằng cách tạo key mới và chuyển alias, khi key có material import từ ngoài hoặc nằm trong custom key store, vì KMS không tự rotate những key đó, hoặc khi chính sách yêu cầu dữ liệu cũ phải được mã hóa lại.
🇬🇧 English:
No. KMS keeps every previous key material under the same key ID, the ciphertext records which material was used, so decrypting old data still works without re-encryption and the ARN does not change. The new material is only used for subsequent encryption. Manual rotation, by creating a new key and moving the alias, is needed when the key has imported material or lives in a custom key store, because KMS does not rotate those automatically, or when policy requires old data to actually be re-encrypted.
Góc thi SAA-C03
Câu 1: Một công ty lưu hồ sơ khách hàng trong S3 và phải chứng minh cho kiểm toán viên ai đã giải mã dữ liệu, khi nào, đồng thời có khả năng thu hồi quyền truy cập toàn bộ dữ liệu ngay lập tức. Yêu cầu operational overhead THẤP NHẤT. Chọn gì?
A. SSE-S3 và bật S3 server access logging.
B. SSE-KMS với customer managed key; audit bằng CloudTrail; disable key khi cần thu hồi.
C. SSE-C với key do công ty tự quản lý và gửi mỗi request.
D. Client-side encryption với key lưu trong Parameter Store.
Đáp án: B. SSE-KMS với CMK: mỗi Decrypt ghi CloudTrail (ai, khi nào, context), disable key là kill switch tức thì cho mọi object. A: SSE-S3 không có audit theo key, không thu hồi được. C: công ty phải tự quản lý và phân phối key, overhead cao, không audit tập trung. D: tự làm mọi thứ, Parameter Store không phải nơi lưu key mã hóa dữ liệu, và không có kill switch.
Câu 2: Ứng dụng trên ECS Fargate kết nối RDS PostgreSQL. Compliance yêu cầu password DB thay đổi mỗi 30 ngày, tự động, và không được lưu trong code hay task definition dạng rõ. Giải pháp nào?
A. Lưu password trong Parameter Store SecureString và dùng EventBridge gọi Lambda đổi password mỗi 30 ngày.
B. Lưu password trong Secrets Manager với automatic rotation 30 ngày; task lấy secret qua tham chiếu secret trong task definition hoặc SDK.
C. Mã hóa password bằng KMS và đặt ciphertext trong biến môi trường; app gọi KMS Decrypt lúc khởi động.
D. Dùng IAM user với access key rotate 30 ngày để kết nối RDS.
Đáp án: B. Secrets Manager có rotation tích hợp cho RDS (managed hoặc Lambda template) và ECS tham chiếu secret bằng ARN. A làm được nhưng tự viết rotation là overhead và không phải dịch vụ sinh ra cho việc này. C không đổi password. D: IAM user không dùng để đăng nhập Postgres (đó là IAM DB auth với role, không phải access key), và access key dài hạn là anti-pattern.
Câu 3: Một ứng dụng xử lý hàng triệu object nhỏ trên S3 mã hóa bằng SSE-KMS. Hóa đơn KMS tăng mạnh và ứng dụng đôi khi bị throttle bởi KMS. Cách giảm chi phí và request KMS với THAY ĐỔI ÍT NHẤT?
A. Chuyển sang SSE-S3.
B. Bật S3 Bucket Keys trên bucket.
C. Tạo thêm nhiều customer managed key và chia object giữa chúng.
D. Xin tăng quota KMS.
Đáp án: B. S3 Bucket Keys dùng một key cấp bucket ngắn hạn để sinh data key cho object, giảm request KMS tới 99%, giữ nguyên SSE-KMS và audit. A mất audit/kiểm soát của KMS (thay đổi yêu cầu bảo mật). C phức tạp và quota KMS tính theo account/region nên không giúp nhiều. D chỉ giải quyết throttle, không giảm chi phí.
Cách nhớ ngắn gọn
- KMS: key không rời HSM; ≤ 4 KB trực tiếp; lớn hơn → envelope (GenerateDataKey → AES-GCM local → lưu encrypted data key).
- Key policy là gốc; IAM chỉ hiệu lực khi key policy ủy quyền root. SSE-KMS =
s3:GetObject+kms:Decrypt. - Rotate KMS: material mới, cũ giữ lại, không re-encrypt. Rotate secret: giá trị mới, app phải đọc lại; alternating users = zero downtime.
- Secrets Manager = rotation + tiền; Parameter Store = config + miễn phí. ACM = cert miễn phí cho ALB/CloudFront; CloudHSM = HSM riêng khi compliance ép.
- Hóa đơn KMS trên S3 → Bucket Keys; thu hồi toàn bộ dữ liệu → disable key.
| Từ khóa trong đề | Nghĩ tới |
|---|---|
| "audit who used the key", "control key policy", "disable key", "cross-account decrypt" | Customer managed KMS key, SSE-KMS |
| "minimal effort encryption at rest", "no key management" | SSE-S3 / AWS managed key |
| "company manages keys, AWS must not store them", key in request header | SSE-C |
| "encrypt before upload", "data never in plaintext in AWS" | Client-side encryption, AWS Encryption SDK |
| "large files" / "many records" + KMS | Envelope encryption, GenerateDataKey, data key caching |
| "reduce KMS request costs on S3" | S3 Bucket Keys |
| "rotate database credentials automatically" | Secrets Manager rotation |
| "store configuration", "hierarchical", "free", "SecureString" | SSM Parameter Store |
| "import own key material", "BYOK" | KMS EXTERNAL origin (manual rotation) |
| "dedicated single-tenant HSM", "FIPS 140-2 Level 3", "Oracle TDE" | CloudHSM |
| "TLS certificate for ALB/CloudFront, auto renewal" | ACM |
| "encrypt in one Region, decrypt in another" | Multi-Region KMS key |
| "existing unencrypted EBS/RDS must be encrypted" | Snapshot → copy with encryption → restore (không bật tại chỗ) |
Tự kiểm tra
Vì sao kms:Decrypt không cần truyền key ID khi dùng symmetric key?
Ciphertext do KMS tạo chứa metadata chỉ ra key (và material) đã mã hóa nó; KMS đọc ra và kiểm tra quyền trên key đó. Với asymmetric key thì phải truyền key ID. Truyền key ID với symmetric vẫn nên làm để chắc chắn đúng key.
Encryption context là gì, có bí mật không?
Cặp key=value không bí mật gắn vào lời gọi Encrypt/GenerateDataKey, phải truyền lại y nguyên khi Decrypt; ghi trong CloudTrail; dùng làm AAD để buộc ciphertext vào đúng ngữ cảnh (bản ghi, tenant), và dùng được trong condition policy kms:EncryptionContext:….
Sau khi bật SSE-KMS cho bucket, Lambda shop-image-resize ghi thumbnail bị lỗi. Cần quyền gì?
kms:GenerateDataKey (để S3 mã hóa object mới) và kms:Decrypt (để đọc ảnh gốc) trên alias/shop-key, được cho phép trong key policy hoặc qua ủy quyền IAM; cộng bucket policy nếu có điều kiện về header mã hóa.
Nhãn AWSCURRENT, AWSPENDING, AWSPREVIOUS của Secrets Manager nghĩa gì?
CURRENT: version app nên đọc. PENDING: version đang được rotation tạo/kiểm tra, chưa dùng. PREVIOUS: version trước đó, giữ để rollback. Rotation hoàn tất bằng việc chuyển nhãn CURRENT sang version mới (bước finishSecret).
Xóa KMS key có xóa ngay không? Dữ liệu mã hóa bằng nó ra sao?
Không; phải lên lịch 7–30 ngày (pending deletion), trong thời gian đó key không dùng được nhưng hủy xóa được. Sau khi xóa, mọi dữ liệu mã hóa bằng key đó (S3 SSE-KMS, EBS, RDS snapshot, secret) mất vĩnh viễn. Nên disable trước, theo dõi CloudTrail xem còn ai dùng, rồi mới lên lịch xóa.
🧪 Thực hành bài này: Intermediate: Ứng dụng HA: ALB + ASG + RDS + S3 + SQS (chưa triển khai) · lab labs/aws