TL;DR
- Cloudflare Access là Zero Trust gateway cho web/API admin — browser-based, JWT cookie, device posture, đặt trước bất kỳ origin (Worker, EC2, on-prem). Không phải IdP, nó federate xuống Okta/Entra/Google.
- AWS IAM Identity Center (IDC) là AWS-native SSO để dev/admin login console + lấy
aws sso logincho CLI/SDK. Cấp temporary credential qua permission set, không phải long-lived access key.- Đừng cố unify trong một IdP duy nhất tự build. Pattern thực dụng: Okta/Entra ID là IdP gốc → federate xuống cả hai qua SAML/OIDC + SCIM provisioning. Mỗi tool làm việc mình giỏi.
- Per-permission-set trong IDC (
AdministratorAccess,BillingViewer,DeveloperPowerUser); per-app policy trong Access (require email ends_with @company.com AND group = engineering AND posture = healthy).- Audit log không chung — IDC log vào CloudTrail, Access log vào Cloudflare Logs. Correlate qua
userIdtrong IdP (Oktauser.id, EntraobjectId) là cách duy nhất để build “session timeline” cross-cloud.- Cost: IDC free trong AWS. Access free tier 50 user; Zero Trust Standard $7/user/tháng cho posture + SCIM. Okta khoảng $6-8/user/tháng. Tổng cho 50 user khoảng $700/tháng — rẻ hơn nhiều so với build identity layer riêng.
Tại sao tôi không unify identity layer
Đây là sai lầm mà ba team trong vòng hai năm qua tôi đã thấy lặp lại: cố build một identity layer “one ring to rule them all”. Họ pick một IdP (Okta hoặc Entra), rồi viết logic để gate cả AWS console + Cloudflare dashboard + admin web app + Kubernetes + database access. Sáu tháng sau: code custom 5000 dòng, ai owner cũng không rõ, audit fail vì không trace được session qua các stack.
Lý do là Cloudflare Access và AWS IAM Identity Center giải quyết hai bài toán khác nhau, không phải hai implementation của cùng một bài toán:
- AWS IDC chỉ care AWS — permission set là tổ hợp IAM policy, temporary credential phát qua STS. Bên ngoài AWS nó không nói gì.
- Cloudflare Access care everything-but-AWS-console — gate web app, gate Worker, gate SSH qua cloudflared, gate self-hosted Grafana. Nhưng nó không cấp AWS credential — bạn không thể
aws sso loginqua Access.
Khi tôi setup cloudsecop.net + Worker API + AWS Bedrock backend, tôi không cố unify. Tôi để Access gate /admin route trên Worker, để IDC gate AWS console + cấp temp credential cho dev local. Cả hai SSO từ cùng Okta workspace nên user chỉ login một lần (Okta cookie cached). Identity object trong Okta là source of truth — mọi tool còn lại consume.
Setup chuẩn: Okta là root, federate xuống cả hai
Kiến trúc đề nghị:
┌──────────────┐
│ Okta │ ← Source of truth: user, group, MFA
│ (hoặc Entra) │
└──────┬───────┘
│
┌────────────┴────────────┐
│ SAML 2.0 / OIDC + SCIM │
│ │
┌─────▼──────┐ ┌───────▼──────┐
│ AWS IDC │ │ Cloudflare │
│ (Identity │ │ Access │
│ source) │ │ │
└─────┬──────┘ └───────┬──────┘
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ AWS │ │ Worker API │
│ Console + │ │ Admin web │
│ CLI temp │ │ SSH/RDP via │
│ creds │ │ cloudflared │
└───────────┘ └─────────────┘
Tách rõ: Okta đảm bảo engineer@company.com là một identity duy nhất. AWS IDC nhận identity đó qua SCIM, map vào permission set. Cloudflare Access nhận identity đó qua SAML, evaluate per-app policy.
AWS IAM Identity Center — permission set + temp credential
IDC khác IAM user truyền thống ở chỗ: không có long-lived access key. Dev login qua browser, aws sso login cấp temp credential ~1h. Khi expire, refresh tự động. Đây là cách đúng để giải bài “IAM key 90 ngày rotate” — không có key thì không cần rotate.
Terraform setup (giả định đã connect Okta qua SCIM):
# Permission set: power user cho dev environment
resource "aws_ssoadmin_permission_set" "dev_power_user" {
instance_arn = local.idc_instance_arn
name = "DevPowerUser"
description = "Read/write dev account, no IAM, no billing"
session_duration = "PT4H" # 4h temp cred, force re-auth
tags = {
Owner = "platform-team"
}
}
resource "aws_ssoadmin_managed_policy_attachment" "dev_power_user" {
instance_arn = local.idc_instance_arn
managed_policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
permission_set_arn = aws_ssoadmin_permission_set.dev_power_user.arn
}
# Inline policy: deny IAM mutate even though PowerUserAccess allows
resource "aws_ssoadmin_permission_set_inline_policy" "dev_no_iam" {
instance_arn = local.idc_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.dev_power_user.arn
inline_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Deny"
Action = ["iam:*", "sso:*", "organizations:*"]
Resource = "*"
}]
})
}
# Assignment: Okta group "engineering-dev" → dev account → DevPowerUser
resource "aws_ssoadmin_account_assignment" "dev_engineering" {
instance_arn = local.idc_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.dev_power_user.arn
principal_id = data.aws_identitystore_group.engineering_dev.group_id
principal_type = "GROUP"
target_id = local.dev_account_id
target_type = "AWS_ACCOUNT"
}
Login flow:
# Một lần — config
aws configure sso
# SSO start URL: https://company.awsapps.com/start
# SSO region: us-east-1
# Mỗi 4h
aws sso login --profile dev-power-user
# Dùng như IAM user thông thường
aws s3 ls --profile dev-power-user
session_duration = "PT4H" là sweet spot — đủ để không phá flow làm việc, đủ ngắn để limit blast radius nếu laptop bị compromise.
Cloudflare Access — per-app policy với device posture
Phía Access, cấu hình khác hoàn toàn. Không phải “user assign permission set” mà là “app gate by policy”. Mỗi app (Worker route, self-hosted service) gắn với một application trong Access dashboard.
# Cloudflare Access application: admin route của Worker
resource "cloudflare_zero_trust_access_application" "admin_api" {
zone_id = var.zone_id
name = "khavan-admin-api"
domain = "admin.khavan.dev"
type = "self_hosted"
session_duration = "8h"
cors_headers = [{
allowed_methods = ["GET", "POST"]
allowed_origins = ["https://khavan.dev"]
allow_credentials = true
}]
}
# Policy 1: engineering group + healthy posture + email domain
resource "cloudflare_zero_trust_access_policy" "admin_engineering" {
application_id = cloudflare_zero_trust_access_application.admin_api.id
zone_id = var.zone_id
name = "engineering-with-posture"
precedence = 1
decision = "allow"
include {
okta {
identity_provider_id = var.okta_idp_id
name = ["engineering"]
}
}
require {
email_domain = ["company.com"]
}
require {
device_posture = [
cloudflare_zero_trust_device_posture_rule.disk_encrypted.id,
cloudflare_zero_trust_device_posture_rule.os_updated.id,
]
}
}
# Posture rule: disk phải encrypted
resource "cloudflare_zero_trust_device_posture_rule" "disk_encrypted" {
account_id = var.account_id
name = "disk-encrypted"
type = "disk_encryption"
input {
require_all = true
}
}
Khác biệt với IDC: policy là evaluation tree (include / exclude / require), không phải static role binding. Bạn có thể nói “allow nếu thuộc engineering group AND (laptop encrypted OR đang ở văn phòng VN IP)”. IDC không làm được kết hợp như vậy.
Trong Worker code, verify JWT mà Access ký:
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://company.cloudflareaccess.com/cdn-cgi/access/certs")
);
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (!url.pathname.startsWith("/admin")) {
return fetch(request);
}
const token = request.headers.get("Cf-Access-Jwt-Assertion");
if (!token) return new Response("Unauthorized", { status: 401 });
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://company.cloudflareaccess.com",
audience: env.ACCESS_AUD, // configured per-application
});
// payload.email, payload.identity_nonce, payload.custom (Okta groups)
console.log("[access] authenticated", payload.email);
return fetch(request);
} catch (e) {
return new Response("Invalid Access JWT", { status: 401 });
}
},
};
Cf-Access-Jwt-Assertion được Access tự inject vào header sau khi user pass policy. Worker chỉ verify signature + audience claim. Trust chain là Cloudflare-signed JWT.
SCIM provisioning — bắt buộc cho ≥ 20 user
Manual provision (tạo user trong Okta xong tạo lại trong AWS IDC) chỉ khả thi ở 5-10 user. Sau đó SCIM là bắt buộc.
Okta (source) → SCIM 2.0 push → AWS IDC (consumer)
↘ SCIM 2.0 push → Cloudflare One (consumer)
Setup SCIM trong Okta:
- AWS IDC: enable “Automatic provisioning” trong Settings → Identity source. Lấy SCIM endpoint URL + bearer token.
- Trong Okta admin: Applications → AWS IDC app → Provisioning tab → enable “Push New Users”, “Push Profile Updates”, “Push Groups”.
- Cloudflare: Zero Trust → Settings → Authentication → SCIM. Tương tự lấy endpoint + token. Trong Okta tạo app thứ hai “Cloudflare Access SCIM”.
Khi user offboard:
- Disable trong Okta → SCIM push deactivate xuống cả hai.
- AWS IDC: user mất quyền permission set (nhưng identity vẫn lưu trong identity store).
- Cloudflare Access: user fail mọi policy có Okta group.
- AWS temp credential đã cấp trước đó? Vẫn valid tới khi expire (max session duration, thường 4-12h). Nếu cần force revoke ngay, dùng
aws sso-admin delete-account-assignment— nhưng đó là extreme case.
Quy trình offboard chuẩn mà tôi recommend:
- HR mark “termination effective
” trong Okta. - Okta workflow disable account tự động vào ngày đó.
- SCIM push xuống AWS IDC + Cloudflare → user lock out trong ≤ 5 phút.
- AWS CloudTrail + Cloudflare audit log xác nhận không có session active.
- Nếu là privileged role (admin, security), force revoke session bằng cách rotate access token:
aws sso-admin delete-account-assignmentcho permission set quan trọng nhất.
Audit log correlation — gắn session qua hai cloud
Đây là phần khó nhất và là vì sao tôi không unify. CloudTrail và Cloudflare Logs không có schema chung. Trick: dùng IdP user ID làm anchor.
Okta user.id là 20-char string (ví dụ 00u1abc2def3ghi4jklm). Cả AWS IDC SAML assertion và Cloudflare Access JWT đều carry value này (qua NameId hoặc custom claim).
-- CloudTrail Athena: session khởi tạo bởi user Okta ID
SELECT
eventTime,
eventName,
userIdentity.userName,
requestParameters.principalArn
FROM cloudtrail_logs
WHERE userIdentity.sessionContext.sessionIssuer.userName LIKE '%00u1abc2def3ghi4jklm%'
OR userIdentity.userName = 'engineer@company.com'
AND eventTime > date_add('day', -7, current_date)
ORDER BY eventTime;
Phía Cloudflare, log Access events qua Logpush vào R2 hoặc S3, query qua Athena/Workers Analytics:
-- Access log: user truy cập admin app trong 7 ngày
SELECT
EventTimestamp,
UserEmail,
AppDomain,
Allowed,
Country,
ConnectionType
FROM access_requests
WHERE UserEmail = 'engineer@company.com'
AND EventTimestamp > now() - INTERVAL '7' DAY
ORDER BY EventTimestamp DESC;
Build dashboard tổng hợp: stream cả hai vào một bảng D1 hoặc Datadog với column okta_user_id. Lúc đó incident response có thể “show me everything user X did in 24h trước incident” cross-cloud.
So sánh tổng — chọn cái nào cho use case nào
| Use case | AWS IDC | Cloudflare Access |
|---|---|---|
| AWS console login | Có (chính) | Không hỗ trợ |
| AWS CLI temp credential | Có (aws sso login) | Không |
| Gate web admin app | Không | Có (chính) |
| Gate Worker route | Không | Có (qua application config) |
| Gate self-hosted Grafana/Jenkins | Không | Có (qua cloudflared) |
| SSH bastion | Không native | Có (cloudflared SSH) |
| Device posture (disk encrypt, OS version) | Không | Có |
| Per-app session timeout | Permission set level | Application level |
| MFA enforcement | Qua IdP | Qua IdP + Access policy |
| Audit log | CloudTrail | Cloudflare Logs / Logpush |
| Cost (50 user) | Free | ~$350/tháng (Zero Trust Standard) |
| SCIM provisioning | Có | Có (Zero Trust Standard+) |
| Customer of last resort khi IdP down | Root user + emergency role | Service token (per-app) |
Pattern thực tế từ cloudsecop.net
Khi tôi deploy khavan và cloudsecop.net Worker:
- Cloudflare Access gate
https://admin.cloudsecop.net/*— chỉ Okta groupsecurity-admin, posturedisk_encrypted, 8h session. - AWS IDC cấp temp credential cho dev tool: pull S3 bucket log, query Athena, deploy Lambda. Permission set
SecurityAdminvới policy custom. - Worker API verify Access JWT trước khi exec admin command (delete post, rotate signing key).
- Background scheduled Worker cần gọi Bedrock không qua user — dùng OIDC federation riêng (xem bài bedrock-workers-oidc-case-study).
Nói cách khác: Access cho human admin, OIDC federation cho service-to-service. IDC chỉ cho AWS native operation.
Cạm bẫy thường gặp
1. Group name không sync giữa Okta và IDC. Okta group “engineering-prod” trở thành IDC group cùng tên qua SCIM, nhưng nếu rename trong Okta thì IDC tạo group mới và để group cũ orphan. Đừng rename — xoá tạo lại nếu cần.
2. Access policy precedence. Policy precedence = 1 chạy trước. Một allow precedence thấp có thể override deny precedence cao — đọc kỹ doc. Tôi đã debug 4h vì policy ordering.
3. IDC permission set session_duration quá dài. Default PT12H. Set xuống 4-8h cho power role, 1-2h cho admin role. Limit blast radius.
4. SCIM partial provisioning. Okta có user group “contractors” nhưng quên check “Push to AWS IDC” trong app config — user bị block dù admin tưởng đã assign. Audit định kỳ “có bao nhiêu user trong Okta nhưng không có trong IDC”.
5. Access không có “deny all by default”. Application không có policy nào assigned thì allow everyone. Phải có ít nhất một block policy với everyone rule làm fallback.
Bottom line
Không có “Cloudflare Access vs AWS IAM Identity Center winner” — chúng giải quyết hai phía của identity stack. AWS IDC là cách đúng để dev login console + cấp temp credential, thay thế IAM user. Cloudflare Access là cách đúng để gate web/API admin + SSH bastion với device posture. Pattern thực dụng: pick Okta hoặc Entra ID làm root, federate xuống cả hai qua SAML + SCIM. Đừng cố build identity layer custom — bạn sẽ tốn 3 tháng eng time để recreate cái IdP commercial đã ship 10 năm trước.
Checklist khi setup cho team mới:
- Okta/Entra là source of truth, không phải spreadsheet hay AD on-prem standalone
- SCIM provisioning bật cho cả AWS IDC + Cloudflare Access
- AWS IDC permission set có inline deny
iam:*,sso:*,organizations:*cho non-admin role -
session_durationAWS IDC ≤ 4h cho power role, ≤ 1h cho admin - Mọi Cloudflare Access application có ít nhất một policy explicit (không phụ thuộc default)
- Device posture rule áp dụng cho admin application (disk encrypt, OS version, EDR running)
- Offboard playbook test mỗi quý: disable trong Okta → user lock out trong ≤ 5 phút
- Audit log: AWS CloudTrail enabled tất cả region, Cloudflare Logpush stream sang R2/S3
- Emergency access: break-glass account riêng cho AWS root + Cloudflare super admin, key cất offline
- Service-to-service auth dùng OIDC federation hoặc service token, không reuse human credential