- Published on
- // 28 min read
Whose CVE is it anyway?
- Authors
- Name
- Shane Boulden
- @shaneboulden
Welcome to Whose CVE Is It Anyway?, the security show where everything's containerised and the responsibility boundaries don't matter! Well, actually, they matter quite a bit.
I'm not big on collecting data on this blog, and I have no idea of the average reader. All I have is anecdotal observations - readers reaching out to me and letting me know articles that are helpful, or sharing ideas.
So - if you do remember the improv comedy show Whose Line Is It Anyway?, you'll recall Drew Carey's famous opening: "Welcome to Whose Line Is It Anyway?, the show where everything's made up and the points don't matter." In container security, we face a similar situation - except that the stakes are considerably higher, the points definitely matter, and nothing should be "made up."
If you need a reminder about the show, here's some of my favourite skits:
In modern containerised applications there's often confusion about who owns what vulnerabilities. When a CVE is discovered, is it the platform team's responsibility because it's in the base image? Is it the application team's problem because they chose that base? Is it both? Neither?
This is where separation of duties becomes critical. Just as Drew Carey handed out points (even though they didn't matter), someone needs to clearly own and track vulnerabilities across the application lifecycle. Red Hat Advanced Cluster Security for Kubernetes (RHACS) provides the tooling to enforce these boundaries – ensuring that base image CVEs and application CVEs are tracked, managed, and remediated by the right teams with the right tools.
In this article, I'll walk through a practical demo that shows how RHACS helps answer the question: "Whose CVE is it anyway?" – and more importantly, ensures that every CVE has a clear owner who knows exactly what they're responsible for fixing.
Introducing base image CVE separation of duties
When I say "CVE separation of duties", I mean that there is a clear distinction between which CVEs should be triaged and prioritised by platform teams, and which CVEs should be triaged and prioritised by application developers (platform users).
I am definitely in the camp that "CVEs in base images should be managed by platform teams". If we think of platform engineering as providing self-service tools, workflows and infrastructure for developers, and creating a "golden path" into production, then reducing the cognitive load on developers and managing CVEs in base images provided by the platform should be a core platform team responsibility.
Red Hat Advanced Cluster Security for Kubernetes (RHACS) introduced CVE detection for base image layers in the 4.10 release. This means that platform engineers can specify base images as a RHACS configuration item, and break out CVEs reported between base images and applications packaged using those images.
Let's take a look!
Exploring base image detection
For this demonstration I'm using RHACS v4.11.4 deployed to an OpenShift 4.22.12 cluster. I've used the OpenShift Plus PolicySet to deploy and manage RHACS using RHACM; you can check my article Who Watches the Watchers? if you want to see how to use this policyset.
RHACS Configuration
The first step to base image detection with RHACS is selecting a supported base image. There's a list available here; for this demonstration I'm using the Red Hat Enterprise Linux 9 Universal Base Image (UBI), a freely distributable and supported base image widely used across enterprise organisations.
Let's start by configuring this base image in RHACS. Under the 'Platform Configuration' menu there is a new option 'Base Images', allowing you to specify base images for RHACS.
Select 'Add base image' and specify the UBI version we are using, including the tag, registry.access.redhat.com/ubi9/ubi:9.5
Great! That's all we need to do to configure RHACS. Let's now build an application that uses this base image.
Build a UBI-based python image
I'm going to build a really basic Python application to demonstrate base image separation of duties. Here's the application structure:
├── app.py
├── Containerfile
├── deployment.yaml
├── README.md
└── requirements.txt
File contents are listed below. Note the base image used in the Containerfile:
"""
Simple Flask web application for testing RHACS with UBI Python images.
Demonstrates base image configuration and CVE scanning with standard UBI.
"""
from flask import Flask, jsonify
import os
import platform
import sys
app = Flask(__name__)
@app.route('/')
def home():
"""Home endpoint with system information."""
return jsonify({
'service': 'UBI Python Demo',
'message': 'Red Hat Universal Base Image Python application',
'python_version': sys.version,
'platform': platform.platform(),
'architecture': platform.machine(),
'base_image': 'registry.access.redhat.com/ubi9/python-311'
})
@app.route('/health')
def health():
"""Health check endpoint."""
return jsonify({
'status': 'healthy',
'service': 'ubi-python-demo'
}), 200
@app.route('/readiness')
def readiness():
"""Readiness probe endpoint."""
return jsonify({
'status': 'ready',
'service': 'ubi-python-demo'
}), 200
@app.route('/info')
def info():
"""Detailed application information."""
return jsonify({
'base_image_type': 'Universal Base Image (UBI)',
'image_family': 'ubi9/python-311',
'supported_by': 'Red Hat',
'cve_scanning': 'Fully supported in RHACS',
'os_layer': 'Red Hat Enterprise Linux 9',
'python_version': platform.python_version(),
'environment': {
'PORT': os.getenv('PORT', '8080'),
'PYTHONPATH': os.getenv('PYTHONPATH', 'default'),
'PYTHONUNBUFFERED': os.getenv('PYTHONUNBUFFERED', 'not set')
}
})
if __name__ == '__main__':
port = int(os.getenv('PORT', 8080))
app.run(host='0.0.0.0', port=port)
# Multi-stage build using Red Hat Universal Base Image (UBI)
# Builder stage
FROM registry.access.redhat.com/ubi9/python-311:1-77 AS build
# Copy requirements first for better layer caching
COPY requirements.txt /tmp/requirements.txt
# Install dependencies to /tmp (writable by non-root user)
RUN pip install --no-cache-dir --prefix=/tmp/deps -r /tmp/requirements.txt
# Production stage - minimal UBI micro image
FROM registry.access.redhat.com/ubi9/ubi:9.5
WORKDIR /app
# Install Python runtime from UBI
COPY /usr/bin/python3.11 /usr/bin/python3.11
COPY /usr/lib64/python3.11 /usr/lib64/python3.11
COPY /usr/lib64/libpython3.11.so.1.0 /usr/lib64/
# Copy installed packages from build stage
COPY /tmp/deps /opt/deps
# Copy application code
COPY app.py .
# Create symlink for python command and set permissions for OpenShift
RUN ln -s /usr/bin/python3.11 /usr/bin/python && \
chgrp -R 0 /app && \
chmod -R g=u /app
# Expose application port
EXPOSE 8080
# Set environment
ENV PORT=8080
ENV PYTHONPATH=/opt/deps/lib/python3.11/site-packages:/opt/deps/lib64/python3.11/site-packages
ENV PATH=/opt/deps/bin:$PATH
ENV PYTHONUNBUFFERED=1
# Run the application
CMD ["python", "app.py"]
flask==3.0.0
werkzeug==3.0.1
---
apiVersion: v1
kind: Namespace
metadata:
name: rhacs-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ubi-python-demo
namespace: rhacs-demo
labels:
app: ubi-python-demo
demo: base-image
image-type: ubi
spec:
replicas: 1
selector:
matchLabels:
app: ubi-python-demo
template:
metadata:
labels:
app: ubi-python-demo
demo: base-image
image-type: ubi
annotations:
# RHACS will scan this image and detect UBI base
rhacs.io/description: "UBI Python demo for base image configuration testing"
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: ubi-python-demo
image: quay.io/smileyfritz/ubi-python-demo:0.1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: PORT
value: "8080"
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
---
apiVersion: v1
kind: Service
metadata:
name: ubi-python-demo
namespace: rhacs-demo
labels:
app: ubi-python-demo
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app: ubi-python-demo
Now I can simply build and push the container image:
podman build \
--platform linux/amd64 \
-t "quay.io/smileyfritz/ubi-python-demo:0.1.0"
-f Containerfile .
podman push quay.io/smileyfritz/ubi-python-demo:0.1.0
... and create the deployment:
oc apply -f deployment.yaml
All things going well my application is now deployed and running on OpenShift in the rhacs-demo namespace:
Exploring CVEs in base images
I've configured base images in Red Hat Advanced Cluster Security for Kubernetes (RHACS) and created and deployed an application to my cluster - the only thing left to do now is start exploring CVEs.
Let's start with a look at this container image, quay.io/smileyfritz/ubi-python-demo:0.1.0
I can see there is a CVE here CVE-2024-34069 impacting the werkzeug library. If you recall earlier in the blog, werkzeug was a component pulled into the container image via the developers' requirements.txt - so it should be an application team responsibility to triage / prioritise this CVE.
This is exactly what RHACS shows - the CVE has an Application badge:
What about for the base image? There's another CVE here CVE-2024-12718 impacting the Python runtime inside the container image. Since this is provided in the base image it should be the platform team's responsibility:
This is also what RHACS shows - this CVE has a Base image badge:
I can also see there is a Red Hat Security Advisory (errata) published for this CVE, meaning the platform team can definitely update this base image and make it available back to developers via their internal development platform (IDP).
Reporting on vulnerabilities
One of the common use cases for security teams is reporting on CVEs across applications and platforms. Red Hat Advanced Cluster Security for Kubernetes (RHACS) doesn't currently support native reporting features that support base image detection (yet), but all of the data is available in the API to support reporting.
I've created a Python script here that uses the RHACS RESTful API to extract data on CVEs, and separates CVEs for both the base image and application layers:
#!/usr/bin/env python3
"""
Export CVEs from RHACS with Base Image vs Application Layer classification.
Based on https://github.com/stackrox/contributions/blob/main/util-scripts/export-cves-to-csv/create-csv.sh
Enhanced to classify CVEs using RHACS baseImageInfo API field.
Usage:
export-cves-with-layers.py <image> [output-file]
Example:
export-cves-with-layers.py quay.io/smileyfritz/ubi-python-demo:0.1.0
export-cves-with-layers.py quay.io/smileyfritz/ubi-python-demo:0.1.0 my-cves.csv
"""
import os
import sys
import csv
import json
import argparse
import requests
from urllib3.exceptions import InsecureRequestWarning
from typing import Dict, List, Optional, Tuple
# Suppress SSL warnings for self-signed certs
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
# ANSI color codes
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
def print_error(msg: str):
print(f"{Colors.RED}{msg}{Colors.NC}", file=sys.stderr)
def print_success(msg: str):
print(f"{Colors.GREEN}{msg}{Colors.NC}")
def print_info(msg: str):
print(f"{Colors.BLUE}{msg}{Colors.NC}")
def print_warning(msg: str):
print(f"{Colors.YELLOW}{msg}{Colors.NC}")
def get_image_sha(endpoint: str, password: str, image_name: str) -> Optional[str]:
"""Fetch the image SHA from RHACS by image name."""
url = f"https://{endpoint}/v1/images"
try:
response = requests.get(
url,
auth=('admin', password),
verify=False,
timeout=30
)
response.raise_for_status()
images = response.json().get('images', [])
# Find matching image
for image in images:
if image.get('name') == image_name:
return image.get('id')
return None
except requests.exceptions.RequestException as e:
print_error(f"Failed to fetch image list: {e}")
return None
def get_image_details(endpoint: str, password: str, image_sha: str) -> Optional[Dict]:
"""Fetch full image details including CVEs and base image info."""
url = f"https://{endpoint}/v1/images/{image_sha}"
try:
response = requests.get(
url,
auth=('admin', password),
verify=False,
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print_error(f"Failed to fetch image details: {e}")
return None
def classify_cve(layer_index: int, max_base_layer: int) -> str:
"""Classify a CVE based on its layer index."""
if max_base_layer == -1:
return "Unknown"
elif layer_index <= max_base_layer:
return "Base Image"
else:
return "Application Layer"
def export_cves(image_data: Dict, output_file: str, max_base_layer: int) -> Tuple[int, int, int]:
"""
Export CVEs to CSV file with layer classification.
Returns:
Tuple of (total_cves, base_cves, app_cves)
"""
total_cves = 0
base_cves = 0
app_cves = 0
# Open CSV file for writing
with open(output_file, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
# Write header
writer.writerow([
'CVE',
'Component',
'Version',
'Severity',
'CVSS',
'Layer Classification',
'Layer Index',
'Fixable',
'Fixed In',
'Published',
'Summary'
])
# Process components
components = image_data.get('scan', {}).get('components', [])
for component in components:
comp_name = component.get('name', 'N/A')
comp_version = component.get('version', 'N/A')
layer_index = component.get('layerIndex', -1)
# Process vulnerabilities for this component
vulns = component.get('vulns', [])
for vuln in vulns:
cve = vuln.get('cve', 'N/A')
# Get severity
cvss_v3 = vuln.get('cvssV3', {})
cvss_v2 = vuln.get('cvssV2', {})
severity = cvss_v3.get('severity') or cvss_v2.get('severity', 'UNKNOWN')
# Get CVSS score
cvss_score = vuln.get('cvss', 0)
# Classify CVE
classification = classify_cve(layer_index, max_base_layer)
# Determine if fixable
fixed_by = vuln.get('fixedBy', '')
fixable = 'Yes' if fixed_by else 'No'
fixed_in = fixed_by if fixed_by else 'N/A'
# Get publication date
published = vuln.get('publishedOn', 'N/A')
# Get summary
summary = vuln.get('summary', 'N/A')
# Write row
writer.writerow([
cve,
comp_name,
comp_version,
severity,
cvss_score,
classification,
layer_index,
fixable,
fixed_in,
published,
summary
])
# Update counters
total_cves += 1
if classification == "Base Image":
base_cves += 1
elif classification == "Application Layer":
app_cves += 1
return total_cves, base_cves, app_cves
def main():
parser = argparse.ArgumentParser(
description='Export CVEs from RHACS with Base Image vs Application Layer classification'
)
parser.add_argument('image', help='Image name (e.g., quay.io/org/image:tag)')
parser.add_argument('output', nargs='?', default='cve-export.csv', help='Output CSV file (default: cve-export.csv)')
args = parser.parse_args()
# Get configuration from environment
endpoint = os.getenv('ROX_ENDPOINT')
password = os.getenv('ROX_ADMIN_PASSWORD')
if not endpoint:
print_error("Error: ROX_ENDPOINT environment variable not set")
sys.exit(1)
if not password:
print_error("Error: ROX_ADMIN_PASSWORD environment variable not set")
sys.exit(1)
# Print header
print_success("=" * 50)
print_success("RHACS CVE Export with Layer Classification")
print_success("=" * 50)
print()
print_info(f"Endpoint: {endpoint}")
print_info(f"Image: {args.image}")
print_info(f"Output: {args.output}")
print()
# Step 1: Get image SHA
print_warning("[1/4] Fetching image list from RHACS...")
image_sha = get_image_sha(endpoint, password, args.image)
if not image_sha:
print_error(f"Error: Image '{args.image}' not found in RHACS")
sys.exit(1)
print_success(f"✓ Found image: {image_sha}")
# Step 2: Get image details
print_warning("[2/4] Fetching image details and CVE data...")
image_data = get_image_details(endpoint, password, image_sha)
if not image_data:
print_error("Error: Failed to fetch image details")
sys.exit(1)
# Extract base image information
base_image_info = image_data.get('baseImageInfo', [])
if base_image_info:
max_base_layer = base_image_info[0].get('maxLayerIndex', -1)
base_image_name = base_image_info[0].get('baseImageFullName', 'Not detected')
print_success(f"✓ Base image detected: {base_image_name}")
print_success(f" Base image layers: 0-{max_base_layer}")
else:
max_base_layer = -1
base_image_name = "Not detected"
print_warning("⚠ Warning: No base image detected. All CVEs will be marked as 'Unknown'")
print_warning(" This may indicate base image detection is not configured in RHACS.")
# Step 3: Process and export CVEs
print_warning("[3/4] Processing components and classifying CVEs...")
total_components = len(image_data.get('scan', {}).get('components', []))
print_info(f" Total components: {total_components}")
total_cves, base_cves, app_cves = export_cves(image_data, args.output, max_base_layer)
# Step 4: Display summary
print_warning("[4/4] Export complete!")
print()
print_success("=" * 50)
print_success("Summary")
print_success("=" * 50)
print_info(f"Total CVEs: {total_cves}")
print_info(f"Base Image CVEs: {base_cves}")
print_info(f"Application Layer CVEs: {app_cves}")
unknown_cves = total_cves - base_cves - app_cves
if unknown_cves > 0:
print_warning(f"Unknown Classification: {unknown_cves}")
print()
print_info(f"Base Image: {base_image_name}")
print_info(f"Output File: {args.output}")
print()
# Show sample output
print_success("Sample output (first 5 CVEs):")
with open(args.output, 'r') as f:
for i, line in enumerate(f):
if i > 5:
break
print(line.rstrip())
print()
print_success("✓ CVE export completed successfully!")
print()
print(f"View full report: {Colors.YELLOW}cat {args.output}{Colors.NC}")
print("Or open in spreadsheet application")
if __name__ == '__main__':
main()
When you run this script it will provide a CSV file with a list of CVEs, and show which CVEs are from the base image, and which from the application layer:
$ export ROX_ENDPOINT=export ROX_ENDPOINT=central-stackrox.apps.cluster.example.com:443
$ export ROX_ADMIN_PASSWORD="your-password"
$ python export-cves-with-layers.py quay.io/smileyfritz/ubi-python-demo:0.1.0
==================================================
RHACS CVE Export with Layer Classification
==================================================
Endpoint: central-stackrox.apps.cluster1.sandbox3439.opentlc.com:443
Image: quay.io/smileyfritz/ubi-python-demo:0.1.0
Output: cve-export.csv
[1/4] Fetching image list from RHACS...
✓ Found image: sha256:b1283278939114c249302c389c23f656b74f98546d9e38d337fdd54687a8eb2c
[2/4] Fetching image details and CVE data...
✓ Base image detected: registry.access.redhat.com/ubi9/ubi:9.5
Base image layers: 0-15
[3/4] Processing components and classifying CVEs...
Total components: 192
[4/4] Export complete!
==================================================
Summary
==================================================
Total CVEs: 756
Base Image CVEs: 749
Application Layer CVEs: 7
Base Image: registry.access.redhat.com/ubi9/ubi:9.5
Output File: cve-export.csv
Sample output (first 5 CVEs):
CVE,Component,Version,Severity,CVSS,Layer Classification,Layer Index,Fixable,Fixed In,Published,Summary
CVE-2025-1376,elfutils-libs,0.191-4.el9,LOW,2.5,Base Image,15,No,N/A,2025-02-17T04:31:08.264Z,A flaw was found in GNU elfutils. This vulnerability allows denial of service via manipulation of the function elf_strptr in /libelf/elf_strptr.c.
CVE-2025-1377,elfutils-libs,0.191-4.el9,LOW,3.3,Base Image,15,No,N/A,2025-02-17T05:00:19.288Z,A flaw was found in GNU elfutils. This vulnerability allows denial of service via manipulation of the gelf_getsymshndx function in strip.c.
CVE-2024-25260,elfutils-libs,0.191-4.el9,MEDIUM,4,Base Image,15,No,N/A,2024-02-20T00:00:00Z,"A NULL pointer dereference vulnerability in the elfutils library has been discovered. This vulnerability occurs within the handle_verdef() function in the readelf.c source file. A NULL pointer dereference typically happens when a program attempts to access memory using a pointer that is not pointing anywhere (i.e., it's NULL), leading to a crash or potentially exploitable behavior."
CVE-2025-1371,elfutils-libs,0.191-4.el9,LOW,3.3,Base Image,15,No,N/A,2025-02-17T02:31:07.921Z,A flaw was found in GNU elfutils. This vulnerability allows a NULL pointer dereference via the handle_dynamic_symtab function in readelf.c.
CVE-2026-54370,acl,2.3.1-4.el9,MEDIUM,6.3,Base Image,15,Yes,0:2.4.0-1.el9_8,2026-06-29T13:00:00Z,"A time-of-check to time-of-use (TOCTOU) race condition vulnerability was found in `acl`. By replacing a pathname component with a symbolic link between a security check and subsequent file operations, an attacker can redirect file access control list operations. This occurs when privileged processes invoke `getfacl` or `setfacl` over an attacker-controlled path, potentially leading to local privilege escalation."
✓ CVE export completed successfully!
View full report: cat cve-export.csv
Or open in spreadsheet application
Where the points do matter
Unlike Drew Carey's show where "the points don't matter," in container security, the points – or rather, the CVEs – matter tremendously. More importantly, knowing whose CVE it is matters even more.
Throughout this article we've explored how Red Hat Advanced Cluster Security for Kubernetes (RHACS) provides clear separation of duties between base image vulnerabilities (platform team responsibility) and application layer vulnerabilities (developer team responsibility). This distinction is critical for several reasons:
- Reduced cognitive load: Developers can focus on securing their application code and dependencies, not the entire OS stack
- Clear accountability: Platform teams own base image updates and can provide hardened, compliant base images through their internal developer platform (IDP)
- Better reporting: Security teams can track remediation efforts separately for platform vs. application vulnerabilities
- Faster remediation: Teams aren't blocked waiting for other teams to address vulnerabilities outside their domain
The base image detection capability demonstrated here – classifying CVEs, exporting reports with layer attribution, and providing clear visual indicators in the RHACS UI – represents a significant step forward in managing the complexity of cloud-native vulnerability management.
It's worth noting that base image detection in RHACS is still actively evolving. While the foundation we've explored today provides tremendous value, the RHACS team continues to enhance these capabilities. For example, work is underway to support more advanced policy capabilities using base image attributes (ROX-33966), which will enable even more sophisticated separation of duties scenarios – such as enforcing different vulnerability thresholds for base image CVEs vs. application CVEs, or creating policies that trigger different workflows based on CVE layer attribution.
As these capabilities mature, the ability to answer "Whose CVE is it anyway?" will only get stronger, providing platform teams, developers, and security teams with the tools they need to maintain security boundaries while moving fast in a cloud-native world.
Thanks for reading!