KMS (envelope encryption, key policy), Secrets Manager vs Parameter Store, rotation, ACM

5 câu hỏi~45 phútTừ khóa: KMS, customer managed key, AWS managed key, envelope encryption, GenerateDataKey, key policy vs IAM policy, grant, key rotation, multi-Region key, SSE-S3 / SSE-KMS / SSE-C, Secrets Manager rotation, Parameter Store SecureString, Spring Cloud AWS, ACM, CloudHSM, SAA: Domain 3 Secure

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 đề

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ĩaGhi nhớ
AWS owned keyKey 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ưuThấ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 + requestalias/shop-key
Symmetric vs asymmetric keySymmetric 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 MACEnvelope → symmetric; ký JWT → asymmetric
Key material originAWS_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 policyResource 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)
GrantQuyề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 conditionChỉ 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 encryptionKMS 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ã local1 lần gọi KMS cho N MB dữ liệu; key không rời KMS
Encryption contextCặ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 rotationKMS 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-encryptKhác với rotate secret
Multi-Region keyKey có cùng key ID/material ở nhiều region (primary + replica); mã hóa region A, giải mã region B; cho DR/Global TablesKhác với key thường (region-bound)
DeletionPhả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 ManagerParameter Store (SSM)
Dành choSecret: password DB, API key, cert private keyConfig: flag, URL, AMI ID; SecureString cho secret nhỏ
Rotation tự độngCó (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 callStandard miễn phí; Advanced ~0,05 USD/tham số/tháng + call
Kích thước64 KB4 KB (Standard), 8 KB (Advanced)
Cross-accountCó (resource policy)Có với Advanced (RAM share), hạn chế
Multi-regionReplica secretKhông tự động
Phân cấp / pathTên có / nhưng không query theo pathGetParametersByPath, policy theo prefix
Tích hợpRDS tạo secret tự động, ECS/Lambda/CloudFormation dynamic referenceECS/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-S3SSE-KMSSSE-CClient-side
Ai giữ keyS3KMS (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 decryptKhôngCloudTrailKhôngTùy
Kiểm soát riêng theo keyKhôngKey policy, ViaService, disable key = khóa toàn bucketBạnBạn
Chi phí thêm0Request KMS (giảm bằng Bucket Keys)0Compute
HeaderAES256aws:kms + key id…customer-algorithm/key/key-MD5Khô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:

KMSCloudHSMACM
Là gìManaged key service, multi-tenant HSMHSM single-tenant của bạn trong VPCCert TLS X.509
Tích hợp dịch vụ AWSHơn 100 dịch vụQua custom key store của KMS; SDK PKCS#11/JCEALB, CloudFront, API Gateway
Ai quản lý user keyAWS (IAM/key policy)Bạn hoàn toànAWS cấp & gia hạn
Giá~1 USD/key/tháng + requestTheo giờ mỗi HSM (đắt), không Free TierPublic cert miễn phí; Private CA tốn tiền
Đề thiMặ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.

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ự:

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ứcLàm gìKết quả
1Alternating 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
2App đọ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
3Dùng aws-secretsmanager-jdbc: khi gặp lỗi auth, driver đọc lại secret (có cache) và retry một lầnTự lành ngay cả khi reload chưa tới
4Bỏ password: RDS IAM auth qua RDS Proxy, hoặc proxy giữ secret và app chỉ auth với proxy bằng IAMKhô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

Chi phí và thời gian

Chuẩn bị

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

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 headerSSE-C
"encrypt before upload", "data never in plaintext in AWS"Client-side encryption, AWS Encryption SDK
"large files" / "many records" + KMSEnvelope 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