Chạy CSPM trên hơn chục AWS Landing Zone

Cách mình thiết kế CSPM engine nội bộ quét song song nhiều AWS Landing Zone bằng Prowler, lưu finding vào D1, artifact vào R2, dashboard duy nhất cho Security Operations.

· 11 phút đọc · Read in English

TL;DR

  • Pipeline enumerate → dispatch → scan → normalize với Prowler làm scanner, D1 giữ metadata, R2 giữ artifact thô (raw JSON, HTML report, screenshot).
  • Landing Zone là đơn vị vận hành chính, không phải account đơn lẻ — mỗi LZ có audit role riêng, SLA riêng, người phụ trách riêng.
  • Giới hạn ~3 account đồng thời mỗi Landing Zone: thử 10 song song thì gặp ThrottlingException AWS, hạ xuống 3 cho data tin cậy hơn dù chậm hơn.
  • Không dedup quá sớm — dedup theo finding_id đơn lẻ làm mất finding ở region khác; key dedup phải là cloud_provider + landing_zone_id + account_id + region + service + resource_id + control_id.
  • Severity ≠ rủi ro: cùng “Critical S3 public bucket” có thể là Low (chứa CSS tĩnh) hoặc Critical (chứa customer export với PII) — phải làm giàu finding bằng ngữ cảnh nghiệp vụ.
  • View hữu ích nhất là “Critical/High trên production, public-facing, có PII, tồn tại >7 ngày, chưa có người phụ trách” — không phải “tất cả finding critical”.
  • Hiện engine refresh account list mỗi 15 phút; hướng tiếp theo là event-driven từ AWS Organizations để phát hiện account/region mới gần thời gian thực.

Khi hệ thống AWS còn nhỏ, việc đánh giá security posture tương đối đơn giản: mở AWS Console, kiểm tra vài dịch vụ quan trọng, chạy một số lệnh bằng AWS CLI, hoặc dùng một tool như Prowler để xuất báo cáo.

Nhưng khi tổ chức bắt đầu vận hành hàng chục AWS account, chia thành nhiều Landing Zone theo business unit, môi trường, khu vực địa lý và nhóm người phụ trách khác nhau, bài toán không còn là “quét một account” nữa.

Bài toán thực tế trở thành:

Làm sao để biết toàn bộ AWS estate đang có posture như thế nào, account nào đang rủi ro nhất, finding nào cần xử lý trước, và ai chịu trách nhiệm khắc phục?

Bài viết này chia sẻ cách mình thiết kế một CSPM engine nội bộ để quét song song hơn chục AWS Landing Zone bằng Prowler, lưu finding vào Cloudflare D1, lưu artifact vào R2, và gom toàn bộ dữ liệu về một dashboard duy nhất phục vụ Security Operations.


Bối cảnh

Trong môi trường enterprise, rất hiếm khi toàn bộ workload chỉ nằm trong một AWS account.

Thông thường, AWS estate được tổ chức theo nhiều lớp:

  • Landing Zone theo business unit hoặc domain vận hành.
  • Account theo workload, môi trường hoặc người phụ trách.
  • Region theo độ trễ, đạt chuẩn hoặc footprint triển khai.
  • Baseline bảo mật khác nhau giữa production, staging, sandbox và shared services.
  • Người phụ trách khác nhau giữa nhóm cloud, nhóm app, nhóm nền tảng và nhóm bảo mật.

Ở quy mô nhỏ, việc chạy CSPM có thể chỉ là:

prowler aws

Nhưng ở quy mô doanh nghiệp, câu hỏi “posture hiện tại thế nào?” sẽ nhanh chóng vỡ ra thành nhiều câu hỏi nhỏ hơn:

  • Landing Zone nào có nhiều finding nghiêm trọng nhất?
  • Account nào đang lệch baseline nhiều nhất?
  • Finding nào xuất hiện lặp lại trên nhiều region?
  • Tài nguyên nào đang public-facing?
  • Finding nào liên quan dữ liệu nhạy cảm?
  • Nhóm nào chịu trách nhiệm xử lý?
  • Finding nào đã tồn tại nhiều ngày nhưng chưa được khắc phục?
  • Framework tuân thủ nào đang fail nhiều nhất?

