IAM Policy Hierarchy & Propagation — Inheritance, Eventual Consistency và Caching
Tại sao propagation model quan trọng trong production
Đây là một scenario phổ biến: bạn vừa revoke role của một user bị compromise, IAM console báo thành công, nhưng user vẫn còn access trong vài phút tiếp theo. Hoặc ngược lại: bạn grant access cho một service account mới, service chạy ngay nhưng nhận 403 PERMISSION_DENIED, rồi tự dưng hoạt động sau 30 giây mà không rõ lý do.
Cả hai scenarios này xuất phát từ IAM propagation model — IAM không phải real-time globally consistent system. Hiểu cơ chế propagation, caching, và eventual consistency không chỉ giúp debug mà còn giúp thiết kế system chịu đựng được những khoảng thời gian access state bất định.
Resource hierarchy và inheritance model
GCP tổ chức tài nguyên theo một cây phân cấp:
Organization
└── Folder
└── Folder (nested, tùy ý sâu)
└── Project
└── Resource (GCS bucket, GCE instance, BigQuery dataset...)IAM policy có thể được gắn vào bất kỳ node nào trong cây này. Policy được gắn ở node cao hơn sẽ kế thừa xuống tất cả các node con.
Nguyên tắc kế thừa: Union, không Override
Effective policy của một resource là union của tất cả policies từ resource đó và tất cả ancestors:
Effective_Policy(R) = Policy(R) ∪ Policy(Project) ∪ Policy(Folder₁) ∪ Policy(Folder₂) ∪ Policy(Org)Điều này có một hệ quả quan trọng: policies ở levels thấp không thể restrict hoặc override policies ở levels cao. Nếu alice@example.com có roles/editor ở Organization level, không có cách nào revoke quyền này ở Project hay Resource level — cô ta vẫn là Editor trên mọi resource trong org, kể cả khi Project policy không có binding nào cho alice.
Ví dụ minh họa hierarchy
Organization: example.com
Policy: security-team@example.com → roles/securityreviewer
Folder: production
Policy: ops-team@example.com → roles/viewer
Project: prod-backend
Policy:
backend-sa@prod-backend.iam.gserviceaccount.com → roles/storage.objectViewer
alice@example.com → roles/storage.admin
Resource: gs://prod-data-bucket
Policy: (trống — không có binding nào)Effective policy của gs://prod-data-bucket:
security-team@example.comcóroles/securityreviewer(từ Organization)ops-team@example.comcóroles/viewer(từ Folder)backend-sa@...córoles/storage.objectViewer(từ Project)alice@example.comcóroles/storage.admin(từ Project)
Một member của security-team@example.com có thể đọc bucket dù bucket policy hoàn toàn trống.
IAM policy không phải RBAC của Kubernetes
Một nhầm lẫn phổ biến với engineer có background Kubernetes: IAM policy inheritance khác hoàn toàn với Kubernetes RBAC.
Trong Kubernetes RBAC:
- Không có inheritance — namespace A không ảnh hưởng namespace B
- ClusterRole + RoleBinding chỉ grant trong một namespace cụ thể
Trong GCP IAM:
- Inheritance xuyên suốt tree từ trên xuống
- Grant ở Organization level = grant trên mọi resource
- Không có built-in deny trong allow policy (phải dùng Deny Policy riêng)
- Scope rất rộng nếu không cẩn thận
Eventual Consistency Model
Theo Google Cloud documentation, IAM implements eventual consistency. Đây là điểm then chốt về kiến trúc:
Tại sao IAM không phải strong consistency
Để hiểu tại sao, cần biết IAM phải phục vụ:
- Hàng tỷ authorization checks mỗi giây trên global scale
- Từ hàng trăm regions và zones
- Với latency yêu cầu sub-millisecond
Nếu IAM là strongly consistent, mỗi policy change cần được replicated và acknowledged bởi tất cả nodes toàn cầu trước khi có effect. Điều này sẽ làm latency của policy changes lên đến nhiều giây, và làm chậm mọi authorization check trong quá trình propagation.
Thay vào đó, GCP chọn eventual consistency: khi bạn thực hiện một IAM change (grant/revoke role), change đó được ghi vào primary datastore, sau đó dần dần propagate đến các authorization check systems ở các regions và zones khác nhau.
Propagation delay: Con số thực tế
Theo kinh nghiệm thực tế và documentation từ Google:
| Loại change | Propagation time điển hình | Maximum |
|---|---|---|
| Grant role | 30–60 giây | Vài phút |
| Revoke role | 30–60 giây | Vài phút |
| Service account key rotate | Gần tức thì (local check) | N/A |
| Project deletion | Có thể đến 30 phút | Hiếm gặp |
Critical security implication: Khi revoke role khẩn cấp (incident response), access không bị cut ngay lập tức. Có một "window of danger" từ vài giây đến vài phút. Nếu cần guaranteed immediate block, phải dùng biện pháp khác — ví dụ: disable service account (ngay lập tức, không phải eventual), hoặc rotate credentials.
Propagation delay ảnh hưởng đến gì
1. Deployment scripts với IAM changes:
# Anti-pattern: tạo SA rồi ngay lập tức sử dụng
gcloud iam service-accounts create my-sa
gcloud projects add-iam-policy-binding MY_PROJECT \
--member="serviceAccount:my-sa@..." \
--role="roles/storage.objectViewer"
# Chạy task ngay — có thể fail vì IAM chưa propagate
gcloud storage objects list gs://my-bucket/ # 403!Solution: Implement retry logic với exponential backoff thay vì hardcode sleep.
2. Security incident response:
Khi phát hiện compromise, workflow thông thường là revoke role → confirm → monitor. Nhưng với eventual consistency, cần monitor access logs trong ít nhất 5 phút sau revoke để confirm.
3. Integration tests:
IAM changes trong test setup có thể chưa có effect khi test bắt đầu chạy. Tests cần account for propagation delay hoặc sử dụng pre-existing SA với stable permissions thay vì tạo/modify roles trong test.
Caching Architecture: IAM không check policy từng request
Một điểm quan trọng về performance: IAM không check policy database cho mỗi API request riêng lẻ. Điều đó sẽ là bottleneck không thể chấp nhận.
Thay vào đó, GCP dùng distributed authorization caching:
- Authorization decision được cache locally gần compute (trong cùng zone hoặc region)
- Cache được invalidate khi IAM policy thay đổi (nhưng có delay)
- Cache TTL được quản lý internally, không configurable
Kết quả: cùng một principal call cùng một API liên tục trong thời gian ngắn thường được served từ cache. Nhưng khi policy thay đổi, không phải mọi cache instance được invalidated ngay lập tức — đây là lý do propagation delay xảy ra.
Service account token caching
Đặc biệt với service accounts, còn có thêm một lớp caching: access tokens. Khi một application lấy access token cho service account, token đó có TTL (thường 1 giờ cho user-managed tokens, ngắn hơn với short-lived credentials). Ngay cả khi SA bị disable hoặc role bị revoke, application đang dùng token cũ vẫn có thể tiếp tục access cho đến khi token expire.
Điều này tạo ra gap trong incident response:
Timeline:
T=0: Phát hiện compromise
T=1: Revoke role của SA
T=2: IAM change propagate
T=3+: Application với cached token vẫn có access (đến khi token expire)Mitigation tức thì: Disable service account (không phải revoke role) để invalidate tất cả existing tokens ngay lập tức. Khi SA bị disabled, tất cả tokens cấp cho SA đó đều trở nên invalid, bất kể còn TTL hay không.
# Disable SA để cut access ngay lập tức — không phải eventual
gcloud iam service-accounts disable SA_EMAIL@PROJECT.iam.gserviceaccount.com
# Sau khi incident resolved, enable lại
gcloud iam service-accounts enable SA_EMAIL@PROJECT.iam.gserviceaccount.comDebugging propagation issues
Khi gặp "access denied" sau khi vừa grant role, hoặc "still have access" sau khi vừa revoke, đây là checklist debugging:
Kiểm tra effective policy
# Xem effective policy cho resource (bao gồm inherited)
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--format='table(bindings.role, bindings.members)'
# Kiểm tra xem principal có permission cụ thể không
gcloud projects test-iam-permissions PROJECT_ID \
--permissions="storage.objects.get"Sử dụng Policy Troubleshooter
GCP có tool Policy Troubleshooter để trace chính xác tại sao một principal có/không có permission:
# Via gcloud
gcloud policy-troubleshoot iam RESOURCE_URL \
--principal-email="alice@example.com" \
--permission="storage.objects.get"Tool này sẽ chỉ ra chính xác binding nào grant/deny permission, ở level nào trong hierarchy, và nếu có conditional binding, condition có evaluate to true không.
Kiểm tra inheritance từ levels cao hơn
Nếu không thấy binding ở project level nhưng user vẫn có access:
# Check từ folder và org
gcloud resource-manager folders get-iam-policy FOLDER_ID | grep "USER_EMAIL"
gcloud organizations get-iam-policy ORG_ID | grep "USER_EMAIL"
# Check group membership (user có thể access qua group)
gcloud identity groups memberships list \
--group-email="group@example.com"Design patterns cho eventual consistency
Pattern 1: Retry với exponential backoff
Khi code phụ thuộc vào IAM change vừa thực hiện, implement retry thay vì hardcode sleep:
import time
import random
def with_iam_propagation_retry(func, max_retries=10, base_delay=2):
for attempt in range(max_retries):
try:
return func()
except Exception as e:
if "403" in str(e) or "PERMISSION_DENIED" in str(e):
if attempt < max_retries - 1:
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
time.sleep(min(delay, 60))
continue
raisePattern 2: Pre-provision, không grant-on-demand
Thay vì dynamically grant access, pre-provision service accounts với quyền cần thiết. Tránh pattern "tạo SA → grant role → ngay lập tức dùng" trong production pipelines.
Pattern 3: Verify trước khi depend
Sau khi thực hiện IAM change quan trọng trong automation pipeline, verify bằng test-iam-permissions call trước khi tiếp tục:
# Verify permission đã có effect trước khi tiếp tục
MAX_RETRIES=12
for i in $(seq 1 $MAX_RETRIES); do
RESULT=$(gcloud projects test-iam-permissions PROJECT_ID \
--permissions="storage.objects.get" \
--format="value(permissions)" 2>/dev/null)
if [ -n "$RESULT" ]; then
echo "Permission ready after ${i} attempts"
break
fi
echo "Waiting for IAM propagation... ($i/$MAX_RETRIES)"
sleep 10
donePolicy change audit trail
Mọi IAM policy change đều được ghi vào Cloud Audit Logs dưới dạng setIamPolicy admin activity event. Đây là cách trace khi nào policy thay đổi:
# Tìm tất cả IAM policy changes trong 24 giờ qua
gcloud logging read \
'logName="projects/PROJECT_ID/logs/cloudaudit.googleapis.com%2Factivity"
AND protoPayload.methodName="SetIamPolicy"' \
--freshness=24h \
--format=json | jq '.[] | {
time: .timestamp,
who: .protoPayload.authenticationInfo.principalEmail,
resource: .resource.labels
}'