Master zero trust identity architecture - covering IAM RBAC and JIT access, OIDC workload federation for Kubernetes on AWS and GCP, HashiCorp Vault PKI and dynamic secrets, SPIFFE/SPIRE workload identity, OAuth2 PKCE, Privileged Access Management, Zero Standing Privileges, Kubernetes service account hardening, and automated secrets rotation.
Identity is the new perimeter. In cloud-native environments, there is no network boundary that separates trusted from untrusted — every service, every user, and every machine must prove who it is before accessing anything. This shift from network-based trust to identity-based trust is the foundation of zero trust architecture. This module covers how modern systems manage identity at every layer — human users, service accounts, machine workloads, and secrets — and how to enforce the principle of least privilege with just-in-time access, cryptographic attestation, and continuous verification. Topics covered: - Zero trust identity principles and AWS Verified Access - IAM deep dive — RBAC, least privilege, and JIT access - OIDC federation for workloads — Kubernetes, AWS, and GCP - HashiCorp Vault — PKI, secrets engines, and dynamic credentials - SPIFFE/SPIRE — workload identity for cloud-native services - OAuth2 PKCE for machine-to-machine authentication - Privileged Access Management (PAM) and Zero Standing Privileges - Secrets lifecycle management and rotation - Service account hardening in Kubernetes ---
Zero trust is centered on one principle: **access to data should not be granted based on network location alone**. Every request must be authenticated, authorized, and continuously evaluated — regardless of whether it originates inside or outside the traditional perimeter. ``` Traditional model: Inside network → trusted Outside network → untrusted Zero trust model: Every identity → verify Every request → authorize Every session → monitor Trust → never assumed, always verified ``` AWS describes this as requiring users and systems to strongly prove their identities and trustworthiness before accessing applications, data, or other systems. The key building blocks on AWS are: ```bash # AWS Verified Access — application access without VPN # Evaluates identity, device state, and context for every request aws verifiedaccess create-verified-access-instance \ --description "Zero trust application access" \ --region us-east-1 # Amazon VPC Lattice — service-to-service zero trust networking # Each service request carries identity, authorization is enforced at the mesh # No blanket "service A can talk to everything in the VPC" rules # Amazon Verified Permissions (Cedar policy language) # Fine-grained, externalized authorization for any application aws verifiedpermissions create-policy-store \ --region us-east-1 ``` The three zero trust use cases organizations implement first: ``` 1. Software-to-software communications Services authenticate to each other using workload identity (SPIFFE, OIDC tokens) No shared passwords, no network-based trust 2. Secure workforce mobility Remote users access applications via identity-aware proxy No VPN that grants broad network access 3. Digital transformation projects New cloud-native services built identity-first from day one Legacy services migrated with identity wrappers ``` ---
### Role-Based Access Control RBAC grants permissions to roles, not directly to users. Users are assigned roles, and roles carry the minimum permissions needed for a specific job function. ```bash # AWS IAM RBAC example — developer role with scoped permissions cat > developer-policy.json << 'EOF' { "Version": "2012-10-17", "Statement": [ { "Sid": "ReadOnlyProd", "Effect": "Allow", "Action": [ "ec2:Describe*", "s3:GetObject", "s3:ListBucket", "logs:GetLogEvents", "cloudwatch:GetMetricData" ], "Resource": "*", "Condition": { "StringEquals": { "aws:RequestedRegion": "us-east-1" } } }, { "Sid": "FullAccessDev", "Effect": "Allow", "Action": "*", "Resource": "*", "Condition": { "StringEquals": { "aws:ResourceTag/Environment": "dev" } } } ] } EOF aws iam create-policy \ --policy-name DeveloperPolicy \ --policy-document file://developer-policy.json aws iam create-role \ --role-name DeveloperRole \ --assume-role-policy-document file://trust-policy.json aws iam attach-role-policy \ --role-name DeveloperRole \ --policy-arn arn:aws:iam::123456789012:policy/DeveloperPolicy ``` ### Least Privilege Enforcement Least privilege means every identity has only the minimum access needed for its current task — and nothing more. The three dimensions of least privilege: ``` Breadth: Access only the specific resources needed (not all S3, but specific bucket) Depth: Access only the specific actions needed (s3:GetObject, not s3:*) Duration: Access only for the time needed (1-hour token, not permanent key) ``` ```bash # Find over-privileged IAM users — users with access they never use aws iam generate-service-last-accessed-details \ --arn arn:aws:iam::123456789012:user/developer # Get the report aws iam get-service-last-accessed-details \ --job-id <job-id> \ --query 'ServicesLastAccessed[?LastAuthenticated==`null`].ServiceName' \ --output table # Find users with unused admin access aws iam list-users --query 'Users[*].UserName' --output text | \ xargs -I{} sh -c 'echo {} && aws iam list-attached-user-policies --user-name {} --query "AttachedPolicies[?PolicyName==\`AdministratorAccess\`]"' # Identify stale access keys (not used in 90+ days) aws iam get-credential-report aws iam generate-credential-report ``` ### Just-In-Time (JIT) Access Standing privileges — always-on admin access — are one of the largest attack surfaces in any organization. JIT access eliminates standing privileges: access is granted only when needed, for only as long as needed. ``` Standing privilege model: Admin has root access 24/7 → 168 hours/week of exposure JIT access model: Admin requests access → approved for 30 minutes → automatically revoked Exposure: 0.5 hours/week — a 99.7% reduction in privilege windows ``` ```bash # AWS STS assume-role for JIT access — temporary credentials, no standing privilege # Developer requests access to production for a specific task aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/ProdEmergencyAccess \ --role-session-name "incident-response-$(date +%Y%m%d-%H%M%S)" \ --duration-seconds 3600 \ # 1 hour maximum --serial-number arn:aws:iam::123456789012:mfa/developer \ --token-code 123456 # The returned credentials expire automatically after 3600 seconds # No keys to rotate — they self-destruct # For Kubernetes: use time-limited RoleBindings with cleanup jobs cat << 'EOF' > jit-rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jit-admin-access namespace: production annotations: expires-at: "2024-01-15T15:00:00Z" # Automation cleans up after this time roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: admin subjects: - kind: User name: developer@company.com apiGroup: rbac.authorization.k8s.io EOF ``` Zero Standing Privileges (ZSP) is the target end state: no identity holds always-on privileged access. Every privileged action requires an explicit grant that expires automatically. ---
OpenID Connect (OIDC) federation allows workloads to authenticate to cloud providers using short-lived cryptographic tokens instead of long-lived static keys. The workload proves its identity using a token issued by a trusted identity provider (the Kubernetes API server, GitHub Actions, etc.), exchanges it for temporary cloud credentials, and the credentials expire automatically. ### Kubernetes to AWS — IRSA (IAM Roles for Service Accounts) ```bash # Step 1: Enable OIDC provider for EKS cluster aws eks describe-cluster --name my-cluster \ --query "cluster.identity.oidc.issuer" --output text # Returns: https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E # Create the OIDC identity provider in IAM aws iam create-open-id-connect-provider \ --url https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E \ --client-id-list sts.amazonaws.com \ --thumbprint-list 9e99a48a9960b14926bb7f3b02e22da2b0ab7280 # Step 2: Create IAM role with trust policy for the service account cat > trust-policy.json << 'EOF' { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:my-app-sa", "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com" } } } ] } EOF aws iam create-role \ --role-name my-app-role \ --assume-role-policy-document file://trust-policy.json # Step 3: Annotate the Kubernetes service account kubectl create serviceaccount my-app-sa -n default kubectl annotate serviceaccount my-app-sa \ -n default \ eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/my-app-role # Step 4: The pod automatically receives projected token credentials # No access keys. No secrets. The token is mounted automatically. cat << 'EOF' > my-app-pod.yaml apiVersion: v1 kind: Pod metadata: name: my-app spec: serviceAccountName: my-app-sa # Token automatically projected containers: - name: app image: my-app:latest # AWS SDK automatically reads the projected token and calls STS # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed EOF ``` ### Kubernetes to Google Cloud — Workload Identity Federation Google Cloud implements the same pattern. The Kubernetes service account token is exchanged for short-lived Google Cloud credentials via the Security Token Service: ```bash # Create a workload identity pool in GCP gcloud iam workload-identity-pools create k8s-pool \ --location="global" \ --description="Kubernetes workload identity pool" \ --display-name="K8s Pool" # Add the Kubernetes cluster as an OIDC provider gcloud iam workload-identity-pools providers create-oidc k8s-provider \ --location="global" \ --workload-identity-pool="k8s-pool" \ --issuer-uri="https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E" \ --attribute-mapping="google.subject=assertion.sub,attribute.namespace=assertion['kubernetes.io']['namespace']" \ --attribute-condition="assertion['kubernetes.io']['namespace'] in ['backend', 'monitoring']" # Grant the Kubernetes service account access to a GCP resource gcloud projects add-iam-policy-binding my-gcp-project \ --role=roles/storage.objectViewer \ --member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/k8s-pool/subject/system:serviceaccount:default:my-app-sa" ``` ---
Vault is the central secrets management platform for most serious DevSecOps teams. It stores, generates, rotates, and revokes secrets — and critically, it generates **dynamic secrets** that exist only for the duration they are needed. ### Vault PKI — Internal Certificate Authority Rather than managing certificates manually or using a third-party CA for internal services, Vault acts as your internal PKI — issuing short-lived certificates on demand. ```bash # Enable Vault PKI secrets engine vault secrets enable pki vault secrets tune -max-lease-ttl=87600h pki # 10 year root CA max # Generate root CA (store the private key internally in Vault) vault write -field=certificate pki/root/generate/internal \ common_name="company.internal" \ issuer_name="root-2024" \ ttl=87600h > root_ca.crt # Configure CRL and OCSP endpoints vault write pki/config/urls \ issuing_certificates="https://vault.company.internal/v1/pki/ca" \ crl_distribution_points="https://vault.company.internal/v1/pki/crl" # Create intermediate CA (this is what issues leaf certificates) vault secrets enable -path=pki_int pki vault secrets tune -max-lease-ttl=43800h pki_int # 5 year max vault write -format=json pki_int/intermediate/generate/internal \ common_name="company.internal Intermediate" \ | jq -r '.data.csr' > intermediate.csr vault write -format=json pki/root/sign-intermediate \ issuer_ref="root-2024" \ csr=@intermediate.csr \ format=pem_bundle ttl="43800h" \ | jq -r '.data.certificate' > intermediate.cert.pem vault write pki_int/intermediate/set-signed certificate=@intermediate.cert.pem # Create a role for issuing service certificates (30-day max TTL) vault write pki_int/roles/service-certs \ issuer_ref="$(vault read -field=default pki_int/config/issuers)" \ allowed_domains="company.internal" \ allow_subdomains=true \ max_ttl="720h" # 30 days max — forces regular rotation # Issue a certificate for a service vault write pki_int/issue/service-certs \ common_name="payments.company.internal" \ ttl="24h" # 24 hours — very short-lived certificates # In CI/CD: automatically rotate certificates before expiry # Services fetch new certificates from Vault, old ones expire harmlessly ``` The philosophy: short-lived certificates mean revocation is rarely needed. A certificate valid for 24 hours expires on its own before an attacker can exploit it. ### Dynamic Database Credentials Instead of a static database password shared across all services, Vault generates a unique credential per request that automatically expires: ```bash # Enable database secrets engine vault secrets enable database # Configure Vault to manage a PostgreSQL database vault write database/config/my-postgres \ plugin_name=postgresql-database-plugin \ allowed_roles="app-role" \ connection_url="postgresql://{{username}}:{{password}}@postgres.company.internal:5432/appdb?sslmode=require" \ username="vault-admin" \ password="$VAULT_ADMIN_PASSWORD" # Create a role that generates read-only credentials vault write database/roles/app-role \ db_name=my-postgres \ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' IN ROLE readonly;" \ default_ttl="1h" \ max_ttl="24h" # Application requests credentials — gets a unique user/pass for this instance vault read database/creds/app-role # Key Value # lease_duration 1h # username v-app-xyz12345 # password A1B2-C3D4-E5F6 ← unique, expires in 1 hour, then deleted # After 1 hour: the user v-app-xyz12345 is automatically revoked in PostgreSQL # No shared password. No rotation needed. Each request gets a fresh credential. ``` ### Vault Auth Methods — OIDC Authentication Vault supports multiple authentication methods so workloads prove their identity before receiving secrets: ```bash # Kubernetes auth — pods authenticate using their service account token vault auth enable kubernetes vault write auth/kubernetes/config \ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_host="https://kubernetes.default.svc:443" \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt # Create a policy granting access to specific secrets cat > app-policy.hcl << 'EOF' path "secret/data/my-app/*" { capabilities = ["read"] } path "database/creds/app-role" { capabilities = ["read"] } EOF vault policy write my-app-policy app-policy.hcl # Bind the policy to the Kubernetes service account vault write auth/kubernetes/role/my-app \ bound_service_account_names=my-app-sa \ bound_service_account_namespaces=default \ policies=my-app-policy \ ttl=1h # The pod authenticates to Vault using its projected service account token # Vault verifies the token with the Kubernetes API server # Vault returns a short-lived Vault token scoped to my-app-policy ``` ---
SPIFFE (Secure Production Identity Framework for Everyone) is a standard for workload identity that works across any platform — Kubernetes, VMs, bare metal, cloud providers. Every workload gets a cryptographic identity called an SVID (SPIFFE Verifiable Identity Document). The SPIFFE ID format is a URI: ``` spiffe://trust-domain/workload-identifier Example: spiffe://company.internal/ns/payments/sa/processor ``` SPIRE is the reference implementation of SPIFFE. It consists of: - **SPIRE Server** — issues SVIDs, stores registration entries - **SPIRE Agent** — runs on each node, attests workloads, caches SVIDs ### Why SPIFFE Solves the Secret Zero Problem The classic bootstrap problem: how does a workload prove its identity to get its first secret? Static credentials embedded in code or environment variables are themselves a secret that needs protecting. SPIFFE solves this by using out-of-band node attestation — the SPIRE agent proves its identity based on verifiable platform properties (AWS instance identity document, Kubernetes service account token), not a pre-shared secret. ```bash # Install SPIRE on Kubernetes via Helm helm repo add spire https://spiffe.github.io/helm-charts-hardened/ helm upgrade --install --create-namespace -n spire spire-crds spire/spire-crds helm upgrade --install -n spire spire spire/spire \ --set global.spire.trustDomain="company.internal" \ --set global.spire.clusterName="production" # Verify agents have attested successfully kubectl exec -n spire spire-server-0 -- \ /opt/spire/bin/spire-server agent list # Found 3 attested agents: # SPIFFE ID: spiffe://company.internal/spire/agent/k8s_psat/production/node-abc123 # Attestation type: k8s_psat # Create a registration entry — maps workload selectors to SPIFFE ID kubectl exec -n spire spire-server-0 -- \ /opt/spire/bin/spire-server entry create \ -spiffeID spiffe://company.internal/ns/payments/sa/processor \ -parentID spiffe://company.internal/spire/agent/k8s_psat/production/node-abc123 \ -selector k8s:ns:payments \ -selector k8s:sa:processor # The payments/processor pod now automatically receives an X.509 SVID # The SVID rotates automatically every hour — no manual intervention ``` ### SPIRE Integration with Istio mTLS SPIRE integrates with Istio to replace the default certificate management with SPIFFE-issued certificates. Every service-to-service connection uses mutual TLS where both sides present their SPIFFE identity: ```yaml # ClusterSPIFFEID — automatically issue identities to Istio sidecars apiVersion: spire.spiffe.io/v1alpha1 kind: ClusterSPIFFEID metadata: name: istio-sidecar-identity spec: spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}" podSelector: matchLabels: spiffe.io/spire-managed-identity: "true" ``` ```bash # Verify the certificate used by the Envoy proxy includes the SPIFFE ID istioctl proxy-config secret my-pod -o json \ | jq -r '.dynamicActiveSecrets[0].secret.tlsCertificate.certificateChain.inlineBytes' \ | base64 --decode \ | openssl x509 -in - -text \ | grep "URI:" # URI:spiffe://company.internal/ns/payments/sa/processor ``` ---
Identity is the new perimeter. In cloud-native environments, there is no network boundary that separates trusted from un...
Zero trust is centered on one principle: access to data should not be granted based on network location alone. Every req...
Role-Based Access Control RBAC grants permissions to roles, not directly to users. Users are assigned roles, and roles c...
OpenID Connect (OIDC) federation allows workloads to authenticate to cloud providers using short-lived cryptographic tok...
Vault is the central secrets management platform for most serious DevSecOps teams. It stores, generates, rotates, and re...
SPIFFE (Secure Production Identity Framework for Everyone) is a standard for workload identity that works across any pla...
For applications that cannot securely store a client secret — mobile apps, single-page apps, CLI tools — OAuth2 PKCE (Pr...
PAM is the discipline of controlling, monitoring, and auditing access to systems and data that have elevated ("privilege...
Kubernetes service accounts are the identity mechanism for pods. Poorly configured service accounts are one of the most ...
The Problem with Static Secrets Static secrets (long-lived API keys, database passwords, SSH keys) accumulate over time ...
Prerequisites AWS CLI configured kubectl configured for a cluster Vault CLI installed (or use vault container) Helm inst...
Identity and access security is the foundation that every other security control depends on. The key principles from thi...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.