Nếu không chuẩn hóa dữ liệu ngay từ đầu, dashboard CSPM rất dễ biến thành một nơi “đổ finding”, nhiều cảnh báo nhưng ít giá trị vận hành.


Mục tiêu thiết kế

Mình không bắt đầu bằng việc xây một “Wiz nội bộ” ngay lập tức.

Thay vào đó, mục tiêu ban đầu rất rõ ràng:

  1. Quét được nhiều AWS Landing Zone
  2. Quét song song nhiều account và nhiều region
  3. Chuẩn hóa finding về một schema chung
  4. Truy vấn nhanh theo Landing Zone, account, region, severity, framework và người phụ trách
  5. Tách metadata và artifact để tối ưu chi phí lưu trữ
  6. Có dashboard đủ tốt cho Security Operations sử dụng hằng ngày

Điểm quan trọng là hệ thống phải phục vụ được cả hai nhu cầu:

  • View kiểm toán / tuân thủ: đang pass/fail framework nào.
  • View rủi ro vận hành: finding nào cần khắc phục trước theo ngữ cảnh nghiệp vụ.

Kiến trúc tổng quan

Ở mức tổng quát, CSPM engine gồm một pipeline enumerate → dispatch → scan → normalize, rồi tách dữ liệu thành D1 (chỉ mục metadata) + R2 (artifact thô), với dashboard đọc từ D1 và drill-down sang R2 khi cần bằng chứng.

Pipeline CSPM cấp cao: AWS Landing Zones đi qua Enumerator → Dispatcher (~3 account song song) → Prowler workers → Normalizer, đầu ra tách thành Cloudflare D1 chỉ mục metadata và R2 artifact thô, dashboard bảo mật đọc từ D1 và drill-down vào R2 khi cần.


1. Enumerate: xác định phạm vi quét

Bước đầu tiên là xác định hệ thống cần quét những gì.

Thay vì hardcode từng AWS account, mình xem Landing Zone là đơn vị vận hành chính. Mỗi Landing Zone có thể có:

  • SSO Start URL riêng.
  • Danh sách account riêng.
  • Role dùng để kiểm toán riêng.
  • Phạm vi region riêng.
  • Người phụ trách riêng.
  • Lịch quét riêng.
  • SLA khắc phục riêng.

Ví dụ metadata đơn giản cho một Landing Zone:

{
  "landing_zone_id": "lz-retail-prod",
  "name": "Retail Production Landing Zone",
  "cloud": "aws",
  "sso_start_url": "https://example.awsapps.com/start",
  "audit_role_name": "SecurityAuditRole",
  "regions": ["ap-southeast-1", "us-east-1"],
  "owner": "retail-platform-team",
  "scan_enabled": true
}

Từ đó, engine enumerate ra danh sách account và region cần quét. Mỗi cặp (account, region) là một đơn vị quét:

Cấu trúc enumerate: một Landing Zone fan ra thành các Account (A, B, C), mỗi account lại enumerate ra các region (ap-southeast-1, us-east-1…) để tạo đơn vị quét. Không có danh sách account tĩnh, account mới được nhận diện ở lần enumerate tiếp theo.

Cách tiếp cận này giúp hệ thống không phụ thuộc vào một danh sách account tĩnh. Khi account mới được thêm vào Landing Zone, engine có thể phát hiện ở lần làm mới tiếp theo.


2. Dispatch: quét song song, nhưng có kiểm soát

Sai lầm phổ biến khi mở rộng quy mô CSPM là nghĩ rằng “càng song song càng nhanh”.

Thực tế, nếu quét quá nhiều account cùng lúc, AWS API rất dễ trả về throttling. Một số API dịch vụ có quota thấp hơn kỳ vọng, đặc biệt khi scanner gọi nhiều API trên nhiều region.

Do đó, mình thiết kế dispatcher theo hướng:

  • Mỗi account-region là một đơn vị quét.
  • Quét được chạy song song.
  • Có giới hạn đồng thời theo Landing Zone.
  • Có retry với backoff khi gặp throttling.
  • Có timeout cho từng scan job.
  • Có trạng thái rõ ràng: pending, running, succeeded, failed, partial.

Ví dụ logic dispatch:

Trạng thái dispatcher: trong Landing Zone A với max_concurrent_accounts=3, có 3 account đang running và 2 account pending. Khi một account xong, scheduler chọn account pending tiếp theo.

