Securing Celery for Production
Running Celery in production requires careful attention to security, as workers often execute code based on messages received from a network service. If compromised, a worker can lead to Remote Code Execution (RCE).
1. Never Run as Root
Rule #1: Never run the Celery worker as the root user. If a task is compromised, the attacker gains root access to your server.
Create a dedicated user for the application:
useradd -m -s /bin/bash celery_user
When using systemd or supervisord, specify this user in the configuration.
Example (systemd):
[Service]
User=celery_user
Group=celery_user
ExecStart=/path/to/venv/bin/celery -A proj worker
2. Avoid Pickle Serialization
By default, older versions of Celery used pickle for serialization. pickle is insecure because it allows arbitrary code execution during deserialization. If an attacker can inject a message into your broker, they can execute any code on your worker.
Solution: Use json (default in modern Celery) or msgpack.
Explicitly disable pickle in your configuration:
# settings.py or celery.py
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
3. Secure the Broker
The broker (Redis, RabbitMQ) is the communication hub. If it is exposed, anyone can queue tasks.
Redis
- Password Protection: Enable
requirepassinredis.conf. - Network Binding: Bind Redis to
127.0.0.1or a private VPC IP. Never expose it to0.0.0.0without a firewall. - TLS: Use
rediss://(note the extra 's') to encrypt traffic between the worker and the broker.
CELERY_BROKER_URL = 'rediss://:password@hostname:6379/0'
RabbitMQ
- Delete Guest: The default
guest/guestuser has full admin rights. Delete it. - Virtual Hosts: Isolate environments using vhosts.
- TLS: Configure RabbitMQ with SSL certificates and use
amqps://.
4. Input Validation
Treat task arguments as untrusted input. Even if the message comes from your own web app, a vulnerability there could lead to malicious payloads in Celery.
- Bad: Passing a raw shell command to a task.
- Good: Passing an ID and looking up the data in the database.
# UNSAFE
@app.task
def backup_files(cmd):
os.system(cmd)
# SAFE
@app.task
def backup_files(user_id):
user = User.objects.get(id=user_id)
# ... logic using user.path ...
5. Process Isolation
If you have tasks with different security levels (e.g., one processes public user uploads, another handles billing), run them on separate worker instances consuming from separate queues.
# Worker 1 (High Security)
celery -A proj worker -Q billing
# Worker 2 (Low Security)
celery -A proj worker -Q uploads
This limits the blast radius if a worker processing risky content is compromised.