Mercurial > repos > other > Puppet
changeset 327:38bb323e8231
Swap from Perl to Python for SPF checks
The Python script has a config, whereas Perl requires code
changes to accept relays as valid (or skipped)
author | IBBoard <dev@ibboard.co.uk> |
---|---|
date | Sat, 07 Mar 2020 14:31:09 +0000 |
parents | 63e0b5149cfb |
children | e48167ee504f |
files | modules/postfix/files/postfix-policyd-spf-perl modules/postfix/manifests/init.pp modules/postfix/templates/master.cf.epp modules/postfix/templates/policyd-spf.conf.epp |
diffstat | 4 files changed, 30 insertions(+), 405 deletions(-) [+] |
line wrap: on
line diff
--- a/modules/postfix/files/postfix-policyd-spf-perl Sat Mar 07 14:29:34 2020 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,396 +0,0 @@ -#!/usr/bin/perl - -# postfix-policyd-spf-perl -# http://www.openspf.org/Software -# version 2.010 -# -# (C) 2007-2008,2012 Scott Kitterman <scott@kitterman.com> -# (C) 2012 Allison Randal <allison@perl.org> -# (C) 2007 Julian Mehnle <julian@mehnle.net> -# (C) 2003-2004 Meng Weng Wong <mengwong@pobox.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -use version; our $VERSION = qv('2.010'); - -use strict; - -use IO::Handle; -use Sys::Syslog qw(:DEFAULT setlogsock); -use NetAddr::IP; -use Mail::SPF; -use Sys::Hostname::Long 'hostname_long'; - -# ---------------------------------------------------------- -# configuration -# ---------------------------------------------------------- - -my $resolver = Net::DNS::Resolver->new( - retrans => 5, # Net::DNS::Resolver default: 5 - retry => 2, # Net::DNS::Resolver default: 4 - # Makes for a total timeout for UDP queries of 5s * 2 = 10s. -); - -# query_rr_type_all will query both type TXT and type SPF. This upstream -# default is changed due to there being essentiall no type SPF deployment. -my $spf_server = Mail::SPF::Server->new( - dns_resolver => $resolver, - query_rr_types => Mail::SPF::Server->query_rr_type_txt, - default_authority_explanation => - 'Please see http://www.openspf.net/Why?s=%{_scope};id=%{S};ip=%{C};r=%{R}' -); - -# Adding more handlers is easy: -my @HANDLERS = ( - { - name => 'exempt_localhost', - code => \&exempt_localhost - }, - { - name => 'exempt_relay', - code => \&exempt_relay - }, - { - name => 'sender_policy_framework', - code => \&sender_policy_framework - } -); - -my $VERBOSE = 0; - -my $DEFAULT_RESPONSE = 'DUNNO'; - -# -# Syslogging options for verbose mode and for fatal errors. -# NOTE: comment out the $syslog_socktype line if syslogging does not -# work on your system. -# - -my $syslog_socktype = 'unix'; # inet, unix, stream, console -my $syslog_facility = 'mail'; -my $syslog_options = 'pid'; -my $syslog_ident = 'postfix/policy-spf'; - -use constant localhost_addresses => map( - NetAddr::IP->new($_), - qw( 127.0.0.0/8 ::ffff:127.0.0.0/104 ::1 ) -); # Does Postfix ever say "client_address=::ffff:<ipv4-address>"? - -use constant relay_addresses => map( - NetAddr::IP->new($_), - qw( ) -); # add addresses to qw ( ) above separated by spaces using CIDR notation. - -# Fully qualified hostname, if available, for use in authentication results -# headers now provided by the localhost and whitelist checks. -my $host = hostname_long; - -my %results_cache; # by message instance - -# ---------------------------------------------------------- -# initialization -# ---------------------------------------------------------- - -# -# Log an error and abort. -# -sub fatal_exit { - syslog(err => "fatal_exit: @_"); - syslog(warning => "fatal_exit: @_"); - syslog(info => "fatal_exit: @_"); - die("fatal: @_"); -} - -# -# Unbuffer standard output. -# -STDOUT->autoflush(1); - -# -# This process runs as a daemon, so it can't log to a terminal. Use -# syslog so that people can actually see our messages. -# -setlogsock($syslog_socktype); -openlog($syslog_ident, $syslog_options, $syslog_facility); - -# ---------------------------------------------------------- -# main -# ---------------------------------------------------------- - -# -# Receive a bunch of attributes, evaluate the policy, send the result. -# -my %attr; -while (<STDIN>) { - chomp; - - if (/=/) { - my ($key, $value) =split (/=/, $_, 2); - $attr{$key} = $value; - next; - } - elsif (length) { - syslog(warning => sprintf("warning: ignoring garbage: %.100s", $_)); - next; - } - - if ($VERBOSE) { - for (sort keys %attr) { - syslog(debug => "Attribute: %s=%s", $_ || '<UNKNOWN>', $attr{$_} || '<UNKNOWN>'); - } - }; - - my $message_instance = $attr{instance}; - my $cache = defined($message_instance) ? $results_cache{$message_instance} ||= {} : {}; - - my $action = $DEFAULT_RESPONSE; - - foreach my $handler (@HANDLERS) { - my $handler_name = $handler->{name}; - my $handler_code = $handler->{code}; - - my $response = $handler_code->(attr => \%attr, cache => $cache); - - if ($VERBOSE) { - syslog(debug => "handler %s: %s", $handler_name || '<UNKNOWN>', $response || '<UNKNOWN>'); - }; - - # Pick whatever response is not 'DUNNO' - if ($response and $response !~ /^DUNNO/i) { - if ($VERBOSE) { - syslog(info => "handler %s: is decisive.", $handler_name || '<UNKNOWN>'); - } - $action = $response; - last; - } - } - - syslog(info => "Policy action=%s", $action || '<UNKNOWN>'); - - STDOUT->print("action=$action\n\n"); - %attr = (); -} - -# ---------------------------------------------------------- -# handler: localhost exemption -# ---------------------------------------------------------- - -sub exempt_localhost { - my %options = @_; - my $attr = $options{attr}; - if ($attr->{client_address} ne '') { - my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for localhost)" - if grep($_->contains($client_address), localhost_addresses); - }; - return 'DUNNO'; -} - -# ---------------------------------------------------------- -# handler: relay exemption -# ---------------------------------------------------------- - -sub exempt_relay { - my %options = @_; - my $attr = $options{attr}; - if ($attr->{client_address} ne '') { - my $client_address = NetAddr::IP->new($attr->{client_address}); - return "PREPEND Authentication-Results: $host; none (SPF not checked for whitelisted relay)" - if grep($_->contains($client_address), relay_addresses); - }; - return 'DUNNO'; -} - -# ---------------------------------------------------------- -# handler: SPF -# ---------------------------------------------------------- - -sub sender_policy_framework { - my %options = @_; - my $attr = $options{attr}; - my $cache = $options{cache}; - - # ------------------------------------------------------------------------- - # Always do HELO check first. If no HELO policy, it's only one lookup. - # This avoids the need to do any MAIL FROM processing for null sender. - # ------------------------------------------------------------------------- - - my $helo_result = $cache->{helo_result}; - - if (not defined($helo_result)) { - # No HELO result has been cached from earlier checks on this message. - - my $helo_request = eval { - Mail::SPF::Request->new( - scope => 'helo', - identity => $attr->{helo_name}, - ip_address => $attr->{client_address} - ); - }; - - if ($@) { - # An unexpected error occurred during request creation, - # probably due to invalid input data! - my $errmsg = $@; - $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "HELO check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '<UNKNOWN>', - $attr->{sender} || '<UNKNOWN>', $attr->{helo_name} || '<UNKNOWN>', - $errmsg || '<UNKNOWN>' - ); - }; - return; - } - - $helo_result = $cache->{helo_result} = $spf_server->process($helo_request); - } - - my $helo_result_code = $helo_result->code; # 'pass', 'fail', etc. - my $helo_local_exp = nullchomp($helo_result->local_explanation); - my $helo_authority_exp = nullchomp($helo_result->authority_explanation) - if $helo_result->is_code('fail'); - my $helo_spf_header = $helo_result->received_spf_header; - - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s, IP Address: %s, Recipient: %s", - $helo_result || '<UNKNOWN>', - $attr->{helo_name} || '<UNKNOWN>', $attr->{client_address} || '<UNKNOWN>', - $attr->{recipient} || '<UNKNOWN>' - ); - }; - - # Reject on HELO fail. Defer on HELO temperror if message would otherwise - # be accepted. Use the HELO result and return for null sender. - if ($helo_result->is_code('fail')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '<UNKNOWN>', - $attr->{helo_name} || '<UNKNOWN>' - ); - }; - return "550 $helo_authority_exp"; - } - elsif ($helo_result->is_code('temperror')) { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO: %s", - $helo_result || '<UNKNOWN>', - $attr->{helo_name} || '<UNKNOWN>' - ); - }; - return "DEFER_IF_PERMIT SPF-Result=$helo_local_exp"; - } - elsif ($attr->{sender} eq '') { - if ($VERBOSE) { - syslog( - info => "SPF %s: HELO/EHLO (Null Sender): %s", - $helo_result || '<UNKNOWN>', - $attr->{helo_name} || '<UNKNOWN>' - ); - }; - return "PREPEND $helo_spf_header" - unless $cache->{added_spf_header}++; - } - - # ------------------------------------------------------------------------- - # Do MAIL FROM check (as HELO did not give a definitive result) - # ------------------------------------------------------------------------- - - my $mfrom_result = $cache->{mfrom_result}; - - if (not defined($mfrom_result)) { - # No MAIL FROM result has been cached from earlier checks on this message. - - my $mfrom_request = eval { - Mail::SPF::Request->new( - scope => 'mfrom', - identity => $attr->{sender}, - ip_address => $attr->{client_address}, - helo_identity => $attr->{helo_name} # for %{h} macro expansion - ); - }; - - if ($@) { - # An unexpected error occurred during request creation, - # probably due to invalid input data! - my $errmsg = $@; - $errmsg = $errmsg->text if UNIVERSAL::isa($@, 'Mail::SPF::Exception'); - if ($VERBOSE) { - syslog( - info => "Mail From (sender) check failed - Mail::SPF->new(%s, %s, %s) failed: %s", - $attr->{client_address} || '<UNKNOWN>', - $attr->{sender} || '<UNKNOWN>', $attr->{helo_name} || '<UNKNOWN>', $errmsg || '<UNKNOWN>' - ); - }; - return; - } - - $mfrom_result = $cache->{mfrom_result} = $spf_server->process($mfrom_request); - } - - my $mfrom_result_code = $mfrom_result->code; # 'pass', 'fail', etc. - my $mfrom_local_exp = nullchomp($mfrom_result->local_explanation); - my $mfrom_authority_exp = nullchomp($mfrom_result->authority_explanation) - if $mfrom_result->is_code('fail'); - my $mfrom_spf_header = $mfrom_result->received_spf_header; - - if ($VERBOSE) { - syslog( - info => "SPF %s: Envelope-from: %s, IP Address: %s, Recipient: %s", - $mfrom_result || '<UNKNOWN>', - $attr->{sender} || '<UNKNOWN>', $attr->{client_address} || '<UNKNOWN>', - $attr->{recipient} || '<UNKNOWN>' - ); - }; - - # Same approach as HELO.... - if ($VERBOSE) { - syslog( - info => "SPF %s: Envelope-from: %s", - $mfrom_result || '<UNKNOWN>', - $attr->{sender} || '<UNKNOWN>' - ); - }; - if ($mfrom_result->is_code('fail')) { - return "550 $mfrom_authority_exp"; - } - elsif ($mfrom_result->is_code('temperror')) { - return "DEFER_IF_PERMIT SPF-Result=$mfrom_local_exp"; - } - else { - return "PREPEND $mfrom_spf_header" - unless $cache->{added_spf_header}++; - } - - return; -} - -# ---------------------------------------------------------- -# utility, string cleaning -# ---------------------------------------------------------- - -sub nullchomp { - my $value = shift; - - # Remove one or more null characters from the - # end of the input. - $value =~ s/\0+$//; - return $value; -} -
--- a/modules/postfix/manifests/init.pp Sat Mar 07 14:29:34 2020 +0000 +++ b/modules/postfix/manifests/init.pp Sat Mar 07 14:31:09 2020 +0000 @@ -166,14 +166,15 @@ } #SPF checking - file { '/usr/local/lib/postfix-policyd-spf-perl/': - ensure => directory + package { 'pypolicyd-spf': + ensure => installed; } - file { '/usr/local/lib/postfix-policyd-spf-perl/postfix-policyd-spf-perl': - source => 'puppet:///modules/postfix/postfix-policyd-spf-perl', - } - $perl_pkgs = [ 'perl', 'perl-NetAddr-IP', 'perl-Mail-SPF', 'perl-version', 'perl-Sys-Hostname-Long'] - package { $perl_pkgs: - ensure => installed, + -> + file { '/etc/python-policyd-spf/policyd-spf.conf': + content => epp('postfix/policyd-spf.conf', + { + 'fallback_relays' => $mailrelays, + } + ), } }
--- a/modules/postfix/templates/master.cf.epp Sat Mar 07 14:29:34 2020 +0000 +++ b/modules/postfix/templates/master.cf.epp Sat Mar 07 14:31:09 2020 +0000 @@ -151,7 +151,7 @@ # ${nexthop} ${user} policy unix - n n - 0 spawn - user=nobody argv=/usr/bin/perl /usr/local/lib/postfix-policyd-spf-perl/postfix-policyd-spf-perl + user=nobody argv=/usr/libexec/postfix/policyd-spf # # spam/virus section
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/postfix/templates/policyd-spf.conf.epp Sat Mar 07 14:31:09 2020 +0000 @@ -0,0 +1,20 @@ +<%- | + Optional[Array[Stdlib::Host]] $fallback_relays = [] + | +-%> +# For a fully commented sample config file see policyd-spf.conf.commented + +debugLevel = 1 +TestOnly = 1 + +HELO_reject = Fail +Mail_From_reject = Fail +<%- if size($fallback_relays) > 0 { -%> +Domain_Whitelist = <%= join(regsubst($fallback_relays, /^(mail|mx)[0-9]*\./, ""), ',') %> +<%- } -%> + +PermError_reject = False +TempError_Defer = False + +skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 +