Ban đầu mình cho quét 10 account song song trong cùng một Landing Zone. Kết quả là quét nhanh hơn trên giấy, nhưng dữ liệu thiếu ổn định vì gặp nhiều lỗi ThrottlingException.

Sau khi giới hạn còn khoảng 3 account đồng thời mỗi Landing Zone, thời gian quét tổng thể có thể tăng nhẹ, nhưng độ ổn định cao hơn đáng kể.

Trong CSPM, quét nhanh là tốt. Nhưng quét nhanh mà dữ liệu không đáng tin thì dashboard không còn giá trị.


3. Normalize: chuẩn hóa finding về schema chung

Prowler cung cấp đầu ra khá đầy đủ, nhưng nếu muốn gom finding từ nhiều account, nhiều region, nhiều Landing Zone và nhiều framework vào một dashboard duy nhất, cần có một schema chung.

Mình chuẩn hoá finding về các nhóm trường chính:

  • Định danh: finding_id, scan_id, landing_zone_id, account_id, account_name, region, cloud_provider
  • Tài nguyên: resource_id, resource_type, resource_name, service
  • Bảo mật: severity, status, risk_score, risk_reason, remediation
  • Tuân thủ: framework, control_id, requirement_id
  • Chủ sở hữu: owner_team, business_unit, environment
  • Thời gian: first_seen_at, last_seen_at, resolved_at

Điểm quan trọng là không dedup quá sớm.

Ban đầu, mình dedup theo finding_id. Điều này làm dashboard gọn hơn, nhưng lại làm mất ngữ cảnh quan trọng.

Ví dụ cùng một lỗi cấu hình xuất hiện ở nhiều region: nếu dedup chỉ theo finding_id, dashboard gom thành một entry và kỹ sư chỉ sửa một region, region còn lại vẫn public. Key dedup phải chứa đủ ngữ cảnh:

So sánh phạm vi dedup: bên trái (quá hẹp) dedup theo finding_id làm một bucket ở eu-west-1 bị ẩn khỏi dashboard sau khi bucket ap-southeast-1 được liệt kê; bên phải (có ngữ cảnh) dùng key tổ hợp cloud_provider + landing_zone_id + account_id + region + service + resource_id + control_id nên cả hai bucket đều hiện, mỗi cái có người phụ trách và đường khắc phục riêng.

Cách này giúp finding vẫn giữ được độ chính xác khi drill down.


4. Render: dashboard cho Security Operations

Dashboard không chỉ để “xem finding”. Dashboard phải giúp nhóm bảo mật ra quyết định vận hành.

Các bộ lọc quan trọng gồm:

  • Landing Zone
  • Account ID
  • Tên account
  • Region
  • Severity
  • Risk score
  • Framework tuân thủ
  • Dịch vụ
  • Loại tài nguyên
  • Nhóm người phụ trách
  • Môi trường
  • Lần đầu thấy / lần cuối thấy
  • Trạng thái khắc phục

Một view hữu ích hơn cho Security Operations thường không phải là “tất cả finding nghiêm trọng”, mà là:

Finding Critical hoặc High trên account production, tài nguyên public-facing, có phân loại dữ liệu nhạy cảm, đã tồn tại hơn 7 ngày, và chưa có người phụ trách khắc phục.

Vì vậy, mình tách rõ hai lớp:

LớpÝ nghĩa
SeverityĐộ nghiêm trọng kỹ thuật từ scanner
Risk scoreMức rủi ro sau khi xét thêm ngữ cảnh nghiệp vụ

Ví dụ:

FindingSeverityNgữ cảnh nghiệp vụRisk
S3 bucket public chứa CSS tĩnhCriticalKhông chứa dữ liệu nhạy cảmLow
S3 bucket public chứa customer exportCriticalCó PII / dữ liệu khách hàngCritical
Security group mở SSH nội bộHighChỉ trong subnet private, có ZTNA kiểm soátMedium
IAM user có access key lâu ngàyMediumCó quyền admin trên productionCritical

Đây là điểm khác biệt lớn giữa một báo cáo scanner và một dashboard CSPM dùng được trong thực tế.


Vì sao dùng D1 và R2?

Mình tách dữ liệu thành hai loại:

Loại dữ liệuNơi lưu
Metadata, finding, trạng thái quét, chỉ mụcCloudflare D1
Prowler JSON thô, báo cáo HTML, file xuất, ảnh chụp màn hìnhCloudflare R2

