IAM & Access Control

Golden Rule: Never use root/owner accounts for daily operations. Create individual IAM users or use federated identities with the minimum required permissions. Root accounts should only be used for initial setup and break-glass scenarios, with hardware MFA enforced.

IAM Core Concepts

Authentication vs Authorization

ConceptQuestion It AnswersMechanismsExamples
Authentication (AuthN) "Who are you?" Passwords, MFA, certificates, biometrics, hardware keys SAML SSO, OIDC login, mTLS client certificates
Authorization (AuthZ) "What can you do?" RBAC, ABAC, ACL, policy engines IAM policies, Kubernetes RBAC, OPA decisions

Identity Providers & Federation

Modern environments use federated identity to allow users to authenticate once and access multiple systems (Single Sign-On). Key protocols:

  • SAML 2.0 (Security Assertion Markup Language): XML-based standard used for enterprise SSO. IdP (Okta, ADFS) issues assertions consumed by SPs. Used for AWS IAM Identity Center, GCP Workforce Identity Federation.
  • OIDC (OpenID Connect): OAuth 2.0 extension using JWT tokens. Widely used for modern web/mobile apps. Used for Kubernetes OIDC, GitHub Actions OIDC, GCP Workload Identity.
  • OAuth 2.0: Authorization framework (not authentication). Grants scoped access tokens to third-party applications without exposing credentials.

Federation Flow (OIDC)

  1. User/workload presents identity to IdP (e.g., GitHub Actions, Okta)
  2. IdP issues a short-lived JWT token signed with its private key
  3. Cloud provider (AWS/GCP) validates the token against the IdP's public JWKS endpoint
  4. Cloud provider issues temporary credentials based on configured trust policy
  5. Workload uses temporary credentials — no long-lived keys stored anywhere

Access Control Models

ModelFull NameHow It WorksBest ForExamples
RBAC Role-Based Access Control Permissions assigned to roles; users assigned to roles Enterprise environments with well-defined job functions Kubernetes RBAC, AWS IAM roles, database roles
ABAC Attribute-Based Access Control Access decisions based on attributes of user, resource, environment Fine-grained, dynamic access control; multi-tenancy AWS IAM Conditions, OPA policies, XACML
ReBAC Relationship-Based Access Control Access based on relationships between entities in a graph Social platforms, document sharing, hierarchical resources Google Zanzibar, SpiceDB, Ory Keto

AWS IAM Deep Dive

IAM Building Blocks

  • Users: Long-term credentials for humans or applications (legacy). Prefer federation and roles.
  • Groups: Collections of users. Attach policies to groups, not individual users.
  • Roles: Short-term credentials assumed by users, services, or cross-account principals. Preferred method for workloads.
  • Policies: JSON documents defining allow/deny actions on resources. Evaluation order: explicit deny > explicit allow > implicit deny.

Policy Types

TypeScopeUse Case
AWS Managed PolicyAccountCommon use cases provided by AWS (ReadOnlyAccess, AdministratorAccess)
Customer Managed PolicyAccountCustom policies reusable across multiple principals
Inline PolicySingle principalTightly coupled policy for one-off permissions — avoid for maintainability
Service Control Policy (SCP)AWS OrganizationGuardrails across all accounts in an OU — cannot grant permissions, only restrict
Permission BoundaryIAM entityMaximum permission ceiling for a user/role — used to delegate IAM administration safely
Resource-based PolicyResourceAttached to S3 buckets, SQS queues, KMS keys — grants cross-account access

