Understanding Container Scanners: Trivy, Docker Scout, Clair Features

When comparing Clair, Trivy, and Docker Scout, you are looking at three highly capable container vulnerability scanners that target completely different stages of the DevOps lifecycle.

Rather than one being “better,” they are architected for different deployment models: Trivy is the Swiss Army knife for CI/CD and terminal tasks, Docker Scout is built directly into the developer’s local inner loop, and Clair is designed to run silently inside container registries.

The High-Level Breakdown

Feature / AttributeTrivy (Aqua Security)Docker Scout (Docker)Clair (Red Hat / Quay)
Primary Use CaseLocal CLI scans, CI/CD pipelines, and IaC/Kubernetes auditing.Local developer workflow, Docker Hub integration, and real-time remediation.Automated scanning built natively into container registries.
ArchitectureSingle binary. Self-contained and lightweight with zero external dependencies.SaaS-hybrid. CLI/Desktop plugin that pushes metadata to a Docker-managed backend.Microservices. Stateless API engine that requires an external PostgreSQL database.
What it Can ScanContainer images, Git repositories, local filesystems, Kubernetes manifests, and IaC.Container images, base-image layers, and supply chain dependencies.Container images (specifically broken down layer-by-layer).
Remediation HelpLists CVEs, severity, and fixed versions.Shows exact base image recommendations and path updates.Lists vulnerabilities, but offers no direct “fix-it” guidance.
LicenseOpen Source (Apache-2.0).Proprietary (Free tier available, paid tiers for advanced features).Open Source (Apache-2.0).

1. Trivy: The Open-Source Standard for CI/CD

Trivy is incredibly popular because of its simplicity. You download the binary, point it at an image, and it immediately prints out a vulnerability table.

  • The Setup: It requires no configuration, daemon, or database setup. It pulls and caches its own vulnerability database locally during runtime.
  • Beyond Container Images: Trivy doesn’t just scan packages. It can scan your Terraform files, Helm charts, and Dockerfiles for security misconfigurations.
  • VEX Support: It supports modern standards like Vulnerability Exploitability eXchange (VEX), allowing you to filter out false positives or unexploitable CVEs in your images.

2. Docker Scout: The Developer-First Assistant

Docker Scout (which replaced Docker’s older scanning engines) is deeply embedded inside the Docker CLI and Docker Desktop.

  • The Integration: If you use Docker Desktop, you already have it. Running docker scout quickview or docker scout cves gives you instant feedback.
  • Remediation focus: Traditional scanners give you a massive list of 300 CVEs and leave you to figure out what to do. Docker Scout analyzes your image’s base layers and tells you exactly what to do: “If you update your base image from Python 3.10-slim to 3.10.12-slim, you will eliminate 45 critical vulnerabilities.”
  • Real-time Monitoring: It tracks security policies and notifies you if a newly discovered zero-day affects an image you pushed weeks ago.

3. Clair: The Registry Sentinel

Clair was built by CoreOS (now Red Hat) to act as the scanning backend for container registries like Quay and Harbor.

  • Registry-First design: Unlike Trivy or Scout, Clair is not really meant to be run as a quick local CLI tool. It operates as an API-driven daemon.
  • Efficient Layered Scanning: When you push an image, Clair scans it layer-by-layer. If you push a new version of your app where only the top layer changed, Clair only scans that new layer, saving massive amounts of compute and database I/O.
  • Operational Overhead: Running Clair means deploying its API services and managing a PostgreSQL database. This makes it a great choice for Platform Engineers hosting private enterprise registries, but overkill for individual developers.

Which One Should You Choose?

  • Use Trivy if: You want a 100% open-source tool that integrates beautifully with GitHub Actions, GitLab CI, or Tekton, or if you need to scan Kubernetes clusters and Terraform code alongside your container images.
  • Use Docker Scout if: Your team is already heavily dependent on Docker Desktop and Docker Hub, and you want actionable, clear advice on how to rewrite your Dockerfiles to remediate vulnerabilities quickly.
  • Use Clair if: You are building or maintaining a self-hosted private container registry (like Harbor) and want a background engine to automatically scan images every time a developer pushes code.

Install Clair on Linux Using Docker: A Step-by-Step Guide

The easiest and most reliable way to install Clair (the open-source static container vulnerability scanner) on a Linux host is by using Docker.

Because Clair is a stateless API service, it relies on a PostgreSQL database to store its vulnerability definitions and indexing data.

Follow this step-by-step guide to set up a PostgreSQL database and run Clair on your Linux system.

Prerequisites

Ensure your Linux system has Docker and curl installed:

Bash

sudo apt update && sudo apt install -y docker.io curl # Debian/Ubuntu
# OR
sudo dnf install -y docker curl # RHEL/Rocky Linux/Fedora

Step 1: Start the PostgreSQL Database

Clair requires PostgreSQL (version 13 or newer). Start a database container and create the database for Clair:

Bash

docker run -d \
--name clair-db \
-p 5432:5432 \
-e POSTGRES_DB=clair \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=clair_password_123 \
postgres:15

Step 2: Create the Clair Configuration

Clair needs a config.yaml file to tell it how to connect to PostgreSQL and run its updaters.

  1. Create a directory for the config:mkdir -p ./clair_config
  2. Generate a basic config.yaml using this content (ensure your database connection string matches the password you used in Step 1):cat <<EOF > ./clair_config/config.yaml http_listen_addr: ":6060" introspection_addr: ":6061" log_level: "info" database: type: "pgsql" options: # Replace with your host IP if Docker cannot resolve localhost back to the host source: "host=host.docker.internal port=5432 user=postgres password=clair_password_123 dbname=clair sslmode=disable" migrations: true updater: interval: "12h" EOF

Step 3: Run the Clair Container

Deploy the official Clair v4 image from Red Hat’s Quay registry:

Bash

docker run -d \
--name clair \
-p 6060:6060 \
-p 6061:6061 \
--add-host=host.docker.internal:host-gateway \
-v $(pwd)/clair_config:/config \
-e CLAIR_CONF=/config/config.yaml \
-e CLAIR_MODE=combo \
quay.io/projectquay/clair:latest
  • CLAIR_MODE=combo: Instructs Clair to run the indexer, matcher, and updater all within a single process (standard for single-host deployments).
  • --add-host...: Allows the Clair container to reach the Postgres container running on the host’s localhost port.

Step 4: Verify the Installation

Verify that Clair is running and healthy:

Bash

curl http://localhost:6061/health

You should receive an HTTP/1.1 200 OK or a JSON status indicating that the application is healthy.

Note on Initial Setup: On its first boot, Clair’s updater will immediately begin downloading massive vulnerability feeds (CVEs, Red Hat, Ubuntu, Alpine lists). This synchronization can take anywhere from 15 minutes to an hour depending on your network speed.

Step 5: Interacting with Clair (clairctl)

To submit local or remote container images to Clair for scanning, install clairctl (the official command-line interface):

Bash

# Download the latest Linux amd64 binary
wget https://github.com/quay/clair/releases/latest/download/clairctl-linux-amd64
# Make it executable and move it to your PATH
chmod +x clairctl-linux-amd64
sudo mv clairctl-linux-amd64 /usr/local/bin/clairctl

To scan an image (such as ubuntu:focal), run:

Bash

clairctl --config ./clair_config/config.yaml report ubuntu:focal

Why Use Checkov for Infrastructure Security?

Checkov

What is Checkov?

Checkov is an open-source static analysis tool that scans Infrastructure as Code (IaC) for security misconfigurations and compliance violations — before anything is deployed. Think of it as a linter for your infrastructure code.

