Bảo Mật: IAM Least-Privilege, Throttling và WAF

K
Kai··6 min read·1 views

Sản phẩm chạy được và quan sát được, nhưng nhiều hàm vẫn cầm quyền rộng hơn việc chúng làm: bài 06 cấp cho mỗi hàm cả bộ đọc-ghi DynamoDB cho gọn. Trước khi mở ra cho người dùng thật, cần siết lại. Bài này thu quyền về đúng nhu cầu, đặt throttle chống lạm dụng, và bàn nơi cất bí mật cùng cách gắn WAF.

Mục tiêu

Thu IAM của từng hàm về đúng hành động nó cần (least-privilege), đặt throttle ở API Gateway, và bàn về Parameter Store cho bí mật và WAF cho HTTP API. Ta kiểm rằng siết quyền không làm hỏng chức năng, và quan sát hành vi gạt tải khi API bị dội. Không phát sinh chi phí đáng kể.

Least-privilege: cấp đúng hành động mỗi hàm cần

DynamoDBCrudPolicy mà ta dùng cho tiện cấp cả một bộ hành động: đọc, ghi, xóa, query, scan, batch. Nhưng mỗi hàm chỉ làm một phần nhỏ trong đó. Hàm tạo link chỉ cần PutItem. Hàm mở link chỉ cần GetItem. Hàm liệt kê chỉ cần Query. Hàm xóa chỉ cần DeleteItem. Nguyên tắc least-privilege nói: cấp đúng những hành động đó, trên đúng tài nguyên đó, không hơn. Nếu một hàm bị khai thác, kẻ tấn công chỉ làm được đúng phần quyền hẹp của nó.

Thay policy rộng bằng một Statement liệt kê hành động cụ thể. Hàm tạo link:

Policies:
  - Statement:
      - Effect: Allow
        Action: dynamodb:PutItem
        Resource: !GetAtt Table.Arn

Hàm mở link chỉ đọc:

Policies:
  - Statement:
      - { Effect: Allow, Action: dynamodb:GetItem, Resource: !GetAtt Table.Arn }
  - EventBridgePutEventsPolicy: { EventBusName: !Ref EventBus }

Hàm liệt kê chỉ query, và phải kể cả ARN của index vì query trên GSI là một tài nguyên riêng:

Policies:
  - Statement:
      - Effect: Allow
        Action: dynamodb:Query
        Resource:
          - !GetAtt Table.Arn
          - !Sub "${Table.Arn}/index/GSI1"

Hàm xóa chỉ DeleteItem. Aggregator phức tạp hơn vì nó cập nhật bộ đếm, ghi marker, query connection và đọc, nên nó cần đúng năm hành động UpdateItem, PutItem, GetItem, DeleteItem, Query trên bảng và index, vẫn là một danh sách hữu hạn rõ ràng thay vì cả bộ. Hai hàm WebSocket mỗi cái chỉ một hành động: connect ghi (PutItem), disconnect xóa (DeleteItem). Và state machine kiểm duyệt chỉ UpdateItem.

Kiểm: siết quyền không được làm hỏng việc

Cái bẫy của least-privilege là siết quá tay làm hỏng chức năng. Nên sau khi thu quyền và deploy, chạy lại đúng các thao tác để xác nhận chúng vẫn chạy:

