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
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
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
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).