PAM Was Built for a Different World
Traditional Privileged Access Management was designed for a world of persistent servers, static administrator accounts, and well-defined network perimeters. A PAM solution would vault the root or Administrator password, record terminal sessions, and require check-out/check-in for credential use.
That model does not map to modern cloud infrastructure. Consider what "privileged access" means today:
- An engineer assumes an IAM role in AWS to debug a production Lambda function
- A CI/CD pipeline uses a service account to deploy containers to a Kubernetes cluster
- A site reliability engineer accesses a production database through a connection proxy
- A Terraform apply modifies infrastructure across three cloud providers
- A developer queries production logs through an observability platform
ssh root@server and entering a password from a vault. The infrastructure is ephemeral, the access is programmatic, the identities are often non-human, and the "privilege" is defined by cloud IAM policies—not by operating system accounts.
Modern PAM must adapt to this reality or become irrelevant.
Cloud PAM Architecture
Just-in-Time Access: The Core Pattern
Just-in-time (JIT) access is the most important pattern in modern PAM. Instead of granting permanent ("standing") privileges that persist until someone remembers to revoke them, JIT access provisions the exact permissions needed, for the exact duration needed, with automatic revocation.
Implementing JIT Access for AWS
Here is a practical implementation of JIT IAM role access using AWS STS with automatic revocation:
import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts';
import {
IAMClient,
AttachRolePolicyCommand,
DetachRolePolicyCommand,
CreatePolicyCommand,
DeletePolicyCommand,
} from '@aws-sdk/client-iam';
interface JITAccessRequest {
userId: string;
justification: string;
ticketId: string;
targetRole: string;
permissions: string[]; // e.g., ["rds:DescribeDBInstances", "rds-data:ExecuteStatement"]
resources: string[]; // e.g., ["arn:aws:rds:us-east-1:123:db/prod-db"]
durationSeconds: number;
}
async function grantJITAccess(request: JITAccessRequest) {
const iam = new IAMClient({});
const sts = new STSClient({});
// Step 1: Create a scoped, inline policy
const policyDocument = {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: request.permissions,
Resource: request.resources,
Condition: {
// Restrict to specific source IP (optional)
'IpAddress': { 'aws:SourceIp': await getUserIp(request.userId) },
// Add MFA requirement
'Bool': { 'aws:MultiFactorAuthPresent': 'true' },
},
}],
};
// Step 2: Create temporary policy
const policy = await iam.send(new CreatePolicyCommand({
PolicyName: jit-${request.userId}-${Date.now()},
PolicyDocument: JSON.stringify(policyDocument),
Tags: [
{ Key: 'jit-request', Value: request.ticketId },
{ Key: 'expires-at', Value: new Date(
Date.now() + request.durationSeconds * 1000
).toISOString() },
],
}));
// Step 3: Attach to target role
await iam.send(new AttachRolePolicyCommand({
RoleName: request.targetRole,
PolicyArn: policy.Policy!.Arn!,
}));
// Step 4: Assume the role with session duration
const credentials = await sts.send(new AssumeRoleCommand({
RoleArn: arn:aws:iam::123456789:role/${request.targetRole},
RoleSessionName: jit-${request.userId},
DurationSeconds: Math.min(request.durationSeconds, 3600),
Tags: [
{ Key: 'ticket', Value: request.ticketId },
{ Key: 'justification', Value: request.justification.slice(0, 256) },
],
}));
// Step 5: Schedule cleanup
await scheduleRevocation(policy.Policy!.Arn!, request.targetRole, request.durationSeconds);
// Step 6: Audit log
await auditLog({
event: 'jit_access_granted',
userId: request.userId,
targetRole: request.targetRole,
permissions: request.permissions,
resources: request.resources,
duration: request.durationSeconds,
ticketId: request.ticketId,
expiresAt: new Date(Date.now() + request.durationSeconds * 1000).toISOString(),
});
return credentials.Credentials;
}
Secrets Management in CI/CD Pipelines
CI/CD pipelines are one of the most dangerous attack surfaces for privileged access. A compromised pipeline can deploy malicious code, exfiltrate secrets, or modify infrastructure. Key principles:
Never Store Secrets in Code or Environment Variables
# WRONG - secrets in environment variables persist in process lists and crash dumps
env:
DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
BETTER - fetch secrets at runtime from a vault
steps:
- name: Retrieve secrets
uses: hashicorp/vault-action@v2
with:
url: https://vault.internal.example.com
method: jwt
role: ci-deploy-role
# Vault generates short-lived database credentials on demand
secrets: |
database/creds/deploy-role username | DB_USERNAME ;
database/creds/deploy-role password | DB_PASSWORD
Workload Identity for Pipelines
Modern CI/CD platforms support workload identity federation—the pipeline authenticates to cloud providers using its platform identity (e.g., GitHub Actions OIDC token) rather than long-lived service account keys:
# GitHub Actions — workload identity federation with AWS
permissions:
id-token: write # Required for OIDC token
contents: read
steps:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-deploy
aws-region: us-east-1
# No secrets! GitHub's OIDC token proves pipeline identity
This eliminates the need to store cloud credentials as repository secrets entirely. The trust relationship is configured once in the cloud provider's IAM, and each pipeline run gets short-lived credentials scoped to exactly what it needs.
Kubernetes RBAC and Privileged Access
Kubernetes introduces unique PAM challenges because it has its own RBAC system that operates independently of cloud IAM. A user might have zero permissions in AWS but full cluster-admin in Kubernetes if RBAC is misconfigured.
Principle of Least Privilege in Kubernetes
# WRONG - overly broad ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dev-team-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin # Full cluster access!
subjects:
- kind: Group
name: dev-team
RIGHT - scoped Role in specific namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-team-debugger
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"] # Read-only
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"] # Exec for debugging
# Further restrict with admission controllers
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-team-debugger-binding
namespace: production
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: dev-team-debugger
subjects:
- kind: Group
name: dev-team
Pod Security and Service Account Hardening
Every Kubernetes pod runs with a service account. By default, this service account has a mounted token that can access the Kubernetes API. Lock this down:
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
automountServiceAccountToken: false # Don't mount API token by default
apiVersion: v1
kind: Pod
metadata:
name: app-pod
spec:
serviceAccountName: app-service-account
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
Zero Standing Privileges: The End State
The ultimate goal of modern PAM is zero standing privileges (ZSP)—a state where no human or machine identity has persistent privileged access to any system. All privileged access is:
- Requested with justification and context
- Evaluated against policy (auto-approved for low-risk, peer-approved for high-risk)
- Provisioned just-in-time with minimum scope
- Monitored with session recording and anomaly detection
- Revoked automatically when the time window expires
Measuring Your ZSP Progress
Track these metrics to measure your journey toward zero standing privileges:
| Metric | Starting State | Target State |
|---|---|---|
| Standing admin accounts | Hundreds | Zero |
| Average privilege duration | Permanent | < 4 hours |
| Percent JIT-provisioned access | 0% | > 95% |
| Long-lived service account keys | Hundreds | Zero (use workload identity) |
| Time to revoke after offboarding | Days-weeks | < 1 hour |
| Percent of access with justification | 0% | 100% |
Session Recording and Forensics
Modern PAM must record privileged sessions for compliance and forensic analysis. But traditional screen recording of SSH sessions is insufficient for cloud-native environments. Modern session recording captures:
- API calls: Every AWS/GCP/Azure API call made during a privileged session
- Kubernetes commands: Every
kubectlcommand and API server request - Database queries: Every SQL statement executed against production databases
- Infrastructure changes: Every Terraform plan and apply, with diff output
Getting Started: A Phased Approach
Phase 1: Inventory and Visibility (Weeks 1-4)
- Catalog all privileged accounts across cloud providers, Kubernetes clusters, and databases
- Identify standing privileges and their owners
- Implement audit logging for all privileged access
Phase 2: Secrets Migration (Weeks 5-8)
- Move all secrets from code, config files, and CI/CD variables to a secrets vault (HashiCorp Vault, AWS Secrets Manager)
- Implement workload identity federation for CI/CD pipelines
- Rotate all long-lived credentials
Phase 3: JIT Access (Weeks 9-16)
- Deploy a JIT access platform (Teleport, StrongDM, Indent, or custom)
- Migrate high-risk access (production databases, cloud admin roles) to JIT
- Implement approval workflows and policy-based auto-approval
Phase 4: Zero Standing Privileges (Ongoing)
- Systematically eliminate remaining standing privileges
- Implement continuous access reviews and separation of duties
- Integrate PAM signals into your risk-based authentication engine