Skip to content

Lan truyền IAM Policy: Eventual Consistency & kiểm thử

Vì sao IAM propagation khó xử lý

Một trong những khía cạnh ít được hiểu đúng nhất của bảo mật GCP là sự thật rằng IAM policies không lan truyền ngay lập tức. Khi bạn cấp một role cho một user:

T+0: gcloud projects add-iam-policy-binding PROJECT_ID \
       --member=user:alice@company.com \
       --role=roles/editor
     Response: Updated policy

T+0.5s: Alice thử truy cập project
     Kết quả: Có thể nhận "permission denied" (policy chưa hiển thị)

T+5s: Alice thử lại
     Kết quả: Nhiều khả năng thành công (policy đã lan truyền)

T+60s+: Tất cả cache đã được cập nhật (đã đảm bảo)

Thực tế trong production:

  • User được cấp role rồi thử dùng ngay — bị từ chối truy cập
  • Script tạo service account rồi dùng ngay — thất bại
  • IAM policies thay đổi nhưng service phụ thuộc vẫn ép buộc quyền cũ
  • Monitoring hiển thị audit log không nhất quán

Kiến trúc lan truyền IAM

IAM policies có hệ thống lan truyền ba lớp:

Lớp 1: Control Plane (ngay lập tức)

T+0: gọi API setIamPolicy()

     Policy được lưu ở master control plane

     Trả response về (đồng bộ)

Ở lớp này, IAM policy được cập nhật ngay. API call hoàn tất thành công.

Lớp 2: Cache của API server (5-30 giây)

T+0 đến T+30s: 
     kube-apiserver (hoặc service GCP tương đương) cập nhật cache policy cục bộ
     Điều này lan sang:
     - Load balancers
     - Triển khai theo region
     - Các bản sao service

Ví dụ: GKE control plane sao chép IAM policies tới tất cả replica trong cluster. Cần thời gian để đạt đồng thuận.

Lớp 3: Data Plane Services (5-60 giây)

T+5 đến T+60s:
     Compute Engine, Cloud Storage, BigQuery, v.v.
     đồng bộ thay đổi policy từ master
     
     Một số service có cache cục bộ:
     - Compute Engine cache ở mức node
     - Cloud Storage cache ở mức region

Trường hợp xấu nhất:

  • User nhận 403 Forbidden (permission denied) khi đáng lẽ phải có quyền
  • Không có log lỗi rõ ràng — chỉ là permission denied
  • Rất khó debug

Độ trễ lan truyền thực tế

Kịch bản 1: Kiểm soát truy cập user

bash
# T+0: Cấp role Editor
gcloud projects add-iam-policy-binding my-project \
  --member=user:alice@company.com \
  --role=roles/editor

# T+0 đến T+10s: Alice thử truy cập Cloud Console
# Kết quả: "You don't have permissions to access this project"

# T+15s: Alice refresh trình duyệt
# Kết quả: Truy cập được (policy đã lan truyền)

Tại sao? Cloud Console cache IAM policies ở cả trình duyệt và backend. Cả hai cache đều cần cập nhật.

Kịch bản 2: Service account impersonation

python
# T+0: Tạo service account + cấp role
sa = create_service_account("app-sa")
grant_role(sa_email, "roles/compute.admin")

# T+0 đến T+5s: Thử tạo VM bằng service account
gcloud compute instances create test-vm \
  --service-account=$SA_EMAIL
# Có thể thất bại: Service account chưa có compute.instances.create

# T+10s: Thử lại
# Thành công: Role đã lan truyền

Kịch bản 3: Deny policies

Deny policies có thời gian lan truyền dài hơn (tối đa 60 giây):

bash
# T+0: Tạo deny policy (explicit deny)
gcloud iam deny-policies create deny-sa-iam-binding \
  --location=organizations/ORG_ID \
  --rules='deny {permissions: ["iam.serviceAccounts.actAs"]; principals: ["principalSet://goog/public:all"]}'

# T+0 đến T+60s: Deny policy lan truyền
# Enforcement có thể không nhất quán trong khoảng này

Hành vi cache

Client-side caching

Google Cloud SDKs cache thông tin policy:

python
from google.cloud import iam_admin_v1
from functools import lru_cache

# Mặc định: SDK cache 5 phút
policy = iam_admin_client.get_iam_policy(resource)

# ❌ Vấn đề: Cache cũ
time.sleep(2)  # User vừa được cấp role mới
policy = iam_admin_client.get_iam_policy(resource)  # Vẫn thấy policy cũ