Least-Privilege S3 Policy Example

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::my-app-data-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["app-data/${aws:PrincipalTag/Environment}/*"]
        }
      }
    },
    {
      "Sid": "AllowObjectOperations",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-app-data-bucket/app-data/${aws:PrincipalTag/Environment}/*"
    },
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-app-data-bucket/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    }
  ]
}

EC2 IAM Role with Least Privilege (SSM + CloudWatch)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SSMCoreAccess",
      "Effect": "Allow",
      "Action": [
        "ssm:UpdateInstanceInformation",
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel",
        "ec2messages:AcknowledgeMessage",
        "ec2messages:DeleteMessage",
        "ec2messages:FailMessage",
        "ec2messages:GetEndpoint",
        "ec2messages:GetMessages",
        "ec2messages:SendReply"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": "arn:aws:logs:ap-southeast-1:123456789012:log-group:/app/*"
    }
  ]
}

Trust Policy for Cross-Account Role Assumption

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/ci-cd-role"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "unique-external-id-abc123"
        },
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

Service Control Policy — Deny Non-Approved Regions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "support:*",
        "sts:GetCallerIdentity"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["ap-southeast-1", "us-east-1"]
        }
      }
    }
  ]
}

GCP IAM Deep Dive

Resource Hierarchy

GCP IAM policies are inherited down the hierarchy. A binding at a higher level grants access to all resources below it:

Organization (example.com)
  └── Folder (Production)
        └── Folder (Engineering)
              └── Project (my-app-prod)
                    └── Resource (Cloud Storage bucket, GKE cluster, etc.)

Role Types

  • Primitive roles: Owner, Editor, Viewer — overly broad, avoid in production
  • Predefined roles: Fine-grained roles managed by Google (e.g., roles/container.developer, roles/storage.objectViewer)
  • Custom roles: User-defined with specific permissions — use when predefined roles are too broad

GCP IAM Binding Examples

# Grant a service account viewer access to a specific bucket
gcloud storage buckets add-iam-policy-binding gs://my-prod-bucket \
  --member="serviceAccount:[email protected]" \
  --role="roles/storage.objectViewer"

# Grant a group access to deploy to Cloud Run in a specific project
gcloud projects add-iam-policy-binding my-app-prod \
  --member="group:[email protected]" \
  --role="roles/run.developer"

# Create a custom role with minimal permissions
gcloud iam roles create AppDeployer \
  --project=my-app-prod \
  --title="App Deployer" \
  --permissions="run.services.update,run.services.get" \
  --stage=GENERAL_AVAILABILITY

Workload Identity Federation (GCP)

Allows workloads outside GCP (GitHub Actions, AWS, on-prem) to authenticate as GCP service accounts without key files:

# Configure Workload Identity Pool for GitHub Actions
gcloud iam workload-identity-pools create "github-pool" \
  --project="my-project" \
  --location="global" \
  --display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc "github-provider" \
  --project="my-project" \
  --location="global" \
  --workload-identity-pool="github-pool" \
  --display-name="GitHub Provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

# Bind the pool to a service account
gcloud iam service-accounts add-iam-policy-binding \
  [email protected] \
  --project="my-project" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/my-org/my-repo"

Kubernetes RBAC

Core RBAC Resources

  • Role: Namespace-scoped permissions
  • ClusterRole: Cluster-wide permissions (or for non-namespaced resources)
  • RoleBinding: Binds a Role to subjects within a namespace
  • ClusterRoleBinding: Binds a ClusterRole to subjects cluster-wide

Developer Role (Namespace-Scoped)

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: my-app
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/portforward", "pods/exec"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-binding
  namespace: my-app
subjects:
  - kind: Group
    name: "developers"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io

Platform Ops ClusterRole

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: platform-ops
rules:
  - apiGroups: [""]
    resources: ["nodes", "persistentvolumes", "namespaces"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "daemonsets", "statefulsets"]
    verbs: ["get", "list", "watch", "update", "patch"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses", "networkpolicies"]
    verbs: ["*"]
  - apiGroups: ["rbac.authorization.k8s.io"]
    resources: ["roles", "rolebindings"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-ops-binding
subjects:
  - kind: Group
    name: "platform-team"
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: platform-ops
  apiGroup: rbac.authorization.k8s.io

MFA Enforcement

AWS: Enforce MFA via IAM Policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowViewAccountInfo",
      "Effect": "Allow",
      "Action": ["iam:GetAccountPasswordPolicy", "iam:ListVirtualMFADevices"],
      "Resource": "*"
    },
    {
      "Sid": "AllowManageOwnMFA",
      "Effect": "Allow",
      "Action": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ResyncMFADevice"
      ],
      "Resource": [
        "arn:aws:iam::*:mfa/${aws:username}",
        "arn:aws:iam::*:user/${aws:username}"
      ]
    },
    {
      "Sid": "DenyAllExceptAllowedIfNoMFA",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:GetUser",
        "iam:ListMFADevices",
        "iam:ResyncMFADevice",
        "sts:GetSessionToken"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

GCP: Enforce MFA via Org Policy

# Require 2-step verification for all users in the organization
# In Google Admin Console: Security > 2-Step Verification > Allow users to turn on 2-Step Verification
# Then enforce: Enforcement > Turn on enforcement

# For GCP API access, use org policy to require OS Login with 2FA
gcloud resource-manager org-policies set-policy \
  --organization=123456789 \
  os_login_2fa_policy.yaml

# os_login_2fa_policy.yaml
constraint: constraints/compute.requireOsLoginTwoFactor
booleanPolicy:
  enforced: true

Hardware Keys & TOTP

  • YubiKey (FIDO2/WebAuthn): Hardware security key — phishing-resistant, supports AWS, GCP, GitHub, Okta. Recommended for privileged users.
  • TOTP apps: Google Authenticator, Authy, 1Password — time-based one-time passwords. Good baseline but susceptible to real-time phishing.
  • Push-based MFA: Duo, Okta Verify — convenient but vulnerable to MFA fatigue attacks (deny all unexpected push requests).

Service Account Best Practices

Rules for Service Accounts

  • No shared accounts: One service account per application/service. Never share credentials between workloads.
  • Workload Identity over key files: On Kubernetes/GKE, use Workload Identity to map K8s service accounts to cloud IAM — no JSON key files.
  • Rotate keys automatically: If key files are unavoidable, automate rotation and store in a secrets manager.
  • Audit regularly: Review service account permissions quarterly. Delete unused accounts and keys.
  • Scope tightly: Grant only the specific permissions the service needs, scoped to specific resources.
# Enable Workload Identity on a GKE cluster
gcloud container clusters update my-cluster \
  --workload-pool=my-project.svc.id.goog \
  --region=asia-southeast1

# Annotate Kubernetes Service Account to bind to GCP SA
kubectl annotate serviceaccount my-app-ksa \
  --namespace=production \
  iam.gke.io/[email protected]

# Allow the KSA to impersonate the GCP SA
gcloud iam service-accounts add-iam-policy-binding \
  [email protected] \
  --role=roles/iam.workloadIdentityUser \
  --member="serviceAccount:my-project.svc.id.goog[production/my-app-ksa]"

Privileged Access Management (PAM)

Just-In-Time (JIT) Access

JIT access eliminates standing privileges. Users request elevated access for a specific time window, it is approved, granted, used, and automatically revoked. This dramatically reduces the blast radius if credentials are compromised.

JIT Access Workflow

  1. Engineer submits access request with justification and time window (e.g., "need prod DB access for incident investigation, 2 hours")
  2. Request goes through approval (manager, security team, automated for low-risk)
  3. Temporary role/permission is granted via automation (AWS IAM role assumption, GCP IAM Conditions with expiry)
  4. All session activity is recorded (session recording via BeyondTrust, Teleport, AWS Session Manager)
  5. Access is automatically revoked after the time window expires

Break-Glass Procedures

Break-glass accounts are emergency access accounts used when normal access paths are unavailable (IdP outage, major incident). They must be:

  • Stored in a physical safe or offline vault, not in a digital secrets manager that might be unavailable
  • Protected by hardware MFA (YubiKey)
  • Monitored with real-time alerting on any use
  • Audited and credentials rotated after each use
  • Tested quarterly to ensure they work when needed

Session Recording with AWS Session Manager

# Start a session with session recording enabled
aws ssm start-session \
  --target i-0abc1234def567890 \
  --document-name AWS-StartInteractiveCommand \
  --region ap-southeast-1

# SSM Document for session recording to S3
{
  "schemaVersion": "1.0",
  "description": "Session recording configuration",
  "sessionType": "Standard_Stream",
  "inputs": {
    "s3BucketName": "my-session-logs-bucket",
    "s3KeyPrefix": "ssm-sessions/",
    "s3EncryptionEnabled": true,
    "cloudWatchLogGroupName": "/aws/ssm/sessions",
    "cloudWatchEncryptionEnabled": true,
    "idleSessionTimeout": "20",
    "maxSessionDuration": "60",
    "runAsEnabled": true,
    "runAsDefaultUser": ""
  }
}
Tip: Use IAM Access Analyzer (AWS) or Policy Analyzer (GCP) to continuously scan for overly permissive policies, public resource access, and cross-account access paths you may not have intended. Run these checks as part of your weekly security review and integrate findings into your ticketing system.

IAM Audit & Governance

# AWS: Find all IAM users with console access but no MFA
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
  awk -F',' '$4 == "true" && $8 == "false" {print $1, "has console access but no MFA"}'

# AWS: List all roles and their last used date
aws iam get-account-authorization-details \
  --filter Role \
  --query 'RoleDetailList[*].[RoleName,RoleLastUsed.LastUsedDate]' \
  --output table

# GCP: Find service account keys older than 90 days
gcloud iam service-accounts list --format="value(email)" | while read SA; do
  gcloud iam service-accounts keys list \
    --iam-account="$SA" \
    --managed-by=user \
    --format="table(name,validAfterTime)"
done

# Kubernetes: Find all ClusterRoleBindings to cluster-admin
kubectl get clusterrolebindings -o json | jq '
  .items[] |
  select(.roleRef.name == "cluster-admin") |
  {name: .metadata.name, subjects: .subjects}'