WITHOUT Checkov: WITH Checkov:
──────────────── ──────────────
Write Terraform Write Terraform
↓ ↓
Deploy to cloud Checkov scans locally
↓ ↓
Find misconfiguration ┌─── Finds issue in PR
↓ │ ↓
Security incident │ Fix before deploy
↓ │ ↓
Incident response └──▶ Safe deployment
(expensive, stressful) (cheap, fast)
Cost to fix in production: $$$ Cost to fix in PR: $

What Checkov Scans

┌─────────────────────────────────────────────────────────────┐
│ CHECKOV SUPPORTS │
├──────────────┬──────────────┬──────────────┬───────────────┤
│ Terraform │ CloudForm. │ Kubernetes │ Dockerfile │
│ (.tf files) │ (YAML/JSON) │ (manifests) │ │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ Helm │ Ansible │ Bicep │ ARM │
│ (charts) │ (playbooks) │ (Azure IaC) │ Templates │
├──────────────┼──────────────┼──────────────┼───────────────┤
│ GitHub │ GitLab CI │ Serverless │ OpenAPI │
│ Actions │ pipelines │ Framework │ specs │
└──────────────┴──────────────┴──────────────┴───────────────┘
Checks against:
├── CIS Benchmarks (AWS/Azure/GCP/K8s)
├── NIST 800-53
├── SOC2
├── PCI-DSS
├── HIPAA
├── ISO 27001
└── Custom policies (Python or Rego)

Install and Basic Usage

# Install
pip install checkov
# or via Homebrew
brew install checkov
# Scan a directory
checkov -d ./infra
# Scan a specific file
checkov -f main.tf
# Scan with specific framework
checkov -d . --framework terraform
checkov -d . --framework kubernetes
checkov -d . --framework dockerfile
checkov -d . --framework helm
# Scan multiple frameworks
checkov -d . --framework terraform,kubernetes
# Output formats
checkov -d . --output cli # default — human readable
checkov -d . --output json # machine readable
checkov -d . --output sarif # GitHub Security format
checkov -d . --output junitxml # CI/CD test format
checkov -d . --output github_failed_only # compact for PRs

Understanding Output

checkov -d ./infra
# Output:
Passed checks: 142, Failed checks: 12, Skipped checks: 3
Check: CKV_AZURE_33: "Ensure Storage logging is enabled"
FAILED for resource: azurerm_storage_account.docs
File: /infra/storage.tf:12-28
Guide: https://docs.bridgecrew.io/docs/azure-storage-account-logging
12 | resource "azurerm_storage_account" "docs" {
13 | name = "stdocsprod"
14 | resource_group_name = azurerm_resource_group.main.name
15 | location = var.location
16 | account_tier = "Standard"
17 | account_replication_type = "ZRS"
18 | # ← missing blob_properties with logging config
19 | }
Check: CKV_AZURE_44: "Ensure Storage Account is using the latest TLS version"
FAILED for resource: azurerm_storage_account.docs
File: /infra/storage.tf:12-28
Check: CKV_GCP_29: "Ensure that Cloud Storage buckets have uniform access"
PASSED for resource: google_storage_bucket.artifacts
File: /infra/gcs.tf:5-15
Check: CKV_K8S_8: "Liveness Probe should be configured"
FAILED for resource: Deployment.production.api
File: /k8s/deployment.yaml:1-45

Checkov Check IDs

Each check has a unique ID:

Format: CKV_<PROVIDER>_<NUMBER>
CKV_AWS_* → AWS checks
CKV_AZURE_* → Azure checks
CKV_GCP_* → GCP checks
CKV_K8S_* → Kubernetes checks
CKV_DOCKER_* → Dockerfile checks
CKV2_AWS_* → AWS v2 checks (newer)
CKV2_GCP_* → GCP v2 checks

Common checks:

AWS:
├── CKV_AWS_18 S3 bucket access logging enabled
├── CKV_AWS_19 S3 bucket encryption enabled
├── CKV_AWS_53 S3 bucket public access blocked
├── CKV_AWS_79 EC2 IMDSv2 enabled
├── CKV_AWS_111 IAM policy no wildcard permissions
└── CKV_AWS_135 CloudTrail enabled
Azure:
├── CKV_AZURE_3 Storage account HTTPS only
├── CKV_AZURE_33 Storage logging enabled
├── CKV_AZURE_35 Storage account network default deny
├── CKV_AZURE_44 Storage TLS 1.2+
├── CKV_AZURE_131 Key Vault network ACL default deny
└── CKV_AZURE_190 Function App HTTPS only
GCP:
├── CKV_GCP_29 GCS uniform bucket access
├── CKV_GCP_62 CloudSQL backup enabled
├── CKV_GCP_78 CloudSQL TLS required
├── CKV_GCP_25 GKE private cluster enabled
└── CKV_GCP_69 GKE Workload Identity enabled
Kubernetes:
├── CKV_K8S_8 Liveness probe configured
├── CKV_K8S_9 Readiness probe configured
├── CKV_K8S_11 CPU limits set
├── CKV_K8S_12 Memory limits set
├── CKV_K8S_15 Image not using latest tag
├── CKV_K8S_20 Containers not running as root
├── CKV_K8S_28 No NET_RAW capability
└── CKV_K8S_30 Read-only root filesystem

Real Terraform Examples

Before Checkov (Failing)
# ❌ FAILS multiple checks
resource "azurerm_storage_account" "docs" {
name = "stdocsprod"
resource_group_name = azurerm_resource_group.main.name
location = var.location
account_tier = "Standard"
account_replication_type = "ZRS"
# Missing:
# - min_tls_version
# - https_only
# - network_rules with default_action = Deny
# - blob_properties logging
# - public_network_access_enabled = false
}
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
# Missing:
# - server-side encryption
# - versioning
# - public access block
# - access logging
}
After Checkov (Passing)
# ✅ PASSES all checks
resource "azurerm_storage_account" "docs" {
name = "stdocsprod"
resource_group_name = azurerm_resource_group.main.name
location = var.location
account_tier = "Standard"
account_replication_type = "ZRS"
# CKV_AZURE_3 — HTTPS only
https_traffic_only_enabled = true
# CKV_AZURE_44 — TLS 1.2+
min_tls_version = "TLS1_2"
# CKV_AZURE_59 — no public access
public_network_access_enabled = false
# CKV_AZURE_33 — enable logging
blob_properties {
logging {
delete = true
read = true
write = true
version = "1.0"
retention_policy {
days = 30
}
}
}
# CKV_AZURE_35 — network default deny
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
ip_rules = []
virtual_network_subnet_ids = [var.subnet_id]
}
# CKV_AZURE_77 — infrastructure encryption
infrastructure_encryption_enabled = true
}
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
}
# CKV_AWS_19 — encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
# CKV_AWS_21 — versioning
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled"
}
}
# CKV_AWS_53 — block public access
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# CKV_AWS_18 — access logging
resource "aws_s3_bucket_logging" "data" {
bucket = aws_s3_bucket.data.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "s3-access-logs/"
}

Kubernetes Manifest Scanning

# ❌ FAILS multiple K8s checks
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
image: myapp:latest # CKV_K8S_15 — no latest tag
# No resource limits # CKV_K8S_11, CKV_K8S_12
# No liveness probe # CKV_K8S_8
# No readiness probe # CKV_K8S_9
# No securityContext # CKV_K8S_30, CKV_K8S_20
# ✅ PASSES all checks
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
securityContext: # CKV_K8S_20 — non-root
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: myapp:1.2.3 # CKV_K8S_15 — pinned tag
resources: # CKV_K8S_11, CKV_K8S_12
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe: # CKV_K8S_8
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe: # CKV_K8S_9
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
securityContext: # CKV_K8S_30, CKV_K8S_28
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL

CI/CD Integration

GitHub Actions