# ✅ Giải pháp: Tắt cache
client = iam_admin_v1.IAMClient()
client.api = iam_admin_v1.services.iam.transports.IAMTransport(
    # Tắt cache
    cache_policy=None
)

Cache ở cấp service

Các service GCP khác nhau cache policy khác nhau:

ServiceTTL cacheThời gian lan truyền
Cloud IAM (API)Ngay lập tức1-5 giây
Cloud Console UI5 phút10-30 giây
Compute Engine10 phút mỗi node5-30 giây mỗi region
Cloud Storage5 phút10-60 giây
BigQuery15 phút5-30 giây
GKETùy biến5-30 giây

Kiểm thử IAM propagation

Test 1: Kiểm tra quyền

python
def test_iam_propagation(resource_name, member, role):
    """Kiểm tra thay đổi IAM policy đã lan truyền hay chưa"""
    import time
    
    # Cấp role
    policy = get_iam_policy(resource_name)
    policy.bindings.append({
        "role": role,
        "members": [member]
    })
    set_iam_policy(resource_name, policy)
    
    # Poll cho tới khi quyền hiển thị
    max_retries = 30
    for attempt in range(max_retries):
        try:
            # Thử thao tác cần role đó
            result = test_permission(resource_name, member, role)
            if result:
                print(f"✓ Role đã lan truyền sau {attempt} giây")
                return True
        except Exception as e:
            if attempt == max_retries - 1:
                print(f"✗ Role chưa lan truyền sau {attempt} giây")
                raise
        
        time.sleep(1)
    
    return False

def test_permission(resource_name, member, role):
    """Xác minh member thật sự có role thông qua testIamPermissions"""
    # testIamPermissions là kiểm tra theo permission cụ thể
    
    # Lấy permissions được cấp bởi role
    role_permissions = get_permissions_for_role(role)
    
    # Kiểm tra member có thể thực hiện các permission đó không
    can_perform = client.test_iam_permissions(
        resource=resource_name,
        permissions=role_permissions,
        identity=member  # Trong API thật, kiểm tra thông qua service account
    )
    
    return len(can_perform) > 0

Test 2: Kiểm thử service account

bash
#!/bin/bash
# test-iam-propagation.sh

PROJECT_ID=$1
SA_EMAIL=$2
TIMEOUT=60

# Cấp role cho service account
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member=serviceAccount:$SA_EMAIL \
  --role=roles/compute.admin

# Poll cho đến khi service account có thể dùng role
end_time=$(($(date +%s) + TIMEOUT))

while [ $(date +%s) -lt $end_time ]; do
    # Tạo credentials của service account (local cho testing)
    gcloud iam service-accounts keys create /tmp/key.json \
      --iam-account=$SA_EMAIL
    
    # Thử thao tác cần role
    if gcloud compute instances list \
           --project=$PROJECT_ID \
           --account=$SA_EMAIL \
           --key-file=/tmp/key.json 2>/dev/null; then
        echo "✓ IAM propagated successfully"
        rm /tmp/key.json
        exit 0
    fi
    
    sleep 2
done

echo "✗ IAM not propagated within $TIMEOUT seconds"
exit 1

Test 3: Lan truyền giữa các service

python
def test_cross_service_propagation():
    """Kiểm tra Compute Engine có thấy thay đổi IAM hay không"""
    import time
    
    sa_email = create_service_account("test-sa")
    
    # Cấp role Compute Instance Admin
    grant_role(sa_email, "roles/compute.instanceAdmin.v1")
    
    # Chờ propagation
    time.sleep(5)
    
    # Kiểm tra: service account có thể tạo VM không?
    try:
        credentials = impersonate_service_account(sa_email)
        compute_client = compute_v1.InstancesClient(credentials=credentials)
        
        # Thử tạo VM
        operation = compute_client.insert(
            project=PROJECT_ID,
            zone="us-central1-a",
            body={"name": "test-vm", "machineType": "..."}
        )
        
        print("✓ Service account có thể tạo VM (IAM đã lan truyền)")
        return True
    except Exception as e:
        if "permission denied" in str(e):
            print(f"✗ IAM chưa lan truyền: {e}")
            return False
        raise

Xử lý propagation trong production

Mẫu 1: Retry loop với exponential backoff

python
import time
from google.api_core import retry