D1 cho metadata

Finding là dữ liệu dạng hàng, có nhu cầu truy vấn/lọc liên tục.

Các truy vấn phổ biến gồm:

SELECT *
FROM findings
WHERE landing_zone_id = ?
  AND severity IN ('HIGH', 'CRITICAL')
  AND status = 'FAIL'
ORDER BY risk_score DESC, last_seen_at DESC;

D1 phù hợp cho lớp metadata vì:

  • Dữ liệu có cấu trúc.
  • Truy vấn theo nhiều chiều.
  • Dashboard đọc nhiều hơn ghi.
  • Không cần vận hành database riêng.
  • Tích hợp tốt với Cloudflare Workers.

R2 cho artifact

Báo cáo thô từ scanner không nên nhét hết vào database.

Các artifact như:

  • Prowler JSON thô
  • Báo cáo HTML
  • File CSV xuất
  • File bằng chứng
  • Ảnh chụp màn hình
  • Log debug

được lưu vào R2. D1 chỉ giữ con trỏ:

{
  "scan_id": "scan_20260428_001",
  "raw_json_url": "r2://cspm-artifacts/prowler/raw/scan_20260428_001.json",
  "html_report_url": "r2://cspm-artifacts/prowler/html/scan_20260428_001.html"
}

Cách này giúp database nhỏ, truy vấn nhanh, artifact lưu rẻ và dễ quản lý vòng đời.


Schema tối thiểu

Một schema tối thiểu cho CSPM engine có thể bắt đầu như sau:

CREATE TABLE landing_zones (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  cloud_provider TEXT NOT NULL,
  owner_team TEXT,
  scan_enabled INTEGER DEFAULT 1,
  created_at TEXT,
  updated_at TEXT
);

CREATE TABLE cloud_accounts (
  id TEXT PRIMARY KEY,
  landing_zone_id TEXT NOT NULL,
  account_id TEXT NOT NULL,
  account_name TEXT,
  environment TEXT,
  owner_team TEXT,
  status TEXT,
  created_at TEXT,
  updated_at TEXT,
  FOREIGN KEY (landing_zone_id) REFERENCES landing_zones(id)
);

CREATE TABLE scans (
  id TEXT PRIMARY KEY,
  landing_zone_id TEXT NOT NULL,
  account_id TEXT,
  region TEXT,
  scanner TEXT NOT NULL,
  status TEXT NOT NULL,
  started_at TEXT,
  finished_at TEXT,
  artifact_url TEXT,
  error_message TEXT,
  FOREIGN KEY (landing_zone_id) REFERENCES landing_zones(id)
);

CREATE TABLE findings (
  id TEXT PRIMARY KEY,
  scan_id TEXT NOT NULL,
  landing_zone_id TEXT NOT NULL,
  account_id TEXT NOT NULL,
  region TEXT,
  service TEXT,
  resource_id TEXT,
  resource_type TEXT,
  control_id TEXT,
  title TEXT,
  severity TEXT,
  status TEXT,
  risk_score INTEGER,
  risk_reason TEXT,
  framework TEXT,
  remediation TEXT,
  owner_team TEXT,
  first_seen_at TEXT,
  last_seen_at TEXT,
  resolved_at TEXT,
  FOREIGN KEY (scan_id) REFERENCES scans(id)
);

Schema này chưa phải cuối cùng, nhưng đủ để chạy được các trường hợp sử dụng cốt lõi:

  • Theo dõi quét
  • Kiểm kê finding
  • Bộ lọc dashboard
  • Ưu tiên rủi ro
  • Gán người phụ trách
  • Xu hướng theo thời gian

Ba bài học quan trọng

1. Không dedup finding quá sớm

Dedup giúp dashboard sạch hơn, nhưng nếu làm sai sẽ mất ngữ cảnh.

Finding phải được giữ theo đúng phạm vi, account + region + resource + control. Không nên chỉ dedup theo tên control hoặc finding ID.

Trong bảo mật cloud, cùng một lỗi nhưng khác account, khác region, khác tài nguyên thì tác động khắc phục hoàn toàn khác nhau.


2. Quét song song phải đi kèm kiểm soát tốc độ

Quét song song là bắt buộc nếu muốn mở rộng quy mô.