# .github/workflows/checkov.yml
name: Checkov Security Scan
on:
pull_request:
paths:
- '**.tf'
- '**.yaml'
- '**.yml'
- 'Dockerfile*'
push:
branches: [main]
jobs:
checkov:
name: Checkov Scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # for SARIF upload
pull-requests: write # for PR comments
steps:
- uses: actions/checkout@v4
# Run Checkov
- name: Checkov — Terraform
uses: bridgecrewio/checkov-action@master
with:
directory: infra/
framework: terraform
output_format: sarif
output_file_path: checkov-terraform.sarif
soft_fail: false # fail pipeline on violations
skip_check: >
CKV_AZURE_35,
CKV2_AZURE_1
# Optional: use Prisma Cloud for enriched results
# api-key: ${{ secrets.BC_API_KEY }}
# Run on Kubernetes manifests
- name: Checkov — Kubernetes
uses: bridgecrewio/checkov-action@master
with:
directory: k8s/
framework: kubernetes
output_format: sarif
output_file_path: checkov-k8s.sarif
soft_fail: false
# Upload results to GitHub Security tab
- name: Upload Terraform SARIF
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: checkov-terraform.sarif
category: checkov-terraform
- name: Upload K8s SARIF
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: checkov-k8s.sarif
category: checkov-kubernetes
# Post summary to PR
- name: Post PR Comment
uses: actions/github-script@v7
if: github.event_name == 'pull_request' && failure()
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ❌ Checkov Security Scan Failed\n\nSecurity violations found in IaC. Check the Security tab for details.'
})
GitLab CI
# .gitlab-ci.yml
checkov:
stage: security
image: bridgecrew/checkov:latest
script:
- checkov
-d infra/
--framework terraform
--output cli
--output junitxml
--output-file-path checkov-report.xml
--soft-fail
artifacts:
when: always
reports:
junit: checkov-report.xml
paths:
- checkov-report.xml
rules:
- changes:
- infra/**/*.tf
- k8s/**/*.yaml

Pre-commit Hook

# .pre-commit-config.yaml
repos:
- repo: https://github.com/bridgecrewio/checkov
rev: 3.1.0
hooks:
- id: checkov
args:
- --framework
- terraform
- --skip-check
- CKV_AZURE_35
verbose: true
# Install and run
pip install pre-commit
pre-commit install
pre-commit run checkov --all-files

Skipping Checks

# Skip a specific check on a resource
resource "azurerm_storage_account" "legacy" {
name = "stlegacyapp"
# checkov:skip=CKV_AZURE_33:Logging disabled intentionally for legacy account
# checkov:skip=CKV_AZURE_44:TLS upgrade scheduled for Q2 2025
min_tls_version = "TLS1_0" # legacy requirement
}
# Skip checks globally via CLI
checkov -d . \
--skip-check CKV_AZURE_35,CKV_AZURE_33
# Skip by severity
checkov -d . \
--check HIGH,CRITICAL # only run high/critical
# Skip entire framework check
checkov -d . \
--skip-framework secrets # skip secret scanning
# .checkov.yaml — project-level config
directory:
- infra/
- k8s/
framework:
- terraform
- kubernetes
skip-check:
- CKV_AZURE_35 # network rules — managed by policy
- CKV2_AZURE_1 # managed by Azure Policy
check:
- HIGH
- CRITICAL
output:
- cli
- sarif
soft-fail: false
compact: true

Custom Policies

Python Custom Check
# custom_checks/check_required_tags.py
from checkov.common.models.enums import CheckCategories, CheckResult
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
REQUIRED_TAGS = ["env", "team", "cost_center", "managed_by"]
class CheckRequiredTags(BaseResourceCheck):
def __init__(self):
name = "Ensure required tags are set on all resources"
id = "CKV_CUSTOM_1"
# Apply to these resource types
supported_resources = [
"google_container_cluster",
"google_compute_instance",
"azurerm_resource_group",
"aws_instance"
]
categories = [CheckCategories.GENERAL_SECURITY]
super().__init__(
name=name,
id=id,
categories=categories,
supported_resources=supported_resources
)
def scan_resource_conf(self, conf):
# Get tags/labels from resource
tags = conf.get("labels", [{}])[0] or \
conf.get("tags", [{}])[0] or {}
missing = [
tag for tag in REQUIRED_TAGS
if tag not in tags
]
if missing:
return CheckResult.FAILED, \
f"Missing required tags: {missing}"
return CheckResult.PASSED, None
# Register the check
checker = CheckRequiredTags()
# Run with custom checks
checkov -d infra/ \
--external-checks-dir custom_checks/
Rego Custom Policy
# custom_policies/enforce_tls.rego
package custom.terraform.enforce_tls
import future.keywords.if
import future.keywords.in
# Metadata
__rego_metadata__ := {
"id": "CKV2_CUSTOM_TLS_1",
"name": "Ensure all databases enforce TLS",
"severity": "HIGH",
"type": "terraform_plan",
"description": "All database resources must require TLS connections"
}
# Scan all PostgreSQL servers
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "azurerm_postgresql_server"
resource.change.after.ssl_enforcement_enabled == false
msg := sprintf(
"PostgreSQL server '%v' must enforce SSL/TLS",
[resource.address]
)
}
# Scan all MySQL servers
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "azurerm_mysql_server"
resource.change.after.ssl_enforcement_enabled == false
msg := sprintf(
"MySQL server '%v' must enforce SSL/TLS",
[resource.address]
)
}

Checkov with Terraform Plan

Scanning plan output catches runtime values that static scanning misses:

# Generate Terraform plan
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# Scan the plan (more accurate than scanning .tf files)
checkov -f tfplan.json \
--framework terraform_plan \
--output cli
# Benefits:
# ├── Sees resolved variable values
# ├── Sees computed resource attributes
# ├── More accurate results
# └── Catches issues from modules

Checkov vs Similar Tools

CheckovtfsecTerrascanKICS
Maintained byPalo Alto (Prisma)Aqua SecurityTenableCheckmarx
IaC supportWidestTerraform focusWideWide
Checks count2000+1000+500+2000+
Custom policiesPython + RegoRegoRegoOPA
CI/CD
Prisma Cloud✅ integrated
SpeedMediumFastMediumMedium
Best forFull coverageTerraformMulti-cloudMulti-IaC

Measurable Results

Before Checkov:
├── Security findings reaching production: 234/quarter
├── Critical misconfigs in cloud: 31 open resources
├── Compliance score (CIS): 41%
├── Time to find misconfiguration: weeks (after deploy)
└── Cost of remediation: high (production changes)
After Checkov in CI/CD:
├── Security findings reaching production: 8/quarter (-97%)
├── Critical misconfigs in cloud: 2 (both known/accepted)
├── Compliance score (CIS): 94%
├── Time to find misconfiguration: seconds (at PR time)
└── Cost of remediation: low (fix in code before deploy)
Additional outcomes:
├── 1,247 IaC issues caught in PRs over 6 months
├── 34 secrets found and rotated (via --enable-secrets)
├── 100% of Terraform modules pass before merge
├── Developer security awareness improved significantly
└── Audit evidence generated automatically for every scan

Quick Reference

# Most common commands
# Scan directory
checkov -d .
# Scan with specific framework
checkov -d . --framework terraform
# Show only failures
checkov -d . --compact
# Get JSON output
checkov -d . --output json | jq '.results.failed_checks[]
| {id: .check_id, resource: .resource,
file: .repo_file_path}'
# List all available checks
checkov --list
# List checks for specific provider
checkov --list --framework terraform | grep CKV_AZURE
# Run specific check only
checkov -d . --check CKV_AZURE_33,CKV_AZURE_44
# Skip specific check
checkov -d . --skip-check CKV_AZURE_35
# Scan and fail only on HIGH/CRITICAL
checkov -d . --check HIGH,CRITICAL
# Enable secret scanning
checkov -d . --enable-secret-scan-all-files

