Replica: feat(dkim): make key size configurable and allow keys to be rotated - #90
Open
lucaforni wants to merge 16 commits into
Open
Replica: feat(dkim): make key size configurable and allow keys to be rotated#90lucaforni wants to merge 16 commits into
lucaforni wants to merge 16 commits into
Conversation
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The app-wide CSP already blocks inline script execution, but the HTML preview iframe for a stored email was same-origin and un-sandboxed, and the html_raw response had no per-action hardening. Add a sandbox on the iframe and tighten the CSP on html_raw to script-src 'none' with nosniff and no-referrer so the preview has defence in depth against a future CSP bypass or regression. Relates to GHSA-f6g9-8555-cw28.
The /img/<server>/<message> endpoint accepted a src=<url> query parameter and proxied the body of that URL back to the caller. Nothing in the codebase ever produces a src= parameter — the parser only inserts a plain tracking pixel and rewrites href links — so this branch is dead code inherited from the original AppMail import. Drop the src branch: requests with src now return 400. The no-src path that serves the tracking pixel and records loads is unchanged, and a spec covers both the pixel-serving path and the removed branch.
The endpoint and domain option helpers interpolated model attributes straight into an HTML string before marking the whole buffer html_safe. Wrap the interpolations in h() so untrusted attributes can't break out of the surrounding tag. Also stop the helpers glob in rails_helper from eagerly requiring _spec.rb files so helper specs can live under spec/helpers/, and add a small application helper spec covering the escape behaviour.
url_with_return_to only checked that return_to started with a forward slash, which also allowed protocol-relative values like //host and /\host. Rails 7.1 already refuses to follow those via redirect_to, so the user just saw a 500. Reject the same shapes in the helper instead so we fall back to the default URL cleanly. Adds a sessions request spec covering the rejected shapes plus the happy-path relative redirect.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…rfpg-3xr5) The Legacy API message lookup endpoints parsed the request body as JSON and passed the `id` parameter straight through to the message database. A JSON object supplied for `id` arrived as a Ruby Hash and was used as a raw set of SQL `WHERE` conditions. `hash_to_sql` interpolated each Hash key directly inside backtick identifier quoting while escaping only the value, so a key containing a backtick could break out of the identifier and inject arbitrary SQL into the SELECT (blind, time-based) against the message database. Fixes: - Escape all identifiers (columns, tables, database names) through a new `escape_identifier` helper that wraps in backticks and doubles embedded backticks. Applied across hash_to_sql, select, insert, insert_multi, update and delete so no caller can inject via an identifier. - Validate the Legacy API `id` parameter at the controller boundary: reject any non-scalar value before it reaches the database and coerce it to an integer. Internal Hash-based lookups (e.g. tracking middleware) are unaffected. Adds regression tests at the unit (hash_to_sql / escape_identifier) and request (legacy messages/deliveries) levels.
Webhook and HTTP message endpoint deliveries both flow through Postal::HTTP, which parsed the user-supplied URL and connected to its host with no address validation. An authenticated user could point a webhook or endpoint at a private, loopback or link-local address (e.g. 127.0.0.1, 169.254.169.254 cloud metadata, RFC1918 hosts) and make the server issue requests into its own internal network. Add Postal::HTTP::AddressGuard, which resolves the destination host and rejects private/loopback/link-local/reserved/multicast IPv4 and IPv6 addresses, then pins the connection to the validated address so it cannot be redirected via a DNS-rebinding race. Administrators can permit specific destinations via the new postal.allowed_request_destinations config option (hostnames or IP/CIDR ranges). Address selection only uses families this server can actually reach so we do not pin to an IPv6 address on a host without IPv6 connectivity; IPv4 is preferred for predictability. HTTPEndpoint now validates that its URL is a well-formed HTTP(S) URL with a host.
The spec relied on the test machine having real IPv6 connectivity, which GitHub Actions runners do not have.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Postal generated 1024-bit DKIM keys with the size hardcoded, and offered no way to replace an existing key. 1024-bit RSA no longer meets current guidance (RFC 8301 recommends 2048 for signers). Key size is now set by a new `dns.dkim_key_size` option which defaults to 2048 and accepts 1024, 2048, 3072 or 4096. An invalid value is rejected when config is loaded. Existing keys can be replaced from the DNS setup page. Replacing a key which is currently verified and signing mail would break DKIM the moment the key changed, so the new key is instead held as a pending key under a new selector while the existing key carries on signing. Both records are shown on the setup page, and the new key is promoted automatically once its record is detected - either by the hourly DNS check or the existing "check my records" button. If the current record was never verified there is nothing to protect, so the key is replaced immediately and any pending change is abandoned. Requesting another key while a change is already in progress is refused, since it would orphan a record the user may have already published. The domain list flags domains with a pending key so a change in progress is visible without opening each domain.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Questa PR replica la PR originale: postalserver#3636
Autore originale: @adamcooke
Branch originale:
dkim-key-size-and-rotationRepository originale: postalserver/postal
Background
DKIM keys were generated with the size hardcoded to 1024 bits (
Domain#generate_dkim_key), and there was no way to replace a key once created. 1024-bit RSA no longer meets current guidance — RFC 8301 recommends 2048 for signers and requires verifiers to support 1024–4096.Configurable key size
New
dns.dkim_key_sizeoption (env:DNS_DKIM_KEY_SIZE), defaulting to 2048 and accepting 1024, 2048, 3072 or 4096. Anything else is rejected when config is loaded rather than failing later at key generation. Config docs are regenerated.Existing installations are unaffected until they choose to rotate — current 1024-bit keys keep working.
Rotating a key without breaking deliverability
Replacing a verified key in place would break DKIM the instant the key changed, because the published DNS record would no longer match the signing key. So regeneration of a verified key uses a pending-key flow:
pending_dkim_*columns. The existing key keeps signing mail.CheckAllDNSScheduledTaskor the existing Check my records button — the pending key is promoted automatically and the user is told they can keep the old record published while messages signed with it may still be queued or held.If the current record was never verified there is nothing to protect, so the key is replaced immediately and any in-flight change is abandoned.
Requesting another key while a change is already in progress is refused, as it would orphan a record the user may already have published. Cancelling a pending change is supported.
No change was needed to the signing path:
DKIMHeaderonly uses the domain key whendkim_status == "OK", and the active key and selector stay valid throughout.UI
The DNS setup page labels each record (
Current/New) rather than describing both as "a new TXT record to add", and replaces the green "looks good" banner with a "key change in progress" notice while a step is outstanding. The domain list flags domains with a pending key so a rotation in progress is visible without opening each domain.Notes for reviewers
db/schema.rbpicks up anActiveRecord::Schema[7.0]→[7.1]header bump, a consequence of the Rails 7.1 upgrade already on main rather than anything in this branch.domainsare reordered by the project's annotate hook when the migration runs.Resolvrejoins split TXT strings, so the exact-match check still passes; the config option documents this.Testing
39 new examples across model, request and view specs covering both regeneration paths, promotion on DNS check, cancellation, the already-in-progress guard, key size configurability, and both states of both pages. Full suite passes apart from
spec/lib/dns_resolver_spec.rb:251, which queries live DNS and fails on main for the same reason.