Securing the Flower dashboard in production
This guide belongs to Flower for Celery monitoring and the wider topic of observability for job queues, and covers hardening a Flower deployment so it is safe to run beyond localhost.
Flower ships with no authentication. A default deployment exposes every task name, argument, result, and worker host to anyone who can reach the port — and the dashboard can revoke, terminate, and rate-limit live tasks. Engineers routinely bind it to 0.0.0.0:5555 "just to check something" and leave it reachable from the public internet, handing attackers a control plane over the queue. This page closes that gap with layered controls: authentication, TLS, a hardened reverse proxy, read-only operation, and network isolation.
Prerequisites
- A running Flower instance against a Celery app. If you have not configured Celery, see setting up Celery with a Redis broker.
- Flower 1.2+ (
pip install flower). - A reverse proxy (nginx assumed) and a TLS certificate, or cert-manager on Kubernetes.
- For OAuth: a Google Workspace or GitHub OAuth app with client ID and secret.
- Ability to set environment variables/secrets and, on Kubernetes, to apply
NetworkPolicy.
Step 1 — Bind to loopback and never expose the port directly
The single biggest mistake is publishing Flower's port. Bind Flower to localhost and let only the reverse proxy reach it. Everything else builds on this.
# Bind to loopback only; the proxy connects over localhost, the world cannot
celery -A celery_app flower \
--address=127.0.0.1 \
--port=5555 \
--url_prefix=flower # serve under /flower behind the proxy
In Docker or Kubernetes, do not map the container port to a host port or LoadBalancer. Expose Flower only through an internal ClusterIP Service that the ingress/proxy targets.
Step 2 — Enable authentication
Pick basic auth for a quick internal lock, or OAuth to tie access to your identity provider. OAuth is strongly preferred in production because it centralizes revocation and avoids shared passwords.
# Option A: HTTP basic auth (multiple users, colon-separated)
celery -A celery_app flower \
--basic_auth="ops:$(cat /run/secrets/flower_ops_pw),oncall:$(cat /run/secrets/flower_oncall_pw)"
# Option B: Google OAuth — restrict to a domain
export FLOWER_OAUTH2_KEY="your-client-id.apps.googleusercontent.com"
export FLOWER_OAUTH2_SECRET="your-client-secret"
export FLOWER_OAUTH2_REDIRECT_URI="https://queues.example.com/flower/login"
celery -A celery_app flower \
--auth=".*@example\.com$" \
--auth_provider=flower.views.auth.GoogleAuth2LoginHandler \
--url_prefix=flower
Never bake credentials into the image or command line history — read them from mounted secrets or environment variables as shown.
Step 3 — Terminate TLS at a hardened reverse proxy
Put nginx in front of Flower to terminate HTTPS, add security headers, and forward the WebSocket connection Flower uses for live updates. This is also where you can layer an IP allowlist.
# /etc/nginx/conf.d/flower.conf
server {
listen 443 ssl;
server_name queues.example.com;
ssl_certificate /etc/ssl/certs/queues.crt;
ssl_certificate_key /etc/ssl/private/queues.key;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "DENY" always;
location /flower/ {
# Defense in depth: allow only the office/VPN ranges
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
proxy_pass http://127.0.0.1:5555;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Flower's live updates need WebSocket upgrade
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
The proxy_http_version 1.1 plus Upgrade/Connection headers are mandatory — without them the dashboard loads but task tables never refresh.
Step 4 — Run Flower read-only for most users
Most viewers should never be able to revoke or terminate tasks. Flower exposes write actions through its API; block them at the proxy so the UI is observe-only while a small admin group keeps full control on a separate, more tightly scoped route.
# Inside the location /flower/ block: reject task-control API calls
location ~ ^/flower/api/task/(revoke|terminate|rate-limit) {
deny all; # read-only for everyone hitting this vhost
return 403;
}
Pair this with a network-level restriction so the underlying control endpoints are only reachable from the proxy, not from arbitrary pods or hosts.
Step 5 — Enforce network isolation
On Kubernetes, a NetworkPolicy ensures only the ingress controller can reach the Flower pod, so even a compromised neighbor pod cannot hit port 5555.
# flower-netpol.yaml — only the ingress namespace may reach Flower
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: flower-ingress-only
spec:
podSelector:
matchLabels:
app: flower
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 5555
Verification
Prove each layer works by trying to bypass it. Anonymous access should be refused, plaintext should be redirected, and write actions should be blocked.
# 1. No credentials -> 401 (auth is enforced)
curl -s -o /dev/null -w "%{http_code}\n" https://queues.example.com/flower/
# 2. Plain HTTP is rejected/redirected, not served
curl -s -o /dev/null -w "%{http_code}\n" http://queues.example.com/flower/
# 3. Authenticated read works
curl -s -u "ops:$PW" https://queues.example.com/flower/api/workers | python -m json.tool
# 4. Write action is blocked -> 403
curl -s -o /dev/null -w "%{http_code}\n" -u "ops:$PW" \
-X POST https://queues.example.com/flower/api/task/revoke/some-id
# 5. Direct port from outside the cluster is unreachable
nc -zv queues.example.com 5555 # expect connection refused/timeout
Codes 401, a redirect/301, 200 on the authenticated read, 403 on revoke, and a refused direct connection together confirm the controls are live.
Gotchas and edge cases
WebSocket upgrade missing behind the proxy. If the dashboard renders but tasks and workers never update live, the proxy is dropping the WebSocket upgrade. Add proxy_http_version 1.1 and the Upgrade/Connection headers (Step 3); this is the most common Flower-behind-nginx failure.
Persisted state is sensitive. Flower's --persistent=True writes task history (including arguments and results) to a local database file. Treat that file as secret data, store it on an encrypted volume, and never commit it. Task arguments often contain tokens or PII.
Basic auth credentials in process listings. Passing --basic_auth inline leaks the password to anyone who can run ps on the host or read shell history. Source it from a mounted secret or environment variable, and prefer OAuth so there is no shared secret to leak.
url_prefix mismatch breaks OAuth redirects. If --url_prefix=flower does not match the proxy location and the OAuth redirect_uri, login loops or 404s on callback. Keep the prefix, the nginx path, and the registered redirect URI identical.
Related
- Flower for Celery monitoring — what Flower exposes and how to operate it day to day.
- Observability and monitoring for job queues — how a secured dashboard fits with metrics, alerting, and tracing.
- Instrumenting Celery with a Prometheus exporter — pair Flower's live view with scrapeable, alertable metrics.
- Celery architecture and configuration — the broker and worker setup Flower observes.