courses

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: authenticationwho are you? — and authorizationwhat 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

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:

# 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:

# 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

References


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.