Password Spraying on Docker
How Password Spraying Manifests in Docker
Password spraying is a brute‑force variant where an attacker tries a small set of common passwords against many usernames, staying below typical account lockout thresholds. In Docker environments this technique often surfaces when Docker‑related services are exposed without proper authentication or when containers run weak credential checks.
One common vector is an unauthenticated Docker Engine API. If the daemon is started with -H tcp://0.0.0.0:2375 (or the equivalent "hosts": ["tcp://0.0.0.0:2375"] in daemon.json) and TLS verification is disabled, anyone who can reach the port can issue Docker CLI commands. An attacker can enumerate usernames by attempting to log in to the Docker registry (docker login) using a list of common passwords, hoping to find a valid credential pair before any lockout triggers.
Another vector is containers that expose internal services (e.g., a web app, SSH, or a database) with hard‑coded or easily guessable credentials. For example, a Dockerfile that copies a credentials file into the image or sets environment variables with weak passwords:
# Dockerfile (insecure)
FROM python:3.11-slim
ENV APP_USER=admin
ENV APP_PASSWORD=Password123 # weak, guessable
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
If the container publishes port 8000 without additional authentication, an attacker can spray passwords against the login endpoint, trying admin with many common passwords across many discovered usernames (e.g., from user enumeration).
Finally, misconfigured Docker registries (like Harbor or a plain Docker Registry v2) that lack rate limiting or account lockout can be sprayed directly via the /v2/_catalog or authentication endpoints.
Docker‑Specific Detection — How to Identify This Issue, Including Scanning with middleBrick
Detecting password spraying in Docker contexts requires looking for exposed authentication surfaces, missing rate‑limiting controls, and weak credential storage. middleBrick’s unauthenticated black‑box scan checks the external attack surface of any submitted URL, which includes Docker‑specific endpoints when they are reachable over HTTP/HTTPS.
When you submit a URL that points to a Docker daemon API (http://host:2375/version), a Docker Registry (https://registry.example.com/v2/), or a containerized web service, middleBrick runs 12 parallel checks. Among them, the Authentication and Rate Limiting tests are most relevant:
- Authentication test attempts to access protected endpoints without credentials and notes whether a 401/403 is returned or whether the service leaks information that aids user enumeration (e.g., distinct error messages for invalid user vs. invalid password).
- Rate Limiting test sends a burst of login‑like requests (e.g., POST to
/auth/login) and measures whether the service enforces throttling or temporary bans after a configurable number of failures.
If the Docker daemon API is exposed without TLS, middleBrick will report a finding under the Authentication category: "Docker Engine API accessible without TLS verification – potential for unauthenticated command execution and credential spraying."
For a containerized application, the scanner might discover a login endpoint (POST /api/login) that returns different HTTP status codes or response bodies for "user not found" versus "wrong password". This distinction enables an attacker to enumerate valid usernames before spraying passwords, a classic precursor to password spraying.
middleBrick also checks for missing security headers and exposed debug endpoints that could leak environment variables (including Docker secrets or credential files) – another avenue where weak credentials might be inadvertently disclosed.
Thus, running middlebrick scan https://your‑api.example.com (or using the CLI in a CI step) will surface these Docker‑specific authentication weaknesses, giving you a concrete starting point for remediation.
Docker‑Specific Remediation — Code Fixes Using Docker’s Native Features/Libraries
Remediation focuses on three areas: securing the Docker daemon, strengthening container‑based authentication, and ensuring credential handling follows Docker best practices.
1. Protect the Docker Engine API Never expose the daemon socket over TCP without TLS. If remote access is required, enforce mutual TLS authentication.
Example /etc/docker/daemon.json with TLS verification:
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tlsverify": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem"
}
After updating, reload the daemon: sudo systemctl reload docker. This forces any client to present a valid certificate, eliminating unauthenticated access that could be used for password spraying.
2. Avoid hard‑coded or weak credentials in images
Use Docker Secrets (for Swarm) or build‑time secrets (--secret with BuildKit) to keep passwords out of image layers.
Example Dockerfile using a build‑time secret for a database password:
# syntax=docker/dockerfile:1.4
FROM postgres:15
# Secret is made available at /run/secrets/db_password
RUN chown -R postgres:postgres /var/lib/postgresql/data
USER postgres
ENV POSTGRES_USER=appuser
# Password is read from the secret file at runtime
ENV POSTGRES_PASSWORD_FILE=/run/secrets/db_password
# Entrypoint will read the file and set POSTGRES_PASSWORD automatically
Build with: DOCKER_BUILDKIT=1 docker build --secret id=db_password,src=./db_password.txt -t myapp .
The password never appears in the image history, reducing the chance that an attacker can extract it from a pulled image.
3. Enforce strong authentication and rate limiting inside containers If your container runs a web application, implement proper password policies, multi‑factor authentication, and account lockout after a small number of failed attempts. For example, using Flask‑Login with Flask‑Limiter:
from flask import Flask, request
from flask_login import LoginManager, UserMixin, login_user
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
login_manager = LoginManager(app)
limiter = Limiter(key_func=get_remote_address, default_limits=["5 per minute"])
class User(UserMixin):
def __init__(self, username):
self.id = username
@login_manager.user_callback
def load_user(user_id):
# lookup user in DB
return User(user_id) if user_id in db else None
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
username = request.form.get('username')
password = request.form.get('password')
user = db.get_user(username)
if user and user.check_password(password):
login_user(user)
return 'OK', 200
# Same generic failure message to avoid user enumeration
return 'Invalid credentials', 401
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
This code returns an identical error message for both unknown users and wrong passwords, thwarting username enumeration, and limits login attempts to five per minute per IP address, directly mitigating password spraying.
By combining daemon TLS protection, secret‑based credential handling, and application‑level rate limiting with generic error responses, you close the primary Docker‑specific avenues that password spraying relies on.
Frequently Asked Questions
Can middleBrick detect if my Docker daemon is exposed without TLS?
Is it safe to store passwords as environment variables in a Dockerfile?
docker history or by inspecting the image. The safer approach is to use build‑time secrets (--secret) or Docker Secrets at runtime, ensuring the credential never persists in the image.