AWS cho kỹ sư DevOps GDev
Xem thửTình huống: job upload artifact bị AccessDenied và cách sửa nguy hiểm
Đây là tình huống dựng cho bài học. Pipeline gdev-ci của đội GDev có một bước đẩy file app.tar lên bucket S3 gdev-artifacts-staging. Một sáng thứ Hai, bước này đỏ với dòng AccessDenied. Người trực thấy lỗi quyền, mở role của CI và gắn thêm một policy cho phép s3:* trên mọi tài nguyên. Job xanh trở lại sau hai phút và mọi người quay lại làm việc.
Vấn đề là cái xanh đó trả giá bằng một thứ không ai thấy. Role của CI giờ đọc và xóa được cả bucket production, vì policy mới không phân biệt staging với production. Sau này chỉ cần một script dọn dẹp dùng nhầm tên bucket là artifact production biến mất. Và nguyên nhân thật của lỗi ban đầu còn chưa được tìm ra: job đang ghi vào sai thư mục, còn quyền thì đã đúng.
Bài này dạy cách đọc lỗi quyền thay vì vá bừa, và đặt nó trong bức tranh nhỏ gồm ba mô hình tư duy về AWS. Không có danh sách dịch vụ để học thuộc. Phần lab chạy hoàn toàn trên máy bạn, không cần tài khoản AWS và không tốn tiền.
Sau bài này bạn làm được các việc sau:
- Nói được AWS lo phần nào và bạn lo phần nào, và vì sao tài khoản (account) là ranh giới quan trọng.
- Đọc một policy JSON, đoán đúng một yêu cầu bị cho phép hay từ chối, và đọc được thông báo AccessDenied.
- Chọn chỗ đặt
gdev-api: Region, Availability Zone, subnet công khai hay riêng tư, và biết triệu chứng khi sai.
Bạn cần có sẵn: terminal, python3, đã học chương Mạng máy tính (IP, DNS, firewall) và bài Kiến thức tiên quyết.
Ba mô hình cần nhớ
- Trách nhiệm chia đôi và account là ranh giới: AWS lo hạ tầng bên dưới, bạn lo những gì bạn cấu hình và chứa trong đó. Mỗi account là một ranh giới tài nguyên, thanh toán và quyền.
- Mặc định là từ chối, và Deny luôn thắng: yêu cầu chỉ qua khi có một Allow khớp và không có Deny nào khớp. Quyền tối thiểu (least privilege) nghĩa là chỉ viết đúng các Allow cần.
- Vị trí quyết định khả năng chịu lỗi và đường vào: Region và Availability Zone quyết định chịu lỗi, subnet công khai hay riêng tư quyết định ai chạm tới được
gdev-api.
Chuẩn bị môi trường lab
Lab chính dùng một script Python nhỏ mô phỏng cách AWS quyết định cho phép hay từ chối. Bạn cần python3 (có sẵn trên Ubuntu) và terminal. Không cần tài khoản AWS, không cần cài AWS CLI.
python3 --version
mkdir gdev-iam-lab && cd gdev-iam-lab
Kết quả minh họa (phiên bản của bạn có thể khác):
Python 3.8.10
Ngoài ra có một phần tùy chọn dành cho bạn nào đã có tài khoản AWS và đã cài AWS CLI. Các lệnh trong phần đó chỉ đọc hoặc mô phỏng, không tạo tài nguyên. Phần đó ghi rõ là kết quả minh họa vì tác giả không chạy nó trên tài khoản thật của bạn.
Về chi phí: toàn bộ bài này không yêu cầu bạn tạo tài nguyên nào, nên không phát sinh chi phí. Mục cuối bài nêu những thứ hay tốn tiền nếu bạn tự thử sau này. Chính sách giá và gói miễn phí của AWS thay đổi, hãy luôn kiểm tra trang chính thức của AWS thay vì tin vào bài viết.
Mô hình 1: trách nhiệm chia đôi và account là ranh giới
Theo mô hình trách nhiệm chung của AWS, AWS chịu trách nhiệm bảo vệ hạ tầng chạy mọi dịch vụ của AWS (security of the cloud). Phần còn lại, security in the cloud, phụ thuộc vào các dịch vụ bạn chọn. Càng gần máy ảo trần, phần bạn lo càng nhiều.
| Dịch vụ | AWS lo | Bạn lo |
|---|---|---|
| EC2 (máy ảo) | Phần cứng, cơ sở vật lý, tầng ảo hóa | Hệ điều hành khách và bản vá, phần mềm cài thêm, security group, IAM role gắn vào, dữ liệu |
| S3 (lưu trữ đối tượng) | Hạ tầng, hệ điều hành và nền tảng bên dưới | Dữ liệu bạn lưu, mã hóa, ai được truy cập qua IAM và bucket policy |
Hai hàng của bảng là hai đầu của một dải. Với dịch vụ do AWS vận hành, phần bạn lo thu hẹp lại nhưng không bao giờ bằng không: quyền truy cập và dữ liệu luôn là của bạn. Trong tình huống đầu bài, lỗi nằm ở đúng phần này.
Account là đơn vị chứa tài nguyên, đơn vị thanh toán và là ranh giới quyền mặc định. Theo tài liệu IAM, nếu bạn gửi yêu cầu tới dịch vụ ở account khác, cả policy gắn với danh tính của bạn lẫn resource-based policy ở phía tài nguyên đều phải cho phép. Trong cùng một account, một trong hai cho phép là đủ. Vì thế đội GDev tách gdev-staging và gdev-prod thành hai account: một thao tác nhầm trong staging không tự có đường tới production.
Hai thói quen vệ sinh account, đều là khuyến nghị phổ biến trong tài liệu AWS:
- Người dùng root là danh tính duy nhất không bị chặn bởi mặc định từ chối, nó có toàn quyền. Hãy bật MFA cho nó, cất đi, không dùng cho công việc hằng ngày.
- Người và máy dùng thông tin xác thực tạm thời (role), không dùng access key dài hạn trong repo. CI nhận thông tin tạm thời bằng cách assume một role; bạn sẽ thấy cách làm ở chương CI/CD.
Lệnh đọc duy nhất và miễn phí để biết "tôi đang là ai": aws sts get-caller-identity. Nó không cần quyền IAM nào. Kết quả minh họa (account 123456789012 là số giữ chỗ trong tài liệu của AWS, không phải account thật):
{
"UserId": "AROAEXAMPLEID:gdev-ci-job-42",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/gdev-ci-runner/gdev-ci-job-42"
}
Dòng Arn có dạng assumed-role/<tên role>/<tên phiên>. Khi một role đang được dùng, danh tính gửi yêu cầu là phiên của role (role session), không phải bản thân role. Thông báo lỗi quyền sẽ in đúng ARN này, vì vậy bạn cần nhận ra nó.
Mô hình 2: IAM, mặc định từ chối và Deny luôn thắng
Trước khi xem sơ đồ, hãy dự đoán: nếu một policy Allow cho phép s3:* trên mọi thứ và một policy khác Deny s3:* trên bucket production, yêu cầu đọc một object trong bucket production có được qua không?
Đang tải sơ đồ…
Cách đọc sơ đồ:
- Thứ tự quan trọng. AWS tìm Deny trước. Chỉ cần một Deny khớp là kết quả cuối cùng là từ chối, bất kể có bao nhiêu Allow. Đây là câu trả lời cho câu hỏi ở trên: yêu cầu bị từ chối, vì theo tài liệu AWS "an explicit deny overrides an explicit allow".
- Nếu không có Deny, AWS cần ít nhất một Allow khớp. Không có Allow nào thì kết quả vẫn là từ chối, gọi là implicit deny (từ chối ngầm), vì mặc định mọi yêu cầu đều bị từ chối.
- Hai nhánh từ chối khác nhau ở chỗ sửa. Với explicit deny, bạn phải tìm và sửa câu Deny. Thêm Allow không giúp được gì. Với implicit deny, bạn thiếu một Allow.
Sơ đồ là phiên bản đơn giản cho một yêu cầu trong cùng một account. Thực tế còn có các lớp khác như service control policy (SCP) của AWS Organizations, permissions boundary và session policy. Theo tài liệu, tất cả các lớp đó cũng phải cho phép, và một Deny ở bất kỳ lớp nào đều thắng. Mô hình đủ dùng để nhớ: mặc định từ chối, cần Allow, Deny thắng tất cả.
Một policy là một văn bản JSON gồm các statement. Mỗi statement có các phần sau:
| Trường | Ý nghĩa | Ví dụ trong lab |
|---|---|---|
Effect | Allow hoặc Deny | Allow |
Action | Thao tác API được áp dụng, hỗ trợ ký tự đại diện * | s3:GetObject |
Resource | Tài nguyên áp dụng, viết bằng ARN (Amazon Resource Name) | arn:aws:s3:::gdev-artifacts-staging/gdev-api/* |
Condition | Điều kiện bổ sung, tùy chọn | s3:prefix khớp gdev-api/* |
Sid | Nhãn của statement, giúp đọc lỗi | ReadWriteOwnPrefix |
Lab: viết policy quyền tối thiểu và tự kiểm chứng
Policy của role gdev-ci-runner chỉ cần ba việc: liệt kê, đọc và ghi object trong thư mục gdev-api/ của bucket staging. Lưu thành ci-runner.json:
cat > ci-runner.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListOwnPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::gdev-artifacts-staging",
"Condition": {"StringLike": {"s3:prefix": ["gdev-api/*"]}}
},
{
"Sid": "ReadWriteOwnPrefix",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::gdev-artifacts-staging/gdev-api/*"
}
]
}
EOF
Hai điểm cần để ý. Thứ nhất, s3:ListBucket tác động lên bucket nên Resource là ARN của bucket (không có /*), còn s3:GetObject và s3:PutObject tác động lên object nên Resource có /gdev-api/*. Nhầm hai dạng ARN này là nguồn lỗi AccessDenied quen thuộc nhất của S3. Thứ hai, s3:prefix là khóa điều kiện cho phép CI chỉ liệt kê trong thư mục của mình. Không có s3:DeleteObject trong policy: CI không cần xóa.
Tiếp theo là script mô phỏng. Nó cài đặt đúng mô hình trong sơ đồ: tìm Deny, rồi Allow, rồi từ chối ngầm. Lưu thành iam_eval.py:
cat > iam_eval.py <<'EOF'
import fnmatch
import json
import sys
def as_list(value):
return [value] if isinstance(value, str) else value
def match(patterns, value, fold=False):
if fold:
value = value.lower()
return any(fnmatch.fnmatchcase(value, p.lower() if fold else p) for p in as_list(patterns))
def applies(stmt, action, resource, context):
if not match(stmt["Action"], action, fold=True):
return False
if not match(stmt["Resource"], resource):
return False
for operator, pairs in stmt.get("Condition", {}).items():
if operator != "StringLike":
sys.exit("unsupported condition operator: " + operator)
for key, patterns in pairs.items():
if not match(patterns, context.get(key, "")):
return False
return True
def evaluate(policies, action, resource, context):
hits = [s for p in policies for s in p["Statement"] if applies(s, action, resource, context)]
for s in hits:
if s["Effect"] == "Deny":
return "explicitDeny", s.get("Sid", "?")
for s in hits:
if s["Effect"] == "Allow":
return "allowed", s.get("Sid", "?")
return "implicitDeny", "-"
def main():
args = sys.argv[1:]
split = args.index("--requests")
policies = [json.load(open(path)) for path in args[:split]]
for line in open(args[split + 1]):
parts = line.split()
if not parts:
continue
action, resource = parts[0], parts[1]
context = dict(p.split("=", 1) for p in parts[2:])
decision, sid = evaluate(policies, action, resource, context)
print("%-13s %-17s %s %s" % (decision, action, resource.replace("arn:aws:s3:::", ""), "(%s)" % sid))
main()
EOF
Đây là mô hình rút gọn để học. Nó chỉ hiểu một policy ở mức identity-based, ký tự đại diện * và ?, và toán tử điều kiện StringLike. Nó không thay thế bộ mô phỏng thật của AWS, SCP, permissions boundary hay các khóa điều kiện khác.
Tạo danh sách bảy yêu cầu cần kiểm tra. Mỗi dòng là thao tác, tài nguyên và (tùy chọn) giá trị của s3:prefix:
cat > requests.txt <<'EOF'
s3:GetObject arn:aws:s3:::gdev-artifacts-staging/gdev-api/1.2.0/app.tar
s3:PutObject arn:aws:s3:::gdev-artifacts-staging/gdev-api/1.2.1/app.tar
s3:GetObject arn:aws:s3:::gdev-artifacts-staging/other-team/report.csv
s3:DeleteObject arn:aws:s3:::gdev-artifacts-staging/gdev-api/1.2.0/app.tar
s3:GetObject arn:aws:s3:::gdev-artifacts-prod/gdev-api/1.2.0/app.tar
s3:ListBucket arn:aws:s3:::gdev-artifacts-staging s3:prefix=gdev-api/
s3:ListBucket arn:aws:s3:::gdev-artifacts-staging s3:prefix=
EOF
python3 iam_eval.py ci-runner.json --requests requests.txt
Kết quả (đây là kết quả thật của script, ổn định trên mọi máy):
allowed s3:GetObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (ReadWriteOwnPrefix)
allowed s3:PutObject gdev-artifacts-staging/gdev-api/1.2.1/app.tar (ReadWriteOwnPrefix)
implicitDeny s3:GetObject gdev-artifacts-staging/other-team/report.csv (-)
implicitDeny s3:DeleteObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (-)
implicitDeny s3:GetObject gdev-artifacts-prod/gdev-api/1.2.0/app.tar (-)
allowed s3:ListBucket gdev-artifacts-staging (ListOwnPrefix)
implicitDeny s3:ListBucket gdev-artifacts-staging (-)
Đọc từng dòng:
- Dòng 1 và 2: đọc và ghi trong
gdev-api/đúng như thiết kế. - Dòng 3: cùng bucket nhưng thư mục
other-team/nằm ngoàigdev-api/*, nên không có Allow nào khớp. - Dòng 4:
s3:DeleteObjectkhông xuất hiện trong policy. Ngay cả trong thư mục của chính mình, CI không xóa được. - Dòng 5: bucket production không có trong policy. Đây là điều ta mong muốn.
- Dòng 6 và 7: cùng thao tác
s3:ListBucket, kết quả khác nhau chỉ vì giá trịs3:prefixkhác nhau (gdev-api/và rỗng).
Mọi từ chối ở bảng này là implicit deny, ký hiệu (-) vì không statement nào khớp.
Tùy chọn: dùng bộ mô phỏng thật của AWS (cần tài khoản)
Nếu bạn đã có tài khoản AWS và AWS CLI, lệnh simulate-custom-policy chạy cùng cách suy luận của lab trên policy bạn truyền vào. Theo tài liệu CLI, lệnh này chỉ kiểm tra quyền chứ không thực hiện thao tác API, và bạn có thể mô phỏng cả tài nguyên không tồn tại. Nó không tạo tài nguyên nên không tốn phí. Bạn cần quyền iam:SimulateCustomPolicy.
aws iam simulate-custom-policy \
--policy-input-list "$(cat ci-runner.json)" \
--action-names s3:GetObject \
--resource-arns arn:aws:s3:::gdev-artifacts-prod/gdev-api/1.2.0/app.tar \
--query 'EvaluationResults[].[EvalActionName,EvalResourceName,EvalDecision]' \
--output text
Kết quả minh họa (không chạy trên tài khoản thật; giá trị EvalDecision theo tài liệu có thể là allowed, implicitDeny hoặc explicitDeny):
s3:GetObject arn:aws:s3:::gdev-artifacts-prod/gdev-api/1.2.0/app.tar implicitDeny
Mô hình 2 theo thời gian: một yêu cầu đi qua các policy
Sơ đồ ở trên cho thấy thứ tự quyết định. Animation dưới đây cho thấy điều sơ đồ không cho thấy: cùng một role với hai policy khác nhau cho hai kết cục, và thông báo lỗi chỉ ra điều gì. Ta theo dõi gdev-ci-runner đọc một object. Trước khi bấm, dự đoán: ở lần thử thứ hai, policy Allow rộng s3:* có cứu được yêu cầu không?
Đang tải Animation…
Failure lab: đọc AccessDenied, sửa đúng chỗ, rồi thấy cái giá của cách sửa nhanh
Quay lại tình huống đầu bài. Bước upload của CI thất bại. Theo tài liệu IAM, đa số thông báo từ chối có dạng User <principal> is not authorized to perform <action> on <resource> because <context>. Kết quả minh họa (dạng thông báo theo tài liệu IAM; với S3, thông báo thực tế có thể ngắn hơn, tùy thao tác và công cụ):
upload failed: dist/app.tar to s3://gdev-artifacts-staging/other-team/app.tar
An error occurred (AccessDenied) when calling the PutObject operation:
User: arn:aws:sts::123456789012:assumed-role/gdev-ci-runner/gdev-ci-job-42 is not authorized to perform: s3:PutObject
on resource: "arn:aws:s3:::gdev-artifacts-staging/other-team/app.tar"
because no identity-based policy allows the s3:PutObject action
Triệu chứng: AccessDenied. Hãy đọc bốn mảnh của thông báo:
- Principal là
assumed-role/gdev-ci-runner/gdev-ci-job-42, tức phiên của role CI, đúng danh tính ta mong đợi. - Action là
s3:PutObject, resource là.../other-team/app.tar. - Cụm cuối
because no identity-based policy allows ...là implicit deny. Nếu là explicit deny, bạn sẽ thấywith an explicit deny in .... Điều này cho biết cần tìm Allow còn thiếu chứ không phải câu Deny.
Cách kiểm chứng: dùng mô hình của ta. Chạy đúng yêu cầu bị lỗi qua mô hình:
echo "s3:PutObject arn:aws:s3:::gdev-artifacts-staging/other-team/app.tar" > one.txt
python3 iam_eval.py ci-runner.json --requests one.txt
Kết quả:
implicitDeny s3:PutObject gdev-artifacts-staging/other-team/app.tar (-)
Nguyên nhân thật: job ghi vào other-team/ trong khi policy chỉ cho gdev-api/. Policy đúng, job sai. Cách sửa: đổi đường dẫn đích của job thành gdev-api/.... Không cần đổi policy.
Bây giờ hãy xem cái giá của cách sửa nhanh trong tình huống đầu bài: gắn thêm policy s3:* trên mọi tài nguyên. Lưu broad.json rồi chạy lại cả bảy yêu cầu với hai policy:
cat > broad.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "QuickFixAllS3",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
EOF
python3 iam_eval.py ci-runner.json broad.json --requests requests.txt
Kết quả:
allowed s3:GetObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (ReadWriteOwnPrefix)
allowed s3:PutObject gdev-artifacts-staging/gdev-api/1.2.1/app.tar (ReadWriteOwnPrefix)
allowed s3:GetObject gdev-artifacts-staging/other-team/report.csv (QuickFixAllS3)
allowed s3:DeleteObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (QuickFixAllS3)
allowed s3:GetObject gdev-artifacts-prod/gdev-api/1.2.0/app.tar (QuickFixAllS3)
allowed s3:ListBucket gdev-artifacts-staging (ListOwnPrefix)
allowed s3:ListBucket gdev-artifacts-staging (QuickFixAllS3)
Cả bảy dòng đều là allowed, kể cả đọc bucket production và xóa artifact. Job xanh, và cũng chính vì thế không ai nhận ra mình vừa mở toang quyền. Cách sửa an toàn hơn cho một lỗi tương tự là thêm một lớp chặn: một Deny rõ ràng cho production. Lưu guardrail.json rồi chạy với cả ba policy:
cat > guardrail.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "NeverTouchProd",
"Effect": "Deny",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::gdev-artifacts-prod",
"arn:aws:s3:::gdev-artifacts-prod/*"
]
}
]
}
EOF
python3 iam_eval.py ci-runner.json broad.json guardrail.json --requests requests.txt
Kết quả (chỉ dòng thứ 5 đổi):
allowed s3:GetObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (ReadWriteOwnPrefix)
allowed s3:PutObject gdev-artifacts-staging/gdev-api/1.2.1/app.tar (ReadWriteOwnPrefix)
allowed s3:GetObject gdev-artifacts-staging/other-team/report.csv (QuickFixAllS3)
allowed s3:DeleteObject gdev-artifacts-staging/gdev-api/1.2.0/app.tar (QuickFixAllS3)
explicitDeny s3:GetObject gdev-artifacts-prod/gdev-api/1.2.0/app.tar (NeverTouchProd)
allowed s3:ListBucket gdev-artifacts-staging (ListOwnPrefix)
allowed s3:ListBucket gdev-artifacts-staging (QuickFixAllS3)
Deny thắng dù QuickFixAllS3 cũng khớp. Đây là sức mạnh và cũng là bẫy của explicit deny: nó bảo vệ production ngay cả khi ai đó lỡ cấp Allow rộng, nhưng cũng chặn luôn cả người có lý do chính đáng. Trong thực tế, một guardrail kiểu này nên đặt ở cấp tổ chức bằng SCP chứ không chỉ gắn vào từng role. Lab này chỉ để bạn thấy cơ chế.
Dọn dẹp sau lab:
cd .. && rm -rf gdev-iam-lab
Mô hình 3: nơi đặt gdev-api, Region, Availability Zone và subnet
Trước khi xem sơ đồ, dự đoán: một yêu cầu từ người dùng đi vào gdev-api. Qua những thành phần mạng nào trước khi chạm tới ứng dụng, và thành phần nào nên có địa chỉ công khai?
Đang tải sơ đồ…
Cách đọc sơ đồ:
- Chỉ load balancer nằm trong subnet công khai (public subnet) và tiếp nhận kết nối từ Internet.
gdev-apivà cơ sở dữ liệu nằm trong subnet riêng tư (private subnet), không có đường vào trực tiếp từ Internet. - Mũi tên có nhãn nghĩa là security group của
gdev-apichỉ cho phép lưu lượng từ security group của load balancer, không mở cho0.0.0.0/0. gdev-apivẫn cần đi ra Internet để tải bản cập nhật hoặc gọi API ngoài, nên đường ra đi qua NAT. NAT là một thành phần có tính phí theo tài liệu AWS, nên đây là chỗ hay gây hóa đơn bất ngờ khi bạn quên xóa lab.
Các khái niệm, theo tài liệu chính thức của AWS:
| Khái niệm | Điều tài liệu nói | Hệ quả khi thiết kế |
|---|---|---|
| Region | Khu vực địa lý chứa nhiều Availability Zone | Chọn gần người dùng và theo yêu cầu về nơi lưu dữ liệu |
| Availability Zone (AZ) | Mỗi Region có ít nhất ba AZ (một số tài khoản mới chỉ truy cập được hai AZ ở một vài Region) | Chạy gdev-api trong ít nhất hai AZ để một AZ hỏng không làm sập dịch vụ |
| AZ ID | Mã như apse1-az1, trỏ tới cùng một vị trí vật lý ở mọi account | Khi so sánh AZ giữa hai account, dùng AZ ID thay vì tên a, b, c |
| VPC | Mạng ảo của bạn, mỗi account có sẵn một default VPC ở mỗi Region | Tự thiết kế dải IP và route thay vì dùng default VPC cho production |
| Subnet | Phải nằm trọn trong một AZ, không thể trải qua hai AZ | Muốn chịu lỗi AZ thì cần subnet ở nhiều AZ |
| Public subnet | Có route trực tiếp tới internet gateway | Chỉ đặt thứ phải tiếp nhận kết nối công khai, như load balancer |
| Private subnet | Không có route trực tiếp tới internet gateway, cần NAT để ra Internet | Nơi đặt gdev-api và cơ sở dữ liệu |
| Security group | Tường lửa gắn vào tài nguyên, có trạng thái (stateful), chỉ có quy tắc cho phép | Mở đúng cổng, đúng nguồn; không mở 0.0.0.0/0 cho SSH |
| Network ACL | Cho phép hoặc từ chối ở mức subnet | Lớp bảo vệ bổ sung, thường không cần chỉnh |
Hai điểm tinh tế:
- Security group là danh sách cho phép. Không có quy tắc từ chối. Quy tắc inbound mới tạo mặc định không cho gì vào, nên thiếu một quy tắc là gói tin bị bỏ lặng lẽ.
- Một subnet "công khai" hay "riêng tư" không phải thuộc tính bạn bật tắt, mà do bảng định tuyến của nó quyết định: có route tới internet gateway hay không.
Lệnh đọc miễn phí để xem các AZ của một Region trong account của bạn (kết quả minh họa, không chạy trên tài khoản thật):
aws ec2 describe-availability-zones \
--region ap-southeast-1 \
--filters Name=zone-type,Values=availability-zone \
--query "AvailabilityZones[].{ZoneName:ZoneName,ZoneId:ZoneId}"
[
{"ZoneName": "ap-southeast-1a", "ZoneId": "apse1-az2"},
{"ZoneName": "ap-southeast-1b", "ZoneId": "apse1-az1"},
{"ZoneName": "ap-southeast-1c", "ZoneId": "apse1-az3"}
]
Số ZoneId trong ví dụ chỉ để minh họa việc tên a, b, c có thể ánh xạ tới AZ ID khác nhau ở mỗi account, không phải kết quả thật của bạn. Theo tài liệu, với các tài khoản tạo từ tháng 11 năm 2025, các tên này được ánh xạ giống nhau ở mọi account. Tài khoản cũ hơn vẫn có thể khác, nên AZ ID mới là mã đáng tin.
Chẩn đoán lỗi mạng bằng mã thoát của curl
Bài Kiến thức tiên quyết dạy bạn đọc mã thoát của curl. Bây giờ bạn dùng nó để đoán tầng mạng của AWS bị sai. Giả sử bạn gọi curl http://gdev-api.internal:8080/healthz từ một máy trong VPC:
| Triệu chứng | Nghĩa là | Nên kiểm tra |
|---|---|---|
Mã 6, Could not resolve host | Chưa có địa chỉ để đi | DNS của VPC, tên đúng chính tả |
| Mã 28, timeout | Gói tin bị bỏ lặng lẽ hoặc không có đường đi | Security group có cho phép nguồn này vào cổng 8080 chưa, route table, network ACL |
Mã 7, Connection refused | Gói tin tới nơi nhưng không ai nghe ở cổng đó | Ứng dụng có chạy, có nghe đúng cổng không (ss -tln trên máy đích) |
Điểm quan trọng nhất là sự khác nhau giữa mã 28 và mã 7. Security group chặn bằng cách bỏ gói tin nên bạn thấy timeout, không phải connection refused. Vì vậy nếu bạn thấy Connection refused, security group không phải thủ phạm: gói tin đã tới được máy đích rồi.
Chi phí và dọn dẹp khi bạn tự thử sau này
Bài này không tạo tài nguyên. Nếu sau này bạn tự dựng một VPC và máy chủ để thử, hãy làm theo các nguyên tắc sau:
- Đặt budget alert và billing alarm ngay từ đầu, bằng công cụ AWS Budgets hoặc cảnh báo thanh toán của AWS, trước khi tạo tài nguyên đầu tiên.
- Gắn tag
Project=gdevvàEnv=labcho mọi thứ tạo ra, để cuối buổi lọc ra và xóa. - Những thứ hay bị bỏ quên và có tính phí: NAT gateway, địa chỉ IPv4 công khai, load balancer, volume EBS và snapshot, máy EC2 đang chạy. Tài liệu VPC của AWS ghi rõ NAT gateway và địa chỉ IPv4 công khai là những thành phần có phí.
- Kiểm tra trang giá và gói miễn phí chính thức của AWS trước khi bắt đầu. Bài này cố ý không nêu con số vì chúng thay đổi.
- Trước mọi lệnh phá hủy, chạy
aws sts get-caller-identityđể chắc bạn đang ở đúng account. - Không bao giờ ghi access key vào repo. Nếu lỡ commit, hãy vô hiệu hóa key đó ngay và tạo key mới, đừng chỉ xóa file.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp | Cách kiểm chứng |
|---|---|---|
AccessDenied khi s3:ListBucket | Resource ghi dạng object (bucket/*) thay vì ARN của bucket | So ARN trong policy với thao tác: ListBucket dùng ARN bucket |
| Thêm Allow rộng mà vẫn bị từ chối | Có một Deny khớp ở đâu đó (policy khác, SCP, boundary) | Đọc thông báo: có cụm explicit deny không |
| Job xanh nhưng quyền quá rộng | Sửa lỗi quyền bằng s3:* trên * | Chạy bộ mô phỏng với yêu cầu vào tài nguyên không liên quan |
| Truy cập chéo account bị từ chối | Chỉ một phía cho phép | Kiểm tra cả identity-based và resource-based policy |
Timeout từ máy khác tới gdev-api | Security group thiếu quy tắc inbound cho nguồn đó | Xem inbound rule của security group gắn vào gdev-api |
| Dịch vụ sập khi một AZ lỗi | Mọi instance nằm trong một AZ | Đếm AZ của các subnet đang chạy workload |
| So sánh AZ giữa hai account bị lệch | Dùng tên a, b, c thay vì AZ ID | Dùng ZoneId để đối chiếu |
| Database có địa chỉ công khai | Đặt trong public subnet | Xem route table của subnet chứa database |
Quy tắc thực tế: khi gặp lỗi quyền, trước tiên hãy hỏi "danh tính nào, thao tác nào, tài nguyên nào" và đọc cụm cuối của thông báo. Đừng chạm vào policy trước khi trả lời được ba câu đó.
Tự kiểm tra
Hãy trả lời trước khi đọc đáp án ngay bên dưới mỗi câu.
Câu 1. Thông báo: ... is not authorized to perform: s3:GetObject ... with an explicit deny in an identity-based policy. Bạn có nên thêm một statement Allow s3:GetObject không?
Đáp án: không. Cụm explicit deny nghĩa là có một statement Deny khớp, và Deny thắng mọi Allow. Phải tìm và sửa câu Deny, hoặc dùng một danh tính khác không bị Deny đó áp dụng.
Câu 2. Policy của CI có s3:GetObject trên arn:aws:s3:::gdev-artifacts-staging/gdev-api/*. CI chạy aws s3 ls s3://gdev-artifacts-staging/gdev-api/ và bị AccessDenied. Thiếu gì?
Đáp án: thiếu s3:ListBucket trên ARN của bucket (không có /*), kèm điều kiện s3:prefix nếu muốn giới hạn thư mục. s3:GetObject chỉ áp dụng cho object, không áp dụng cho thao tác liệt kê.
Câu 3. Máy A gọi gdev-api ở máy B và nhận curl: (28). Máy A khác nhận curl: (7) với cùng đích. Hai kết quả cho bạn biết gì về security group của B?
Đáp án: ở máy A, gói tin bị bỏ lặng lẽ nên rất có thể security group của B chưa cho phép nguồn A. Ở máy khác, gói tin tới được B nhưng không ai nghe ở cổng đó, nên security group cho phép nguồn đó nhưng ứng dụng không chạy hoặc nghe sai cổng.
Nếu chỉ nhớ 3 điều
- AWS lo hạ tầng, bạn lo dữ liệu và quyền truy cập. Account là ranh giới: tách staging và production thành hai account.
- Mặc định từ chối, cần một Allow khớp, một Deny khớp thắng tất cả. Đọc cụm cuối của thông báo để biết đó là implicit hay explicit deny.
- Đặt load balancer ở subnet công khai,
gdev-apivà database ở subnet riêng tư, trải qua ít nhất hai AZ. Timeout và connection refused chỉ ra hai tầng khác nhau.
Bài tiếp theo là Google Cloud Platform, nơi ta gặp lại cùng ba mô hình với tên gọi và cách tổ chức khác.
Nguồn: Mô hình trách nhiệm chung của AWS, Logic đánh giá policy của IAM, Khắc phục thông báo access denied, Ví dụ policy S3, Availability Zones, Subnet của VPC, Security group.