spf-discuss
[Top] [All Lists]

SRS integration with qmail (was: A couple of thoughts)

2004-02-15 16:56:42
OK, here we go.  This is my second implementation of "SRS integration
with qmail".  I believe Shevek will find this one less objectionable. ;-)

PURPOSE:

  The purpose of this project is to prevent being "spammed" by bounce
  messages.  That is, I should not have to read any alleged bounce
  messages which aren't actually responses to messages that I have sent
  out.  Ideally, this will avoid wasting my time reading both spam that
  is sent with an empty envelope sender in the first place, and also
  bounces which are sent to me by innocent "joe-jobbed" MTAs which are
  not yet SPF-enabled, when a spammer forges my address as the envelope
  sender.

BACKGROUND:

  First, we must understand exactly what the problem is.  SMTP specifies
  that bounce messages (responses to the sender of undeliverable mail)
  should be sent with an empty envelope sender, to avoid bounce loops.
  This means that "spam" (unsolicited bulk/commercial e-mail) can be
  sent this way:

    spammer
    | MAIL FROM:<>
    V RCPT TO:<greg(_at_)wooledge(_dot_)org>
    wooledge.org

  There's no way for the MTA at wooledge.org to know whether this is a
  legitimate bounce message (a reply to a previous outgoing message).
  Schemes like SPF which validate the envelope sender address can't
  be used here either, because there is no sender address to validate.

  The Sender Rewriting Scheme (SRS) allows me to work around this.
  By changing my own envelope sender address on all of my outgoing
  messages, I can reject (or filter) any incoming bounce messages which
  are not legitimate responses.  The criteria are simply: any incoming
  messages with an empty envelope sender which go to my main e-mail
  address, greg(_at_)wooledge(_dot_)org, are spam.  Real bounces will go to an
  SRS address instead, and will then be forwarded to my real address.
  For example:

    legitimate bounce
    | MAIL FROM:<>
    V RCPT TO:<SRS0=HHHHHHHH=TT=wooledge(_dot_)org(_at_)Xê?µ/?ï®Br'ý
    wooledge.org
    | Run SRS validation on the address.
    V Hash HHHHHHHH must be valid, and timestamp TT must be recent.
    Forward to greg(_at_)wooledge(_dot_)org(_dot_)

    spam
    | MAIL FROM:<>
    V RCPT TO:<greg(_at_)wooledge(_dot_)org>
    wooledge.org
    | Empty envelope sender!
    V
    Drop it in the spam folder.

  This scheme does not rely on any external cooperation whatsoever.  I
  can do it simply on my own mail system(s), and it works immediately.

