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_key loads a PEM public key, and .public_key_set builds a JWT::JWK::Set from several PEM files. Purely additive; HS256 consumers (Workhorse, KAS, Pages, Knowledge Graph) are unchanged.
  • decode_jwt gains an additive jwks: option. When present, the verification key is selected from the set by the token's kid header.
  • Gitlab::MailRoom::Authenticator pins 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.pem

About the kid and key rotation

  • Rails is given a list of valid public keys (public_key_files). It does not assign kids; each key's kid is its RFC 7638 thumbprint, derived deterministically from the key itself.
  • The mail_room service (the signer, configured outside Rails) puts the kid of the key it signed with into the JWT header. On verification, JWT.decode reads that kid and selects the matching public key from the set. A token whose kid is 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 from public_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)

Edited by Heinrich Lee Yu

Merge request reports

Loading
Loading