Nhưng song song không có nghĩa là bắn API không giới hạn.

Cần có các cơ chế:

  • Giới hạn đồng thời theo Landing Zone
  • Giới hạn đồng thời theo account
  • Retry với exponential backoff
  • Timeout cho từng job
  • Xử lý kết quả một phần
  • Trạng thái quét rõ ràng
  • Phân loại lỗi

Một CSPM engine tốt không chỉ biết quét thành công. Nó phải biết quét lỗi ở đâu, lỗi do credential, lỗi do throttling, lỗi do region bị tắt, hay lỗi do thiếu permission.


3. Severity không đồng nghĩa với rủi ro

Severity của scanner là tín hiệu kỹ thuật. Rủi ro là kết quả sau khi xét thêm ngữ cảnh nghiệp vụ.

Để dashboard thực sự hữu ích, cần làm giàu finding bằng các thông tin như:

  • Tài nguyên có public-facing không?
  • Có chứa dữ liệu nhạy cảm không?
  • Thuộc production hay non-production?
  • Có phơi ra internet không?
  • Có kiểm soát bảo mật bù trừ không?
  • Người phụ trách là nhóm nào?
  • Finding đã tồn tại bao lâu?
  • Có đường khai thác thực tế không?

Nếu không có lớp chấm điểm rủi ro, dashboard sẽ bị ngập trong finding critical/high nhưng không giúp đội vận hành biết nên khắc phục cái gì trước.


Kết quả đạt được

Sau khi triển khai hướng này, mình có được một dashboard CSPM tập trung với các khả năng cơ bản:

  • Xem posture theo Landing Zone.
  • Drill down theo account, region và dịch vụ.
  • Lọc finding theo severity, risk score, framework và người phụ trách.
  • Theo dõi trạng thái quét.
  • Lưu bằng chứng thô để phục vụ kiểm toán.
  • Ưu tiên khắc phục theo rủi ro nghiệp vụ thay vì chỉ theo severity kỹ thuật.

Quan trọng hơn, nhóm bảo mật không còn phải mở từng AWS Console hoặc từng báo cáo riêng lẻ để trả lời câu hỏi:

AWS estate hiện tại đang rủi ro ở đâu?


Điều vẫn còn mở

Hiện tại, engine làm mới danh sách account theo chu kỳ định kỳ, ví dụ mỗi 15 phút.

Cách này đủ tốt cho kiểm toán và rà soát posture, nhưng chưa đủ tốt nếu muốn phát hiện gần thời gian thực khi có Landing Zone, account hoặc region mới được thêm vào.

Hướng tiếp theo là chuyển sang mô hình hướng sự kiện hơn:

  • Phát hiện account mới từ sự kiện AWS Organizations.
  • Kích hoạt quét khi có account hoặc region mới.
  • Pipeline ingestion idempotent.
  • Tách điều phối quét khỏi ingestion finding.
  • Hỗ trợ đa đám mây với cùng schema: AWS, Azure, GCP.

Kết luận

Khi xây CSPM cho môi trường doanh nghiệp, vấn đề chính không chỉ là chọn scanner.

Prowler, ScoutSuite, Steampipe hay các scanner open-source khác đều có thể cung cấp finding. Phần khó hơn nằm ở lớp vận hành:

  • Phạm vi quét thế nào?
  • Chạy song song ra sao?
  • Chuẩn hóa finding thế nào?
  • Lưu metadata và artifact ở đâu?
  • Dedup theo logic nào?
  • Gán người phụ trách ra sao?
  • Ưu tiên khắc phục dựa trên gì?
  • Dashboard có giúp nhóm bảo mật ra quyết định không?

Với mình, hướng tiếp cận hiệu quả là:

  • Xem Landing Zone là đơn vị vận hành chính.
  • Dùng Prowler làm scanner ban đầu.
  • Lưu metadata vào D1.
  • Lưu artifact thô vào R2.
  • Chuẩn hóa finding về schema chung.
  • Xây thêm lớp chấm điểm rủi ro theo nghiệp vụ phía trên severity.

Đây chưa phải một CSPM hoàn chỉnh như các sản phẩm thương mại, nhưng là nền tảng đủ tốt để bắt đầu xây một CSPM nền tảng nội bộ có thể mở rộng theo account, region, Landing Zone và xa hơn là đa đám mây.