Identity and Access Management
- Identity and Access Management
Who are you, and what may you do?
Identity and Access Management (IAM) answers the two questions every security decision reduces to: authentication — who are you? — and authorization — what may you do? It is the connective tissue of security: confidentiality and integrity (the CIA triad) both depend on it, social-engineering attacks target it (a phished credential is just an authentication bypass), and a large fraction of real cloud breaches trace back to one IAM mistake — an over-permissive role, a leaked long-lived key, a missing second factor. Getting IAM wrong undoes every other control.
This page covers identity types, authentication (MFA/TOTP, OAuth 2.0/OIDC, workload identity), authorization (ACLs, RBAC, ABAC, policy engines), least privilege in practice, and credential management — with runnable examples spanning Linux, Kubernetes, and cloud, because modern IAM lives at all three layers.
ℹ️ Keep the two halves distinct: AuthN proves identity, AuthZ decides permission. A valid login does not imply authorization — every request from an authenticated subject must still be checked against policy for each resource it touches. Conflating them is a classic source of broken-access-control bugs.
Identity Basics
An identity is a verifiable representation of a user, service, or device. Authentication establishes that a claimed identity is genuine; authorization determines what that identity may do.
Identity types
| Identity type | Examples | Common mechanism |
|---|---|---|
| Human users | Developers, operators, admins | Username/password + MFA |
| Service accounts | Applications, CI/CD runners | API keys, certificates, OIDC tokens |
| Workload identities | Kubernetes pods, Lambda functions | Instance metadata, projected service account tokens |
| Machine identities | Servers, IoT devices | TLS client certificates, TPM-attested keys |
Authentication vs Authorization
- Authentication (AuthN) — proves who you are (identity verification)
- Authorization (AuthZ) — determines what you are allowed to do (permission enforcement)
These are distinct steps. A valid authentication does not guarantee authorization. A request from a legitimately authenticated user must still be checked against the authorization policy for every resource it touches.
Authentication
Multi-Factor Authentication
MFA requires two or more independent factors:
| Factor type | Examples |
|---|---|
| Something you know | Password, PIN |
| Something you have | TOTP app, hardware key (YubiKey), SMS code |
| Something you are | Fingerprint, face recognition |
TOTP (Time-based One-Time Password, RFC 6238) is the most widely used software MFA:
import pyotp
import qrcode
# Generate a new TOTP secret for a user
secret = pyotp.random_base32() # store this server-side, encrypted
# Generate the provisioning URI (for QR code in authenticator apps)
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(name='alice@example.com', issuer_name='MyApp')
# Verify a TOTP code submitted by the user
def verify_totp(secret: str, user_code: str) -> bool:
totp = pyotp.TOTP(secret)
# valid_window=1 allows ±30 seconds of clock skew
return totp.verify(user_code, valid_window=1)
Phishing-resistant MFA: FIDO2 and passkeys
TOTP and SMS codes are better than nothing but still phishable — a fake login page can relay the code to the real site in real time. FIDO2/WebAuthn closes that gap: the authenticator (a hardware key like a YubiKey, or a passkey synced to your phone/laptop) signs a challenge cryptographically bound to the real site’s origin, so a credential entered on paypa1.com simply will not authenticate to paypal.com. There is no shared secret to phish, replay, or steal from a breached server database — the server only ever stores a public key. This is the single most effective control against credential phishing, and the reason passkeys are now replacing passwords outright.
| MFA method | Phishing-resistant? |
|---|---|
| SMS / email code | No — relayable, and vulnerable to SIM-swap |
| TOTP authenticator app | No — relayable in real time |
| Push approval | Weak — prompt-bombing / MFA fatigue |
| FIDO2 security key / passkey | Yes — origin-bound public-key cryptography |
OAuth 2.0 and OIDC
OAuth 2.0 is an authorization delegation framework. OpenID Connect (OIDC) adds an identity layer on top, enabling SSO.
User ──▶ App ──▶ Identity Provider (Google, Okta, Keycloak)
◀── Authorization Code
──▶ Token endpoint (exchange code for tokens)
◀── access_token + id_token
Key token types:
- Access token — bearer token granting API access; typically short-lived (minutes)
- ID token — JWT containing user identity claims; for the application, not APIs
- Refresh token — long-lived; used to obtain new access tokens without re-authentication
# Validate a JWT access token (e.g., from an OIDC provider)
import jwt
import requests
JWKS_URI = 'https://accounts.google.com/.well-known/openid-configuration'
def get_public_keys(jwks_uri: str) -> dict:
config = requests.get(jwks_uri).json()
jwks = requests.get(config['jwks_uri']).json()
return {key['kid']: jwt.algorithms.RSAAlgorithm.from_jwk(key)
for key in jwks['keys']}
def validate_id_token(token: str, client_id: str) -> dict:
keys = get_public_keys(JWKS_URI)
header = jwt.get_unverified_header(token)
public_key = keys[header['kid']]
payload = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience=client_id,
issuer='https://accounts.google.com',
)
return payload
OIDC for workload identity (Kubernetes)
Kubernetes can issue OIDC tokens to pods, enabling passwordless authentication to external services:
# Pod spec: mount a projected service account token
spec:
serviceAccountName: my-service-account
volumes:
- name: oidc-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: openbao # the target service that accepts this token
containers:
- name: app
volumeMounts:
- name: oidc-token
mountPath: /var/run/secrets/tokens
# Application reads the OIDC token and authenticates to OpenBao
import hvac
import os
def openbao_login_with_k8s_token() -> hvac.Client:
with open('/var/run/secrets/tokens/token') as f:
jwt_token = f.read()
client = hvac.Client(url=os.environ['BAO_ADDR'])
client.auth.kubernetes.login(
role='my-app-role',
jwt=jwt_token,
)
return client
Authorization
Access Control Lists
An ACL (Access Control List) is a list of permissions attached to a resource. Each entry specifies a subject (user or group) and the operations they may perform.
# Linux filesystem ACLs (extend standard Unix permissions)
# Install acl package if needed: apt install acl
# View ACL on a file
getfacl /var/log/myapp/app.log
# Grant user alice read access without changing group ownership
setfacl -m u:alice:r /var/log/myapp/app.log
# Grant group auditors read-only access to a directory recursively
setfacl -R -m g:auditors:rX /var/log/myapp/
# Set default ACL (new files inherit it)
setfacl -d -m g:auditors:rX /var/log/myapp/
# Remove a specific ACL entry
setfacl -x u:alice /var/log/myapp/app.log
# Remove all ACL entries
setfacl -b /var/log/myapp/app.log
Role-Based Access Control
RBAC assigns permissions to roles, then assigns roles to users. This is far easier to manage than per-user ACLs at scale.
Users ──▶ Roles ──▶ Permissions ──▶ Resources
alice ──▶ developer ──▶ read, write ──▶ /api/v1/*
bob ──▶ auditor ──▶ read ──▶ /api/v1/*
carol ──▶ admin ──▶ read, write, delete ──▶ /*
Kubernetes RBAC:
# Role: read-only access to pods in the production namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
# RoleBinding: bind the role to a service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: production
name: read-pods-binding
subjects:
- kind: ServiceAccount
name: monitoring-agent
namespace: monitoring
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
# Audit Kubernetes RBAC
kubectl auth can-i list pods --namespace production --as system:serviceaccount:monitoring:monitoring-agent
kubectl get rolebindings,clusterrolebindings -A -o wide
# Find all subjects bound to cluster-admin (high privilege)
kubectl get clusterrolebindings -o json \
| python3 -c "
import json,sys
data=json.load(sys.stdin)
for rb in data['items']:
if rb['roleRef']['name']=='cluster-admin':
for s in rb.get('subjects',[]):
print(f\"{rb['metadata']['name']}: {s.get('kind')} {s.get('name')}\")
"
Attribute-Based Access Control
ABAC makes access decisions based on attributes of the user, resource, environment, and action — more expressive than RBAC.
# Simple ABAC engine
from dataclasses import dataclass
from typing import Any
@dataclass
class Request:
user_department: str
user_clearance: int
resource_classification: int
resource_owner_department: str
action: str
time_of_day: int # hour 0-23
def is_authorized(req: Request) -> bool:
"""Bell-LaPadula-inspired: user clearance >= resource classification."""
if req.user_clearance < req.resource_classification:
return False
# Write only allowed during business hours for non-admins
if req.action == 'write' and not (8 <= req.time_of_day <= 18):
return False
# Users can only write to their own department's resources
if req.action == 'write' and req.user_department != req.resource_owner_department:
return False
return True
Open Policy Agent (OPA)
OPA is a general-purpose policy engine that decouples policy from application logic. Policies are written in Rego and evaluated against JSON input.
# Install OPA
curl -L -o /usr/local/bin/opa \
https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod +x /usr/local/bin/opa
opa version
# policy/allow.rego — example: require image from approved registry
package kubernetes.admission
import future.keywords.if
deny[msg] if {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not starts_with(container.image, "registry.example.com/")
msg := sprintf("Image %q is not from the approved registry", [container.image])
}
# Evaluate a policy against input
opa eval --input request.json --data policy/ "data.kubernetes.admission.deny"
# Run OPA as a server (for Kubernetes admission webhook)
opa run --server --addr :8181 --log-level info policy/
OPA/Gatekeeper (Kubernetes):
# ConstraintTemplate: define the policy schema
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredregistry
spec:
crd:
spec:
names:
kind: K8sRequiredRegistry
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredregistry
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not startswith(container.image, input.parameters.registry)
msg := sprintf("image %q must be from registry %q", [container.image, input.parameters.registry])
}
---
# Constraint: enforce the policy
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredRegistry
metadata:
name: require-approved-registry
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
registry: "registry.example.com/"
Least Privilege in Practice
Linux: minimizing process privileges
# Run a service as a dedicated non-root user
useradd --system --no-create-home --shell /sbin/nologin appuser
chown -R appuser:appuser /opt/myapp
su -s /bin/bash appuser -c "/opt/myapp/start.sh"
# Use Linux capabilities instead of running as root
# Example: allow binding to port 80 without root
setcap 'cap_net_bind_service=+ep' /opt/myapp/server
# Verify
getcap /opt/myapp/server
# Drop capabilities in a Docker container
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
Kubernetes: restrict pod privileges
# PodSecurity admission (Kubernetes 1.25+): enforce restricted profile
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
# Pod spec that complies with the restricted profile
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
Cloud IAM: least privilege roles
# AWS: create a minimal IAM policy for a Lambda that reads from one S3 bucket
aws iam create-policy \
--policy-name LambdaReadOnlyS3Reports \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-reports-bucket",
"arn:aws:s3:::my-reports-bucket/*"
]
}]
}'
# Attach to the Lambda execution role (not to a user)
aws iam attach-role-policy \
--role-name my-lambda-execution-role \
--policy-arn arn:aws:iam::123456789012:policy/LambdaReadOnlyS3Reports
# Python: use IAM Roles Anywhere or instance roles — never hardcode credentials
import boto3
# boto3 automatically uses the instance/task/Lambda execution role
# No credentials in code or environment variables
s3 = boto3.client('s3')
response = s3.get_object(Bucket='my-reports-bucket', Key='report.pdf')
Secrets and Credential Management
Credentials are a special category of identity material. See the CI/CD, Secret Management, and GitOps page for OpenBao, Sealed Secrets, and SOPS. Key principles:
- No long-lived static credentials — prefer short-lived tokens issued on demand
- No credentials in code or environment variables — use a secrets manager
- Rotate credentials — automatically and on schedule, not just after incidents
- Audit credential access — every secret read should be logged
# Pattern: fetch credentials from OpenBao at startup, not from environment variables
import hvac
import os
import psycopg2
def get_db_connection():
bao = hvac.Client(
url=os.environ['BAO_ADDR'],
token=os.environ['BAO_TOKEN'], # this token itself is short-lived
)
# Dynamic credentials: new username/password created on demand, expire in 1h
creds = bao.secrets.database.generate_credentials(name='myapp-role')
return psycopg2.connect(
host='db.internal',
database='myapp',
user=creds['data']['username'],
password=creds['data']['password'],
)
Key takeaways
- AuthN (who you are) and AuthZ (what you may do) are distinct — a valid login never implies permission; check authorization for every resource on every request.
- MFA combines factors (know / have / are); prefer phishing-resistant FIDO2 / passkeys over TOTP or SMS, which can be relayed by a fake site in real time.
- OAuth 2.0 delegates authorization; OIDC layers identity on top for SSO. Mind the token types (access vs ID vs refresh) and always validate a JWT’s signature, issuer, and audience.
- Authorization models scale differently: ACLs (per-resource), RBAC (roles — the workhorse), ABAC (attributes — most expressive), and policy engines (OPA/Rego) that decouple policy from application code.
- Apply least privilege at every layer: dedicated non-root users + capabilities on Linux,
drop: ALL+ non-root + read-only-root on Kubernetes, minimal scoped roles in the cloud — and never hardcode credentials; issue short-lived, rotated, audited ones.
References
- NIST SP 800-63B — Digital Identity Guidelines (authentication & lifecycle). https://pages.nist.gov/800-63-3/sp800-63b.html
- W3C WebAuthn / FIDO2 — origin-bound public-key authentication (passkeys). https://www.w3.org/TR/webauthn-2/
- RFC 6749 — The OAuth 2.0 Authorization Framework. https://datatracker.ietf.org/doc/html/rfc6749
- OpenID Connect Core 1.0. https://openid.net/specs/openid-connect-core-1_0.html
- RFC 6238 — TOTP: Time-Based One-Time Password Algorithm. https://datatracker.ietf.org/doc/html/rfc6238
- OWASP Authentication and Authorization Cheat Sheets. https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Open Policy Agent documentation. https://www.openpolicyagent.org/docs/latest/
- Kubernetes RBAC documentation. https://kubernetes.io/docs/reference/access-authn-authz/rbac/
Related course pages: DevSecOps Fundamentals · Introduction to Networking
🛠️ Maintenance note: authentication principles are stable, but this page leans on fast-moving infrastructure — the Kubernetes PodSecurity admission profiles, OPA/Gatekeeper API versions (
v1/v1beta1), and AWS IAM/CLI syntax all drift, and FIDO2/passkey adoption is changing quickly. Re-verify the Kubernetes and cloud examples against current versions each term.