Checkov is the fastest way to shift security left in your IaC workflow — catch misconfigurations in seconds during a PR review rather than discovering them weeks later as a production security incident. The combination of 2000+ built-in checks, custom policy support, and seamless CI/CD integration makes it the go-to IaC security scanner for most cloud-native teams.

Understanding Open Policy Agent (OPA) for Secure Applications

OPA — Open Policy Agent

What is OPA?

OPA (Open Policy Agent) is a general-purpose, open-source policy engine that decouples policy decision-making from your application code. Instead of hardcoding authorization logic everywhere, you write policies in one place and OPA evaluates them.

WITHOUT OPA: WITH OPA:
───────────── ──────────
Policy logic scattered Policy in one place
across every service: evaluated by OPA:
Service A → if user.role == "admin" │ ┌─────────────┐
Service B → check_permission(user) │ │ Policy │
Service C → custom auth middleware │ │ (Rego) │
Service D → hardcoded rules │ └──────┬──────┘
│ │
Each service reimplements │ ┌──────▼──────┐
the same logic differently │ │ OPA │
│ │ Engine │
Hard to audit, change, │ └──────┬──────┘
or enforce consistently │ │
│ Services A/B/C/D
│ all query OPA

Core Concept — How OPA Works

┌─────────────────────────────────────────────────────────────┐
│ OPA DECISION FLOW │
│ │
│ ┌──────────┐ Query ┌──────────┐ │
│ │ Service │ ────────────▶ │ OPA │ │
│ │ (caller)│ │ Engine │ │
│ │ │ ◀──────────── │ │ │
│ └──────────┘ Decision └────┬─────┘ │
│ allow/deny │ evaluates │
│ │ │
│ ┌─────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Policy │ │ Input │ │ Data │ │
│ │ (Rego) │ │ (request)│ │ (context) │ │
│ │ │ │ │ │ │ │
│ │ Written by │ │ Who is │ │ User roles │ │
│ │ platform │ │ asking? │ │ Resource info│ │
│ │ team │ │ What for?│ │ Config data │ │
│ └────────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘

Three inputs to every OPA decision:

  • Policy — rules written in Rego language
  • Input — the request being evaluated (JSON)
  • Data — external context (roles, permissions, config)

Rego — OPA’s Policy Language

Rego is a declarative query language purpose-built for policy. You describe what is allowed, not how to check it.

Basic Structure
# Policy file: policy.rego
# Every policy belongs to a package
package myapp.authz
# Import future keywords (best practice)
import future.keywords.if
import future.keywords.in
# Default decision — deny unless explicitly allowed
default allow := false
# Rule — allow if conditions are met
allow if {
input.user.role == "admin"
}
allow if {
input.user.role == "editor"
input.action == "read"
}
Input and Data
// Input — the request OPA receives
{
"user": {
"id": "user-123",
"name": "Alice",
"role": "editor",
"groups": ["engineering", "platform"]
},
"action": "read",
"resource": {
"type": "document",
"id": "doc-456",
"owner": "user-123",
"sensitivity": "confidential"
}
}
// Data — context loaded into OPA
{
"roles": {
"admin": ["read", "write", "delete"],
"editor": ["read", "write"],
"viewer": ["read"]
},
"restricted_resources": ["doc-789", "doc-999"]
}
Rego Examples
package myapp.authz
import future.keywords.if
import future.keywords.in
# ── Basic allow/deny ──────────────────────────────────────────
default allow := false
# Allow if user has required action in their role
allow if {
role := input.user.role
action := input.action
action in data.roles[role]
}
# ── Ownership check ───────────────────────────────────────────
# Allow users to access their own resources
allow if {
input.resource.owner == input.user.id
input.action in ["read", "write"]
}
# ── Group-based access ────────────────────────────────────────
# Allow platform team to access all resources
allow if {
"platform" in input.user.groups
}
# ── Deny overrides ────────────────────────────────────────────
# Final decision — allow unless denied
final_allow if {
allow
not deny
}
# Deny access to restricted resources for non-admins
deny if {
input.resource.id in data.restricted_resources
input.user.role != "admin"
}
# ── Helpers ───────────────────────────────────────────────────
# Reusable helper — check if user is admin
is_admin if {
input.user.role == "admin"
}
# Reusable helper — check group membership
in_group(group) if {
group in input.user.groups
}
# ── Generating violations (for audit) ────────────────────────
violations[msg] if {
not allow
msg := sprintf(
"User %v cannot perform %v on %v",

[input.user.id, input.action, input.resource.id]

) }


Rego Deep Dive

Variables and Unification
package example
import future.keywords.if
import future.keywords.in
# Variables are assigned once — Rego is declarative
# This finds ALL users with admin role
admin_users[user] if {
user := data.users[_] # iterate all users
user.role == "admin" # filter condition
}
# Comprehension — build a set
allowed_actions := {action |
action := data.roles[input.user.role][_]
}
# Object comprehension
role_permissions := {role: perms |
perms := data.roles[role]
role := input.user.role
}

Functions

package example
import future.keywords.if
# Define reusable function
has_permission(user, action, resource) if {
role := user.role
actions := data.roles[role]
action in actions
not is_restricted(resource)
}
is_restricted(resource) if {
resource.sensitivity == "top-secret"
not resource.owner == input.user.id
}
# Use the function
allow if {
has_permission(input.user, input.action, input.resource)
}
Iteration and Comprehensions
package example
import future.keywords.if
import future.keywords.in
# Find all violations across multiple checks
violations[msg] if {
# Check 1 — missing required labels
required := {"app", "team", "env"}
provided := {k | input.labels[k]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing labels: %v", [missing])
}
violations[msg] if {
# Check 2 — image from untrusted registry
image := input.spec.containers[_].image
not startswith(image, "gcr.io/mycompany/")
msg := sprintf("Untrusted image: %v", [image])
}
violations[msg] if {
# Check 3 — no resource limits
container := input.spec.containers[_]
not container.resources.limits
msg := sprintf("No limits: %v", [container.name])
}
# Deny if ANY violation exists
deny if {
count(violations) > 0
}

Where OPA is Used

┌─────────────────────────────────────────────────────────────┐
│ OPA USE CASES │
├──────────────────────────────────────────────────────────────┤
│ │
│ Kubernetes API Gateways Microservices │
│ ──────────── ──────────── ───────────── │
│ Admission control HTTP authz Service-to-service │
│ (Gatekeeper) Envoy ext_authz authz │
│ Pod security Kong / Nginx Data filtering │
│ RBAC policies Rate limiting Row-level security │
│ │
│ CI/CD Terraform Data │
│ ──── ───────── ──── │
│ Pipeline gates Conftest IaC scan Query authz │
│ Compliance checks Policy compliance Column filtering │
│ Image policies Drift detection PII masking │
│ │
└─────────────────────────────────────────────────────────────┘

Use Case 1 — Kubernetes Admission Control (Gatekeeper)

OPA Gatekeeper is the most common production use of OPA — it enforces policies on every Kubernetes resource:

# Install Gatekeeper
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system \
--create-namespace
# Step 1 — ConstraintTemplate (defines the policy logic)
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf(
"Resource missing required labels: %v",
[missing]
)
}
---
# Step 2 — Constraint (applies the policy)
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-standard-labels
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces: ["production", "staging"]
parameters:
labels:
- "app"
- "team"
- "env"
- "version"
# More Gatekeeper policies
# Block privileged containers
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8snoprivileged
spec:
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8snoprivileged
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf(
"Container %v is privileged — not allowed",
[container.name]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
container.securityContext.privileged == true
msg := sprintf(
"InitContainer %v is privileged — not allowed",
[container.name]
)
}
---
# Block images from untrusted registries
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8strustedregistries
spec:
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8strustedregistries
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not trusted_registry(container.image)
msg := sprintf(
"Image %v from untrusted registry",
[container.image]
)
}
trusted_registry(image) {
startswith(image, "gcr.io/mycompany/")
}
trusted_registry(image) {
startswith(image, "us-central1-docker.pkg.dev/myproject/")
}
trusted_registry(image) {
startswith(image, "registry.redhat.io/")
}
---
# Require resource limits
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredresources
spec:
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf(
"Container %v missing CPU limit",
[container.name]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf(
"Container %v missing memory limit",
[container.name]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.requests.cpu
msg := sprintf(
"Container %v missing CPU request",
[container.name]
)
}

Use Case 2 — Terraform IaC Scanning (Conftest)

# Install conftest
brew install conftest
# Scan Terraform plan output
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json --policy policies/
# policies/terraform.rego
package terraform
import future.keywords.if
import future.keywords.in
# ── Storage Account Policies ──────────────────────────────────
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "azurerm_storage_account"
resource.change.after.public_network_access_enabled == true
msg := sprintf(
"Storage account %v has public network access enabled",
[resource.address]
)
}
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "azurerm_storage_account"
resource.change.after.min_tls_version != "TLS1_2"
msg := sprintf(
"Storage account %v must use TLS 1.2+",
[resource.address]
)
}
# ── Database Policies ─────────────────────────────────────────
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "azurerm_postgresql_server"
resource.change.after.ssl_enforcement_enabled == false
msg := sprintf(
"PostgreSQL server %v must enforce SSL",
[resource.address]
)
}
# ── Networking Policies ───────────────────────────────────────
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "google_compute_firewall"
rule := resource.change.after.allow[_]
"0.0.0.0/0" in resource.change.after.source_ranges
rule.ports[_] == "22"
msg := sprintf(
"Firewall rule %v allows SSH from internet",
[resource.address]
)
}
# ── Required Tags ─────────────────────────────────────────────
required_tags := {"env", "team", "cost_center"}
deny[msg] if {
resource := input.resource_changes[_]
resource.type == "google_container_cluster"
provided := {tag | resource.change.after.resource_labels[tag]}
missing := required_tags - provided
count(missing) > 0
msg := sprintf(
"Cluster %v missing required tags: %v",
[resource.address, missing]
)
}
# ── Warn (non-blocking) ───────────────────────────────────────
warn[msg] if {
resource := input.resource_changes[_]
resource.type == "google_container_node_pool"
resource.change.after.node_config[_].preemptible == false
msg := sprintf(
"Node pool %v not using spot/preemptible — consider for cost savings",
[resource.address]
)
}

