Access Control: IAM, ACLs, Signed URLs & Requester Pays
Tại Sao Quan Trọng Trong Production
GCS có hai hệ thống access control song song: IAM (identity-centric, resource-level) và ACLs (object-level legacy). Hiểu sai model này dẫn đến hoặc là overly permissive (data leak) hoặc access denied không giải thích được. Signed URLs tạo ra attack surface khi không được revoke đúng cách. Requester Pays là billing model ít được biết nhưng quan trọng cho public data distribution.
Internal Model — IAM vs ACLs
Hai Hệ Thống Song Song
GCS ban đầu được thiết kế với ACL model (kế thừa từ Google Storage thời kỳ đầu). IAM được thêm vào sau để integrate với GCP resource hierarchy. Hai hệ thống này evaluate độc lập nhau:
Khi client request tới GCS bucket/object:
IAM check:
principal có role phù hợp (roles/storage.objectViewer etc.) trên bucket? → ALLOW
ACL check (nếu không có Uniform Bucket-Level Access):
object/bucket có ACL entry cho principal này? → ALLOW
Kết quả: ALLOW nếu BẤT KỲ check nào passLogic là OR — IAM grant access OR ACL grant access đều cho phép. Điều này có hệ quả security: nếu team dùng IAM để control access nhưng quên rằng ACLs vẫn tồn tại, data có thể exposed qua ACL path.
Uniform Bucket-Level Access (UBLA) — Cách Đúng
Uniform Bucket-Level Access (UBLA) là setting trên bucket disable ACL system hoàn toàn. Khi UBLA bật:
- Mọi access control phải qua IAM
- Object-level ACLs không được apply, không thể set
- ACL APIs vẫn hoạt động nhưng trả về fixed "bucket owner controls"
Google strongly recommend bật UBLA cho tất cả buckets mới. Đây là default setting kể từ 2022.
# Bật UBLA
gcloud storage buckets update gs://my-bucket \
--uniform-bucket-level-access
# Verify
gcloud storage buckets describe gs://my-bucket \
--format="json(iamConfiguration.uniformBucketLevelAccess)"Lưu ý quan trọng: UBLA có thể tắt trở lại trong vòng 90 ngày sau khi bật. Sau 90 ngày, không thể tắt (hard lock). Đây là tính năng bảo mật — prevent rollback sau một khoảng thời gian đủ dài để "bake in" security.
IAM Roles Cho GCS
GCS dùng IAM roles ở hai cấp: bucket level và project level.
Predefined roles phổ biến:
| Role | Bucket Level | Quyền |
|---|---|---|
roles/storage.objectViewer | ✓ | Đọc objects |
roles/storage.objectCreator | ✓ | Upload objects (không xóa, không đọc) |
roles/storage.objectUser | ✓ | Đọc + upload objects |
roles/storage.objectAdmin | ✓ | Full object control |
roles/storage.legacyBucketReader | ✓ | Đọc bucket metadata + objects |
roles/storage.admin | Project | Full control tất cả buckets trong project |
Một subtlety quan trọng: storage.objectCreator cho phép upload nhưng không cho phép list bucket hay download. Điều này hữu ích cho write-only upload endpoints (sensor data, logs) mà không expose data đã upload.
Với IAM conditions, có thể restrict access theo:
- Object name prefix:
resource.name.startsWith("projects/_/buckets/b/objects/user-data/") - Thời gian trong ngày
- IP range (thông qua Access Level)
ACL Model (Legacy) — Khi Nào Còn Liên Quan
ACLs vẫn relevant trong hai scenario:
- Buckets tạo trước UBLA era và chưa migrate
- Public objects: Để make object public, set
allUsersACL vớiREADERrole
# Make object public (không cần UBLA tắt - đây là object-level IAM)
gcloud storage objects update gs://bucket/file.txt \
--add-acl-grant=entity=allUsers,role=READERVới UBLA enabled, public access được control qua IAM:
# Make bucket publicly readable với UBLA
gcloud storage buckets add-iam-policy-binding gs://my-bucket \
--member=allUsers \
--role=roles/storage.objectViewerSigned URLs V4 — Delegated Access
Vấn Đề Signed URLs Giải Quyết
Có kịch bản phổ biến: user của ứng dụng cần upload/download file trực tiếp lên/từ GCS mà không đi qua backend server (để tránh double-transfer bandwidth). Nhưng GCS cần authentication — user không có GCP credentials.
Signed URLs giải quyết: backend server ký một URL tạm thời, user dùng URL đó để access trực tiếp GCS mà không cần credentials GCP.
Cơ Chế Signing V4
Signed URL V4 dùng HMAC-SHA256 (với service account private key) để sign một canonical request string. Chữ ký được embed trong URL params.
Cấu trúc một Signed URL V4:
https://storage.googleapis.com/BUCKET_NAME/OBJECT_NAME
?X-Goog-Algorithm=GOOG4-RSA-SHA256
&X-Goog-Credential=SERVICE_ACCOUNT%40PROJECT.iam.gserviceaccount.com
%2FYYYYMMDD%2Fauto%2Fstorage%2Fgoog4_request
&X-Goog-Date=YYYYMMDDTHHMMSSZ
&X-Goog-Expires=SECONDS
&X-Goog-SignedHeaders=host
&X-Goog-Signature=SIGNATURE_HEXX-Goog-Credential format: SA_EMAIL/DATE/LOCATION/SERVICE/REQUEST_TYPE
Signature được tính từ:
HMAC-SHA256(
signing_key,
canonical_request_string
)
canonical_request = METHOD + "\n" +
canonical_uri + "\n" +
canonical_query_string + "\n" +
canonical_headers + "\n" +
signed_headers + "\n" +
hashed_payloadSignature ràng buộc URL với:
- Đúng method (GET, PUT, DELETE...)
- Đúng bucket + object
- Đúng date (không reuse được sau ngày ký)
- Đúng expiry window
Maximum expiry: 7 ngày (604800 giây). Sau đó URL không còn valid.
Signing Methods
Method 1: Service Account Key (JSON key file)
import google.auth.crypt
import google.auth.transport.requests
from google.oauth2 import service_account
credentials = service_account.Credentials.from_service_account_file("sa-key.json")
signed_url = bucket.blob("object").generate_signed_url(
version="v4",
expiration=datetime.timedelta(hours=1),
method="GET",
credentials=credentials
)Downside: Phải có private key file — nên avoid trong containerized environments vì key management phức tạp.
Method 2: Impersonation (không cần key file)
from google.auth import impersonated_credentials
import google.auth
# Dùng Workload Identity / ADC
source_credentials, _ = google.auth.default()
target_credentials = impersonated_credentials.Credentials(
source_credentials=source_credentials,
target_principal="sa-for-signing@project.iam.gserviceaccount.com",
target_scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
# SA này cần roles/iam.serviceAccountTokenCreator trên target SAMethod 3: signBlob IAM API
# Gọi IAM API để sign bytes
curl -X POST \
"https://iam.googleapis.com/v1/projects/-/serviceAccounts/SA@PROJECT.iam.gserviceaccount.com:signBlob" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-d '{"payload": "BASE64_ENCODED_BYTES"}'Signed URLs Và Revocation — The Hard Problem
Signed URL V4 là stateless — không có server-side state hay revocation list. Một URL đã ký không thể bị revoke trước expiry.
Hệ quả: Nếu signed URL bị leak (logged, shared, cached):
- Không thể invalidate URL đó
- Chỉ cách "revoke" là rotate service account key (nếu dùng method 1) — nhưng điều này invalidate tất cả signed URLs của SA đó, bao gồm URLs hợp lệ
Design patterns để mitigate:
- Short expiry cho upload URLs (5–15 phút), longer cho download
- Never log signed URLs (filter trong logging middleware)
- One-time use pattern: backend invalidate một flag sau khi URL được dùng (application-level)
- Nếu cần revocation: dùng server-side redirect — URL trỏ về backend, backend validate rồi redirect sang GCS
Giới Hạn Quan Trọng
Signed URLs chỉ work với XML API endpoint của GCS (storage.googleapis.com), không phải JSON API (storage.googleapis.com/upload/storage/v1/...).
✓ https://storage.googleapis.com/BUCKET/OBJECT?X-Goog-...
✗ https://storage.googleapis.com/upload/storage/v1/b/BUCKET/o?...Requester Pays
Cơ Chế
Mặc định, owner của bucket trả tiền cho mọi operations (API calls, egress). Requester Pays là setting đảo ngược: caller trả tiền cho API calls và egress từ bucket đó.
Use case chính: Public datasets (genomics data, satellite imagery, open datasets) mà owner muốn share nhưng không muốn chịu egress bill từ hàng nghìn users download.
# Enable Requester Pays
gcloud storage buckets update gs://my-bucket --requester-pays
# Caller phải specify billing project khi request
gcloud storage cp gs://requester-pays-bucket/data.csv ./data.csv \
--billing-project=MY_PROJECT_IDClient libraries cũng cần truyền userProject parameter:
# Python
blob = bucket.blob("data.csv")
blob.download_to_filename("data.csv", client=storage.Client(project="billing-project"))Security Consideration
Requester Pays không thay đổi access control — vẫn cần đúng IAM permissions. Nhưng anonymous access (allUsers) không thể dùng với Requester Pays vì không có billing account để charge.
Nếu bucket là public với Requester Pays, users phải authenticate để specify billing project — effectively loại bỏ anonymous access.
Anti-Patterns Phổ Biến
Anti-Pattern 1: Dùng ACLs Song Song Với IAM
Bucket không có UBLA → IAM và ACLs cùng active
Team set IAM permissions chặt chẽ
Nhưng một object cũ có ACL "allUsers READER" từ trước
→ Object đó public dù IAM không cho phépFix: Bật UBLA, audit và remove legacy ACLs trước khi bật.
Anti-Pattern 2: Signed URL Expiry Quá Dài
Backend tạo signed URL với expiry = 7 ngày cho upload
URL được log trong access logs
Attacker có log access → có URL hợp lệ 7 ngày để upload arbitrary data vào bucketFix: Upload URLs: 5–15 phút expiry. Download URLs: tùy risk tolerance, nhưng < 1 giờ cho sensitive data.
Anti-Pattern 3: Service Account Thừa Quyền Cho Signing
SA dùng để sign URLs có roles/storage.admin trên toàn project
→ Compromised SA = attacker có thể sign URLs cho mọi bucket trong projectFix: SA chuyên dụng cho signing với quyền tối thiểu (chỉ trên bucket cần thiết), không có key file (dùng impersonation).