IMPLEMENTATION:

  This implementation is done in small pieces.  I will describe each one
  in turn.  Alternate implementations are of course also possible; in
  particular, patching qmail to include "native" SRS support would make
  this a more efficient operation.  However, this implementation works
  right now.

  First, I installed the Mail::SRS software (a perl module from CPAN).
  I have safecat installed already (provides the maildir command).

  Next, I set up a virtual domain called "srs.wooledge.org" which will
  handle all of the incoming messages with SRS addresses.  This requires
  an MX record in DNS, and the following configuration in qmail on my
  qmail server:

    * Add srs.wooledge.org to control/rcpthosts.
    * Add srs.wooledge.org:srs to control/virtualdomains.
    * Add |bin/srsfilter to alias/.qmail-srs-default.
    * Create a Maildir for the alias user (don't forget to chown it).
    * Write the following script to alias/bin/srsfilter (and don't forget
      to chmod +x it):

      #!/bin/sh
      PATH=/var/qmail/bin:/usr/local/bin:/usr/bin:/bin
      QMAILSUSER=MAILER-DAEMON
      QMAILSHOST=wooledge.org
      export PATH QMAILSUSER QMAILSHOST

      ad=`perl -e 'use Mail::SRS;
                   my $s = new Mail::SRS(
                       Secret => "'"\`cat $HOME/.srs-secret\`"'",
                       MaxAge => 10, HashLength => 8);
                   print $s->reverse("'"$EXT2"'\(_at_)wooledge(_dot_)org")' 
2>/dev/null`
      if [ -n "$ad" ]; then
          qmail-inject "$ad"
      else
          maildir ./Maildir/
      fi

    * Create alias/.srs-secret which will contain a "password" (secret)
      for generating and validating SRS addresses.  This can be anything
      you want.  I used a base-64 encoding of 16 bytes from /dev/random.
      I also used "chmod 600" on it.

  Notes on the shell script above:

    * I used qmail-inject instead of forward so that I could use the
      qmail-inject environment variables (QMAILSUSER, QMAILSHOST).
      This is necessary so that when the message is forwarded to my
      real address, it will have a non-empty envelope sender.

    * Mail::SRS's "reverse" method requires an address with an "@" sign
      and domain on it, so I had to append "@wooledge.org" to the SRS
      address.

    * If the Mail::SRS "reverse" fails, the output will be empty (because
      stderr is discarded).  Any such message will not be forwarded; it
      will be delivered to the Maildir instead.  You could choose to
      forward it to a different address, or to delete it, instead.  Also
      note that arbitrary random addresses within the srs.wooledge.org
      domain will not be valid SRS addresses, so we will *not* be an
      open SRS relay.

    * The hash length and maximum age are not the default values; choose
      whatever values suit your own needs.

    * The .srs-secret file shouldn't contain any characters that will
      cause the shell script quoting to blow up.  See "FINAL NOTES" below.

  That takes care of getting valid SRS bounces to me, and puts a valid
  envelope sender on them (MAILER-DAEMON(_at_)wooledge(_dot_)org).  The next 
piece
  "discards" all other bounces to my real address 
(greg(_at_)wooledge(_dot_)org):

    * Put |bin/spamcheck in ~greg/.qmail.
    * Create maildirs named Maildir and Spamdir (if not already done).
    * Write the following script to ~greg/bin/spamcheck (and don't forget
      to chmod +x):

      #!/bin/sh

      PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
      TMP=/tmp/spamcheck.$$
      trap 'rm -rf $TMP; exit 111' 1 2 3 15
      spamc >$TMP
      if grep -q '^X-Spam-Flag: YES$' $TMP; then
        maildir Spamdir <$TMP
        rm -f $TMP
        exit 99
      else
        if [ -z "$SENDER" ]; then
          maildir Spamdir <$TMP
          rm -f $TMP
          exit 99
        else
          maildir Maildir <$TMP
          rm -f $TMP
          exit 0
        fi
      fi

  Notes on the preceding script:

    * I'm also running spamassassin, so I already had a script in place
      which sent all messages through spamc for validation, and then
      sent them either to my normal mailbox (Maildir) or to my special
      "spam" mailbox (Spamdir).  I simply extended that to write all
      messages with an empty "SENDER" variable to Spamdir as well.  If
      you're doing this from the ground up with no preexisting .qmail
      file, then a *much* simpler script will suffice.  The following
      example is UNTESTED:

        #!/bin/sh
        if [ -z "$SENDER" ]; then
          maildir Spamdir
          exit 99
        else
          maildir Maildir
        fi

  Finally, I needed a way to change my envelope sender on a per-outgoing-
  message basis.  I'm using mutt as my MUA, so this was easy using mutt's
  advanced flexibility.  Users of other MUAs will have to find their own
  way to achieve the same thing.  I did this:

    * Copy .srs-secret from ~alias on the mail server to ~greg on the
      box where mutt runs.  (And chmod 600.)
    * Put this in .muttrc:

      set sendmail=$HOME/bin/sendmailwrapper

    * Write the following script to ~greg/bin/sendmailwrapper (and don't
      forget to chmod +x):

      #!/bin/sh

      QMAILSUSER=`perl -e 'use Mail::SRS;
          my $s = new Mail::SRS(Secret => "'"\`cat $HOME/.srs-secret\`"'",
            MaxAge => 10, HashLength => 8);
          my $a = $s->forward("greg\(_at_)wooledge(_dot_)org");
          $a =~ s/\(_at_)$//; print $a' 2>/dev/null`
      QMAILSHOST=srs.wooledge.org
      export QMAILSUSER QMAILSHOST
      exec /usr/sbin/sendmail "$@"

  Note that the HashLength and the secret used here must match the ones
  used on the qmail side, so that the SRS addresses will validate properly.
  As you can see, I'm also using QMAILSUSER and QMAILSHOST here, so of
  course this means that I'm running qmail on the box where mutt runs.
  If your client-side MTA is not qmail, then this will have to be adjusted
  as well.

FINAL NOTES:

  I hate using multiple levels of quoting in shell scripts. :(  But I
  didn't want to write the whole thing in perl.

-- 
Greg Wooledge                  |   "Truth belongs to everybody."
greg(_at_)wooledge(_dot_)org              |    - The Red Hot Chili Peppers
http://wooledge.org/~greg/     |