Set up automated container image security scanning using Trivy with Podman to detect vulnerabilities, misconfigurations, and security issues before deploying containers to production.
Prerequisites
- Root or sudo access
- Internet connectivity for downloading vulnerability databases
- At least 2GB free disk space
- Basic knowledge of containers and JSON
What this solves
Container images often contain vulnerabilities in base operating systems, libraries, and dependencies that attackers can exploit. Trivy security scanner integrates with Podman to automatically detect CVEs, secrets, misconfigurations, and license issues in your container images before deployment. This tutorial shows you how to set up automated scanning workflows that catch security problems early in your development pipeline.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest security updates.
sudo apt update && sudo apt upgrade -y
sudo dnf update -y
Install Podman
Install Podman container engine if not already present on your system.
sudo apt install -y podman
sudo dnf install -y podman
Install Trivy security scanner
Download and install the latest version of Trivy from the official repository.
sudo apt update
sudo apt install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt update
sudo apt install -y trivy
sudo tee /etc/yum.repos.d/trivy.repo << EOF
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
EOF
sudo dnf update -y
sudo dnf install -y trivy
Configure Trivy database location
Set up Trivy database directory with proper permissions for vulnerability database updates.
sudo mkdir -p /var/lib/trivy
sudo chown $USER:$USER /var/lib/trivy
export TRIVY_CACHE_DIR=/var/lib/trivy
Update Trivy vulnerability database
Download the latest vulnerability database which contains CVE information for scanning.
trivy image --download-db-only
Configure Podman with Trivy integration
Create scanning configuration file
Set up Trivy configuration file with security scanning policies and output formats.
cache:
dir: /var/lib/trivy
db:
skip-update: false
download-timeout: 10m
vulnerability:
type:
- os
- library
scanners:
- vuln
- secret
- config
severity:
- UNKNOWN
- LOW
- MEDIUM
- HIGH
- CRITICAL
format: table
output: /var/log/trivy-scan.log
ignore-unfixed: false
timeout: 5m
Create log directory with proper permissions
Set up logging directory where Trivy will store scan results and reports.
sudo mkdir -p /var/log/trivy
sudo chown $USER:$USER /var/log/trivy
sudo chmod 755 /var/log/trivy
Create scanning wrapper script
Build an automation script that integrates Trivy scanning with Podman image operations.
#!/bin/bash
# Podman + Trivy security scanning wrapper
set -e
# Configuration
TRIVY_CONFIG="/etc/trivy/trivy.yaml"
LOG_DIR="/var/log/trivy"
DATE=$(date +%Y%m%d_%H%M%S)
# Function to scan image
scan_image() {
local image="$1"
local output_file="${LOG_DIR}/scan_${DATE}_$(echo "$image" | tr '/:' '_').json"
echo "Scanning image: $image"
echo "Output file: $output_file"
# Run Trivy scan
trivy image \
--config "$TRIVY_CONFIG" \
--format json \
--output "$output_file" \
--severity HIGH,CRITICAL \
"$image"
# Check for critical vulnerabilities
critical_count=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
high_count=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
echo "Critical vulnerabilities: $critical_count"
echo "High vulnerabilities: $high_count"
# Generate summary report
trivy image \
--config "$TRIVY_CONFIG" \
--format table \
--severity HIGH,CRITICAL \
"$image"
# Return exit code based on findings
if [ "$critical_count" -gt 0 ]; then
echo "ERROR: Critical vulnerabilities found in $image"
return 1
elif [ "$high_count" -gt 5 ]; then
echo "WARNING: More than 5 high-severity vulnerabilities found in $image"
return 2
fi
echo "Image $image passed security scan"
return 0
}
# Function to pull and scan
pull_and_scan() {
local image="$1"
echo "Pulling image: $image"
podman pull "$image"
echo "Scanning pulled image..."
scan_image "$image"
}
# Main execution
case "$1" in
scan)
if [ -z "$2" ]; then
echo "Usage: $0 scan
Make scanning script executable
Set proper permissions on the scanning wrapper script.
sudo chmod 755 /usr/local/bin/podman-scan
sudo chown root:root /usr/local/bin/podman-scan
Install jq for JSON parsing
Install jq utility for parsing Trivy JSON output in the scanning script.
sudo apt install -y jq
sudo dnf install -y jq
Implement automated vulnerability scanning workflows
Create systemd timer for database updates
Set up automatic vulnerability database updates to ensure current CVE data.
[Unit]
Description=Update Trivy vulnerability database
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/bin/trivy image --download-db-only
Environment=TRIVY_CACHE_DIR=/var/lib/trivy
[Install]
WantedBy=multi-user.target
Create systemd timer configuration
Schedule daily vulnerability database updates during low-traffic hours.
[Unit]
Description=Update Trivy database daily
Requires=trivy-db-update.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the update timer
Activate the systemd timer for automatic vulnerability database updates.
sudo systemctl daemon-reload
sudo systemctl enable trivy-db-update.timer
sudo systemctl start trivy-db-update.timer
sudo systemctl status trivy-db-update.timer
Create CI/CD integration script
Build a script for integrating security scanning into continuous integration pipelines.
#!/bin/bash
# CI/CD security scanning integration
set -e
# Configuration
FAIL_ON_CRITICAL=${FAIL_ON_CRITICAL:-true}
FAIL_ON_HIGH=${FAIL_ON_HIGH:-false}
MAX_HIGH_VULNS=${MAX_HIGH_VULNS:-10}
SCAN_TIMEOUT=${SCAN_TIMEOUT:-300}
OUTPUT_FORMAT=${OUTPUT_FORMAT:-json}
# Function to scan and evaluate
ci_scan() {
local image="$1"
local build_id="${2:-$(date +%s)}"
local output_file="/tmp/trivy-ci-${build_id}.json"
echo "=== Security Scan Report ==="
echo "Image: $image"
echo "Build ID: $build_id"
echo "Timestamp: $(date)"
echo "=============================="
# Run security scan
timeout "$SCAN_TIMEOUT" trivy image \
--format json \
--output "$output_file" \
--severity HIGH,CRITICAL \
--no-progress \
"$image" || {
echo "ERROR: Scan failed or timed out"
exit 1
}
# Parse results
if [ -f "$output_file" ]; then
critical_vulns=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
high_vulns=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
echo "Critical vulnerabilities: $critical_vulns"
echo "High vulnerabilities: $high_vulns"
# Display summary table
trivy image \
--format table \
--severity HIGH,CRITICAL \
--no-progress \
"$image"
# Apply CI/CD policies
exit_code=0
if [ "$FAIL_ON_CRITICAL" = "true" ] && [ "$critical_vulns" -gt 0 ]; then
echo "❌ FAIL: Critical vulnerabilities found (Policy: FAIL_ON_CRITICAL=true)"
exit_code=1
fi
if [ "$FAIL_ON_HIGH" = "true" ] && [ "$high_vulns" -gt "$MAX_HIGH_VULNS" ]; then
echo "❌ FAIL: Too many high-severity vulnerabilities ($high_vulns > $MAX_HIGH_VULNS)"
exit_code=1
fi
if [ $exit_code -eq 0 ]; then
echo "✅ PASS: Security scan completed successfully"
fi
# Copy results to permanent location if specified
if [ -n "$SCAN_RESULTS_DIR" ]; then
mkdir -p "$SCAN_RESULTS_DIR"
cp "$output_file" "$SCAN_RESULTS_DIR/scan-${build_id}.json"
fi
# Cleanup
rm -f "$output_file"
return $exit_code
else
echo "ERROR: Scan output file not found"
return 1
fi
}
# Main execution
if [ $# -lt 1 ]; then
echo "Usage: $0
Make CI script executable
Set proper permissions on the CI/CD integration script.
sudo chmod 755 /usr/local/bin/ci-security-scan
sudo chown root:root /usr/local/bin/ci-security-scan
Set up security policies and reporting
Create security policy configuration
Define organizational security policies for container image scanning and compliance.
# Container Security Policy Configuration
policy:
# Vulnerability severity thresholds
severity_thresholds:
critical: 0 # Block deployment if any critical vulns
high: 5 # Block if more than 5 high vulns
medium: 20 # Warn if more than 20 medium vulns
low: 50 # Info if more than 50 low vulns
# Scanner types to enable
scanners:
- vuln # CVE vulnerabilities
- secret # Exposed secrets/keys
- config # Configuration issues
# Base image policies
base_images:
approved:
- "docker.io/library/ubuntu:22.04"
- "docker.io/library/alpine:3.18"
- "registry.redhat.io/ubi8/ubi:latest"
blocked:
- "docker.io/library/ubuntu:16.04" # EOL
- "docker.io/library/centos:7" # EOL
# File scanning policies
file_patterns:
secrets:
- "**/.env"
- "**/config/*.key"
- "**/secrets/**"
configs:
- "**/nginx.conf"
- "**/apache2.conf"
- "**/ssh_config"
# Compliance requirements
compliance:
require_signed_images: true
block_unpatched_critical: true
max_image_age_days: 90
require_minimal_base: false
Create vulnerability report generator
Build a comprehensive reporting script for security scan results and trends.
#!/bin/bash
# Security vulnerability reporting tool
set -e
# Configuration
REPORT_DIR="/var/log/trivy/reports"
DATE=$(date +%Y-%m-%d)
REPORT_FILE="${REPORT_DIR}/security-report-${DATE}.html"
JSON_FILE="${REPORT_DIR}/security-report-${DATE}.json"
# Create report directory
mkdir -p "$REPORT_DIR"
# Function to scan multiple images
scan_images() {
local images=("$@")
local results="[]"
for image in "${images[@]}"; do
echo "Scanning $image..."
# Scan image
scan_file="/tmp/scan-$(echo "$image" | tr '/:' '_').json"
trivy image --format json --output "$scan_file" "$image" || {
echo "Failed to scan $image"
continue
}
# Extract summary
if [ -f "$scan_file" ]; then
critical=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL") | .VulnerabilityID' "$scan_file" 2>/dev/null | wc -l)
high=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH") | .VulnerabilityID' "$scan_file" 2>/dev/null | wc -l)
medium=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "MEDIUM") | .VulnerabilityID' "$scan_file" 2>/dev/null | wc -l)
low=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "LOW") | .VulnerabilityID' "$scan_file" 2>/dev/null | wc -l)
# Build JSON result
result=$(jq -n \
--arg image "$image" \
--arg critical "$critical" \
--arg high "$high" \
--arg medium "$medium" \
--arg low "$low" \
--arg timestamp "$(date -Iseconds)" \
'{
image: $image,
timestamp: $timestamp,
vulnerabilities: {
critical: ($critical | tonumber),
high: ($high | tonumber),
medium: ($medium | tonumber),
low: ($low | tonumber)
}
}')
results=$(echo "$results" | jq --argjson new "$result" '. + [$new]')
rm -f "$scan_file"
fi
done
echo "$results"
}
# Function to generate HTML report
generate_html_report() {
local json_data="$1"
cat > "$REPORT_FILE" << EOF
<h1>Container Security Report</h1>
<p>Generated: $(date)</p>
<div class="summary">
<h2>Summary</h2>
EOF
# Calculate totals
total_images=$(echo "$json_data" | jq '. | length')
total_critical=$(echo "$json_data" | jq '[.[].vulnerabilities.critical] | add')
total_high=$(echo "$json_data" | jq '[.[].vulnerabilities.high] | add')
total_medium=$(echo "$json_data" | jq '[.[].vulnerabilities.medium] | add')
total_low=$(echo "$json_data" | jq '[.[].vulnerabilities.low] | add')
cat >> "$REPORT_FILE" << EOF
<p>Total Images Scanned: $total_images</p>
<p>Total Critical Vulnerabilities: $total_critical</p>
<p>Total High Vulnerabilities: $total_high</p>
<p>Total Medium Vulnerabilities: $total_medium</p>
<p>Total Low Vulnerabilities: $total_low</p>
</div>
<h2>Detailed Results</h2>
EOF
# Add table rows
echo "$json_data" | jq -r '.[] | ""' >> "$REPORT_FILE"
cat >> "$REPORT_FILE" << EOF
<table>
<tbody><tr>
<th>Image</th>
<th>Critical</th>
<th>High</th>
<th>Medium</th>
<th>Low</th>
<th>Scan Time</th>
</tr><tr><td>\(.image)</td><td>\(.vulnerabilities.critical)</td><td>\(.vulnerabilities.high)</td><td>\(.vulnerabilities.medium)</td><td>\(.vulnerabilities.low)</td><td>\(.timestamp)</td></tr></tbody></table>
EOF
}
# Main execution
if [ $# -eq 0 ]; then
echo "Usage: $0
Make report generator executable
Set proper permissions on the security report generation script.
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration variables
TRIVY_CONFIG="/etc/trivy/trivy.yaml"
TRIVY_CACHE_DIR="/var/lib/trivy"
SCAN_SCRIPT="/usr/local/bin/podman-scan"
# Cleanup function for error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
rm -f "$SCAN_SCRIPT" 2>/dev/null || true
rm -rf "$TRIVY_CACHE_DIR" 2>/dev/null || true
rm -f /etc/systemd/system/trivy-db-update.* 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
}
trap cleanup ERR
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root or with sudo${NC}"
exit 1
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution. /etc/os-release not found.${NC}"
exit 1
fi
echo -e "${BLUE}Installing Podman with Trivy security scanning on $PRETTY_NAME${NC}"
echo
# Step 1: Update system packages
echo -e "${YELLOW}[1/8] Updating system packages...${NC}"
$PKG_UPDATE
echo -e "${GREEN}✓ System packages updated${NC}"
echo
# Step 2: Install Podman
echo -e "${YELLOW}[2/8] Installing Podman...${NC}"
$PKG_INSTALL podman
echo -e "${GREEN}✓ Podman installed${NC}"
echo
# Step 3: Install prerequisites for Trivy
echo -e "${YELLOW}[3/8] Installing prerequisites...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL wget apt-transport-https gnupg lsb-release jq
else
$PKG_INSTALL wget jq
fi
echo -e "${GREEN}✓ Prerequisites installed${NC}"
echo
# Step 4: Install Trivy security scanner
echo -e "${YELLOW}[4/8] Installing Trivy security scanner...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" > /etc/apt/sources.list.d/trivy.list
apt update
$PKG_INSTALL trivy
else
cat > /etc/yum.repos.d/trivy.repo << 'EOF'
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$basearch/
gpgcheck=0
enabled=1
EOF
$PKG_INSTALL trivy
fi
echo -e "${GREEN}✓ Trivy installed${NC}"
echo
# Step 5: Configure Trivy
echo -e "${YELLOW}[5/8] Configuring Trivy...${NC}"
mkdir -p /etc/trivy "$TRIVY_CACHE_DIR"
cat > "$TRIVY_CONFIG" << 'EOF'
cache:
dir: /var/lib/trivy
format: json
severity:
- UNKNOWN
- LOW
- MEDIUM
- HIGH
- CRITICAL
scan:
skip-dirs:
- /usr/share/man
- /tmp
skip-files:
- "*.md"
- "*.txt"
timeout: 600
vulnerability:
type:
- os
- library
secret:
config: /etc/trivy/secret.yaml
EOF
chown -R root:root /etc/trivy "$TRIVY_CACHE_DIR"
chmod 755 /etc/trivy "$TRIVY_CACHE_DIR"
chmod 644 "$TRIVY_CONFIG"
echo -e "${GREEN}✓ Trivy configured${NC}"
echo
# Step 6: Create scanning wrapper script
echo -e "${YELLOW}[6/8] Creating scanning wrapper script...${NC}"
cat > "$SCAN_SCRIPT" << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
TRIVY_CONFIG="/etc/trivy/trivy.yaml"
SCAN_REPORTS_DIR="/var/log/trivy"
# Create reports directory
mkdir -p "$SCAN_REPORTS_DIR"
scan_image() {
local image="$1"
local timestamp=$(date +%Y%m%d_%H%M%S)
local output_file="$SCAN_REPORTS_DIR/scan_${image//\//_}_${timestamp}.json"
echo "Scanning image: $image"
trivy image \
--config "$TRIVY_CONFIG" \
--format json \
--output "$output_file" \
"$image"
critical_count=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
high_count=$(jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH") | .VulnerabilityID' "$output_file" 2>/dev/null | wc -l)
echo "Critical vulnerabilities: $critical_count"
echo "High vulnerabilities: $high_count"
trivy image \
--config "$TRIVY_CONFIG" \
--format table \
--severity HIGH,CRITICAL \
"$image"
if [ "$critical_count" -gt 0 ]; then
echo "ERROR: Critical vulnerabilities found in $image"
return 1
elif [ "$high_count" -gt 5 ]; then
echo "WARNING: More than 5 high-severity vulnerabilities found in $image"
return 2
fi
echo "Image $image passed security scan"
return 0
}
pull_and_scan() {
local image="$1"
echo "Pulling image: $image"
podman pull "$image"
echo "Scanning pulled image..."
scan_image "$image"
}
case "$1" in
scan)
if [ -z "${2:-}" ]; then
echo "Usage: $0 scan <image>"
exit 1
fi
scan_image "$2"
;;
pull-scan)
if [ -z "${2:-}" ]; then
echo "Usage: $0 pull-scan <image>"
exit 1
fi
pull_and_scan "$2"
;;
*)
echo "Usage: $0 {scan|pull-scan} <image>"
echo " scan - Scan existing local image"
echo " pull-scan - Pull image from registry and scan"
exit 1
;;
esac
EOF
chown root:root "$SCAN_SCRIPT"
chmod 755 "$SCAN_SCRIPT"
echo -e "${GREEN}✓ Scanning script created${NC}"
echo
# Step 7: Set up systemd services for automatic updates
echo -e "${YELLOW}[7/8] Setting up automatic database updates...${NC}"
cat > /etc/systemd/system/trivy-db-update.service << 'EOF'
[Unit]
Description=Update Trivy vulnerability database
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/bin/trivy image --download-db-only
Environment=TRIVY_CACHE_DIR=/var/lib/trivy
[Install]
WantedBy=multi-user.target
EOF
cat > /etc/systemd/system/trivy-db-update.timer << 'EOF'
[Unit]
Description=Update Trivy database daily
Requires=trivy-db-update.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1800
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable trivy-db-update.timer
systemctl start trivy-db-update.timer
echo -e "${GREEN}✓ Automatic updates configured${NC}"
echo
# Step 8: Verification
echo -e "${YELLOW}[8/8] Verifying installation...${NC}"
# Check Podman installation
if ! command -v podman &> /dev/null; then
echo -e "${RED}✗ Podman not found${NC}"
exit 1
fi
echo -e "${GREEN}✓ Podman is installed${NC}"
# Check Trivy installation
if ! command -v trivy &> /dev/null; then
echo -e "${RED}✗ Trivy not found${NC}"
exit 1
fi
echo -e "${GREEN}✓ Trivy is installed${NC}"
# Initial database download
echo "Downloading initial vulnerability database..."
trivy image --download-db-only
echo -e "${GREEN}✓ Vulnerability database downloaded${NC}"
# Test scan script
if [[ -x "$SCAN_SCRIPT" ]]; then
echo -e "${GREEN}✓ Scanning script is executable${NC}"
else
echo -e "${RED}✗ Scanning script not executable${NC}"
exit 1
fi
# Check systemd timer
if systemctl is-active --quiet trivy-db-update.timer; then
echo -e "${GREEN}✓ Automatic updates are active${NC}"
else
echo -e "${RED}✗ Automatic updates not active${NC}"
exit 1
fi
echo
echo -e "${GREEN}Installation completed successfully!${NC}"
echo
echo -e "${BLUE}Usage examples:${NC}"
echo " $SCAN_SCRIPT scan nginx:latest"
echo " $SCAN_SCRIPT pull-scan alpine:latest"
echo
echo -e "${BLUE}Scan reports are saved to: /var/log/trivy/${NC}"
echo -e "${BLUE}Configuration file: $TRIVY_CONFIG${NC}"
Review the script before running. Execute with: bash install.sh