Symfony 7.4 introduced signed messages in the Messenger component, so a forged message injected into your queues can't trigger sensitive handlers, like the ones that run commands or processes. Symfony 8.2 moves signing from handlers to transports and lets you rotate the secrets used to sign messages and other data.

Signing All Messages of a Transport

Nicolas Grekas
Contributed by Nicolas Grekas in #66505

In Symfony 7.4, you enable signing in the handlers that need it (e.g. with #[AsMessageHandler(sign: true)]). When a message arrives, Symfony must read its type to know if its handler requires a signature, which means reading unverified data and autoloading the class named in it.

Symfony 8.2 adds a sign option to transports. All messages sent to these transports are signed, and received messages without a valid signature are refused before reading anything from them:

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed

        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                sign: true
            failed:
                dsn: 'doctrine://default?queue_name=failed'
                sign: true

The failure transport is the only signing transport that also accepts messages from transports that don't sign, so you can still retry them. However, handlers that require a signature (see next section) refuse them.

Keep in mind that:

  • The failure transport and the outbox of a signing transport must sign their messages too (otherwise, the container doesn't compile);
  • Messages queued before enabling this option have no valid signature, so they are refused. Enable it only on transports with no pending messages.

Deprecated Signing per Handler

Nicolas Grekas
Contributed by Nicolas Grekas in #66898

In Symfony 7.4, the sign option of a handler makes Symfony sign the messages handled by it on any transport. Symfony 8.2 deprecates this behavior because it requires reading the message type before checking the signature (as explained above).

The handler option stays, but in Symfony 9.0 it will no longer sign messages. Instead, the handler will only accept messages received from a signing transport or handled synchronously.

Symfony enables this option in the handlers of RunCommandMessage, RunProcessMessage and ServiceCallMessage. If you handle these messages asynchronously, route them to a signing transport:

1
2
3
4
5
6
7
8
9
10
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async_signed:
                dsn: '%env(MESSENGER_SIGNED_TRANSPORT_DSN)%'
                sign: true

        routing:
            Symfony\Component\Console\Messenger\RunCommandMessage: async_signed

You can also enable sign in your existing transport (and its failure transport), but only once their queues have no pending messages. If you route these messages to a transport that doesn't sign, Symfony triggers a deprecation when compiling the container.

Rotating Signing Secrets

Nicolas Grekas
Contributed by Nicolas Grekas in #66506

Changing the kernel.secret parameter has many side effects: users logged in with remember-me cookies are logged out, signed URIs and login links that were already sent stop working, and the signed messages waiting in your queues are refused. It also changes all the IDs returned by Uuid47Transformer, which can't be rotated at all.

Symfony 8.2 keeps kernel.secret as the stable secret of your application and gives each feature that signs data its own option to set the secret. These options accept a list of secrets: the first one signs new data and all of them verify signatures. They default to kernel.secret, so nothing changes unless you configure them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# config/packages/framework.yaml
framework:
    uri_signer:
        secret: ['%env(URI_SIGNER_SECRET)%', '%kernel.secret%']

    messenger:
        serializer:
            signing_secret: ['%env(MESSENGER_SIGNING_SECRET)%', '%kernel.secret%']

# config/packages/security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: ['%env(REMEMBER_ME_SECRET)%', '%kernel.secret%']
            login_link:
                # ...
                secret: ['%env(LOGIN_LINK_SECRET)%', '%kernel.secret%']

Messages are signed again each time they're retried and remember-me cookies each time they log a user in, so they move to the new secret on their own. Remove the previous secret once nothing signed with it is in use (e.g. after the signed URIs and login links sent with it have expired).

Published in #Living on the edge