=== FUNCTIONAL voi IAM da siet ===
  create  -> code=86Fgqtq
  resolve -> 301
  list    -> {"links":[{"code":"86Fgqtq","target":"https://aws.amazon.com...
  delete  -> {"deleted":"86Fgqtq"}

Cả bốn thao tác chạy đúng với quyền đã thu hẹp, nghĩa là mỗi hàm vẫn làm được việc của nó với đúng phần quyền tối thiểu. Đây là loại thay đổi đáng kiểm bằng test thật, vì một quyền thiếu chỉ lộ ra lúc chạy chứ template không báo lỗi.

Throttling: chống dội

API công khai cần một trần để một client (hay một kẻ tấn công) không dội đủ mạnh làm sập hay đội hóa đơn. HTTP API đặt throttle ngay ở tầng API qua route settings:

HttpApi:
  Type: AWS::Serverless::HttpApi
  Properties:
    DefaultRouteSettings:
      ThrottlingBurstLimit: 2
      ThrottlingRateLimit: 5

Kiểm lại trên stage thấy throttle đã áp:

$ aws apigatewayv2 get-stage --api-id "$APIID" --stage-name '$default' \
    --query 'DefaultRouteSettings'
{ "ThrottlingBurstLimit": 2, "ThrottlingRateLimit": 5.0 }

Một điểm cần nói thẳng về hành vi thật. API Gateway áp throttle theo kiểu best-effort, nên ở các giá trị rất nhỏ và lưu lượng giật cục, nó không cắt chính xác từng request. Khi dội 30 request gần như đồng thời vào hàm mở link, kết quả quan sát được là:

phan bo status:  10 301   20 503

Mười request qua, hai mươi bị từ chối với 503. Đáng chú ý là 503 này đến từ một lớp bảo vệ khác: giới hạn concurrency Lambda của tài khoản là 10 (gặp ở bài 06), nên chỉ mười môi trường chạy đồng thời, phần còn lại bị gạt. Tức là hệ thống có hai lớp chặn dội: throttle của API Gateway (trả 429 khi vượt rate) và giới hạn concurrency Lambda (trả 503 khi vượt số môi trường). Trên tài khoản concurrency thấp này, lớp 503 chạm trước. Điều quan trọng về mặt vận hành là tải thừa bị gạt thay vì kéo sập backend; muốn thấy 429 thuần từ API Gateway thì nâng quota concurrency rồi vượt rate dứt khoát hơn.

Bí mật: đừng để trong code hay biến môi trường thường

URL shortener hiện chưa có khóa bí mật nào, nhưng khi thêm (ví dụ khóa API của một dịch vụ quét link thật ở bài 12), đừng nhúng vào code hay đặt thẳng vào biến môi trường dạng chữ thường. Hai nơi đúng để cất là AWS Systems Manager Parameter Store (kiểu SecureString, mã hóa bằng KMS) cho cấu hình và bí mật đơn giản, và AWS Secrets Manager cho bí mật cần xoay vòng tự động. Hàm đọc giá trị lúc khởi tạo (ở tầng module, theo nguyên tắc bài 02), và IAM chỉ cấp quyền đọc đúng tham số đó. Cách này giữ bí mật khỏi mã nguồn, khỏi log, và khỏi những người chỉ có quyền xem cấu hình hàm.

WAF: phải đi vòng cho HTTP API

Web Application Firewall lọc các mẫu tấn công thường gặp (SQL injection, dò quét, IP xấu) trước khi request tới ứng dụng. Nhưng như đã ghi nhận ở bài 03, HTTP API không gắn AWS WAF trực tiếp được; chỉ REST API và CloudFront mới gắn được. Cách chuẩn cho một HTTP API là đặt một CloudFront distribution phía trước, gắn WAF web ACL vào CloudFront, rồi để CloudFront chuyển tiếp về HTTP API.

   client ─▶ CloudFront (gan WAF web ACL) ─▶ HTTP API ─▶ Lambda
                 │ chan SQLi, rate-based rule, IP block...

Series này không dựng CloudFront (nó nặng và lâu, và ta đang giữ hạ tầng gọn để nghiệm thu), nhưng đây là điểm phải biết khi đưa một HTTP API ra production có yêu cầu WAF: kiến trúc phải có CloudFront ở giữa, không phải gắn thẳng. Nếu bắt buộc gắn WAF thẳng vào API Gateway, đó là lý do để chọn REST API ngay từ đầu thay vì HTTP API.

🧹 Dọn dẹp

Các thao tác kiểm để lại link và user demo, đã xóa; throttle và IAM là cấu hình của stack nên giữ lại:

aws dynamodb scan --table-name url-shortener --query 'Items[].{PK:PK.S,SK:SK.S}' --output text | \
  while read pk sk; do aws dynamodb delete-item --table-name url-shortener \
    --key "{\"PK\":{\"S\":\"$pk\"},\"SK\":{\"S\":\"$sk\"}}"; done
aws cognito-idp admin-delete-user --user-pool-id "$POOL" --username sec@example.com

Tổng kết

Sản phẩm giờ siết hơn nhiều. Mỗi hàm chỉ cầm đúng hành động IAM nó cần, và phép kiểm xác nhận thu quyền không làm hỏng chức năng. API Gateway có throttle chống dội, và cùng với giới hạn concurrency Lambda, tải thừa bị gạt thay vì làm sập backend. Bí mật thuộc về Parameter Store hay Secrets Manager, không phải code. Và WAF cho HTTP API phải đi vòng qua CloudFront, một ràng buộc kiến trúc cần biết trước.

Phần V khép lại ở đây: sản phẩm đã quan sát được, tối ưu được, và siết bảo mật. Phần VI là vận hành vòng đời: bài sau dựng CI/CD để mỗi thay đổi đi qua build và deploy tự động có kiểm soát, với canary và rollback, thay cho việc gõ sam deploy bằng tay như suốt series tới giờ.