Tháng 11/2025, một developer update AMI template, vô tình bỏ tick “delete storage volumes on termination”. Hai tháng sau, bill AWS từ $1,000/tháng vọt lên $40,000 – hàng trăm EBS volume 100GB mồ côi tích tụ. AWS chỉ miễn 40%.

Cùng năm, một startup bị DDoS vào ASG không set max capacity. Kết quả: 2,000 instance m5.24xlarge spin up trong 72 giờ, bill $120,000. Một team khác deploy Lambda Friday evening, trigger S3 event set * thay vì prefix cụ thể, Lambda tự trigger chính nó qua log file – $47,283 trong một weekend.

Tháng 3/2026, một AWS Community Builder dùng Bedrock qua Project Mantle gặp lỗi billing: raw token count bị tính như 1K-token billing units, $46 thực tế thành $46,337. Billing data mất 7 ngày mới hiển thị trong Cost Explorer – nhảy từ $140 lên $56,000 trong một đêm, bypass hoàn toàn $100 budget alarm mà tác giả đã cấu hình.

Điểm chung của tất cả những incident này: không có billing alarm đa lớp, không có anomaly detection, không có kênh alert đa dạng.

Bài này setup 3 lớp bảo vệ cost cho AWS account – dựa trên chính những gì các team trên đã ước họ làm trước khi incident xảy ra.


  flowchart TB
    subgraph "3-Layer Cost Protection"
        L1["Layer 1: CloudWatch Billing Alarms<br/>Reactive · Threshold-based · 4 mức leo thang"]
        L2["Layer 2: AWS Budgets<br/>Proactive · Forecasted alerts · Budget Actions"]
        L3["Layer 3: Cost Anomaly Detection<br/>ML-driven · Pattern-based · Tự phát hiện spike"]
    end
    subgraph "Security Foundation"
        Root["Root User<br/>Hardware MFA (2 YubiKeys)"]
        IAM["IAM Admin<br/>Virtual MFA · dùng hàng ngày"]
    end
    SNS["SNS Fan-out → Email + Slack + PagerDuty"]
    L1 --> SNS
    L2 --> SNS
    L3 --> SNS
    Root -.->|"tạo + khóa"| IAM
```text

---

## Root user: chìa khóa vạn năng -- khóa nó lại

Root user là email + password bạn đăng ký AWS. Nó có quyền **vô hạn và không thể bị giới hạn**: xóa account, đóng billing, thay đổi support plan. **Không IAM policy nào, không SCP nào có thể chặn root** -- đây là design cố ý của AWS để đảm bảo bạn không bao giờ bị lock out hoàn toàn.

Chính vì vậy, root user là **mục tiêu số một của attacker**. RubyGems.org tháng 9/2025: attacker giành được root password, trong 4 phút enumerate toàn bộ IAM roles, lấy credential. Phát hiện 11 ngày sau vì không có CloudTrail alert cho root activity.

### MFA: 4 loại, 1 lựa chọn đúng cho root

| Loại MFA | Cơ chế | Chống phishing? | Dùng cho |
|----------|--------|-----------------|----------|
| **FIDO2 Security Key** (YubiKey, Google Titan) | Cryptographic challenge-response, verify origin website | **Có** -- key từ chối ký nếu domain không khớp | **Root user** |
| Hardware TOTP Token (Gemalto) | PIN + OTP hiển thị trên màn hình key fob | Không | Compliance (PCI-DSS yêu cầu hardware token) |
| Virtual MFA (Google Authenticator, Authy, 1Password) | TOTP 6 số, 30s rotation | Không -- OTP có thể bị phishing | IAM users hàng ngày |
| SMS MFA | OTP qua tin nhắn | Không -- dễ bị SIM-swap | **Đừng dùng cho ai cả** |

**Tại sao FIDO2 security key mạnh hơn virtual MFA?**

Virtual MFA sinh OTP 6 số mỗi 30 giây. Nếu attacker tạo trang login AWS giả, bạn nhập OTP vào trang giả → attacker có 30 giây để dùng OTP đó login vào AWS thật. Đây gọi là **real-time phishing attack**.

FIDO2 security key ký cryptographic challenge từ browser. Chữ ký này bao gồm **origin domain** (ví dụ `https://signin.aws.amazon.com`). Nếu bạn đang ở trang giả (`https://signin.aws-amazon.com`), key sẽ từ chối ký vì domain không khớp. Không có OTP nào để phishing.

