TL;DR
- AWS Secrets Manager tính phí
$0.40/secret/tháng+$0.05/10K API call(pricing). Đắt khi bạn có hàng trăm secret, nhưng có tính năng mà không công cụ khác có: auto-rotation qua Lambda chạy theo schedule, transactional (AWSPENDING→AWSCURRENT), tích hợp native với RDS/Redshift/DocumentDB.- Cloudflare Secrets Store miễn phí trên Workers Paid Plan ($5/tháng/account). Account-level, có thể bind vào nhiều Worker — replace cho
wrangler secret putper-worker. Nhưng không có rotation logic và immutable: muốn update phải tạo version mới + redeploy.- Pair tốt nhất với bài aws-iam-access-key-auto-rotation: IAM access key rotate qua Lambda → ghi vào Secrets Manager → replicate sang Cloudflare Secrets Store qua scheduled Worker.
- Quy tắc của tôi: regulated workload (PCI/HIPAA/SOC 2) + DB credential → AWS Secrets Manager với rotation policy 30-90 ngày. Worker binding (API key cho upstream, signing key, OIDC private key) → CF Secrets Store. Đừng cố unify trong một store.
- Custom rotator pattern: Lambda với 4 step
createSecret→setSecret→testSecret→finishSecret. Là contract mà Secrets Manager bắt buộc — đừng viết Lambda kiểu “tự nghĩ flow”.- Đo cost thực: 50 secret × 30 ngày × 1000 API/ngày =
$20 + $7.50 = $27.50/tháng. So với CF Secrets Store: $5 flat. Đó là delta$22.50/tháng— đáng nếu cần rotation, lãng phí nếu chỉ store API key cho Worker.
Vì sao đây không phải so sánh “AWS vs Cloudflare”
Tôi đã thấy nhiều bài đặt câu hỏi như chọn một bên là thắng. Thực tế hai service giải quyết hai vấn đề khác nhau. AWS Secrets Manager là secret lifecycle management — rotation, versioning, replication cross-region, audit qua CloudTrail. Cloudflare Secrets Store là secret distribution — đưa secret vào runtime của Worker mà không phải wrangler secret put cho từng Worker một.
Khi tôi build cloudsecop.net (Astro blog deploy lên Workers, có Worker API gọi Bedrock + send email + verify Turnstile), tôi đụng phải vấn đề secret ở hai nơi:
- DB credential cho RDS ở AWS side (nếu có service nội bộ join D1 với RDS qua Hyperdrive)
- API key Bedrock, JWT signing key cho OIDC ở Cloudflare side
Lúc đầu tôi đặt tất cả vào Secrets Manager, rồi viết một Worker pull về KV mỗi giờ. Tệ — tăng latency cold start, tốn cost AWS API call, và secret ở KV không có ACL chi tiết. Refactor: AWS Secrets Manager cho DB cred (auto-rotate); CF Secrets Store cho Worker binding (immutable, account-scoped). Hai pool tách biệt.
AWS Secrets Manager — flow rotation thực tế
Phần mạnh nhất của Secrets Manager không phải store, mà rotation. Lambda function phải implement 4 step:
# rotation Lambda — viết theo contract AWS Secrets Manager
import boto3
import json
def lambda_handler(event, context):
secret_id = event["SecretId"]
token = event["ClientRequestToken"]
step = event["Step"]
client = boto3.client("secretsmanager")
if step == "createSecret":
create_secret(client, secret_id, token)
elif step == "setSecret":
set_secret(client, secret_id, token)
elif step == "testSecret":
test_secret(client, secret_id, token)
elif step == "finishSecret":
finish_secret(client, secret_id, token)
else:
raise ValueError(f"Invalid step: {step}")
def create_secret(client, secret_id, token):
# Generate new version, mark AWSPENDING
current = client.get_secret_value(SecretId=secret_id, VersionStage="AWSCURRENT")
new_password = client.get_random_password(PasswordLength=32, ExcludeCharacters='/@"')
new_secret = json.loads(current["SecretString"])
new_secret["password"] = new_password["RandomPassword"]
client.put_secret_value(
SecretId=secret_id,
ClientRequestToken=token,
SecretString=json.dumps(new_secret),
VersionStages=["AWSPENDING"],
)
def set_secret(client, secret_id, token):
# Apply AWSPENDING to upstream (RDS, API provider, etc.)
pending = client.get_secret_value(SecretId=secret_id, VersionStage="AWSPENDING")
cred = json.loads(pending["SecretString"])
# e.g. ALTER USER 'app'@'%' IDENTIFIED BY new_password
apply_to_rds(cred)
def test_secret(client, secret_id, token):
# Connect with new credentials — fail if upstream not updated
pending = client.get_secret_value(SecretId=secret_id, VersionStage="AWSPENDING")
cred = json.loads(pending["SecretString"])
assert can_connect(cred), "AWSPENDING credential test failed"
def finish_secret(client, secret_id, token):
# Promote AWSPENDING to AWSCURRENT, demote old AWSCURRENT to AWSPREVIOUS
metadata = client.describe_secret(SecretId=secret_id)
current_version = next(
v for v, stages in metadata["VersionIdsToStages"].items()
if "AWSCURRENT" in stages
)
client.update_secret_version_stage(
SecretId=secret_id,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current_version,
)
Cái đáng học từ contract này là transactional: nếu testSecret fail, AWSCURRENT vẫn là cred cũ. Application không bao giờ thấy moment “credential nửa cũ nửa mới”. Đây là điều khó tự build bằng cron + script.
Đối với DB credential, AWS có sẵn rotation function template — clone về và customize:
# Repo Lambda template chính thức
git clone https://github.com/aws-samples/aws-secrets-manager-rotation-lambdas
cd aws-secrets-manager-rotation-lambdas/SecretsManagerRDSPostgreSQLRotationSingleUser
# 4 step đã được implement sẵn — chỉ thay env var connection string
Repo này ~370 stars — chứa template cho MySQL, PostgreSQL, Oracle, MongoDB, Redis. Cho custom secret (API key Stripe, Twilio, etc.) bạn phải tự viết — nhưng giữ đúng 4 step.
Terraform setup cho Secrets Manager + rotation
resource "aws_secretsmanager_secret" "rds_app" {
name = "prod/rds/app-user"
description = "RDS app user credential, rotated every 30 days"
kms_key_id = aws_kms_key.secrets.arn
recovery_window_in_days = 7
}
resource "aws_secretsmanager_secret_version" "rds_app_initial" {
secret_id = aws_secretsmanager_secret.rds_app.id
secret_string = jsonencode({
username = "app_user"
password = random_password.initial.result
engine = "postgres"
host = aws_db_instance.app.address
port = 5432
dbname = "app"
})
lifecycle {
ignore_changes = [secret_string] # rotation Lambda owns updates
}
}
resource "aws_secretsmanager_secret_rotation" "rds_app" {
secret_id = aws_secretsmanager_secret.rds_app.id
rotation_lambda_arn = aws_lambda_function.rds_rotator.arn
rotation_rules {
automatically_after_days = 30
}
}
# Resource policy: chỉ application role mới read được
resource "aws_secretsmanager_secret_policy" "rds_app" {
secret_arn = aws_secretsmanager_secret.rds_app.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = aws_iam_role.app.arn }
Action = ["secretsmanager:GetSecretValue"]
Resource = "*"
Condition = {
StringEquals = {
"secretsmanager:VersionStage" = "AWSCURRENT"
}
}
}]
})
}
recovery_window_in_days = 7 quan trọng — nếu lỡ aws_secretsmanager_secret bị xoá vì Terraform plan sai, có 7 ngày để recover. Đừng set 0.
ignore_changes = [secret_string] cũng quan trọng — Lambda rotator là owner, không phải Terraform. Nếu không có lifecycle này, mỗi lần terraform apply sẽ revert secret về initial value.
Cloudflare Secrets Store — flow distribution
Phía CF, khác hoàn toàn. Không có rotation. Không có version stage. Secrets Store chỉ là account-level storage mà bạn bind vào Worker bằng config.
// wrangler.jsonc
{
"name": "khavan",
"secrets_store_secrets": [
{
"binding": "BEDROCK_API_KEY",
"store_id": "your-store-id-uuid",
"secret_name": "bedrock-api-key"
},
{
"binding": "OIDC_PRIVATE_KEY",
"store_id": "your-store-id-uuid",
"secret_name": "oidc-rsa-private-key"
}
]
}
Trong Worker code:
export default {
async fetch(request: Request, env: Env) {
// Secret là async — không phải string global
const apiKey = await env.BEDROCK_API_KEY.get();
const privateKey = await env.OIDC_PRIVATE_KEY.get();
// ... dùng để sign JWT, gọi Bedrock, v.v.
}
};
Tạo secret qua CLI:
# Tạo store account-level (một lần)
wrangler secrets-store store create --name production
# Push secret vào store
wrangler secrets-store secret create \
--store-id <uuid> \
--name bedrock-api-key \
--value "$(cat /tmp/api-key)"
# Update value — tạo version mới, ALL Worker binding vào secret này sẽ thấy ngay
wrangler secrets-store secret update \
--store-id <uuid> \
--name bedrock-api-key \
--value "$(cat /tmp/api-key-new)"
Đây là điểm khác biệt lớn: trong AWS, secret có versionId, application chọn stage. Trong CF, update là destructive — phiên bản cũ vẫn lưu trong history nhưng env.X.get() luôn trả về version mới nhất. Không có concept “AWSPENDING test trước khi promote”.
Pattern replication AWS ↔ CF
Khi cần một secret ở cả hai bên (ví dụ: shared HMAC key cho webhook signing giữa Lambda và Worker), tôi dùng pattern này:
// Worker scheduled handler — chạy mỗi 6h
export default {
async scheduled(event: ScheduledEvent, env: Env) {
// Pull từ AWS Secrets Manager (qua OIDC federation, không long-lived key)
const creds = await getAwsTempCreds(env);
const aws = new AwsClient({
...creds,
region: "us-east-1",
service: "secretsmanager",
});
const response = await aws.fetch(
"https://secretsmanager.us-east-1.amazonaws.com/",
{
method: "POST",
headers: {
"X-Amz-Target": "secretsmanager.GetSecretValue",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify({ SecretId: "prod/webhook/signing-key" }),
}
);
const { SecretString } = await response.json();
const { current_value } = JSON.parse(SecretString);
// Push vào CF Secrets Store via API
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/secrets_store/stores/${env.STORE_ID}/secrets/webhook-signing-key`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${await env.CF_API_TOKEN.get()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ value: current_value }),
}
);
}
};
AWS là source of truth, CF là replica. Khi Lambda rotator thay secret, scheduled Worker tự pull về trong 6h. Đừng ngược lại — CF Secrets Store không có rotation, không nên làm source.
Lưu ý: secret update không tự redeploy Worker. Worker đang chạy sẽ thấy value mới ở lần env.X.get() tiếp theo (vì call này luôn fetch fresh từ Secrets Store binding, không cache).
Cost thực tế — số liệu sau 6 tháng
Setup của tôi: 12 secret AWS-side (RDS cred, Stripe webhook, Twilio API key, signing key), 8 secret CF-side (Bedrock, OIDC private key, Turnstile, AI Gateway token).
AWS Secrets Manager (us-east-1):
12 secret × $0.40 = $4.80/tháng
~50K API call/tháng × $0.05/10K = $0.25/tháng
KMS encryption call ~50K × $0.03/10K = $0.15/tháng
Lambda rotation (12 secret × 1 run/tháng × ~3s) = ~$0.02/tháng
─────────────────────────────────────────────
Total: ~$5.22/tháng
Cloudflare Secrets Store (đã có Workers Paid):
Store + secret + binding: $0 (bao trong Workers Paid $5/account/month)
Read operation: không limit
─────────────────────────────────────────────
Total: $0 incremental
Delta: ~$5/tháng cho 12 secret AWS-side. Hoàn toàn phù hợp với value vì rotation tự động.
Cảnh báo cost: ở scale lớn (500 secret) thì AWS Secrets Manager tăng tuyến tính: 500 × $0.40 = $200/tháng. Với mỗi secret bạn phải tự hỏi: có thật sự cần rotation? Nếu là static API key không bao giờ rotate (Slack webhook URL, Sentry DSN), nên dùng Parameter Store ($0/secret cho standard tier) hoặc CF Secrets Store thay vì Secrets Manager.
Khi nào dùng cái gì — quyết định trong 30 giây
| Use case | AWS Secrets Manager | CF Secrets Store | Parameter Store |
|---|---|---|---|
| DB credential cần rotate | Có | Không | Không |
| API key SaaS (Stripe, Twilio) | Có (rotation Lambda) | Có (manual rotate) | Có (manual) |
| Worker binding (signing key, upstream auth) | Có nhưng đắt | Có | Không hỗ trợ |
| Compliance SOC 2 / PCI scope | Có (CloudTrail audit) | Có (audit log) | Có |
| Cross-region replication tự động | Có | Account-global mặc định | Có nhưng pay |
| Cross-AWS-account share | Có (resource policy) | Không native | Không |
| Webhook secret (HMAC verify) | OK nhưng overkill | Tốt | OK |
| OIDC RSA private key (JWT signing) | OK | Tốt nhất | Có |
| Config flag không sensitive | Không (lãng phí) | Không (overkill) | Có |
Audit và compliance — chỗ AWS thắng rõ ràng
AWS CloudTrail log mọi GetSecretValue, PutSecretValue, RotateSecret. Tích hợp tự nhiên với Athena query, GuardDuty alert, Security Hub finding. Đây là chuẩn để pass SOC 2 Type II audit cho “credential management” control.
-- Athena query: ai đọc secret prod/rds/app-user trong 30 ngày qua
SELECT
eventTime,
userIdentity.arn AS principal,
sourceIPAddress,
requestParameters.versionStage AS stage
FROM cloudtrail_logs
WHERE eventSource = 'secretsmanager.amazonaws.com'
AND eventName = 'GetSecretValue'
AND requestParameters.secretId LIKE '%prod/rds/app-user%'
AND eventTime > date_add('day', -30, current_date)
ORDER BY eventTime DESC;
Cloudflare Secrets Store cũng có audit log qua Cloudflare Audit Logs API, nhưng không chi tiết bằng — log “secret created/updated/deleted”, không log từng get() operation từ Worker. Đó là trade-off: less observability, less cost overhead.
Cạm bẫy thường gặp
1. Quên recovery_window_in_days. Default 30 ngày. Khi đang test mà set 0 để xoá nhanh — sau đó muốn recover không được. Để default 7-30 cho production.
2. Rotation Lambda quên RoleAssumePolicy đúng. Lambda phải có quyền secretsmanager:GetSecretValue, PutSecretValue, UpdateSecretVersionStage cho chính secret đó. Test với aws secretsmanager rotate-secret --secret-id <arn> rồi xem CloudWatch Logs.
3. Application cache secret quá lâu. SDK AWS SDK v3 có SecretsManagerCachingClient — đặt TTL ≤ 1h. Nếu cache 24h thì rotation 30 phút sau, app vẫn dùng cred cũ → DB connection fail.
4. CF Secrets Store binding trong dev. Local wrangler dev không tự sync secret từ remote store. Phải wrangler secrets-store secret get rồi đưa vào .dev.vars. Hoặc dùng --remote flag để hit remote runtime.
5. Hardcode KMS key ID trong Terraform. Mỗi region có KMS key khác. Dùng aws_kms_alias hoặc aws/secretsmanager default key trừ khi cần customer-managed.
Bottom line
AWS Secrets Manager và Cloudflare Secrets Store không phải competitor — chúng giải quyết hai nửa khác nhau của secret management. AWS thắng ở rotation logic + audit + cross-account, là default cho mọi regulated workload và DB credential. CF Secrets Store thắng ở distribution vào Worker runtime + cost free trên Workers Paid, là default cho Worker binding. Pattern thực tế là dùng cả hai với AWS là source of truth, CF là edge replica cho những secret nào Worker cần. Đừng cố unify — đó là cách tốt nhất để build một abstraction layer mà sau 3 tháng không ai hiểu.
Checklist trước khi commit setup:
- Mọi secret AWS có
recovery_window_in_days≥ 7 - Mọi rotation Lambda implement đủ 4 step (createSecret → setSecret → testSecret → finishSecret)
- Resource policy giới hạn principal (chỉ application role read được, không
*) - Application cache secret TTL ≤ 1h
- CloudTrail data event bật cho secret nhạy cảm (
prod/*) - CF Secrets Store binding khai trong
wrangler.jsonc, không hardcode trong code - Scheduled Worker replicate (nếu cần) chạy ít nhất mỗi 6h, idempotent
- KMS key cho Secrets Manager là customer-managed cho regulated workload
- Cost monitoring: alert khi secret count > 100 (rà soát có dùng nữa không)
- DR plan: backup secret value ra một bucket encrypted khác (cross-region) cho top-10 critical secret