mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
1094 lines
41 KiB
Bash
Executable File
1094 lines
41 KiB
Bash
Executable File
#!/bin/bash
|
|
# OIDC Workload Identity E2E Test
|
|
#
|
|
# This script tests OIDC workload identity federation with Kubernetes ServiceAccount tokens.
|
|
# It uses the native Kubernetes ServiceAccount issuer (not an external OIDC provider like Dex).
|
|
#
|
|
# The test:
|
|
# 1. Creates a Kind cluster with the API server OIDC discovery endpoint exposed
|
|
# 2. Exports the Kind cluster's CA certificate
|
|
# 3. Deploys Zot with OIDC bearer authentication
|
|
# 4. Creates a test Pod with a projected ServiceAccount token
|
|
# 5. Verifies authentication succeeds with the token
|
|
# 6. Verifies authentication fails without the token
|
|
#
|
|
# Usage:
|
|
# ./kind-oidc-workload-identity.sh [OPTIONS]
|
|
#
|
|
# Options:
|
|
# --skip-setup Skip cluster creation, image building, and initial setup
|
|
# (assumes resources already exist from a previous run)
|
|
# --only-crane Only run crane e2e tests
|
|
# --only-curl Only run curl-based tests (includes conditional access)
|
|
# --keep-resources Don't clean up resources on exit (useful for debugging)
|
|
# --help Show this help message
|
|
|
|
set -o errexit
|
|
set -o pipefail
|
|
|
|
# Parse command line arguments
|
|
SKIP_SETUP=false
|
|
ONLY_CRANE=false
|
|
ONLY_CURL=false
|
|
KEEP_RESOURCES=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--skip-setup)
|
|
SKIP_SETUP=true
|
|
shift
|
|
;;
|
|
--only-crane)
|
|
ONLY_CRANE=true
|
|
shift
|
|
;;
|
|
--only-curl)
|
|
ONLY_CURL=true
|
|
shift
|
|
;;
|
|
--keep-resources)
|
|
KEEP_RESOURCES=true
|
|
shift
|
|
;;
|
|
--help)
|
|
sed -n '2,/^$/p' "$0" | grep -E "^#" | sed 's/^# *//'
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
echo "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Check prerequisites
|
|
check_prerequisites() {
|
|
local missing=""
|
|
for cmd in docker kubectl jq openssl curl git; do
|
|
if ! command -v "$cmd" &> /dev/null; then
|
|
missing="$missing $cmd"
|
|
fi
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
echo "Error: Missing required tools:$missing"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_prerequisites
|
|
|
|
ROOT_DIR=$(git rev-parse --show-toplevel)
|
|
cd "${ROOT_DIR}"
|
|
|
|
# Use project's kind if available, otherwise fall back to system kind
|
|
if [ -x "${ROOT_DIR}/hack/tools/bin/kind" ]; then
|
|
KIND="${ROOT_DIR}/hack/tools/bin/kind"
|
|
elif command -v kind &> /dev/null; then
|
|
KIND="kind"
|
|
else
|
|
echo "Error: kind not found. Install kind or run 'make ${ROOT_DIR}/hack/tools/bin/kind'"
|
|
exit 1
|
|
fi
|
|
|
|
CLUSTER_NAME="kind-oidc-wid"
|
|
ZOT_REG_NAME="zot-oidc-wid"
|
|
ZOT_PORT="5000"
|
|
TEST_NAMESPACE="oidc-test"
|
|
TEST_SA_NAME="test-workload"
|
|
AUDIENCE="zot-registry"
|
|
|
|
# Pin image versions for reproducibility and to avoid Docker Hub rate limiting issues
|
|
# These versions should be updated periodically
|
|
# Note: BUSYBOX_IMAGE uses gcr.io to avoid Docker Hub rate limits for crane operations
|
|
CURL_IMAGE="curlimages/curl:8.5.0"
|
|
ALPINE_IMAGE="alpine:3.19"
|
|
BUSYBOX_IMAGE="gcr.io/google-containers/busybox:1.27"
|
|
KIND_NODE_IMAGE="kindest/node:v1.28.7"
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
# Helper function to ensure a pod exists and is ready
|
|
# Usage: ensure_pod_ready <pod-name> <namespace> <pod-yaml>
|
|
# Returns 0 if pod is ready, 1 if it couldn't be created/started
|
|
ensure_pod_ready() {
|
|
local pod_name="$1"
|
|
local namespace="$2"
|
|
local timeout="${3:-120}"
|
|
|
|
if kubectl get pod "$pod_name" -n "$namespace" &>/dev/null; then
|
|
local pod_status
|
|
pod_status=$(kubectl get pod "$pod_name" -n "$namespace" -o jsonpath='{.status.phase}')
|
|
if [ "$pod_status" = "Running" ]; then
|
|
log_info "Pod '$pod_name' already exists and is running (reusing)"
|
|
return 0
|
|
else
|
|
log_info "Pod '$pod_name' exists but status is '$pod_status', waiting..."
|
|
fi
|
|
else
|
|
return 1 # Pod doesn't exist, caller should create it
|
|
fi
|
|
|
|
# Wait for pod to be ready
|
|
kubectl wait --for=condition=Ready "pod/$pod_name" -n "$namespace" --timeout="${timeout}s"
|
|
}
|
|
|
|
cleanup() {
|
|
if [ "$KEEP_RESOURCES" = true ]; then
|
|
log_info "Keeping resources (--keep-resources specified)"
|
|
log_info "To clean up manually, run:"
|
|
log_info " ${KIND} delete cluster --name ${CLUSTER_NAME}"
|
|
log_info " docker rm -f ${ZOT_REG_NAME}"
|
|
return
|
|
fi
|
|
log_info "Cleaning up..."
|
|
"${KIND}" delete cluster --name "${CLUSTER_NAME}" 2>/dev/null || true
|
|
docker rm -f "${ZOT_REG_NAME}" 2>/dev/null || true
|
|
rm -f /tmp/kind-ca.pem /tmp/zot-oidc-config.json /tmp/test-token.txt 2>/dev/null || true
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
# Set no_proxy if applicable
|
|
if [ -n "${no_proxy}" ]; then
|
|
log_info "Updating no_proxy env var"
|
|
export no_proxy="${no_proxy},${ZOT_REG_NAME}"
|
|
export NO_PROXY="${no_proxy}"
|
|
fi
|
|
|
|
# Pre-pull images to avoid Docker Hub rate limiting issues in CI
|
|
# This is done early so failures are caught before cluster creation
|
|
prepull_images() {
|
|
log_info "Pre-pulling container images (helps avoid Docker Hub rate limiting)..."
|
|
local images=("${CURL_IMAGE}" "${ALPINE_IMAGE}" "${BUSYBOX_IMAGE}" "${KIND_NODE_IMAGE}")
|
|
for img in "${images[@]}"; do
|
|
log_info "Pulling ${img}..."
|
|
if ! docker pull "${img}" 2>/dev/null; then
|
|
log_warn "Failed to pull ${img} - will retry during test (may be rate limited)"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Skip setup if requested
|
|
if [ "$SKIP_SETUP" = true ]; then
|
|
log_info "Skipping setup (--skip-setup specified)"
|
|
log_info "Using existing cluster '${CLUSTER_NAME}' and zot container '${ZOT_REG_NAME}'"
|
|
|
|
# Verify resources exist
|
|
if ! "${KIND}" get clusters 2>/dev/null | grep -q "${CLUSTER_NAME}"; then
|
|
log_error "Cluster '${CLUSTER_NAME}' does not exist. Run without --skip-setup first."
|
|
exit 1
|
|
fi
|
|
if ! docker ps --format '{{.Names}}' | grep -q "^${ZOT_REG_NAME}$"; then
|
|
log_error "Zot container '${ZOT_REG_NAME}' is not running. Run without --skip-setup first."
|
|
exit 1
|
|
fi
|
|
|
|
# Set kubectl context
|
|
kubectl config use-context "kind-${CLUSTER_NAME}"
|
|
|
|
# Get the OIDC issuer URL
|
|
CONTROL_PLANE_CONTAINER="${CLUSTER_NAME}-control-plane"
|
|
OIDC_ISSUER="https://${CONTROL_PLANE_CONTAINER}:6443"
|
|
else
|
|
# Delete existing cluster if it exists
|
|
log_info "Cleaning up any existing resources..."
|
|
"${KIND}" delete cluster --name "${CLUSTER_NAME}" 2>/dev/null || true
|
|
docker rm -f "${ZOT_REG_NAME}" 2>/dev/null || true
|
|
|
|
# Pre-pull images to avoid rate limiting
|
|
prepull_images
|
|
|
|
# Create Kind cluster with custom configuration
|
|
# - Configure the ServiceAccount issuer to be accessible from zot (via docker network)
|
|
# - Add the container name as a SAN to the API server certificate
|
|
log_info "Creating Kind cluster '${CLUSTER_NAME}'..."
|
|
cat <<EOF | "${KIND}" create cluster --name "${CLUSTER_NAME}" --config=-
|
|
kind: Cluster
|
|
apiVersion: kind.x-k8s.io/v1alpha4
|
|
nodes:
|
|
- role: control-plane
|
|
image: ${KIND_NODE_IMAGE}
|
|
kubeadmConfigPatches:
|
|
- |
|
|
kind: ClusterConfiguration
|
|
apiServer:
|
|
certSANs:
|
|
- "localhost"
|
|
- "127.0.0.1"
|
|
- "${CLUSTER_NAME}-control-plane"
|
|
extraArgs:
|
|
# Configure the ServiceAccount issuer to use the container name
|
|
# This URL must be reachable from zot (via docker network)
|
|
service-account-issuer: "https://${CLUSTER_NAME}-control-plane:6443"
|
|
# Enable anonymous auth (required for OIDC discovery)
|
|
anonymous-auth: "true"
|
|
# Configure API audiences
|
|
api-audiences: "api,${AUDIENCE}"
|
|
EOF
|
|
|
|
# Wait for the cluster to be ready
|
|
log_info "Waiting for cluster to be ready..."
|
|
kubectl wait --for=condition=Ready nodes --all --timeout=120s
|
|
|
|
# Load pre-pulled images into Kind cluster to avoid Docker Hub rate limiting
|
|
# This makes the images available to pods inside the cluster without pulling from Docker Hub
|
|
log_info "Loading images into Kind cluster..."
|
|
for img in "${CURL_IMAGE}" "${ALPINE_IMAGE}" "${BUSYBOX_IMAGE}"; do
|
|
log_info "Loading ${img}..."
|
|
"${KIND}" load docker-image "${img}" --name "${CLUSTER_NAME}" || log_warn "Failed to load ${img}"
|
|
done
|
|
|
|
# Get the control plane container name and IP
|
|
CONTROL_PLANE_CONTAINER="${CLUSTER_NAME}-control-plane"
|
|
CONTROL_PLANE_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${CONTROL_PLANE_CONTAINER}")
|
|
log_info "Control plane container: ${CONTROL_PLANE_CONTAINER}"
|
|
log_info "Control plane IP: ${CONTROL_PLANE_IP}"
|
|
|
|
# Create ClusterRoleBinding to allow unauthenticated access to OIDC discovery
|
|
# This is required for external services (like zot) to verify ServiceAccount tokens
|
|
log_info "Creating ClusterRoleBinding for OIDC discovery..."
|
|
kubectl create clusterrolebinding oidc-reviewer \
|
|
--clusterrole=system:service-account-issuer-discovery \
|
|
--group=system:unauthenticated || true
|
|
|
|
# Export the Kind cluster's CA certificate
|
|
log_info "Exporting Kind cluster CA certificate..."
|
|
docker cp "${CONTROL_PLANE_CONTAINER}:/etc/kubernetes/pki/ca.crt" /tmp/kind-ca.pem
|
|
|
|
# Verify the CA certificate
|
|
log_info "Verifying CA certificate..."
|
|
openssl x509 -in /tmp/kind-ca.pem -text -noout | head -20
|
|
|
|
# Get the OIDC issuer URL (this is the Kubernetes API server)
|
|
OIDC_ISSUER="https://${CONTROL_PLANE_CONTAINER}:6443"
|
|
log_info "OIDC Issuer: ${OIDC_ISSUER}"
|
|
|
|
# Test OIDC discovery endpoint is accessible (via docker exec since we're not on kind network)
|
|
log_info "Testing OIDC discovery endpoint..."
|
|
docker exec "${CONTROL_PLANE_CONTAINER}" curl -sk "https://localhost:6443/.well-known/openid-configuration" | jq . || log_warn "OIDC discovery endpoint test inconclusive"
|
|
|
|
# Build zot docker image
|
|
# Using minimal Dockerfile since bearer OIDC is part of the core (no build tags)
|
|
log_info "Building zot docker image (minimal)... this may take a few minutes"
|
|
COMMIT_HASH=$(git describe --always --tags --long)
|
|
IMAGE_NAME="zot-linux-amd64-minimal:${COMMIT_HASH}"
|
|
|
|
# Build using docker buildx with load to make it available locally
|
|
# Use --quiet to suppress verbose build output
|
|
docker buildx build \
|
|
--platform linux/amd64 \
|
|
--build-arg BASE_IMAGE=gcr.io/distroless/base-debian12:latest-amd64 \
|
|
--build-arg COMMIT="${COMMIT_HASH}" \
|
|
-t "${IMAGE_NAME}" \
|
|
--load \
|
|
--quiet \
|
|
-f build/Dockerfile-minimal .
|
|
|
|
log_info "Image built: ${IMAGE_NAME}"
|
|
|
|
# Create zot configuration for OIDC bearer authentication
|
|
# The username mapping uses the default (iss + '/' + sub) which results in:
|
|
# "https://<control-plane>:6443/system:serviceaccount:<namespace>:<sa-name>"
|
|
cat <<EOF > /tmp/zot-oidc-config.json
|
|
{
|
|
"distSpecVersion": "1.1.1",
|
|
"storage": {
|
|
"rootDirectory": "/var/lib/zot"
|
|
},
|
|
"http": {
|
|
"address": "0.0.0.0",
|
|
"port": "${ZOT_PORT}",
|
|
"auth": {
|
|
"bearer": {
|
|
"realm": "zot",
|
|
"service": "zot-registry",
|
|
"oidc": [
|
|
{
|
|
"issuer": "${OIDC_ISSUER}",
|
|
"audiences": ["${AUDIENCE}"],
|
|
"certificateAuthorityFile": "/etc/zot/kind-ca.pem"
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"accessControl": {
|
|
"repositories": {
|
|
"**": {
|
|
"policies": [
|
|
{
|
|
"users": ["${OIDC_ISSUER}/system:serviceaccount:${TEST_NAMESPACE}:${TEST_SA_NAME}"],
|
|
"actions": ["read", "create", "update", "delete"]
|
|
}
|
|
],
|
|
"defaultPolicy": []
|
|
},
|
|
"cond-*/**": {
|
|
"policies": [
|
|
{
|
|
"users": ["${OIDC_ISSUER}/system:serviceaccount:${TEST_NAMESPACE}:other-sa"],
|
|
"actions": ["read", "create", "update", "delete"],
|
|
"conditions": [
|
|
{
|
|
"expression": "req.repository.startsWith(\"cond-allowed/\")",
|
|
"message": "other-sa may only push to cond-allowed/*"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"defaultPolicy": []
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"log": {
|
|
"level": "debug"
|
|
}
|
|
}
|
|
EOF
|
|
|
|
log_info "Zot configuration:"
|
|
cat /tmp/zot-oidc-config.json
|
|
|
|
# Run zot container connected to the kind network
|
|
log_info "Starting zot container..."
|
|
docker run -d \
|
|
--name "${ZOT_REG_NAME}" \
|
|
--network kind \
|
|
-p "127.0.0.1:${ZOT_PORT}:${ZOT_PORT}" \
|
|
-v /tmp/zot-oidc-config.json:/etc/zot/config.json:ro \
|
|
-v /tmp/kind-ca.pem:/etc/zot/kind-ca.pem:ro \
|
|
"${IMAGE_NAME}" \
|
|
serve /etc/zot/config.json
|
|
|
|
# Wait for zot to be ready
|
|
log_info "Waiting for zot to be ready..."
|
|
sleep 5
|
|
|
|
# Check zot logs
|
|
log_info "Zot container logs:"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
|
|
# Get zot container IP on the kind network
|
|
ZOT_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${ZOT_REG_NAME}")
|
|
log_info "Zot container IP: ${ZOT_IP}"
|
|
|
|
# Verify zot is running and responding
|
|
log_info "Checking zot health..."
|
|
for i in {1..30}; do
|
|
# zot should return 401 for unauthenticated requests when bearer auth is configured
|
|
HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${ZOT_PORT}/v2/" 2>/dev/null || echo "000")
|
|
if [ "$HTTP_RESPONSE" = "401" ] || [ "$HTTP_RESPONSE" = "200" ]; then
|
|
log_info "Zot is responding (HTTP $HTTP_RESPONSE)"
|
|
break
|
|
fi
|
|
if [ $i -eq 30 ]; then
|
|
log_error "Zot failed to start (HTTP $HTTP_RESPONSE)"
|
|
docker logs "${ZOT_REG_NAME}"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
fi # End of setup section (skip-setup conditional)
|
|
|
|
# Create test namespace and ServiceAccount
|
|
log_info "Creating test namespace and ServiceAccount..."
|
|
kubectl create namespace "${TEST_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
|
|
kubectl create serviceaccount "${TEST_SA_NAME}" -n "${TEST_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Create a test Pod with projected ServiceAccount token (or reuse existing)
|
|
# Using a lightweight image with wget/curl for testing
|
|
if ! ensure_pod_ready "oidc-test-pod" "${TEST_NAMESPACE}" 120; then
|
|
log_info "Creating test Pod with projected ServiceAccount token..."
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: oidc-test-pod
|
|
namespace: ${TEST_NAMESPACE}
|
|
spec:
|
|
serviceAccountName: ${TEST_SA_NAME}
|
|
containers:
|
|
- name: test
|
|
image: ${CURL_IMAGE}
|
|
command: ["sleep", "infinity"]
|
|
volumeMounts:
|
|
- name: token
|
|
mountPath: /var/run/secrets/tokens
|
|
readOnly: true
|
|
env:
|
|
- name: ZOT_REGISTRY
|
|
value: "${ZOT_REG_NAME}:${ZOT_PORT}"
|
|
volumes:
|
|
- name: token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
path: zot-token
|
|
expirationSeconds: 3600
|
|
audience: ${AUDIENCE}
|
|
restartPolicy: Never
|
|
EOF
|
|
log_info "Waiting for test pod to be ready..."
|
|
kubectl wait --for=condition=Ready pod/oidc-test-pod -n "${TEST_NAMESPACE}" --timeout=120s
|
|
fi
|
|
|
|
# Verify the projected token exists and has correct audience
|
|
log_info "Verifying projected ServiceAccount token..."
|
|
kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod -- cat /var/run/secrets/tokens/zot-token > /tmp/test-token.txt
|
|
log_info "Token claims (decoded):"
|
|
# Decode the JWT payload (second part, base64url encoded)
|
|
PAYLOAD=$(cat /tmp/test-token.txt | cut -d'.' -f2)
|
|
# Add padding if needed and decode
|
|
PAYLOAD_PADDED="${PAYLOAD}$(printf '%*s' $((4 - ${#PAYLOAD} % 4)) | tr ' ' '=')"
|
|
echo "${PAYLOAD_PADDED}" | base64 -d 2>/dev/null | jq . || log_warn "Could not decode token (may need jq)"
|
|
|
|
# =============================================================================
|
|
# CURL-BASED TESTS (Tests 1-7)
|
|
# =============================================================================
|
|
if [ "$ONLY_CRANE" = true ]; then
|
|
log_info "Skipping curl-based tests (--only-oras specified)"
|
|
else
|
|
|
|
# Test 1: Verify PUSH fails without token
|
|
log_info "TEST 1: Verifying push (blob upload) fails without token..."
|
|
HTTP_CODE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod -- \
|
|
curl -s -o /dev/null -w "%{http_code}" -X POST "http://${ZOT_REG_NAME}:${ZOT_PORT}/v2/test-repo/blobs/uploads/" 2>/dev/null || echo "000")
|
|
|
|
if [ "$HTTP_CODE" = "401" ]; then
|
|
log_info "TEST 1 PASSED: Push correctly rejected without token (HTTP $HTTP_CODE)"
|
|
else
|
|
log_error "TEST 1 FAILED: Expected 401, got HTTP $HTTP_CODE"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# Test 2: Verify authentication SUCCEEDS with token
|
|
log_info "TEST 2: Verifying authentication succeeds with token..."
|
|
RESPONSE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -w "\n%{http_code}" -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/_catalog"')
|
|
|
|
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
BODY=$(echo "$RESPONSE" | head -n -1)
|
|
|
|
if [ "$HTTP_CODE" = "200" ]; then
|
|
log_info "TEST 2 PASSED: Authentication succeeded with token (HTTP $HTTP_CODE)"
|
|
log_info "Response body: $BODY"
|
|
else
|
|
log_error "TEST 2 FAILED: Authentication failed with valid token (HTTP $HTTP_CODE)"
|
|
log_error "Response: $BODY"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -50
|
|
exit 1
|
|
fi
|
|
|
|
# Test 3: Initiate a blob upload (tests write permissions)
|
|
log_info "TEST 3: Testing write permissions (initiate blob upload)..."
|
|
RESPONSE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -w "\n%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/test-repo/blobs/uploads/"')
|
|
|
|
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
|
|
if [ "$HTTP_CODE" = "202" ]; then
|
|
log_info "TEST 3 PASSED: Write operation succeeded (HTTP $HTTP_CODE - upload initiated)"
|
|
else
|
|
log_error "TEST 3 FAILED: Write operation failed (HTTP $HTTP_CODE)"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# Test 4: List the catalog to verify repository was created
|
|
log_info "TEST 4: Listing catalog to verify repository exists..."
|
|
CATALOG=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/_catalog"')
|
|
log_info "Catalog: ${CATALOG}"
|
|
|
|
if echo "$CATALOG" | grep -q "test-repo"; then
|
|
log_info "TEST 4 PASSED: Repository 'test-repo' found in catalog"
|
|
else
|
|
log_warn "TEST 4: Repository may not appear immediately in catalog (this is expected)"
|
|
fi
|
|
|
|
# Test 5: Verify wrong audience token fails
|
|
log_info "TEST 5: Verifying wrong audience token fails..."
|
|
|
|
# Create another pod with a different audience (or reuse existing)
|
|
if ! ensure_pod_ready "oidc-test-pod-wrong-aud" "${TEST_NAMESPACE}" 60; then
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: oidc-test-pod-wrong-aud
|
|
namespace: ${TEST_NAMESPACE}
|
|
spec:
|
|
serviceAccountName: ${TEST_SA_NAME}
|
|
containers:
|
|
- name: test
|
|
image: ${CURL_IMAGE}
|
|
command: ["sleep", "infinity"]
|
|
volumeMounts:
|
|
- name: token
|
|
mountPath: /var/run/secrets/tokens
|
|
readOnly: true
|
|
env:
|
|
- name: ZOT_REGISTRY
|
|
value: "${ZOT_REG_NAME}:${ZOT_PORT}"
|
|
volumes:
|
|
- name: token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
path: zot-token
|
|
expirationSeconds: 3600
|
|
audience: wrong-audience
|
|
restartPolicy: Never
|
|
EOF
|
|
log_info "Waiting for wrong-audience test pod to be ready..."
|
|
kubectl wait --for=condition=Ready pod/oidc-test-pod-wrong-aud -n "${TEST_NAMESPACE}" --timeout=60s
|
|
fi
|
|
|
|
HTTP_CODE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod-wrong-aud -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/_catalog"' 2>/dev/null || echo "000")
|
|
|
|
if [ "$HTTP_CODE" = "401" ]; then
|
|
log_info "TEST 5 PASSED: Wrong audience token correctly rejected (HTTP $HTTP_CODE)"
|
|
else
|
|
log_error "TEST 5 FAILED: Expected 401 for wrong audience, got HTTP $HTTP_CODE"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# Test 6: Verify different ServiceAccount with correct audience is authenticated
|
|
# Note: Currently, OIDC bearer auth only performs authentication, not repository-level authorization.
|
|
# The BaseAuthzHandler in zot bypasses authorization checks for bearer auth.
|
|
# This test verifies that a different SA with the correct audience CAN authenticate (proving OIDC works)
|
|
# but has NO permissions because it's not in the accessControl config.
|
|
# The username is derived from the token and used for authorization checks.
|
|
log_info "TEST 6: Verifying different ServiceAccount authenticates but has NO permissions..."
|
|
|
|
# Create a different ServiceAccount
|
|
kubectl create serviceaccount other-sa -n "${TEST_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
# Create pod with the other ServiceAccount (or reuse existing)
|
|
if ! ensure_pod_ready "oidc-test-pod-other-sa" "${TEST_NAMESPACE}" 60; then
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: oidc-test-pod-other-sa
|
|
namespace: ${TEST_NAMESPACE}
|
|
spec:
|
|
serviceAccountName: other-sa
|
|
containers:
|
|
- name: test
|
|
image: ${CURL_IMAGE}
|
|
command: ["sleep", "infinity"]
|
|
volumeMounts:
|
|
- name: token
|
|
mountPath: /var/run/secrets/tokens
|
|
readOnly: true
|
|
env:
|
|
- name: ZOT_REGISTRY
|
|
value: "${ZOT_REG_NAME}:${ZOT_PORT}"
|
|
volumes:
|
|
- name: token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
path: zot-token
|
|
expirationSeconds: 3600
|
|
audience: ${AUDIENCE}
|
|
restartPolicy: Never
|
|
EOF
|
|
log_info "Waiting for other-sa test pod to be ready..."
|
|
kubectl wait --for=condition=Ready pod/oidc-test-pod-other-sa -n "${TEST_NAMESPACE}" --timeout=60s
|
|
fi
|
|
|
|
# Verify that other-sa authenticates but cannot see test-repo (which lives
|
|
# under the `**` pattern where other-sa has no policy). The catalog may
|
|
# still contain repos under `cond-*/**`, since the conditional policy on
|
|
# that pattern is *optimistically* included in glob-time filtering — the
|
|
# real condition enforcement happens at per-request authz time. Asserting
|
|
# "test-repo is absent" expresses the intent without depending on whether
|
|
# cond-allowed/test exists from a previous test run (re-runs with
|
|
# --skip-setup keep storage around).
|
|
CATALOG_RESPONSE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod-other-sa -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/_catalog"' 2>/dev/null || echo "{}")
|
|
|
|
if ! echo "$CATALOG_RESPONSE" | jq -e '.repositories | index("test-repo")' >/dev/null 2>&1; then
|
|
log_info "TEST 6 PASSED: Other ServiceAccount authenticated but cannot see test-repo"
|
|
log_info " The username '${OIDC_ISSUER}/system:serviceaccount:${TEST_NAMESPACE}:other-sa' was extracted from the token."
|
|
log_info " Authorization is enforced via accessControl config."
|
|
log_info " Catalog: $CATALOG_RESPONSE"
|
|
else
|
|
log_error "TEST 6 FAILED: Expected test-repo to be absent from other-sa's catalog"
|
|
log_error "Got: $CATALOG_RESPONSE"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 7: Verify other-sa gets 403 when trying to write (authorization enforced)
|
|
# This is a NON-conditional deny: no policy on the matched pattern grants
|
|
# other-sa, so the response body must NOT carry a `reason` field — that
|
|
# field is reserved for condition-driven denies (see Test 9 for the contrast).
|
|
# =============================================================================
|
|
log_info "TEST 7: Verifying other-sa gets 403 Forbidden when trying to write..."
|
|
|
|
# `-w "\n%{http_code}"` appends the status code on its own line after the body,
|
|
# so we can split with shell builtins.
|
|
RESPONSE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod-other-sa -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -w "\n%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/unauthorized-repo/blobs/uploads/"' 2>/dev/null || echo $'\n000')
|
|
TEST7_HTTP=$(echo "$RESPONSE" | tail -n1)
|
|
TEST7_BODY=$(echo "$RESPONSE" | sed '$d')
|
|
|
|
if [ "$TEST7_HTTP" != "403" ]; then
|
|
log_error "TEST 7 FAILED: Expected 403 for write operation, got HTTP $TEST7_HTTP"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# A non-conditional deny must NOT surface a reason. jq -e returns non-zero if
|
|
# .errors[0].detail.reason is absent or null, which is what we want here.
|
|
if echo "$TEST7_BODY" | jq -e '.errors[0].detail.reason // empty' >/dev/null 2>&1; then
|
|
log_error "TEST 7 FAILED: Non-conditional 403 unexpectedly carried a reason: $TEST7_BODY"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "TEST 7 PASSED: Other ServiceAccount rejected (HTTP 403, no reason in body)"
|
|
|
|
# =============================================================================
|
|
# TEST 8: Conditional access GRANTS other-sa write on cond-allowed/* (condition true)
|
|
# =============================================================================
|
|
# The accessControl config grants other-sa read/create/update/delete on the
|
|
# `cond-*/**` pattern only when `req.repository.startsWith("cond-allowed/")`
|
|
# is true. A push to `cond-allowed/test` should be authorized by the
|
|
# conditional policy (HTTP 202 Accepted on blob upload start).
|
|
log_info "TEST 8: Verifying CEL condition grants other-sa write on cond-allowed/*..."
|
|
|
|
HTTP_CODE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod-other-sa -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/cond-allowed/test/blobs/uploads/"' 2>/dev/null || echo "000")
|
|
|
|
if [ "$HTTP_CODE" = "202" ]; then
|
|
log_info "TEST 8 PASSED: Conditional policy grants write on cond-allowed/* (HTTP 202)"
|
|
else
|
|
log_error "TEST 8 FAILED: Expected 202 for cond-allowed/test write, got HTTP $HTTP_CODE"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 9: Conditional access DENIES other-sa on cond-denied/* and surfaces the
|
|
# operator-authored Message in the 403 response body's error detail
|
|
# =============================================================================
|
|
# Same conditional policy, but `cond-denied/*` does not satisfy
|
|
# `startsWith("cond-allowed/")`. The policy's `message` should appear in the
|
|
# response body's error detail under the `reason` key, so the client knows
|
|
# why access was denied.
|
|
log_info "TEST 9: Verifying CEL condition denies on cond-denied/* and surfaces reason..."
|
|
|
|
RESPONSE=$(kubectl exec -n "${TEST_NAMESPACE}" oidc-test-pod-other-sa -- \
|
|
sh -c 'TOKEN=$(cat /var/run/secrets/tokens/zot-token); curl -s -w "\n%{http_code}" -X POST -H "Authorization: Bearer $TOKEN" "http://${ZOT_REGISTRY}/v2/cond-denied/test/blobs/uploads/"' 2>/dev/null || echo $'\n000')
|
|
TEST9_HTTP=$(echo "$RESPONSE" | tail -n1)
|
|
TEST9_BODY=$(echo "$RESPONSE" | sed '$d')
|
|
|
|
if [ "$TEST9_HTTP" != "403" ]; then
|
|
log_error "TEST 9 FAILED: Expected 403 for cond-denied/* write, got HTTP $TEST9_HTTP"
|
|
log_error "Body: $TEST9_BODY"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# Conditional denies must surface the operator-authored Message in
|
|
# detail.reason — that's the contrast with Test 7.
|
|
if ! echo "$TEST9_BODY" | jq -e '.errors[0].detail.reason | contains("only push to cond-allowed/*")' >/dev/null 2>&1; then
|
|
log_error "TEST 9 FAILED: Expected deny reason in response body, got: $TEST9_BODY"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
log_info "TEST 9 PASSED: Conditional deny returned HTTP 403 with reason in body"
|
|
|
|
fi # End of curl-based tests conditional
|
|
|
|
# =============================================================================
|
|
# E2E Tests using Crane CLI for real OCI image operations
|
|
# =============================================================================
|
|
# NOTE: Crane (from go-containerregistry) properly supports the `registryToken`
|
|
# field in Docker config, which sends the token directly as a Bearer header.
|
|
# This is compatible with zot's OIDC bearer authentication.
|
|
#
|
|
# Other tools like oras and skopeo do NOT properly support this - they expect
|
|
# the token service authentication flow (exchanging credentials with a token
|
|
# endpoint) which is different from direct bearer token authentication.
|
|
# =============================================================================
|
|
if [ "$ONLY_CURL" = true ]; then
|
|
log_info "Skipping crane e2e tests (--only-curl specified)"
|
|
else
|
|
|
|
# Create a Pod with crane CLI for e2e artifact push/pull tests (or reuse existing)
|
|
if ! ensure_pod_ready "crane-test-pod" "${TEST_NAMESPACE}" 120; then
|
|
log_info "Creating crane test Pod for e2e artifact operations..."
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: crane-test-pod
|
|
namespace: ${TEST_NAMESPACE}
|
|
spec:
|
|
serviceAccountName: ${TEST_SA_NAME}
|
|
initContainers:
|
|
- name: install-crane
|
|
image: ${ALPINE_IMAGE}
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
apk add --no-cache curl
|
|
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar -xzf - -C /tools crane
|
|
chmod +x /tools/crane
|
|
volumeMounts:
|
|
- name: tools
|
|
mountPath: /tools
|
|
containers:
|
|
- name: crane
|
|
image: ${ALPINE_IMAGE}
|
|
command: ["sleep", "infinity"]
|
|
volumeMounts:
|
|
- name: token
|
|
mountPath: /var/run/secrets/tokens
|
|
readOnly: true
|
|
- name: tools
|
|
mountPath: /usr/local/bin
|
|
env:
|
|
- name: ZOT_REGISTRY
|
|
value: "${ZOT_REG_NAME}:${ZOT_PORT}"
|
|
volumes:
|
|
- name: token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
path: zot-token
|
|
expirationSeconds: 3600
|
|
audience: ${AUDIENCE}
|
|
- name: tools
|
|
emptyDir: {}
|
|
restartPolicy: Never
|
|
EOF
|
|
log_info "Waiting for crane test pod to be ready..."
|
|
kubectl wait --for=condition=Ready pod/crane-test-pod -n "${TEST_NAMESPACE}" --timeout=180s
|
|
fi
|
|
|
|
# Helper function to set up Docker config with registryToken
|
|
setup_crane_auth() {
|
|
kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- sh -c '
|
|
mkdir -p ~/.docker
|
|
TOKEN=$(cat /var/run/secrets/tokens/zot-token)
|
|
cat > ~/.docker/config.json << EOFCONFIG
|
|
{
|
|
"auths": {
|
|
"$ZOT_REGISTRY": {
|
|
"registryToken": "$TOKEN"
|
|
}
|
|
}
|
|
}
|
|
EOFCONFIG
|
|
'
|
|
}
|
|
|
|
# Helper function to remove Docker config (no auth)
|
|
remove_crane_auth() {
|
|
kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- sh -c '
|
|
rm -f ~/.docker/config.json
|
|
rm -f /tmp/auth.json
|
|
rm -rf ~/.config/containers
|
|
' 2>/dev/null || true
|
|
}
|
|
|
|
# =============================================================================
|
|
# TEST 8: Copy OCI image using crane WITH auth (should SUCCEED)
|
|
# Note: This runs first to populate zot, so subsequent tests don't need Docker Hub
|
|
# We check if the image already exists to avoid Docker Hub rate limiting on reruns
|
|
# =============================================================================
|
|
log_info "TEST 8: Copying OCI image using crane WITH auth (should succeed)..."
|
|
setup_crane_auth
|
|
|
|
# Check if image already exists in zot from a previous run
|
|
IMAGE_EXISTS=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane manifest --insecure $ZOT_REGISTRY/crane-test:v1 2>&1 && echo EXISTS' || true)
|
|
|
|
if echo "$IMAGE_EXISTS" | grep -q "EXISTS"; then
|
|
log_info "Image crane-test:v1 already exists in zot, skipping Docker Hub pull"
|
|
# Verify we can still access it with auth (do a copy to v1-test to verify write works)
|
|
PUSH_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane copy --insecure $ZOT_REGISTRY/crane-test:v1 $ZOT_REGISTRY/crane-test:v1-test 2>&1') || true
|
|
if echo "$PUSH_OUTPUT" | grep -qiE "pushed|existing|copied|digest"; then
|
|
log_info "TEST 8 PASSED: crane copy within zot succeeded with auth"
|
|
log_info "Push output: $(echo "$PUSH_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 8 FAILED: crane copy within zot failed"
|
|
log_error "Output: $PUSH_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -50
|
|
exit 1
|
|
fi
|
|
else
|
|
# Image doesn't exist, need to pull from Docker Hub
|
|
log_info "Image not found in zot, pulling from Docker Hub..."
|
|
PUSH_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c "crane copy --insecure ${BUSYBOX_IMAGE} \$ZOT_REGISTRY/crane-test:v1 2>&1") || true
|
|
|
|
if echo "$PUSH_OUTPUT" | grep -qiE "pushed|existing|crane-test:v1.*digest|copied"; then
|
|
log_info "TEST 8 PASSED: crane copy from Docker Hub succeeded with auth"
|
|
log_info "Push output: $(echo "$PUSH_OUTPUT" | tail -3)"
|
|
else
|
|
log_error "TEST 8 FAILED: crane copy failed with valid auth"
|
|
log_error "Output: $PUSH_OUTPUT"
|
|
log_error "Note: If you see rate limit errors, this may be due to Docker Hub throttling"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -50
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Verify the image was pushed by listing tags
|
|
log_info "Verifying pushed image (listing tags)..."
|
|
TAGS=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane ls --insecure $ZOT_REGISTRY/crane-test 2>&1') || true
|
|
log_info "Tags for crane-test: $TAGS"
|
|
|
|
if echo "$TAGS" | grep -q "v1"; then
|
|
log_info "Verified: Tag 'v1' found in crane-test repository"
|
|
else
|
|
log_warn "Warning: Tag 'v1' not found in crane-test repository"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 9: Copy OCI image using crane WITHOUT auth (should FAIL)
|
|
# Note: Uses zot-to-zot copy to avoid Docker Hub rate limiting
|
|
# =============================================================================
|
|
log_info "TEST 9: Copying OCI image using crane WITHOUT auth (should fail)..."
|
|
remove_crane_auth
|
|
|
|
COPY_NO_AUTH_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane copy --insecure $ZOT_REGISTRY/crane-test:v1 $ZOT_REGISTRY/crane-test:v2 2>&1' || true)
|
|
|
|
if echo "$COPY_NO_AUTH_OUTPUT" | grep -qiE "401|unauthorized|UNAUTHORIZED"; then
|
|
log_info "TEST 9 PASSED: crane copy correctly rejected without auth (401)"
|
|
log_info "Output: $(echo "$COPY_NO_AUTH_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 9 FAILED: Expected 401 authentication failure"
|
|
log_error "Output: $COPY_NO_AUTH_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 10: List tags using crane WITHOUT auth (should FAIL)
|
|
# =============================================================================
|
|
log_info "TEST 10: Listing tags using crane WITHOUT auth (should fail)..."
|
|
|
|
PULL_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane ls --insecure $ZOT_REGISTRY/crane-test 2>&1' || true)
|
|
|
|
if echo "$PULL_OUTPUT" | grep -qiE "401|unauthorized|authentication|UNAUTHORIZED"; then
|
|
log_info "TEST 10 PASSED: crane ls correctly rejected without auth"
|
|
log_info "Output: $(echo "$PULL_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 10 FAILED: Expected authentication failure"
|
|
log_error "Output: $PULL_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 11: Pull manifest using crane WITH auth (should SUCCEED)
|
|
# =============================================================================
|
|
log_info "TEST 11: Pulling manifest using crane WITH auth (should succeed)..."
|
|
setup_crane_auth
|
|
|
|
PULL_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-test-pod -- \
|
|
sh -c 'crane manifest --insecure $ZOT_REGISTRY/crane-test:v1 2>&1') || true
|
|
|
|
if echo "$PULL_OUTPUT" | grep -qiE "schemaVersion|mediaType|manifests"; then
|
|
log_info "TEST 11 PASSED: crane manifest succeeded with auth"
|
|
log_info "Manifest preview: $(echo "$PULL_OUTPUT" | head -5)"
|
|
else
|
|
log_error "TEST 11 FAILED: crane manifest failed with valid auth"
|
|
log_error "Output: $PULL_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -50
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# CRANE TESTS FOR other-sa (NOT in accessControl config)
|
|
# These tests verify that authorization is enforced for real OCI operations
|
|
# =============================================================================
|
|
|
|
# Create crane pod for other-sa (or reuse existing)
|
|
if ! ensure_pod_ready "crane-other-sa-pod" "${TEST_NAMESPACE}" 120; then
|
|
log_info "Creating crane pod for other-sa..."
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: crane-other-sa-pod
|
|
namespace: ${TEST_NAMESPACE}
|
|
spec:
|
|
serviceAccountName: other-sa
|
|
initContainers:
|
|
- name: install-crane
|
|
image: ${ALPINE_IMAGE}
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
apk add --no-cache curl
|
|
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar -xzf - -C /tools crane
|
|
chmod +x /tools/crane
|
|
volumeMounts:
|
|
- name: tools
|
|
mountPath: /tools
|
|
containers:
|
|
- name: crane
|
|
image: ${ALPINE_IMAGE}
|
|
command: ["sleep", "infinity"]
|
|
volumeMounts:
|
|
- name: token
|
|
mountPath: /var/run/secrets/tokens
|
|
readOnly: true
|
|
- name: tools
|
|
mountPath: /usr/local/bin
|
|
env:
|
|
- name: ZOT_REGISTRY
|
|
value: "${ZOT_REG_NAME}:${ZOT_PORT}"
|
|
volumes:
|
|
- name: token
|
|
projected:
|
|
sources:
|
|
- serviceAccountToken:
|
|
path: zot-token
|
|
expirationSeconds: 3600
|
|
audience: ${AUDIENCE}
|
|
- name: tools
|
|
emptyDir: {}
|
|
restartPolicy: Never
|
|
EOF
|
|
log_info "Waiting for crane-other-sa-pod to be ready..."
|
|
kubectl wait --for=condition=Ready pod/crane-other-sa-pod -n "${TEST_NAMESPACE}" --timeout=180s
|
|
fi
|
|
|
|
# Helper to set up auth for other-sa crane pod
|
|
setup_other_sa_crane_auth() {
|
|
kubectl exec -n "${TEST_NAMESPACE}" crane-other-sa-pod -- sh -c '
|
|
mkdir -p ~/.docker
|
|
TOKEN=$(cat /var/run/secrets/tokens/zot-token)
|
|
cat > ~/.docker/config.json << EOFCONFIG
|
|
{
|
|
"auths": {
|
|
"$ZOT_REGISTRY": {
|
|
"registryToken": "$TOKEN"
|
|
}
|
|
}
|
|
}
|
|
EOFCONFIG
|
|
'
|
|
}
|
|
|
|
# =============================================================================
|
|
# TEST 12: Copy OCI image using crane with other-sa (should FAIL with 403)
|
|
# Note: Uses zot-to-zot copy to avoid Docker Hub rate limiting
|
|
# =============================================================================
|
|
log_info "TEST 12: Copying OCI image using crane with other-sa (should fail with 403)..."
|
|
setup_other_sa_crane_auth
|
|
|
|
PUSH_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-other-sa-pod -- \
|
|
sh -c 'crane copy --insecure $ZOT_REGISTRY/crane-test:v1 $ZOT_REGISTRY/other-sa-crane-test:v1 2>&1' || true)
|
|
|
|
if echo "$PUSH_OUTPUT" | grep -qiE "403|forbidden|denied"; then
|
|
log_info "TEST 12 PASSED: crane copy correctly rejected for other-sa (403 Forbidden)"
|
|
log_info "Output: $(echo "$PUSH_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 12 FAILED: Expected 403 for other-sa push"
|
|
log_error "Output: $PUSH_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 13: List tags using crane with other-sa (should FAIL with 403)
|
|
# =============================================================================
|
|
log_info "TEST 13: Listing tags using crane with other-sa (should fail with 403)..."
|
|
|
|
LIST_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-other-sa-pod -- \
|
|
sh -c 'crane ls --insecure $ZOT_REGISTRY/crane-test 2>&1' || true)
|
|
|
|
if echo "$LIST_OUTPUT" | grep -qiE "403|forbidden|unauthorized|denied"; then
|
|
log_info "TEST 13 PASSED: crane ls correctly rejected for other-sa (access denied)"
|
|
log_info "Output: $(echo "$LIST_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 13 FAILED: Expected 403 for other-sa list"
|
|
log_error "Output: $LIST_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
# =============================================================================
|
|
# TEST 14: Crane operation with NO token (should FAIL with 401, not 403)
|
|
# This verifies the difference between authentication failure (401) and
|
|
# authorization failure (403)
|
|
# =============================================================================
|
|
log_info "TEST 14: Crane operation with NO token (should fail with 401)..."
|
|
|
|
# Remove auth config from other-sa pod
|
|
kubectl exec -n "${TEST_NAMESPACE}" crane-other-sa-pod -- sh -c 'rm -f ~/.docker/config.json' 2>/dev/null || true
|
|
|
|
NO_TOKEN_OUTPUT=$(kubectl exec -n "${TEST_NAMESPACE}" crane-other-sa-pod -- \
|
|
sh -c 'crane ls --insecure $ZOT_REGISTRY/crane-test 2>&1' || true)
|
|
|
|
if echo "$NO_TOKEN_OUTPUT" | grep -qiE "401|unauthorized"; then
|
|
log_info "TEST 14 PASSED: No token correctly returns 401 Unauthorized"
|
|
log_info "Output: $(echo "$NO_TOKEN_OUTPUT" | tail -2)"
|
|
else
|
|
log_error "TEST 14 FAILED: Expected 401 for no token, got different error"
|
|
log_error "Output: $NO_TOKEN_OUTPUT"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -30
|
|
exit 1
|
|
fi
|
|
|
|
fi # End of crane tests conditional
|
|
|
|
# Print final zot logs for debugging
|
|
log_info "Final zot logs:"
|
|
docker logs "${ZOT_REG_NAME}" 2>&1 | tail -50
|
|
|
|
log_info "=========================================="
|
|
if [ "$ONLY_CRANE" = true ]; then
|
|
log_info "Crane e2e tests (8-14) PASSED!"
|
|
elif [ "$ONLY_CURL" = true ]; then
|
|
log_info "Curl-based tests (1-7) PASSED!"
|
|
else
|
|
log_info "All OIDC Workload Identity tests PASSED!"
|
|
fi
|
|
log_info "=========================================="
|
|
log_info ""
|
|
log_info "Iteration tips:"
|
|
log_info " --skip-setup Skip cluster/image/zot setup (reuse existing)"
|
|
log_info " --only-crane Run only crane tests (8-14)"
|
|
log_info " --only-curl Run only curl tests (1-7)"
|
|
log_info " --keep-resources Keep cluster/zot running after exit"
|