2 min read

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

  1. Password Protection: Enable requirepass in redis.conf.
  2. Network Binding: Bind Redis to 127.0.0.1 or a private VPC IP. Never expose it to 0.0.0.0 without a firewall.
  3. 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

  1. Delete Guest: The default guest/guest user has full admin rights. Delete it.
  2. Virtual Hosts: Isolate environments using vhosts.
  3. 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.

programming/python/celery