# Decorator xử lý retry tự động
@retry.Retry(
    initial=1,           # Bắt đầu 1 giây
    maximum=10,          # Tối đa 10 giây
    multiplier=2,        # Tăng gấp đôi mỗi lần
    deadline=60          # Timeout tổng: 60 giây
)
def use_service_account(sa_email):
    """Dùng service account (có thể thất bại lúc đầu nếu IAM chưa lan truyền)"""
    try:
        # Thử thao tác
        create_resource_with_sa(sa_email)
        return True
    except google.api_core.exceptions.PermissionDenied:
        # Retry nếu bị permission denied (nhiều khả năng là propagation)
        raise

# Cách dùng:
use_service_account("app-sa@project.iam.gserviceaccount.com")

Mẫu 2: Idempotent operations

python
def create_vm_idempotent(instance_name, sa_email):
    """Tạo VM, xử lý IAM propagation một cách an toàn"""
    import time
    
    for attempt in range(5):
        try:
            # Kiểm tra VM đã tồn tại chưa
            try:
                instance = get_instance(instance_name)
                print(f"✓ VM đã tồn tại")
                return instance
            except NotFound:
                pass
            
            # Tạo VM (có thể thất bại nếu IAM chưa lan truyền)
            instance = create_vm(instance_name, service_account=sa_email)
            print(f"✓ Đã tạo VM sau {attempt} lần thử")
            return instance
            
        except PermissionDenied as e:
            if attempt < 4:
                wait_time = 2 ** attempt  # exponential backoff
                print(f"! Bị từ chối quyền, thử lại sau {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise

create_vm_idempotent("app-vm", sa_email)

Mẫu 3: Pre-warming services

python
def setup_project_with_service_account(project_id, sa_email):
    """Thiết lập project, pre-warm services để đảm bảo IAM đã lan truyền"""
    
    # Tạo service account
    sa = create_service_account(project_id, sa_email)
    
    # Cấp các role cần thiết
    grant_role(project_id, sa_email, "roles/compute.admin")
    grant_role(project_id, sa_email, "roles/storage.admin")
    
    # Pre-warm: gọi thử API bằng service account
    # Điều này buộc các service tải và cache IAM policies
    
    print("Pre-warming Compute Engine...")
    try:
        compute_client = compute_v1.InstancesClient(
            credentials=impersonate_service_account(sa_email)
        )
        compute_client.list(project=project_id, zone="us-central1-a")
    except Exception:
        pass  # Có thể thất bại nếu chưa có resource, nhưng vẫn làm nóng cache
    
    print("Pre-warming Cloud Storage...")
    try:
        storage_client = storage.Client(
            project=project_id,
            credentials=impersonate_service_account(sa_email)
        )
        list(storage_client.list_buckets())
    except Exception:
        pass
    
    # Chờ cache ổn định
    time.sleep(5)
    
    print("✓ Project đã được pre-warm, sẵn sàng vận hành")

Giám sát vấn đề propagation IAM

Phát hiện qua audit logs

bash
# Truy vấn audit logs cho thay đổi IAM
gcloud logging read \
  'severity=WARNING AND 
   resource.type="service_account" AND 
   protoPayload.methodName=~"SetIamPolicy"' \
  --limit=10 \
  --format=json

# Theo dõi lỗi permission denied
gcloud logging read \
  'severity=ERROR AND 
   httpRequest.status="403"' \
  --limit=20

Triển khai monitoring tùy chỉnh

python
from prometheus_client import Gauge
import time

iam_propagation_delay = Gauge(
    'iam_propagation_delay_seconds',
    'Thời gian để IAM policy lan truyền hoàn toàn'
)

def measure_iam_propagation(resource, member, role):
    """Đo thời gian lan truyền thực tế"""
    start_time = time.time()
    
    grant_role(resource, member, role)
    
    # Poll cho đến khi thấy được
    while True:
        elapsed = time.time() - start_time
        
        if can_member_perform_action(resource, member, role):
            iam_propagation_delay.observe(elapsed)
            print(f"IAM propagation mất {elapsed:.1f}s")
            break
        
        if elapsed > 60:
            print("⚠️  IAM propagation > 60s (có thể có vấn đề)")
            break
        
        time.sleep(1)

Các kiểu lỗi thường gặp

Mẫu lỗiTriệu chứngCách khắc phục
Không có retry logicAPI call đầu tiên thất bại ngayThêm retry exponential backoff
Giả định tức thìRace condition trong testThêm delay 5-10s hoặc retry loop
Liên serviceService A thấy role, B không thấyChờ lâu hơn, pre-warm services
Client cachingPolicy cũ vẫn hiển thịXóa cache client hoặc tạo client mới
Deny policiesMất 60s để lan truyềnTăng delay khi đổi deny policy

Tham khảo