Vào AWS của khách bằng cross-account role: trust policy, ExternalId và cách gỡ AccessDenied
Khách đã tạo role và gửi ARN mà lệnh đầu tiên vẫn trả về AccessDenied: lỗi gần như luôn nằm ở một trong sáu chỗ quen thuộc, từ Principal đến SCP, còn ExternalId thì không phải mật khẩu như nhiều người vẫn nghĩ.
Tóm tắt nhanh
- Muốn assume role xuyên tài khoản thì cả hai phía phải cho phép: trust policy bên khách và quyền sts:AssumeRole bên bạn.
- ExternalId chặn confused deputy. Vendor phải sinh nó, mỗi khách một giá trị, và lệnh gọi nào cũng phải truyền nó. Nó không phải mật khẩu.
- Gặp AccessDenied thì kiểm tra Principal trước, rồi đến tên role phân biệt hoa thường, các Condition, quyền phía mình, SCP của Organizations, và cuối cùng là giới hạn 1 giờ khi role chaining.
Khách hàng đã tạo role và gửi ARN cho bạn qua Slack, kèm lời nhắn “xong rồi nhé”. Bạn chạy lệnh assume-role đầu tiên thì nhận AccessDenied. Hai mươi phút nữa là đến buổi kickoff, và lúc này cả bạn lẫn khách đều chưa biết lỗi nằm bên nào.
FDE gặp tình huống này rất thường xuyên khi tích hợp với khách dùng AWS. Gần như sản phẩm nào cũng cần đọc dữ liệu từ tài khoản của khách, dù đó là pipeline dữ liệu, agent gọi tool trên hạ tầng của khách hay công cụ giám sát.
Cách làm chuẩn là dùng cross-account role. Khi đã nắm chắc cơ chế này, bạn sẽ không còn mất những buổi sáng như thế, và khách cũng yên tâm rằng bạn không mở cửa hậu vào hệ thống của họ.
Muốn vào được thì cả hai bên phải đồng ý
Tài liệu API của AWS STS nói rõ rằng người dùng muốn truy cập role ở tài khoản khác cũng cần được cấp quyền từ chính tài khoản của mình. Như vậy có hai cánh cửa.
Cánh thứ nhất là trust policy của role bên khách, quy định ai được phép assume. Cánh thứ hai là IAM policy gắn với identity của bạn, cho phép gọi sts:AssumeRole lên ARN đó.
Chỉ cần một trong hai cánh đóng là bạn nhận AccessDenied. Đừng mặc định rằng thông báo lỗi sẽ cho bạn biết cánh nào đang đóng. Vì thế, khi gỡ lỗi, việc đầu tiên là xác định lỗi nằm bên khách hay bên mình, chưa cần đọc lại policy từng dòng.
ExternalId sinh ra để chặn confused deputy
Thử hình dung công ty bạn phục vụ hai khách, A và B. Khách B biết được ARN role của khách A, rồi khai ARN đó vào cấu hình của mình.
Hệ thống của bạn có quyền hợp lệ để assume role đó, nên nó sẽ ngoan ngoãn đọc dữ liệu của A và trả kết quả cho B. AWS gọi tình huống này là confused deputy: bạn là bên được ủy quyền, và bạn bị lừa dùng quyền đó sai chỗ.
Theo tài liệu IAM, chức năng chính của ExternalId là xử lý và ngăn chặn đúng vấn đề này. Cơ chế của nó rất đơn giản.
Bạn gán cho mỗi khách một ExternalId riêng, khách ghi giá trị đó vào trust policy, và hệ thống của bạn luôn gửi ExternalId của khách đang yêu cầu.
Khi B khai ARN của A, lệnh gọi sẽ mang ExternalId của B và bị role của A từ chối.
Muốn cơ chế này có tác dụng thì ExternalId phải do bạn sinh ra, mỗi khách một giá trị, không để khách tự đặt. Giá trị dài từ 2 đến 1.224 ký tự, gồm chữ, số và một số ký hiệu, không chứa khoảng trắng.
Điều này quan trọng vì AWS cũng nói rằng ai có quyền xem role đều đọc được ExternalId. Vậy nên đừng giải thích với đội bảo mật của khách rằng nó là “secret”. Câu đúng là: ExternalId bảo đảm rằng hệ thống của bạn chỉ dùng role này khi đang phục vụ đúng khách đó.
Ví dụ: dựng một kết nối từ đầu
Giả sử tài khoản vendor của bạn là 111122223333, tài khoản của khách là 444455556666, và ExternalId bạn sinh cho khách là acme-7f3k9q2x. Khách tạo role VendorReadOnly với trust policy dưới đây, theo đúng mẫu trong tài liệu IAM: Principal là tài khoản của bạn, Action là sts:AssumeRole, kèm điều kiện StringEquals trên sts:ExternalId.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "acme-7f3k9q2x" } }
}]
}
Ở phía bạn, identity chạy dịch vụ (user, role của EC2 hay task container) cần một policy cho phép gọi lên đúng ARN này:
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::444455556666:role/VendorReadOnly"
}
Sau đó bạn gọi bằng CLI và lần nào cũng truyền ExternalId, như AWS Security Blog khuyến nghị:
aws sts assume-role \
--role-arn arn:aws:iam::444455556666:role/VendorReadOnly \
--role-session-name acme-onboarding \
--external-id acme-7f3k9q2x
Lệnh trên chạy được rồi cũng đừng dừng lại. Hãy chạy lại đúng lệnh đó nhưng bỏ --external-id. Nếu vẫn assume được thì trust policy đang thiếu điều kiện, và AWS khuyên rằng khi đó bạn không nên lưu ARN của khách vào hệ thống. Bước kiểm tra ngược này nên nằm trong code onboarding, đừng để nó phụ thuộc vào trí nhớ của ai.
Thêm một điểm thực tế: Switch Role trên AWS Console chỉ dùng được với role không yêu cầu ExternalId. Nếu khách muốn bạn “vào console xem thử”, bạn sẽ phải làm qua CLI hoặc SDK. Hãy báo trước để không ai nghĩ role đang hỏng.
Gỡ AccessDenied theo thứ tự nào?
Thứ tự dưới đây đi từ chỗ dễ sai nhất và dễ xác minh nhất đến chỗ khó thấy nhất.
Principal. Tài liệu troubleshooting của IAM viết rằng nếu số tài khoản của bạn không có trong phần tử Principal của trust policy thì bạn không thể assume role, kể cả khi các policy khác cho phép. Hãy nhờ khách dán nguyên trust policy, đừng chỉ hỏi “anh đã thêm tài khoản bên em chưa”.
ARN và tên role. Tên role phân biệt hoa thường khi assume. VendorReadOnly và vendorreadonly là hai role khác nhau. Lỗi gõ sai trong ARN gửi qua chat xảy ra thường hơn bạn nghĩ.
Condition. Mọi điều kiện trong trust policy đều phải được thỏa mãn, dù là ExternalId, IP nguồn hay thời hạn. Nếu khách bắt buộc MFA mà bạn không gửi TokenCode hoặc gửi sai, AssumeRole cũng trả về access denied. Nếu khách giới hạn IP nguồn, hãy xác nhận lệnh đang chạy từ đúng địa chỉ mà khách đã cho phép.
Quyền phía bạn. Nếu ba bước trên đều ổn thì hãy quay về phía mình. Kiểm tra xem identity đang chạy lệnh có thật sự mang policy cho phép sts:AssumeRole lên ARN đó hay không. Rất có thể bạn đang dùng nhầm profile.
SCP của AWS Organizations. Ở các tổ chức có nhiều tài khoản, tài liệu của Datadog cảnh báo rằng SCP có thể chặn AssumeRole ngay cả khi IAM policy và trust policy đều đúng. FDE hay bỏ sót lỗi này, vì người tạo role bên khách thường không phải người quản lý SCP. Hãy hỏi thẳng: “Tài khoản này có nằm trong OU nào áp SCP lên STS không?”
Thời hạn session. Nếu bạn đang đứng trong một role và assume tiếp role của khách, đó là role chaining, và session bị giới hạn tối đa 1 giờ. Đặt DurationSeconds lớn hơn mức này thì thao tác sẽ thất bại. Job chạy dài cần có cơ chế làm mới credential.
Những lỗi khiến khách mất niềm tin
Lỗi tệ nhất là để khách tự đặt ExternalId, hoặc dùng chung một giá trị cho mọi khách. Cả hai cách đều vô hiệu hóa cơ chế chống confused deputy. Kế đến là xin quyền admin “cho nhanh” thay vì một role chỉ đọc có phạm vi rõ ràng: đội bảo mật của khách sẽ nhớ chuyện đó lâu hơn nhiều so với tính năng bạn demo.
Một lỗi ít ai để ý là bỏ qua bước thử assume không kèm ExternalId, rồi lưu một ARN có trust policy lỏng vào hệ thống. Đến khi có khách thứ hai, lỗ hổng đó trở thành vấn đề của bạn chứ không còn là của khách.
Kỹ năng này cũng dễ đưa vào CV: thay vì ghi “biết AWS IAM”, hãy viết một dòng như “thiết kế quy trình onboarding cross-account có ExternalId cho từng khách và bước kiểm tra trust policy tự động”, rồi chuẩn bị sẵn ví dụ ở trên để trả lời khi người phỏng vấn hỏi tiếp.
Lần sau gặp AccessDenied trước giờ kickoff, bạn không cần đoán nữa. Hãy mở trust policy, đi lần lượt từng bước trong danh sách trên và nói cho khách biết lỗi đang nằm ở bước nào.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!
7 nguồn
- The confused deputy problem - AWS Identity and Access Management
- Access to AWS accounts owned by third parties - AWS IAM
- AssumeRole - AWS Security Token Service API Reference
- Troubleshoot IAM roles - AWS Identity and Access Management
- IAM tutorial: Delegate access across AWS accounts using IAM roles
- How to use external ID when granting access to your AWS resources to a third party · 2014-12-23
- Amazon Web Services integration - Datadog Docs