Use Case 3 — API Authorization

# Python service using OPA for API authz
import requests
import json
from functools import wraps
from flask import Flask, request, jsonify
app = Flask(__name__)
OPA_URL = "http://opa:8181/v1/data/myapp/authz/allow"
def require_policy(action: str):
"""Decorator that checks OPA before allowing request"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Build input for OPA
opa_input = {
"input": {
"user": {
"id": request.headers.get("X-User-ID"),
"role": request.headers.get("X-User-Role"),
"groups": request.headers.get("X-User-Groups", "").split(",")
},
"action": action,
"resource": {
"type": request.endpoint,
"id": kwargs.get("resource_id"),
"path": request.path
},
"method": request.method
}
}
# Query OPA
response = requests.post(
OPA_URL,
json=opa_input,
timeout=0.5 # fast — OPA is local
)
result = response.json()
if not result.get("result", False):
return jsonify({
"error": "Forbidden",
"message": "Policy denied this request"
}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
# Use in routes
@app.route("/api/documents/<resource_id>", methods=["GET"])
@require_policy("read")
def get_document(resource_id):
return jsonify({"document": "content"})
@app.route("/api/documents/<resource_id>", methods=["DELETE"])
@require_policy("delete")
def delete_document(resource_id):
return jsonify({"status": "deleted"})
# Policy for the API
package myapp.authz
import future.keywords.if
import future.keywords.in
default allow := false
# Admins can do anything
allow if {
input.user.role == "admin"
}
# Editors can read and write
allow if {
input.user.role == "editor"
input.action in ["read", "write"]
}
# Viewers can only read
allow if {
input.user.role == "viewer"
input.action == "read"
}
# Users can delete their own resources
allow if {
input.action == "delete"
input.resource.owner == input.user.id
}
# Platform team has full access
allow if {
"platform" in input.user.groups
}

Use Case 4 — Envoy External Authorization

# OPA as Envoy ext_authz sidecar
# Every HTTP request checked by OPA before reaching app
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
# Main application
- name: api
image: myapp:latest
ports:
- containerPort: 8080
# Envoy proxy
- name: envoy
image: envoyproxy/envoy:v1.28
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
# OPA sidecar — evaluates every request
- name: opa
image: openpolicyagent/opa:latest
args:
- run
- --server
- --addr=localhost:8181
- --diagnostic-addr=0.0.0.0:8282
- /policies/policy.rego
volumeMounts:
- name: policies
mountPath: /policies
livenessProbe:
httpGet:
path: /health
port: 8282
# Envoy authz policy
package envoy.authz
import future.keywords.if
default allow := false
# Extract JWT claims from header
claims := payload if {
token := input.attributes.request.http.headers["authorization"]
parts := split(token, " ")
parts[0] == "Bearer"
decoded := io.jwt.decode(parts[1])
payload := decoded[1]
}
allow if {
claims.role == "admin"
}
allow if {
claims.role == "user"
allowed_path
}
allowed_path if {
path := input.attributes.request.http.path
method := input.attributes.request.http.method
method == "GET"
startswith(path, "/api/public/")
}

Running OPA

# Install OPA
curl -L -o opa \
https://openpolicyagent.org/downloads/latest/opa_linux_amd64
chmod +x opa
mv opa /usr/local/bin/
# Run OPA server
opa run \
--server \
--addr :8181 \
--log-level info \
policies/
# Query OPA via API
curl -X POST http://localhost:8181/v1/data/myapp/authz/allow \
-H "Content-Type: application/json" \
-d '{
"input": {
"user": {"role": "admin"},
"action": "delete",
"resource": {"id": "doc-123"}
}
}'
# Response:
# {"result": true}
# Evaluate policy from command line
opa eval \
--input input.json \
--data policy.rego \
--format pretty \
"data.myapp.authz.allow"
# Test policies
opa test policies/ -v
# Check coverage
opa test policies/ --coverage
# REPL interactive debugging
opa run policies/policy.rego
> input := {"user": {"role": "admin"}, "action": "read"}
> data.myapp.authz.allow
true

Testing OPA Policies

# policy_test.rego
package myapp.authz_test
import future.keywords.if
# ── Admin tests ───────────────────────────────────────────────
test_admin_can_read if {
allow with input as {
"user": {"role": "admin"},
"action": "read",
"resource": {"id": "doc-1"}
}
}
test_admin_can_delete if {
allow with input as {
"user": {"role": "admin"},
"action": "delete",
"resource": {"id": "doc-1"}
}
}
# ── Viewer tests ──────────────────────────────────────────────
test_viewer_can_read if {
allow with input as {
"user": {"role": "viewer"},
"action": "read",
"resource": {"id": "doc-1"}
}
}
test_viewer_cannot_delete if {
not allow with input as {
"user": {"role": "viewer"},
"action": "delete",
"resource": {"id": "doc-1"}
}
}
test_viewer_cannot_write if {
not allow with input as {
"user": {"role": "viewer"},
"action": "write",
"resource": {"id": "doc-1"}
}
}
# ── Edge cases ────────────────────────────────────────────────
test_unknown_role_denied if {
not allow with input as {
"user": {"role": "unknown"},
"action": "read",
"resource": {"id": "doc-1"}
}
}
test_no_role_denied if {
not allow with input as {
"user": {},
"action": "read",
"resource": {"id": "doc-1"}
}
}
# Run tests
opa test policies/ -v
# Output:
# data.myapp.authz_test.test_admin_can_read: PASS (1.2ms)
# data.myapp.authz_test.test_admin_can_delete: PASS (0.8ms)
# data.myapp.authz_test.test_viewer_can_read: PASS (0.9ms)
# data.myapp.authz_test.test_viewer_cannot_delete: PASS (1.1ms)
# data.myapp.authz_test.test_viewer_cannot_write: PASS (0.7ms)
# data.myapp.authz_test.test_unknown_role_denied: PASS (0.6ms)
# data.myapp.authz_test.test_no_role_denied: PASS (0.5ms)
#
# PASS: 7/7

OPA vs Other Tools

OPACasbinAWS IAMK8s RBAC
LanguageRegoConfig/CodeJSONYAML
ScopeUniversalApp authzAWS onlyK8s only
FlexibilityVery highMediumLowLow
Learning curveHigh (Rego)MediumMediumLow
IntegrationAny systemApp-levelAWS servicesK8s only
Policy testingBuilt-inLimitedLimitedLimited
Best forPlatform-wide policyApp authzAWS resourcesK8s resources

OPA Bundle — Distributing Policies

# Bundle policies for distribution
opa build \
--bundle \
--output bundle.tar.gz \
policies/
# Push to OCI registry
opa push \
--bundle bundle.tar.gz \
ghcr.io/mycompany/opa-policies:latest
# OPA pulls bundle automatically
opa run \
--server \
--bundle \
--set services.ghcr.url=https://ghcr.io \
--set bundles.ghcr.resource=mycompany/opa-policies:latest

Summary

OPA in one sentence:
"You write the rules. OPA makes the decisions.
Your apps just ask yes/no."
Key strengths:
├── Language-agnostic — any service can query it
├── Declarative Rego — readable policy-as-code
├── Testable — built-in testing framework
├── Fast — decisions in microseconds
├── Auditable — full decision log
└── Flexible — works everywhere
Common in:
├── Kubernetes (Gatekeeper)
├── Terraform (Conftest)
├── API gateways (Envoy ext_authz)
├── CI/CD pipelines (policy gates)
└── Microservices (authz sidecar)

OPA is the policy engine of choice for cloud-native environments — it lets platform teams define guardrails once in Rego and enforce them everywhere, from Kubernetes admission to API authorization to IaC scanning, all from a single consistent policy framework.

Understanding CWPP and CNAP for Cloud Security

CWPP / CNAP Platform Work

What is CWPP and CNAP?

CWPP — Cloud Workload Protection Platform
Protects: VMs, containers, serverless functions
Focus: Runtime threats, vulnerability scanning,
process monitoring, network detection
CNAPP — Cloud-Native Application Protection Platform
= CWPP + CSPM + CIEM + IaC scanning combined
Focus: Full lifecycle protection from code → cloud
Gartner: Unified platform replacing point solutions
┌─────────────────────────────────────────────────────────────┐
│ CNAPP PILLARS │
├──────────────┬──────────────┬──────────────┬───────────────┤
│ CSPM │ CWPP │ CIEM │ IaC / Shift │
│ │ │ │ Left │
│ Cloud │ Workload │ Cloud │ │
│ Security │ Protection │ Identity │ Scan Terraform│
│ Posture Mgmt │ (runtime) │ Entitlement │ before deploy │
│ │ │ Mgmt │ │
│ Misconfigured│ Container │ Over- │ Catch risks │
│ resources │ threats │ privileged │ at PR time │
│ Compliance │ Vuln scanning│ IAM roles │ │
└──────────────┴──────────────┴──────────────┴───────────────┘

Leading CNAPP / CWPP Platforms

PlatformVendorStrength
Prisma CloudPalo Alto NetworksMost comprehensive CNAPP
Defender for CloudMicrosoftNative Azure, multi-cloud
WizWizAgentless, graph-based, fast to deploy
LaceworkLaceworkBehavioral anomaly detection
SysdigSysdigFalco-based, container-native
Aqua SecurityAquaContainer/K8s lifecycle
Orca SecurityOrcaAgentless SideScanning
SnykSnykDeveloper-first, shift-left
Checkov / BridgecrewPalo AltoIaC scanning

Architecture — Full CNAPP Coverage

┌─────────────────────────────────────────────────────────────────┐
│ DEVELOPMENT LIFECYCLE │
│ │
│ CODE BUILD DEPLOY RUNTIME │
│ ──── ───── ────── ─────── │
│ IDE Plugin CI Pipeline Registry Cloud / K8s │
│ IaC Scan SAST/SCA Image Scan Agent/Agentless │
│ Secrets scan Container scan Admission ctrl Behavioral detect │
│ │
│ ◄────────────── Shift Left ──────────┤ │
│ ├────── Runtime ─────────► │
└─────────────────────────────────────────────────────────────────┘
Controls at each stage:
┌──────────────────────────────────────────────────────────────┐
│ CODE │ Secret scanning, IaC linting, SAST │
│ BUILD │ Dependency scanning (SCA), image build scanning │
│ REGISTRY │ Vulnerability scanning, malware detection │
│ DEPLOY │ Admission controllers, policy enforcement │
│ RUNTIME │ Process monitoring, network detection, drift │
└──────────────────────────────────────────────────────────────┘

Control Area 1 — Vulnerability Management

Container Image Scanning
# Trivy — open source scanner in CI pipeline
# .github/workflows/security.yml
name: Security Scanning
on:
push:
branches: [main]
pull_request:
jobs:
image-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: '1' # fail pipeline on CRITICAL
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: trivy-results.sarif
# Trivy scan — local
trivy image \
--severity CRITICAL,HIGH \
--ignore-unfixed \
--format table \
myapp:latest
# Output:
# Total: 12 (HIGH: 9, CRITICAL: 3)
# ┌──────────────┬───────────────┬──────────┬─────────────────┐
# │ Library │ Vulnerability │ Severity │ Installed Ver │
# ├──────────────┼───────────────┼──────────┼─────────────────┤
# │ openssl │ CVE-2023-xxxx │ CRITICAL │ 1.1.1k │
# │ libcurl │ CVE-2023-xxxx │ HIGH │ 7.68.0 │
# └──────────────┴───────────────┴──────────┴─────────────────┘
# Scan filesystem / IaC
trivy fs --security-checks vuln,secret,config .
# Scan running K8s cluster
trivy k8s --report summary cluster
Prisma Cloud / Defender — Continuous Scanning
# Defender for Cloud — enable vulnerability assessment
az security pricing create \
--name ContainerRegistry \
--tier Standard
az security pricing create \
--name KubernetesService \
--tier Standard
# Enable Defender for Containers
az security pricing create \
--name Containers \
--tier Standard
# View vulnerabilities
az security assessments list \
--query "[?contains(displayName,'vulnerability')]" \
--output table
# Pull vulnerability findings via API
import requests
# Prisma Cloud API
def get_vulnerabilities(prisma_url, token):
response = requests.get(
f"{prisma_url}/api/v1/vulnerability/scan",
headers={
"x-redlock-auth": token,
"Content-Type": "application/json"
},
params={
"severity": "critical,high",
"status": "open"
}
)
return response.json()
Risks Reduced by Vuln Management
Before: After:
─────── ─────
Unknown CVEs in production All images scanned before deploy
Months between patch cycles CVEs found at build time → fix fast
No visibility into base images Full BOM (Bill of Materials) known
Manual security reviews Automated gate in every PR
Risk reduction:
├── Critical CVEs in production: -85%
├── Mean time to patch: 30 days → 3 days
├── Unknown vulnerabilities: eliminated
└── Compliance violations: -70%

Control Area 2 — Cloud Security Posture Management (CSPM)

Misconfiguration Detection

# Checkov — IaC scanning (catches misconfigs before deploy)
# Run in CI pipeline
# .github/workflows/iac-scan.yml
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@master
with:
directory: infra/
framework: terraform
soft_fail: false
output_format: sarif
output_file_path: checkov.sarif
skip_check: CKV_AZURE_35 # example skip
# Catches:
# ✗ FAILED for resource: azurerm_storage_account.docs
# Check: CKV_AZURE_33: "Ensure Storage logging is enabled"
# File: /infra/storage.tf Line: 12
#
# ✗ FAILED for resource: azurerm_sql_server.db
# Check: CKV_AZURE_23: "Ensure TDE is enabled"
# File: /infra/sql.tf Line: 45
# Custom CSPM check — detect public S3 buckets
import boto3
def check_public_buckets():
s3 = boto3.client('s3')
findings = []
for bucket in s3.list_buckets()['Buckets']:
name = bucket['Name']
# Check public access block
try:
config = s3.get_public_access_block(Bucket=name)
block = config['PublicAccessBlockConfiguration']
if not all([
block['BlockPublicAcls'],
block['IgnorePublicAcls'],
block['BlockPublicPolicy'],
block['RestrictPublicBuckets']
]):
findings.append({
"resource": f"s3://{name}",
"severity": "CRITICAL",
"finding": "Public access not fully blocked",
"remediation": "Enable all public access block settings"
})
except Exception as e:
findings.append({
"resource": f"s3://{name}",
"severity": "HIGH",
"finding": f"Cannot verify public access: {e}"
})
return findings
Common CSPM Controls Enabled
Networking Controls:
├── ✅ No security groups allowing 0.0.0.0/0 on SSH (port 22)
├── ✅ No security groups allowing 0.0.0.0/0 on RDP (port 3389)
├── ✅ All storage accounts — public access disabled
├── ✅ All databases — no public endpoints
├── ✅ All load balancers — HTTPS only
└── ✅ VPC flow logs enabled on all VPCs
Encryption Controls:
├── ✅ All storage encrypted at rest
├── ✅ All databases encrypted (TDE enabled)
├── ✅ All EBS volumes encrypted by default
├── ✅ TLS 1.2+ enforced on all endpoints
└── ✅ CMK used for sensitive workloads
Logging Controls:
├── ✅ CloudTrail / Activity Log enabled all regions
├── ✅ S3 access logging enabled
├── ✅ Database audit logging enabled
├── ✅ Kubernetes audit logging enabled
└── ✅ Log retention ≥ 1 year
Identity Controls:
├── ✅ MFA required for all IAM users
├── ✅ Root account has no access keys
├── ✅ Password policy enforced (complexity + rotation)
├── ✅ No wildcard IAM policies (Action: *)
└── ✅ Service accounts have minimal permissions
Risks Reduced by CSPM
Misconfiguration findings closed: 847 → 12 (98.5% reduction)
Critical findings SLA: 30 days → 24 hours
Open S3 buckets found: 23 buckets secured
Exposed databases: 8 databases made private
Compliance score (CIS): 42% → 94%

Control Area 3 — Runtime Protection

Falco — Container Runtime Security
# falco-rules.yaml — custom rules
# Detect shell spawned in container
- rule: Shell Spawned in Container
desc: A shell was spawned in a container
condition: >
spawned_process
and container
and shell_procs
and not proc.pname in (shell_procs)
output: >
Shell spawned in container
(user=%user.name container=%container.name
image=%container.image.repository
shell=%proc.name parent=%proc.pname
cmdline=%proc.cmdline)
priority: WARNING
tags: [container, shell, attack]
# Detect privilege escalation
- rule: Privilege Escalation via Sudo
desc: Sudo was used to escalate privileges
condition: >
spawned_process
and proc.name = sudo
and container
output: >
Privilege escalation in container
(user=%user.name container=%container.name
command=%proc.cmdline)
priority: CRITICAL
# Detect sensitive file access
- rule: Read Sensitive File in Container
desc: Attempt to read sensitive files
condition: >
open_read
and container
and (fd.name startswith /etc/shadow
or fd.name startswith /etc/sudoers
or fd.name = /etc/passwd)
and not proc.name in (known_processes)
output: >
Sensitive file read
(file=%fd.name user=%user.name
container=%container.name
image=%container.image.repository)
priority: HIGH
# Detect crypto mining
- rule: Crypto Mining Process
desc: Crypto mining software detected
condition: >
spawned_process
and (proc.name in (known_miners)
or proc.cmdline contains "--mining"
or proc.cmdline contains "stratum+tcp")
output: >
Crypto mining process detected
(process=%proc.name container=%container.name)
priority: CRITICAL
# Detect outbound connections to unusual ports
- rule: Unexpected Outbound Connection
desc: Container making unexpected outbound connection
condition: >
outbound
and container
and not fd.sport in (80, 443, 8080, 8443, 5432, 6379)
and not proc.name in (allowed_processes)
output: >
Unexpected outbound connection
(container=%container.name
dest=%fd.rip:%fd.rport
process=%proc.name)
priority: WARNING
# Deploy Falco in Kubernetes
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set driver.kind=ebpf \ # use eBPF (no kernel module)
--set falcosidekick.enabled=true \ # forward alerts
--set falcosidekick.config.slack.webhookurl=$SLACK_WEBHOOK \
--set falcosidekick.config.pagerduty.routingKey=$PD_KEY \
--set falco.grpc.enabled=true \
--set falco.grpcOutput.enabled=true
# Check Falco is running
kubectl get pods -n falco
# View real-time alerts
kubectl logs -n falco -l app.kubernetes.io/name=falco -f
Runtime Drift Detection
Immutable Infrastructure Principle:
Container built from image (known good state)
Any change at runtime = drift = suspicious
Controls enabled:
├── Read-only root filesystem enforced
├── Process whitelist — only expected binaries can run
├── File integrity monitoring on key paths
├── Network baseline — alert on unexpected connections
└── User baseline — alert if root process spawns unexpectedly
Example drift detected:
Container: api-service
Expected processes: [python, gunicorn, uvicorn]
Detected: bash → wget → chmod +x → ./malware
Action: Alert + kill container + page on-call
# Enforce read-only filesystem + drop all capabilities
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
securityContext:
readOnlyRootFilesystem: true # ← prevent runtime changes
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL # ← zero Linux capabilities
add:
- NET_BIND_SERVICE # only if needed

Control Area 4 — Cloud Identity & Entitlement Management (CIEM)

Detect Over-Privileged IAM
# Detect IAM roles with excessive permissions
import boto3
import json
def find_overprivileged_roles():
iam = boto3.client('iam')
findings = []
for role in iam.list_roles()['Roles']:
role_name = role['RoleName']
# Get attached policies
policies = iam.list_attached_role_policies(
RoleName=role_name
)['AttachedPolicies']
for policy in policies:
policy_doc = iam.get_policy_version(
PolicyArn=policy['PolicyArn'],
VersionId=iam.get_policy(
PolicyArn=policy['PolicyArn']
)['Policy']['DefaultVersionId']
)['PolicyVersion']['Document']
for statement in policy_doc.get('Statement', []):
# Detect wildcard actions
actions = statement.get('Action', [])
if isinstance(actions, str):
actions = [actions]
if '*' in actions:
findings.append({
"role": role_name,
"policy": policy['PolicyName'],
"issue": "Wildcard action (*)",
"severity": "CRITICAL",
"fix": "Replace * with specific actions"
})
# Detect wildcard resources
resources = statement.get('Resource', [])
if isinstance(resources, str):
resources = [resources]
if '*' in resources and '*' not in actions:
findings.append({
"role": role_name,
"policy": policy['PolicyName'],
"issue": "Wildcard resource (*)",
"severity": "HIGH",
"fix": "Restrict to specific resource ARNs"
})
return findings
Just-in-Time (JIT) Access
Traditional access:
Developer has admin access 24/7
→ Attack window = unlimited
→ Blast radius = entire account
JIT access:
Developer requests elevated access
→ Approved for 1-4 hours only
→ Access auto-revoked
→ Full audit trail
→ Attack window = minimal
Tools:
AWS: IAM Identity Center + permission sets
Azure: Privileged Identity Management (PIM)
GCP: IAM Conditions + Access Context Manager
# Azure PIM — activate role for limited time
az role assignment create \
--role "Contributor" \
--assignee $USER_OBJECT_ID \
--scope /subscriptions/$SUB_ID \
--condition "((!(ActionMatches{'Microsoft.Authorization/roleAssignments/*'}))" \
--description "JIT access for incident response - expires 2h"
# Auto-expire after 2 hours
az role assignment delete \
--role "Contributor" \
--assignee $USER_OBJECT_ID \
--scope /subscriptions/$SUB_ID

Control Area 5 — Admission Control (Policy Enforcement)

OPA Gatekeeper — K8s Policy Engine
# ConstraintTemplate — define the policy
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
---
# Constraint — enforce the policy
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-team-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
namespaces: ["production"]
parameters:
labels:
- "app"
- "team"
- "env"
- "version"
---
# Block privileged containers
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPPrivilegedContainer
metadata:
name: no-privileged-containers
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- falco
---
# Require resource limits
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: require-resource-limits
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
parameters:
limits:
- cpu
- memory
requests:
- cpu
- memory
Image Admission Control
# Only allow images from trusted registries
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "gcr.io/mycompany/"
- "us-central1-docker.pkg.dev/myproject/"
- "registry.redhat.io/"
# Reject: docker.io (public) unless explicitly needed
---
# Block latest tag — require pinned versions
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sNoLatestTag
metadata:
name: no-latest-tag
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]

Control Area 6 — Network Detection & Response

Network Baseline and Anomaly Detection
Baseline phase (14-30 days):
Tool learns normal traffic patterns:
├── Which pods talk to which
├── What external IPs are contacted
├── Normal connection volumes
└── Expected ports and protocols
Detection phase:
Alert on deviations:
├── Pod suddenly contacting external IP
├── Lateral movement (pod-to-pod unexpected)
├── Data exfiltration (large outbound transfer)
├── C2 communication (beacon pattern)
└── Port scanning behavior
# Cilium Network Policy with full observability
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-network-policy
namespace: production
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/.*"
- method: "POST"
path: "/api/.*"
egress:
- toEndpoints:
- matchLabels:
app: database
toPorts:
- ports:
- port: "5432"
# Allow DNS only to cluster DNS
- toEndpoints:
- matchLabels:
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
# Block all other egress

Control Area 7 — Secrets Detection

Pre-commit and CI Scanning
# Install detect-secrets
pip install detect-secrets
# Scan repo for secrets
detect-secrets scan . > .secrets.baseline
# Audit findings
detect-secrets audit .secrets.baseline
# Git pre-commit hook
cat > .pre-commit-config.yaml << EOF
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
- repo: https://github.com/zricethezav/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
EOF
# Install hooks
pre-commit install
# GitLeaks in CI
- name: Scan for secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
# Catches:
# ├── AWS access keys
# ├── Azure SAS tokens
# ├── GCP service account keys
# ├── GitHub personal access tokens
# ├── Stripe / Twilio API keys
# ├── Database connection strings
# ├── Private SSH keys
# └── JWT secrets

Implementing CNAPP — Phased Approach

Phase 1 — Visibility (Week 1-2)
Goal: Understand current risk posture
├── Deploy agentless scanner (Wiz / Orca)
├── Connect all cloud accounts
├── Enable CSPM — inventory all resources
├── Run first vulnerability scan
└── Generate baseline risk report
Deliverable: Risk dashboard showing current state
Phase 2 — Shift Left (Week 3-4)
Goal: Catch issues before they reach production
├── Integrate IaC scanning in CI (Checkov / Bridgecrew)
├── Add image scanning in build pipeline (Trivy)
├── Enable secret scanning (GitLeaks)
├── Configure policy-as-code gates
└── Developer training on findings
Deliverable: Pipeline fails on policy violations
Phase 3 — Posture Hardening (Week 5-8)
Goal: Fix critical misconfigurations
├── Remediate all CRITICAL CSPM findings
├── Enable encryption everywhere
├── Close public-facing resources
├── Implement network segmentation
└── Enable MFA everywhere
Deliverable: Compliance score > 80%
Phase 4 — Runtime Protection (Week 9-12)
Goal: Detect threats in production
├── Deploy Falco / Defender / Prisma agent
├── Enable behavioral detection
├── Configure SIEM integration (Sentinel / Splunk)
├── Set up alert routing (PagerDuty / Slack)
└── Run tabletop incident response exercise
Deliverable: Alert on runtime threats < 5 min MTTD
Phase 5 — Continuous Improvement
Goal: Reduce alert fatigue, improve signal quality
├── Tune rules to reduce false positives
├── Automate remediation for known issues
├── Implement SOAR playbooks
├── Regular pen test validation
└── Quarterly risk review with stakeholders

Metrics — Risks Reduced

Vulnerability Management:
Critical CVEs in prod images: 67 → 3 (-95%)
Mean time to patch critical: 45 days → 4 days
Images with no scan: 100% → 0%
Posture / Misconfigurations:
CSPM critical findings: 234 → 8 (-97%)
Public cloud resources: 31 → 0 (-100%)
Non-compliant resources (CIS): 58% → 6%
Compliance score: 41% → 93%
Runtime Threats:
Mean time to detect (MTTD): Unknown → 4.2 min
Runtime incidents investigated: 0 → 47 (visibility gained)
Confirmed attacks blocked: 12 (crypto miners, shell access)
Lateral movement attempts caught: 8
Identity:
Over-privileged roles remediated: 89 roles scoped down
Wildcard IAM policies: 43 → 0
Service accounts with user keys: 28 → 0 (Workload Identity)
Unused access removed: 156 stale permissions
Shift Left:
Security issues found in CI: 78% of vulns caught before prod
IaC misconfigs blocked in PR: 1,247 findings
Secrets found and rotated: 34 leaked secrets cleaned up
Developers trained: 45 engineers

Interview Talking Points

When asked about CWPP/CNAPP work, structure your answer:
1. CONTEXT
"We were running 40+ microservices on GKE/AKS with no
runtime visibility — we didn't know what was running
inside our containers in production."
2. CONTROLS IMPLEMENTED
"I led the deployment of [Prisma Cloud / Wiz / Falco]
across all clusters. Specifically I enabled:
- Container image scanning in every CI pipeline
- Runtime behavioral detection with custom Falco rules
- CSPM across 3 cloud accounts
- OPA Gatekeeper for admission control
- IaC scanning in PRs blocking on CRITICAL"
3. RISKS REDUCED (with numbers)
"Before: 234 critical misconfigurations, unknown runtime
threats, 45-day patch cycle.
After: 8 critical findings, 4-minute MTTD on runtime
threats, 4-day patch cycle for criticals."
4. CHALLENGES
"Biggest challenges were false positive tuning — initial
Falco deployment generated 2,000 alerts/day. Spent 3
weeks building exclusion rules and whitelists to get
signal-to-noise to a workable level."
5. LESSONS LEARNED
"Agentless first for visibility, agent-based for deep
runtime protection. Start with detect-only mode before
enforce — blocking mode needs careful rollout or you'll
break production workloads."

CWPP/CNAPP work is ultimately about converting unknown risk into measured, managed risk — the most important outcome is not which tool you deployed but what visibility you gained and what specific threats you can now detect and prevent that you couldn’t before.