feat(jwt-asm): support AWS Secrets Manager for JWT verification (#3763)

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
This commit is contained in:
Matheus Pimenta
2026-02-03 17:25:38 +00:00
committed by GitHub
parent 7f629b5d67
commit 0e5a339f11
12 changed files with 1853 additions and 27 deletions
@@ -0,0 +1,279 @@
# AWS Secrets Manager Bearer Authentication
This document describes how to configure Zot to retrieve JWT verification keys from AWS Secrets Manager, enabling dynamic key rotation without restarting the registry.
## Overview
AWS Secrets Manager bearer authentication allows Zot to retrieve public keys for JWT verification from AWS Secrets Manager instead of loading them from a static file on disk. This is useful when keys need to be rotated without downtime, or when the same keys are shared across multiple Zot instances.
Zot periodically refreshes the keys from AWS Secrets Manager, so key rotations are picked up automatically.
## Benefits
- **Dynamic Key Rotation**: Rotate public keys in AWS Secrets Manager without restarting Zot
- **Centralized Key Management**: Manage verification keys for multiple Zot instances in a single place
- **Lazy Loading**: Keys are fetched on first authentication, so startup is not blocked by network issues
- **Caching**: Keys are cached locally and refreshed periodically to minimize API calls
- **Multiple Keys**: Support multiple key IDs (`kid`) for seamless key rotation
## Configuration
### Basic Configuration
Add AWS Secrets Manager configuration to your bearer authentication settings:
```json
{
"http": {
"auth": {
"bearer": {
"realm": "zot",
"service": "zot-service",
"awsSecretsManager": {
"region": "us-east-1",
"secretName": "zot/jwt-verification-keys"
}
}
}
}
}
```
### Configuration Options
- **`region`** (required): The AWS region where the secret is stored.
- Example: `"us-east-1"`
- **`secretName`** (required): The name or ARN of the secret in AWS Secrets Manager.
- Example: `"zot/jwt-verification-keys"`
- Example: `"arn:aws:secretsmanager:us-east-1:123456789012:secret:zot/keys-AbCdEf"`
- **`refreshInterval`** (optional): How often to refresh keys from AWS Secrets Manager. Default: `1m` (1 minute).
- Example: `"5m"` (5 minutes)
- Example: `"30s"` (30 seconds)
## Secret Format
The secret stored in AWS Secrets Manager must be a JSON object where each key is a key ID (`kid`) and each value is either a PEM-encoded public key or a JWKS key set with a single key:
### PEM Format
```json
{
"key-id-1": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
"key-id-2": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG...\n-----END PUBLIC KEY-----\n"
}
```
### JWKS Format
Each value can also be a JWKS key set containing a single key:
```json
{
"key-id-1": "{\"keys\":[{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"...\"}]}"
}
```
### Supported Key Types
- **Ed25519** (EdDSA) - Recommended for new deployments
- **RSA** (RS256, RS384, RS512, PS256, PS384, PS512)
- **ECDSA** (ES256, ES384, ES512)
## Complete Example
```json
{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080",
"auth": {
"bearer": {
"realm": "zot",
"service": "zot-service",
"awsSecretsManager": {
"region": "us-east-1",
"secretName": "zot/jwt-verification-keys",
"refreshInterval": "5m"
}
}
},
"accessControl": {
"repositories": {
"**": {
"policies": [
{
"users": ["service-account-1"],
"actions": ["read", "create", "update"]
}
]
}
}
}
},
"log": {
"level": "info"
}
}
```
## JWT Token Requirements
JWTs presented to Zot must include a `kid` (Key ID) header that matches one of the key IDs in the secret. This is how Zot selects the correct public key for verification.
### Example JWT Header
```json
{
"alg": "EdDSA",
"kid": "key-id-1",
"typ": "JWT"
}
```
### Example JWT Payload
```json
{
"iss": "https://auth.example.com",
"sub": "service-account-1",
"aud": ["zot"],
"exp": 1705258800,
"iat": 1705255200,
"access": [
{
"type": "repository",
"name": "my-app",
"actions": ["pull", "push"]
}
]
}
```
## Key Rotation
To rotate keys without downtime:
1. **Add the new key** to the secret in AWS Secrets Manager alongside the existing key(s):
```json
{
"old-key-id": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
"new-key-id": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
```
2. **Update your token issuer** to sign new tokens with the new key ID.
3. **Wait for the refresh interval** to elapse. Zot will automatically pick up the new key.
4. **Remove the old key** from the secret once all tokens signed with the old key have expired.
## Compatibility
### With OIDC Workload Identity
AWS Secrets Manager bearer authentication can coexist with OIDC workload identity. If both are configured, Zot will try OIDC authentication first, then fall back to traditional bearer token authentication using keys from AWS Secrets Manager:
```json
{
"http": {
"auth": {
"bearer": {
"realm": "zot",
"service": "zot-service",
"awsSecretsManager": {
"region": "us-east-1",
"secretName": "zot/jwt-verification-keys"
},
"oidc": [
{
"issuer": "https://kubernetes.default.svc.cluster.local",
"audiences": ["zot"]
}
]
}
}
}
}
```
### With Static Certificate
AWS Secrets Manager and the static `cert` option are mutually exclusive. Zot will refuse to start if both are configured.
## AWS Authentication
Zot uses the default AWS credential chain to authenticate with AWS Secrets Manager. This means you can use any of the following:
- **Environment variables**: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
- **Shared credentials file**: `~/.aws/credentials`
- **IAM role** (for EC2, ECS, or EKS workloads)
- **Web identity token** (for EKS with IRSA)
### Required IAM Permissions
The IAM principal used by Zot needs the following permission:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:zot/jwt-verification-keys-*"
}
]
}
```
## Troubleshooting
### Enable Debug Logging
Set log level to `debug` to see detailed authentication logs:
```json
{
"log": {
"level": "debug"
}
}
```
### Common Issues
1. **"region must be specified"**: The `region` field is required in the configuration.
2. **"secret name must be specified"**: The `secretName` field is required in the configuration.
3. **"failed to load AWS configuration"**: Check that AWS credentials are available and valid.
4. **"failed to retrieve secret"**: Verify the secret exists in the specified region and the IAM principal has `secretsmanager:GetSecretValue` permission.
5. **"failed to parse secret JSON"**: The secret value must be a valid JSON object mapping key IDs to PEM-encoded public keys.
6. **"no public key found for kid"**: The JWT's `kid` header does not match any key ID in the secret. Check that the key ID in the JWT matches one of the keys in the secret.
7. **"token missing 'kid' header"**: JWTs must include a `kid` header when using AWS Secrets Manager keys.
8. **"cannot configure both cert and AWS Secrets Manager"**: The static `cert` and `awsSecretsManager` options are mutually exclusive. Remove one of them.
## Security Considerations
1. **Least Privilege**: Grant only `secretsmanager:GetSecretValue` permission, scoped to the specific secret ARN.
2. **Secret Encryption**: AWS Secrets Manager encrypts secrets at rest using KMS. Use a customer-managed KMS key for additional control.
3. **Refresh Interval**: Use a short refresh interval (e.g., 1-5 minutes) to pick up key rotations quickly. The default of 1 minute is appropriate for most use cases.
4. **Key Types**: Prefer Ed25519 (EdDSA) keys for new deployments. They are faster and produce smaller signatures than RSA.
5. **TLS**: Always use TLS for all communication to protect tokens in transit.
6. **Access Control**: Always configure access control policies to limit what authenticated clients can do.
+535
View File
@@ -0,0 +1,535 @@
#!/bin/bash
# AWS Secrets Manager Bearer Authentication E2E Test
#
# This script tests bearer authentication with JWT verification keys
# stored in AWS Secrets Manager. It uses hardcoded Ed25519 JWKS keys
# to sign JWTs and verifies that Zot correctly authenticates requests
# using keys retrieved from AWS Secrets Manager.
#
# The test:
# 1. Starts LocalStack (or uses real AWS) for Secrets Manager
# 2. Creates a secret with Ed25519 public keys
# 3. Builds and starts Zot with AWS Secrets Manager bearer auth
# 4. Tests push/pull operations with crane using pre-signed JWTs
# 5. Verifies authentication fails without a valid token
#
# By default, the script uses LocalStack (a local AWS emulator running in Docker)
# so no real AWS credentials are needed. Use --use-real-aws to test against
# a real AWS account with credentials from ~/.aws/credentials.
#
# Usage:
# ./aws-secrets-manager-bearer-auth.sh [OPTIONS]
#
# Options:
# --use-real-aws Use real AWS Secrets Manager (requires ~/.aws/credentials)
# --skip-build Skip building zot (reuse existing binary)
# --keep-resources Don't clean up resources on exit
# --region REGION AWS region (default: us-east-1)
# --help Show this help message
#
# Prerequisites:
# go, crane, jq, curl
# For LocalStack mode (default): docker
# For real AWS mode: aws CLI with configured credentials
set -o errexit
set -o pipefail
# Parse command line arguments
USE_REAL_AWS=false
SKIP_BUILD=false
KEEP_RESOURCES=false
AWS_REGION="us-east-1"
while [[ $# -gt 0 ]]; do
case $1 in
--use-real-aws)
USE_REAL_AWS=true
shift
;;
--skip-build)
SKIP_BUILD=true
shift
;;
--keep-resources)
KEEP_RESOURCES=true
shift
;;
--region)
AWS_REGION="$2"
shift 2
;;
--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
# 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"
}
# Check prerequisites
check_prerequisites() {
local missing=""
for cmd in go crane jq curl; do
if ! command -v "$cmd" &> /dev/null; then
missing="$missing $cmd"
fi
done
if [ "$USE_REAL_AWS" = true ]; then
if ! command -v aws &> /dev/null; then
missing="$missing aws"
fi
else
if ! command -v docker &> /dev/null; then
missing="$missing docker"
fi
# Also need aws CLI to talk to LocalStack
if ! command -v aws &> /dev/null; then
missing="$missing aws"
fi
fi
if [ -n "$missing" ]; then
log_error "Missing required tools:$missing"
exit 1
fi
if [ "$USE_REAL_AWS" = true ]; then
# Verify AWS credentials are configured
if ! aws sts get-caller-identity --region "${AWS_REGION}" &>/dev/null; then
log_error "AWS credentials not configured or invalid. Check ~/.aws/credentials"
exit 1
fi
log_info "AWS credentials verified (real AWS mode)"
else
log_info "Using LocalStack mode (no real AWS credentials needed)"
fi
}
check_prerequisites
ROOT_DIR=$(git rev-parse --show-toplevel)
cd "${ROOT_DIR}"
# Configuration
ZOT_PORT="5000"
ZOT_BINARY="/tmp/zot-asm-test"
ZOT_CONFIG="/tmp/zot-asm-config.json"
ZOT_STORAGE="/tmp/zot-asm-storage"
ZOT_PID_FILE="/tmp/zot-asm-test.pid"
SECRET_NAME="zot/e2e-test-jwt-keys-$(date +%s)"
LOCALSTACK_CONTAINER="localstack-zot-asm-test"
LOCALSTACK_PORT="4566"
BUSYBOX_IMAGE="gcr.io/google-containers/busybox:1.27"
# AWS CLI flags: when using LocalStack, point the AWS CLI at the local endpoint
# and set dummy credentials (LocalStack doesn't validate them).
if [ "$USE_REAL_AWS" = true ]; then
AWS_CMD_FLAGS="--region ${AWS_REGION}"
else
AWS_CMD_FLAGS="--region ${AWS_REGION} --endpoint-url http://127.0.0.1:${LOCALSTACK_PORT}"
export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"
fi
# Ed25519 public JWKS key stored in AWS Secrets Manager for JWT verification.
PUBLIC_JWKS='{"keys":[{"use":"sig","kty":"OKP","kid":"01f0ff96-0286-62c9-9fe0-68c6ac4f48e0","crv":"Ed25519","alg":"EdDSA","x":"3pL95mHbZYNG6-YT_MqXKibGQrXF7WziWk25EcgEJGs"}]}'
KID="01f0ff96-0286-62c9-9fe0-68c6ac4f48e0"
# Pre-signed long-lived JWTs (exp ~2126) for test use only. Signed with the Ed25519
# private key corresponding to the public key above (private key not stored in repo).
# JWT with push+pull access to "test-repo"
TOKEN_PUSH_PULL="eyJhbGciOiJFZERTQSIsImtpZCI6IjAxZjBmZjk2LTAyODYtNjJjOS05ZmUwLTY4YzZhYzRmNDhlMCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzZXJ2aWNlLWFjY291bnQtMSIsImV4cCI6NDkyMzY0NzIxMCwiaWF0IjoxNzcwMDQ3MjEwLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InRlc3QtcmVwbyIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.D8CN1Yt5gUV9ZEJHOkJWmEa54Ame5oHyERjH0-_TDkLBa2hjHRq6StJOUCl8wejZ2O_oFGspdlz2X_MVwWXMCQ"
# JWT with pull-only access to "test-repo"
TOKEN_PULL_ONLY="eyJhbGciOiJFZERTQSIsImtpZCI6IjAxZjBmZjk2LTAyODYtNjJjOS05ZmUwLTY4YzZhYzRmNDhlMCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzZXJ2aWNlLWFjY291bnQtMSIsImV4cCI6NDkyMzY0NzIxMiwiaWF0IjoxNzcwMDQ3MjEyLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InRlc3QtcmVwbyIsImFjdGlvbnMiOlsicHVsbCJdfV19.QqVzME9mO61QBhw1gQhBFleBv76Aju3hUVxv-KWZdyYKwHiXINX6vMW8aKWG81DMam26Y19GeMK7QRz5Sg6rAw"
# JWT with no access claims (authentication only)
TOKEN_NO_ACCESS="eyJhbGciOiJFZERTQSIsImtpZCI6IjAxZjBmZjk2LTAyODYtNjJjOS05ZmUwLTY4YzZhYzRmNDhlMCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzZXJ2aWNlLWFjY291bnQtMSIsImV4cCI6NDkyMzY0NzIxMiwiaWF0IjoxNzcwMDQ3MjEyfQ.t-1eng92zu7W4tfskVMHkynlvojSEnwHlWOCIc2MN234rMeqyPZrD9tFmkKsEWlznJpKIo-wMXY70JQkOCQ8Bg"
# JWT with wrong kid (for rejection test)
TOKEN_WRONG_KID="eyJhbGciOiJFZERTQSIsImtpZCI6Indyb25nLWtpZC1kb2VzLW5vdC1leGlzdCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzZXJ2aWNlLWFjY291bnQtMSIsImV4cCI6NDkyMzY0NzIxMiwiaWF0IjoxNzcwMDQ3MjEyLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InRlc3QtcmVwbyIsImFjdGlvbnMiOlsicHVsbCJdfV19.4WKgZIQwAxTG3xJsn83_qpq1w_b1WvQR977Hj-1LlTKred8bfrcbJt9Fy7_gzP5ee6UgFs-IitldL6hy6emYAA"
cleanup() {
if [ "$KEEP_RESOURCES" = true ]; then
log_info "Keeping resources (--keep-resources specified)"
log_info "To clean up manually:"
if [ "$USE_REAL_AWS" = true ]; then
log_info " aws secretsmanager delete-secret --secret-id '${SECRET_NAME}' --force-delete-without-recovery --region '${AWS_REGION}'"
else
log_info " docker rm -f ${LOCALSTACK_CONTAINER}"
fi
log_info " kill \$(cat ${ZOT_PID_FILE}) 2>/dev/null"
log_info " rm -rf ${ZOT_STORAGE} ${ZOT_CONFIG}"
return
fi
log_info "Cleaning up..."
# Stop zot
if [ -f "${ZOT_PID_FILE}" ]; then
kill "$(cat "${ZOT_PID_FILE}")" 2>/dev/null || true
rm -f "${ZOT_PID_FILE}"
fi
if [ "$USE_REAL_AWS" = true ]; then
# Delete the secret from AWS Secrets Manager
aws secretsmanager delete-secret \
--secret-id "${SECRET_NAME}" \
--force-delete-without-recovery \
--region "${AWS_REGION}" 2>/dev/null || true
else
# Stop LocalStack
docker rm -f "${LOCALSTACK_CONTAINER}" 2>/dev/null || true
fi
# Clean up local files
rm -rf "${ZOT_STORAGE}" "${ZOT_CONFIG}" /tmp/zot-asm-docker 2>/dev/null || true
}
trap cleanup EXIT
# =============================================================================
# SETUP
# =============================================================================
# Step 1: Start LocalStack if not using real AWS
if [ "$USE_REAL_AWS" = false ]; then
log_info "Starting LocalStack..."
docker rm -f "${LOCALSTACK_CONTAINER}" 2>/dev/null || true
docker run -d \
--name "${LOCALSTACK_CONTAINER}" \
-p "${LOCALSTACK_PORT}:4566" \
ghcr.io/project-zot/ci-images/localstack:3.3.0
# Wait for LocalStack to be ready
log_info "Waiting for LocalStack to be ready..."
for i in {1..30}; do
if curl -s "http://127.0.0.1:${LOCALSTACK_PORT}/_localstack/health" 2>/dev/null | grep -q '"secretsmanager"'; then
log_info "LocalStack is ready"
break
fi
if [ "$i" -eq 30 ]; then
log_error "LocalStack failed to start"
docker logs "${LOCALSTACK_CONTAINER}" 2>&1 | tail -20
exit 1
fi
sleep 1
done
fi
# Step 2: Create the secret in AWS Secrets Manager
log_info "Creating secret '${SECRET_NAME}' in Secrets Manager (${AWS_REGION})..."
# The secret format is a JSON object: {"kid": "JWKS-or-PEM-string"}
# We store the public key in JWKS format.
SECRET_VALUE=$(jq -n --arg kid "$KID" --arg key "$PUBLIC_JWKS" '{($kid): $key}')
# shellcheck disable=SC2086
aws secretsmanager create-secret \
--name "${SECRET_NAME}" \
--secret-string "${SECRET_VALUE}" \
${AWS_CMD_FLAGS} \
--output json | jq .
log_info "Secret created successfully"
# Verify the secret can be retrieved
log_info "Verifying secret retrieval..."
# shellcheck disable=SC2086
RETRIEVED=$(aws secretsmanager get-secret-value \
--secret-id "${SECRET_NAME}" \
${AWS_CMD_FLAGS} \
--query 'SecretString' \
--output text)
if [ "$(echo "$RETRIEVED" | jq -r ".[\"$KID\"]")" = "$PUBLIC_JWKS" ]; then
log_info "Secret verification passed"
else
log_error "Secret verification failed"
log_error "Expected: $PUBLIC_JWKS"
log_error "Got: $(echo "$RETRIEVED" | jq -r ".[\"$KID\"]")"
exit 1
fi
# Step 3: Build zot
if [ "$SKIP_BUILD" = true ] && [ -f "${ZOT_BINARY}" ]; then
log_info "Skipping build (--skip-build specified, using existing binary)"
else
log_info "Building zot..."
go build -o "${ZOT_BINARY}" ./cmd/zot
log_info "Zot built: ${ZOT_BINARY}"
fi
# Step 4: Create zot configuration
log_info "Creating zot configuration..."
rm -rf "${ZOT_STORAGE}"
mkdir -p "${ZOT_STORAGE}"
cat > "${ZOT_CONFIG}" <<EOF
{
"distSpecVersion": "1.1.1",
"storage": {
"rootDirectory": "${ZOT_STORAGE}"
},
"http": {
"address": "127.0.0.1",
"port": "${ZOT_PORT}",
"compat": ["docker2s2"],
"auth": {
"bearer": {
"realm": "zot",
"service": "zot-service",
"awsSecretsManager": {
"region": "${AWS_REGION}",
"secretName": "${SECRET_NAME}",
"refreshInterval": "30s"
}
}
}
},
"log": {
"level": "debug"
}
}
EOF
log_info "Zot configuration:"
jq . "${ZOT_CONFIG}"
# Step 5: Start zot
# When using LocalStack, set AWS_ENDPOINT_URL so the AWS SDK in zot talks to LocalStack.
# Also set dummy credentials for LocalStack (it doesn't validate them).
log_info "Starting zot..."
if [ "$USE_REAL_AWS" = true ]; then
"${ZOT_BINARY}" serve "${ZOT_CONFIG}" &
else
AWS_ENDPOINT_URL="http://127.0.0.1:${LOCALSTACK_PORT}" \
AWS_ACCESS_KEY_ID="test" \
AWS_SECRET_ACCESS_KEY="test" \
"${ZOT_BINARY}" serve "${ZOT_CONFIG}" &
fi
ZOT_PID=$!
echo "${ZOT_PID}" > "${ZOT_PID_FILE}"
log_info "Zot started with PID ${ZOT_PID}"
# Wait for zot to be ready
log_info "Waiting for zot to be ready..."
for i in {1..30}; do
HTTP_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${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 (last HTTP response: $HTTP_RESPONSE)"
exit 1
fi
sleep 1
done
# Helper: configure crane docker config with a given token
setup_crane_docker_config() {
local token="$1"
mkdir -p /tmp/zot-asm-docker
cat > /tmp/zot-asm-docker/config.json <<EOFCONFIG
{
"auths": {
"127.0.0.1:${ZOT_PORT}": {
"registryToken": "${token}"
}
}
}
EOFCONFIG
}
remove_crane_auth() {
rm -f /tmp/zot-asm-docker/config.json
}
REGISTRY="127.0.0.1:${ZOT_PORT}"
# =============================================================================
# TESTS
# =============================================================================
# TEST 1: Basic authentication check (GET /v2/ with valid token)
log_info "TEST 1: Verifying basic authentication with valid token..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN_NO_ACCESS}" \
"http://${REGISTRY}/v2/")
if [ "$HTTP_CODE" = "200" ]; then
log_info "TEST 1 PASSED: Authentication succeeded (HTTP $HTTP_CODE)"
else
log_error "TEST 1 FAILED: Expected 200, got HTTP $HTTP_CODE"
exit 1
fi
# TEST 2: Authentication fails without token
log_info "TEST 2: Verifying authentication fails without token..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"http://${REGISTRY}/v2/")
if [ "$HTTP_CODE" = "401" ]; then
log_info "TEST 2 PASSED: No token correctly rejected (HTTP $HTTP_CODE)"
else
log_error "TEST 2 FAILED: Expected 401, got HTTP $HTTP_CODE"
exit 1
fi
# TEST 3: Authentication fails with invalid token
log_info "TEST 3: Verifying authentication fails with invalid token..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer invalid.token.here" \
"http://${REGISTRY}/v2/")
if [ "$HTTP_CODE" = "401" ]; then
log_info "TEST 3 PASSED: Invalid token correctly rejected (HTTP $HTTP_CODE)"
else
log_error "TEST 3 FAILED: Expected 401, got HTTP $HTTP_CODE"
exit 1
fi
# TEST 4: Push OCI image using crane WITH push+pull token
log_info "TEST 4: Pushing OCI image using crane with push+pull token..."
setup_crane_docker_config "$TOKEN_PUSH_PULL"
PUSH_OUTPUT=$(DOCKER_CONFIG=/tmp/zot-asm-docker crane copy --insecure --platform linux/amd64 \
"${BUSYBOX_IMAGE}" "${REGISTRY}/test-repo:v1" 2>&1) || true
if echo "$PUSH_OUTPUT" | grep -qiE "error|UNAUTHORIZED|unauthorized|401|403"; then
log_error "TEST 4 FAILED: crane copy failed"
log_error "Output: $PUSH_OUTPUT"
exit 1
else
log_info "TEST 4 PASSED: crane copy succeeded with push+pull token"
log_info "Output: $(echo "$PUSH_OUTPUT" | tail -3)"
fi
# TEST 5: List tags using crane WITH pull token
log_info "TEST 5: Listing tags using crane with pull-only token..."
setup_crane_docker_config "$TOKEN_PULL_ONLY"
TAGS_OUTPUT=$(DOCKER_CONFIG=/tmp/zot-asm-docker crane ls --insecure \
"${REGISTRY}/test-repo" 2>&1) || true
if echo "$TAGS_OUTPUT" | grep -q "v1"; then
log_info "TEST 5 PASSED: crane ls succeeded and found tag 'v1'"
log_info "Tags: $TAGS_OUTPUT"
else
log_error "TEST 5 FAILED: Expected to find tag 'v1'"
log_error "Output: $TAGS_OUTPUT"
exit 1
fi
# TEST 6: Pull manifest using crane WITH pull token
log_info "TEST 6: Pulling manifest using crane with pull-only token..."
setup_crane_docker_config "$TOKEN_PULL_ONLY"
MANIFEST_OUTPUT=$(DOCKER_CONFIG=/tmp/zot-asm-docker crane manifest --insecure \
"${REGISTRY}/test-repo:v1" 2>&1) || true
if echo "$MANIFEST_OUTPUT" | grep -qiE "schemaVersion|mediaType|manifests"; then
log_info "TEST 6 PASSED: crane manifest succeeded with pull token"
log_info "Manifest preview: $(echo "$MANIFEST_OUTPUT" | head -5)"
else
log_error "TEST 6 FAILED: crane manifest failed with valid pull token"
log_error "Output: $MANIFEST_OUTPUT"
exit 1
fi
# TEST 7: Push fails with pull-only token
log_info "TEST 7: Verifying push fails with pull-only token..."
setup_crane_docker_config "$TOKEN_PULL_ONLY"
PUSH_FAIL_OUTPUT=$(DOCKER_CONFIG=/tmp/zot-asm-docker crane copy --insecure --platform linux/amd64 \
"${BUSYBOX_IMAGE}" "${REGISTRY}/test-repo:v2" 2>&1 || true)
if echo "$PUSH_FAIL_OUTPUT" | grep -qiE "401|unauthorized|UNAUTHORIZED"; then
log_info "TEST 7 PASSED: Push correctly rejected with pull-only token"
log_info "Output: $(echo "$PUSH_FAIL_OUTPUT" | tail -2)"
else
log_error "TEST 7 FAILED: Expected 401 for push with pull-only token"
log_error "Output: $PUSH_FAIL_OUTPUT"
exit 1
fi
# TEST 8: crane operations fail WITHOUT any token
log_info "TEST 8: Verifying crane operations fail without token..."
remove_crane_auth
NOTOK_OUTPUT=$(DOCKER_CONFIG=/tmp/zot-asm-docker crane ls --insecure \
"${REGISTRY}/test-repo" 2>&1 || true)
if echo "$NOTOK_OUTPUT" | grep -qiE "401|unauthorized|UNAUTHORIZED|error|Error"; then
log_info "TEST 8 PASSED: crane ls failed without token"
log_info "Output: $(echo "$NOTOK_OUTPUT" | tail -2)"
else
log_error "TEST 8 FAILED: Expected error for unauthenticated request"
log_error "Output: $NOTOK_OUTPUT"
exit 1
fi
# TEST 9: Token with wrong kid is rejected
log_info "TEST 9: Verifying token with wrong kid is rejected..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN_WRONG_KID}" \
"http://${REGISTRY}/v2/")
if [ "$HTTP_CODE" = "401" ]; then
log_info "TEST 9 PASSED: Wrong kid correctly rejected (HTTP $HTTP_CODE)"
else
log_error "TEST 9 FAILED: Expected 401 for wrong kid, got HTTP $HTTP_CODE"
exit 1
fi
# TEST 10: Token accessing unauthorized repository is rejected
log_info "TEST 10: Verifying access to unauthorized repository fails..."
# The push+pull token only grants access to "test-repo"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN_PUSH_PULL}" \
"http://${REGISTRY}/v2/other-repo/tags/list")
if [ "$HTTP_CODE" = "401" ]; then
log_info "TEST 10 PASSED: Access to unauthorized repo correctly rejected (HTTP $HTTP_CODE)"
else
log_error "TEST 10 FAILED: Expected 401 for unauthorized repo, got HTTP $HTTP_CODE"
exit 1
fi
# =============================================================================
# SUMMARY
# =============================================================================
log_info "=========================================="
log_info "All AWS Secrets Manager Bearer Auth tests PASSED!"
log_info "=========================================="
log_info ""
log_info "Options:"
log_info " --use-real-aws Use real AWS (default: LocalStack)"
log_info " --skip-build Skip building zot"
log_info " --keep-resources Keep resources after exit"
log_info " --region REGION AWS region (default: us-east-1)"