### Quy trình setup root user

```bash
# 1. Tạo AWS account → nhận email verify + phone verify + credit card

# 2. Login root → IAM → Security credentials → Assign MFA device
#    Chọn "Security key" → cắm YubiKey → chạm để đăng ký
#    Làm lại với key backup thứ 2

# 3. Xóa tất cả access key của root (nếu lỡ tạo)
# Console: IAM → Security credentials → Root user → Access keys → Delete
# Không có CLI -- root user không phải IAM user, không dùng --user-name root được

# 4. Dùng password generator 20+ ký tự
#    Lưu trong password manager (1Password / Bitwarden)

# 5. Tạo IAM admin user -- từ giờ chỉ dùng user này
aws iam create-user --user-name admin
aws iam attach-user-policy \
  --user-name admin \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-login-profile \
  --user-name admin \
  --password 'Generated20CharPass!' \
  --password-reset-required

# 6. Enable virtual MFA cho IAM admin
#    Console → IAM → Users → admin → Security credentials → Assign MFA device
#    Chọn "Virtual MFA" → quét QR bằng Authy/1Password

# 7. Logout root → login bằng IAM admin
#    Từ giờ KHÔNG BAO GIỜ dùng root, trừ 4 exceptions bên dưới
```text

Root user **chỉ dùng cho 4 việc** (vài tháng hoặc vài năm một lần):

- Thay đổi billing/payment method
- Đóng AWS account
- Enable MFA Delete trên S3 bucket (cần root + MFA)
- Đăng ký AWS Support plan mới

### Break-glass procedure -- khi mất cả 2 YubiKey

```text
Tình huống: YubiKey chính bị mất, key backup trong safe ngân hàng (tối thứ 7)
→ Cần root access gấp để respond incident

Quy trình khẩn:
1. Gọi AWS Support (cần: credit card gốc + email gốc + photo ID)
2. AWS verify identity qua phone/video call (2-24 giờ)
3. AWS tạm thời disable MFA trên root
4. Login root → đăng ký MFA device mới NGAY LẬP TỨC
5. Review CloudTrail: có activity lạ trong thời gian mất key không?

Phòng tránh:
- 2 hardware key (primary + backup trong safe)
- 1 virtual MFA backup (điện thoại cũ, chỉ cài Authy, để trong safe)
- In backup code ra giấy, cất trong safe
```text

