Support asymmetric JWT auth for mail_room mailboxes
What does this MR do?
Adds an opt-in asymmetric (ES256) JWT verification path for the internal mail_room API, so the cells mail_room service can authenticate without sharing a forge-capable secret across cells.
Today authentication is HS256 (symmetric): the signer and verifier share the same secret, so any cell able to verify a token can also forge one. In a cells topology a leak from a single cell would let an attacker forge mail_room tokens against every cell's ingestion endpoint.
How
Gitlab::JwtAuthenticatable.read_public_keyloads a PEM public key, and.public_key_setbuilds aJWT::JWK::Setfrom several PEM files. Purely additive; HS256 consumers (Workhorse, KAS, Pages, Knowledge Graph) are unchanged.decode_jwtgains an additivejwks:option. When present, the verification key is selected from the set by the token'skidheader.Gitlab::MailRoom::Authenticatorpins the algorithm per-mailbox from server-side configuration (secret_algorithm) and never reads it from the token header, avoiding algorithm-confusion. Asymmetric mailboxes verify against the configured public key set; symmetric mailboxes are unchanged.
How is this configured?
Asymmetric mailboxes are configured under their own keys in
gitlab.yml. Rails (the verifier) only needs the public keys; the
mail_room service holds the matching private key and signs with it.
incoming_email:
enabled: true
address: "incoming+%{key}@gitlab.example.com"
# One or more PEM-encoded public keys. Rails trusts ALL of them at once,
# which is what makes rotation seamless.
public_key_files:
- /etc/gitlab/mailroom/incoming_email_v2_pub_current.pem
- /etc/gitlab/mailroom/incoming_email_v2_pub_previous.pemAbout the kid and key rotation
- Rails is given a list of valid public keys (
public_key_files). It does not assignkids; each key'skidis its RFC 7638 thumbprint, derived deterministically from the key itself. - The mail_room service (the signer, configured outside Rails) puts the
kidof the key it signed with into the JWT header. On verification,JWT.decodereads thatkidand selects the matching public key from the set. A token whosekidis not in the set is rejected. - Rotation is therefore: add the new public key to
public_key_files(Rails now trusts old + new) → switch the signer to the new private key → once no tokens are signed with the old key, drop it frompublic_key_files. No hard cutover, no downtime.
So to answer the configuration questions directly: in Rails you simply
provide an array of valid public keys; the kid is computed from each
key and matched automatically. Assigning/emitting the kid is the signer's
responsibility (the mail_room service), configured outside Rails.
Scope
- Verify side only (this repo). The mail_room service signing side (private
key handling, emitting
kid) lands separately. - Asymmetric mailboxes are opt-in via uncommitted
gitlab.yml; defaults, the example config, and docs are intentionally unchanged for now and will be added once the approach is validated.
Resolves #604265 (closed)