Skip to content

Encryption

Mauricio Gomes edited this page Jun 25, 2026 · 2 revisions

Senna can encrypt job arguments before they are written to Redis or Valkey. This is useful for sensitive payloads such as identifiers, payment references, or other data that should not be visible in queue storage.

How It Works

Encrypted jobs use AES-GCM with a random nonce for each job. Senna serializes the argument map, encrypts it, and stores the ciphertext under the _encrypted argument key. The worker decrypts the arguments before calling the handler.

Only job arguments are encrypted. Job metadata such as job type, queue, retry count, schedule time, batch ID, and unique key remains visible because workers need those fields for routing and reliability.

Key Requirements

Use an AES key length of 16, 24, or 32 bytes. Prefer 32 bytes for AES-256.

Store keys in a secret manager or environment variable. Never commit encryption keys to source control, logs, fixtures, or documentation examples.

The client and every worker that processes encrypted jobs must use the same key. If a worker does not have the matching key, it cannot decrypt the job arguments.

Client Setup

Decode the key from secure storage and configure the client:

rawKey := os.Getenv("SENNA_ENCRYPTION_KEY")
key, err := base64.StdEncoding.DecodeString(rawKey)
if err != nil {
    log.Fatal(err)
}

c, err := client.New(&client.Config{
    Redis:     redisConfig,
    Namespace: "myapp",
    Encryption: &senna.EncryptionSettings{
        Enabled: true,
        Key:     key,
    },
})
if err != nil {
    log.Fatal(err)
}

Then opt in per job:

_, err = c.Enqueue(ctx, "process_pii", map[string]any{
    "ssn":         "123-45-6789",
    "card_number": "4111111111111111",
}, client.WithEncryption())

WithEncryption requires the client to have encryption configured and enabled. Without that client configuration, enqueue returns client.ErrEncryptionUnavailable and no job is written.

Worker Setup

Configure workers with the same key:

w, err := worker.New(&worker.Config{
    Redis:     redisConfig,
    Namespace: "myapp",
    Encryption: &senna.EncryptionSettings{
        Enabled: true,
        Key:     key,
    },
})
if err != nil {
    log.Fatal(err)
}

w.Register("process_pii", func(ctx context.Context, job *senna.Job) error {
    ssn := job.Args["ssn"].(string)
    return processSSN(ctx, ssn)
})

Decryption happens before the handler runs. The handler sees the original argument map.

Batches

Batch jobs do not currently support client.WithEncryption. Batch.Add rejects encryption, unique-key, delay, schedule-at, and nested batch options so batch enqueue remains atomic and predictable.

Use encrypted standalone jobs for sensitive payloads, or store sensitive data in your own encrypted system and pass only a reference in the batch job arguments.

Key Rotation

Senna currently supports one active encryption key per client or worker. It does not store key IDs with jobs and does not try multiple keys during decryption.

To rotate keys without losing encrypted jobs:

  1. Stop enqueueing new encrypted jobs with the old key.
  2. Let encrypted queued, scheduled, and retry jobs that use the old key drain.
  3. Verify there are no remaining encrypted jobs that require the old key.
  4. Deploy clients and workers with the new key.
  5. Resume enqueueing encrypted jobs.

If a key is compromised and you cannot safely drain old encrypted jobs, discard or re-enqueue those jobs with a new key according to your incident process.

Operational Notes

  • Keep the key available to every worker queue that can receive encrypted jobs.
  • Rotate keys during a quiet period because old encrypted jobs cannot be read by workers configured only with the new key.
  • Encryption does not replace careful argument design. Prefer passing stable identifiers over large sensitive snapshots when possible.

Clone this wiki locally