Tuyệt đối không tạo access key cho root user. Nếu key lộ trên GitHub (chuyện xảy ra mỗi ngày), attacker có toàn quyền account – không IAM policy nào chặn được root access key. IAM admin user cũng mạnh tương đương, nhưng ít nhất bạn có thể xóa nó, rotate key, hoặc restrict bằng SCP – còn root thì không.
--- ## Layer 1: CloudWatch Billing Alarm -- cái phao $0 Billing alarm miễn phí (10 alarm metrics trong Free Tier), mất 5 phút setup. Là thứ **rẻ nhất và ROI cao nhất** trên AWS. ### Tại sao billing alarm chỉ hoạt động ở us-east-1? AWS billing data được aggregate từ tất cả region về một central location. `AWS/Billing` CloudWatch namespace **chỉ tồn tại ở us-east-1**. Đây là AWS design decision từ khi CloudWatch mới ra mắt -- billing là global data, nhưng metric infrastructure lúc đó chỉ support single-region namespace. Đến nay vẫn vậy. Hệ quả: mọi billing alarm, budget, Cost Explorer setup đều phải thực hiện với `--region us-east-1`. ### Billing data delay -- cạm bẫy 6-8 giờ `EstimatedCharges` metric KHÔNG real-time. Nó cập nhật mỗi **6-8 giờ**. Điều này có nghĩa: - Bạn có thể đã tiêu $5,000 trong 3 giờ nhưng alarm chưa kêu - Bedrock billing bug $46K: mất **7 ngày** data mới xuất hiện trong Cost Explorer - Đây là lý do bạn cần **3 lớp bảo vệ**, không chỉ billing alarm ### Setup đầy đủ: 4 mức alarm + SNS fan-out ```bash # ===== BƯỚC 1: Enable billing alerts ===== # Console: Account → Billing Preferences → Receive CloudWatch Billing Alerts # Không có CLI tương đương -- phải dùng Console # ===== BƯỚC 2: Tạo SNS topic cho alert ===== TOPIC_ARN=$(aws sns create-topic \ --name billing-critical \ --region us-east-1 \ --query 'TopicArn' --output text) # Subscribe email aws sns subscribe \ --topic-arn $TOPIC_ARN \ --protocol email \ --region us-east-1 \ --notification-endpoint [email protected] # Subscribe Lambda (→ Slack #aws-cost) aws sns subscribe \ --topic-arn $TOPIC_ARN \ --protocol lambda \ --region us-east-1 \ --notification-endpoint arn:aws:lambda:us-east-1:123456789012:function:cost-to-slack # ===== BƯỚC 3: 4 mức alarm leo thang ===== MONTHLY_BUDGET=500 # Mức 1: 25% -- "FYI, đang tiêu bình thường" aws cloudwatch put-metric-alarm --region us-east-1 \ --alarm-name "billing-25pct-info" \ --metric-name EstimatedCharges --namespace AWS/Billing \ --statistic Maximum --period 21600 --evaluation-periods 1 \ --threshold $((MONTHLY_BUDGET * 25 / 100)) \ --comparison-operator GreaterThanThreshold \ --dimensions Name=Currency,Value=USD \ --alarm-actions $TOPIC_ARN # Mức 2: 50% -- "Cần chú ý, kiểm tra Cost Explorer" aws cloudwatch put-metric-alarm --region us-east-1 \ --alarm-name "billing-50pct-warning" \ --metric-name EstimatedCharges --namespace AWS/Billing \ --statistic Maximum --period 21600 --evaluation-periods 1 \ --threshold $((MONTHLY_BUDGET * 50 / 100)) \ --comparison-operator GreaterThanThreshold \ --dimensions Name=Currency,Value=USD \ --alarm-actions $TOPIC_ARN # Mức 3: 80% -- "Nguy hiểm, kiểm tra ngay!" aws cloudwatch put-metric-alarm --region us-east-1 \ --alarm-name "billing-80pct-critical" \ --metric-name EstimatedCharges --namespace AWS/Billing \ --statistic Maximum --period 21600 --evaluation-periods 1 \ --threshold $((MONTHLY_BUDGET * 80 / 100)) \ --comparison-operator GreaterThanThreshold \ --dimensions Name=Currency,Value=USD \ --alarm-actions $TOPIC_ARN # Mức 4: 100% -- "Vượt ngân sách! Hành động khẩn!" aws cloudwatch put-metric-alarm --region us-east-1 \ --alarm-name "billing-100pct-emergency" \ --metric-name EstimatedCharges --namespace AWS/Billing \ --statistic Maximum --period 21600 --evaluation-periods 1 \ --threshold $MONTHLY_BUDGET \ --comparison-operator GreaterThanThreshold \ --dimensions Name=Currency,Value=USD \ --alarm-actions $TOPIC_ARN ```text ### Per-service alarms: biết chính xác service nào đốt tiền Tổng bill $500 không nói lên điều gì. Per-service alarm cho biết **EC2 hay NAT Gateway đang là thủ phạm**: ```bash # EC2-specific aws cloudwatch put-metric-alarm --region us-east-1 \ --alarm-name "EC2-Bill-Exceeds-200" \ --metric-name EstimatedCharges --namespace AWS/Billing \ --statistic Maximum --period 21600 --evaluation-periods 1 \ --threshold 200 --comparison-operator GreaterThanThreshold \ --dimensions Name=ServiceName,Value=AmazonEC2 Name=Currency,Value=USD \ --alarm-actions $TOPIC_ARN # Tương tự: AmazonRDS, AmazonS3, AWSLambda, AmazonDynamoDB, AWSDataTransfer, AmazonCloudWatch # NATGateway, AmazonSageMaker, AmazonBedrock -- những service ĐẮT nhất ```text ### Giải thích các parameter - `--period 21600` (6 giờ = 21,600 giây): Billing data cập nhật mỗi ~6 giờ, poll nhanh hơn vô ích, chỉ tốn cost - `--statistic Maximum`: `EstimatedCharges` là cumulative metric -- nó chỉ tăng trong billing cycle, không giảm. Maximum = giá trị mới nhất (current running total) - `--evaluation-periods 1`: Alarm kích hoạt sau 1 datapoint vượt threshold (không cần chờ confirm) - `--dimensions Name=Currency,Value=USD`: Bắt buộc -- mọi billing metric đều là USD ### SNS fan-out: đừng chỉ gửi email Email alert dễ bị ignore (100+ email/ngày, alert chìm trong inbox). Pattern đúng: **SNS fan-out đến nhiều channel với severity khác nhau**: ```text SNS Topic (billing-critical) ├── Lambda → Slack #aws-cost-alerts (cả team thấy) ├── AWS Chatbot → Microsoft Teams ├── Lambda → PagerDuty (critical alert → page oncall) └── Email subscription (backup, finance team) ```text ```typescript // Lambda: forward billing alert → Slack import { SNSHandler } from "aws-lambda"; import { WebClient } from "@slack/web-api"; const slack = new WebClient(process.env.SLACK_BOT_TOKEN!); export const handler: SNSHandler = async (event) => { const message = JSON.parse(event.Records[0].Sns.Message); const newState = message.NewStateValue; // "ALARM" hoặc "OK" if (newState === "ALARM") { await slack.chat.postMessage({ channel: "#aws-cost-alerts", text: [ `:rotating_light: *AWS Billing Alert: $${message.NewStateReason}*`, `Threshold: ${message.Trigger.MetricName}`, `Time: ${message.StateChangeTime}`, `Dashboard: <https://console.aws.amazon.com/cloudwatch/...|CloudWatch>`, ].join("\n"), }); } }; ```text --- ## Layer 2: AWS Budgets -- proactive, không reactive CloudWatch alarm chỉ nhìn **actual spend**. Budgets nhìn **forecasted spend** -- dự đoán cuối tháng bạn sẽ tiêu bao nhiêu dựa trên run rate hiện tại. **Forecasted alert là tính năng quan trọng nhất** -- nó có thể kích hoạt vào ngày 10 của tháng (khi bạn mới tiêu 30% budget nhưng run rate dự đoán sẽ vượt), cho bạn 20 ngày để điều chỉnh. ```bash aws budgets create-budget \ --account-id 123456789012 \ --budget '{ "BudgetName": "monthly-500-usd", "BudgetLimit": {"Amount": "500", "Unit": "USD"}, "TimeUnit": "MONTHLY", "BudgetType": "COST" }' \ --notifications-with-subscribers '[ { "Notification": { "NotificationType": "FORECASTED", "ComparisonOperator": "GREATER_THAN", "Threshold": 80, "ThresholdType": "PERCENTAGE" }, "Subscribers": [ {"SubscriptionType": "EMAIL", "Address": "[email protected]"}, {"SubscriptionType": "SNS", "Address": "'$TOPIC_ARN'"} ] }, { "Notification": { "NotificationType": "ACTUAL", "ComparisonOperator": "GREATER_THAN", "Threshold": 100, "ThresholdType": "PERCENTAGE" }, "Subscribers": [ {"SubscriptionType": "EMAIL", "Address": "[email protected]"} ] } ]' ```text ### Budget Actions: tự động phản ứng ```bash # Khi forecast vượt 100% budget → tự động deny tạo resource mới aws budgets create-budget-action \ --budget-name monthly-500-usd \ --action-type APPLY_IAM_POLICY \ --action-threshold '{"Value": 100, "Type": "PERCENTAGE"}' \ --definition '{ "PolicyArn": "arn:aws:iam::123456789012:policy/deny-new-resources" }' \ --execution-role-arn arn:aws:iam::123456789012:role/budget-action-execution \ --notification-type FORECASTED ```text
Budget Actions mạnh tay cho non-production, thận trọng cho production. Dev account: auto-stop EC2 instances, deny tạo resource mới khi vượt budget 100%. Production: chỉ gửi alert, không tự động action – bạn không muốn budget action tắt production app lúc 3h sáng.
--- ## Layer 3: Cost Anomaly Detection -- ML phát hiện spike Fixed threshold (25%, 50%, 80%) không phát hiện được **spike bất thường**. Cost Anomaly Detection dùng ML để phát hiện pattern lạ so với baseline lịch sử: ```bash aws ce create-anomaly-monitor \ --anomaly-monitor '{ "MonitorName": "daily-cost-spike", "MonitorType": "DIMENSIONAL", "MonitorDimension": "SERVICE" }' aws ce create-anomaly-subscription \ --anomaly-subscription '{ "SubscriptionName": "alert-on-spike", "Frequency": "IMMEDIATE", "Threshold": 100, "Subscribers": [ {"Type": "EMAIL", "Address": "[email protected]"}, {"Type": "SNS", "Address": "'$TOPIC_ARN'"} ] }' ```text **Hạn chế:** Cần ~10 ngày data cho service/account mới. Trong 2 tuần đầu sau khi mở account hoặc launch service mới, dựa vào Budgets + CloudWatch alarms. --- ## Cost Explorer: drill-down khi alarm kêu Mất ~24 giờ sau khi enable mới có data đầu tiên. ```bash aws ce get-cost-and-usage \ --time-period Start=2026-06-01,End=2026-06-21 \ --granularity DAILY \ --metrics "UnblendedCost" \ --group-by Type=DIMENSION,Key=SERVICE \ --filter '{"Dimensions": {"Key": "REGION", "Values": ["ap-southeast-1"]}}' ```text ```mermaid flowchart TD Bill["Charges tháng này: $580"] EC2["EC2: $210<br/>g4dn.xlarge quên tắt"] NAT["NAT Gateway: $96<br/>3 cái × $32/tháng"] S3["S3: $134<br/>bucket public bị leech"] RDS["RDS: $95<br/>db.r5.large thay vì t4g"] Lambda["Lambda: $5"] Other["Khác: $40"] Bill --> EC2 Bill --> NAT Bill --> S3 Bill --> RDS Bill --> Lambda Bill --> Other ```text --- ## Free Tier: thay đổi lớn từ 07/2025 **Tài khoản tạo từ 15/07/2025:** Credit-based. **$100 upfront** tự động + kiếm thêm tối đa **$100** qua 5 guided activities (launch EC2, configure RDS, build Lambda web app, submit Bedrock prompt, set up Budgets). Tổng tối đa **$200 credits**. **Tài khoản tạo trước 15/07/2025:** Giữ nguyên 3-tier cũ: | Loại | Thời hạn | Ví dụ quan trọng | |------|----------|-----------------| | **Always Free** | Không giới hạn | Lambda 1M requests/tháng, DynamoDB 25GB, S3 5GB, CloudWatch 10 metrics | | **12-Month Free** | 12 tháng từ khi tạo | EC2 t2.micro 750h/tháng, RDS db.t2.micro 750h/tháng, 30GB EBS | | **Short-Term Trial** | 30-90 ngày | SageMaker 250h, Redshift 750h/tháng (2 tháng), GuardDuty 30 ngày | ### 3 cái bẫy Free Tier 1. **NAT Gateway KHÔNG trong Free Tier** -- $32/tháng. Nhiều người nghĩ "Free Tier" rồi bật NAT Gateway → ngạc nhiên bill $32 2. **EC2 t2.micro hết 12 tháng → tự động charge on-demand** (~$72/tháng nếu chạy 24/7). **Không có cảnh báo trước** 3. **RDS backup storage vượt allocated** → charge thêm. Free Tier cho 20GB backup, DB 30GB → backup vượt --- ## Checklist 30 phút cho account mới ```mermaid flowchart LR subgraph "1-5" A1["Tạo account"] --> A2["Hardware MFA root"] end subgraph "5-10" A2 --> B1["Xóa access key root"] B1 --> B2["Tạo IAM admin + MFA"] end subgraph "10-20" B2 --> C1["Enable billing alerts"] C1 --> C2["SNS topic + fan-out"] C2 --> C3["4 mức alarm"] end subgraph "20-30" C3 --> D1["Budget + forecasted alert"] D1 --> D2["Cost Explorer + Anomaly Detection"] end ```text 1. Tạo account, verify phone + credit card 2. **Hardware MFA cho root** (2 YubiKeys -- 1 backup) 3. Password 20+ ký tự → password manager 4. **Xóa tất cả access key của root** 5. Tạo IAM admin → `AdministratorAccess` → virtual MFA 6. **Logout root, login IAM admin** 7. Region → **us-east-1** 8. Enable billing alerts 9. SNS topic → email + Lambda (→ Slack) 10. 4 mức billing alarm: 25%, 50%, 80%, 100% 11. Per-service alarms: EC2, RDS, Lambda, NAT GW 12. Budget + FORECASTED 80% alert 13. Cost Explorer + Cost Anomaly Detection 14. Calendar reminder: Free Tier hết hạn (12 tháng) --- - **3 lớp bảo vệ cost**: CloudWatch Billing Alarms (threshold) + AWS Budgets (forecast) + Cost Anomaly Detection (ML spike detection). Mỗi lớp bắt một kiểu failure khác nhau - **Root user = hardware MFA (2 keys), không access key, không dùng hàng ngày**. IAM admin + virtual MFA cho mọi việc - **Billing alarm 4 mức (25/50/80/100%)** -- mỗi mức route đến channel khác nhau (email, Slack, PagerDuty) - **Billing data delay 6-8 giờ** (có thể lên đến 7 ngày trong edge case) -- đừng rely vào một lớp duy nhất - **Free Tier thay đổi 07/2025** -- credit-based cho tài khoản mới. Đánh dấu calendar 12 tháng nếu dùng 3-tier cũ Bài sau: [Phần 2: IAM user, group, role, policy -- least privilege](/posts/aws/02-iam-user-group-role-policy/) ## Câu hỏi hay gặp **Q: Tôi có cần hardware MFA (YubiKey $50) hay virtual MFA đủ?** A: Với root user, hardware MFA là **best practice 2025-2026** vì chống phishing. YubiKey giá $25-50, rẻ hơn 1 phút downtime production. Với IAM user hàng ngày, virtual MFA là đủ. **Q: Billing alarm có guarantee tôi không bị bill shock không?** A: **Không.** Billing data delay 6-8 giờ, edge case lên đến 7 ngày (Bedrock Mantle incident). Billing alarm là lớp 1. Cần Budgets (lớp 2) + Anomaly Detection (lớp 3) + per-service alarm. **Q: Cost Anomaly Detection khác gì Budget forecast?** A: Budget forecast dựa trên **run rate tuyến tính** (tiêu $X/ngày × 30 ngày). Anomaly Detection dùng **ML pattern matching** -- phát hiện spike đột ngột mà run rate không dự đoán được (DDoS → 2,000 instances trong 3 giờ).