From 6be613d07ddc6d0b1e4b73f93c0fa1c0b1f7ba10 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 24 Nov 2013 03:53:39 +0100 Subject: Postfix master (nullmailer) configuration We use a dedicated instance for each role: MDA, MTA out, MX, etc. --- ansible.cfg | 2 +- common.yml | 8 +++ lib/postmap | 94 +++++++++++++++++++++++++++ lib/postmulti | 84 ++++++++++++++++++++++++ roles/common/files/etc/postfix/generic.pcre | 1 + roles/common/files/etc/postfix/master.cf | 35 ++++++++++ roles/common/handlers/main.yml | 7 ++ roles/common/tasks/ipsec.yml | 5 +- roles/common/tasks/mail.yml | 62 ++++++++++++++++++ roles/common/tasks/main.yml | 1 + roles/common/templates/etc/postfix/main.cf.j2 | 57 ++++++++++++++++ site.yml | 7 -- vars.yml | 9 +++ 13 files changed, 360 insertions(+), 12 deletions(-) create mode 100644 common.yml create mode 100644 lib/postmap create mode 100644 lib/postmulti create mode 100644 roles/common/files/etc/postfix/generic.pcre create mode 100644 roles/common/files/etc/postfix/master.cf create mode 100644 roles/common/tasks/mail.yml create mode 100644 roles/common/templates/etc/postfix/main.cf.j2 delete mode 100644 site.yml create mode 100644 vars.yml diff --git a/ansible.cfg b/ansible.cfg index c7343c6..b94c4c2 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -10,7 +10,7 @@ # location of ansible library, eliminates need to specify --module-path -library = /usr/share/ansible +library = /usr/share/ansible/:./lib # default module name used in /usr/bin/ansible when -m is not specified diff --git a/common.yml b/common.yml new file mode 100644 index 0000000..9720dba --- /dev/null +++ b/common.yml @@ -0,0 +1,8 @@ +--- +- name: Common tasks + hosts: all + sudo: True + vars_files: + - vars.yml + roles: + - common diff --git a/lib/postmap b/lib/postmap new file mode 100644 index 0000000..8c6c319 --- /dev/null +++ b/lib/postmap @@ -0,0 +1,94 @@ +#!/usr/bin/python +# +# Create or update postfix's alias and lookup tables through ansible +# playbooks. +# +# Copyright 2013 Guilhem Moulin +# +# Licensed under the GNU GPL version 3 or higher. +# + +try: + import selinux + HAVE_SELINUX=True +except ImportError: + HAVE_SELINUX=False + + +# Look up for the file suffix corresponding to 'db'. If 'db' is unset, +# pick the default_detabase_type of the given instance instead. +def file_suffix(instance, db): + if not db: + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postconf') ] + if instance: + config = os.path.join(os.sep, 'etc', 'postfix-%s' % instance) + cmd.extend([ '-c', config ]) + cmd.extend([ '-h', 'default_database_type' ]) + null = open (os.devnull, 'wb') + db = subprocess.check_output(cmd, stderr=null).rstrip() + null.closed + + # See postmap(1) and postalias(1) + suffixes = { 'btree': 'db', 'cdb': 'cdb', 'hash': 'db' } + return suffixes[db] + + +# Compile the given (alias/lookup) table +def compile(cmd, instance, db, src): + cmd = [ os.path.join(os.sep, 'usr', 'sbin', cmd) ] + if instance: + config = os.path.join(os.sep, 'etc', 'postfix-%s' % instance) + cmd.extend([ '-c', config ]) + + if db: + src = "%s:%s" % (db,src) + + cmd.append(src) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + +def main(): + module = AnsibleModule( + argument_spec = dict( + src = dict( required=True ), + db = dict( choices=['btree','cdb','hash'] ), + cmd = dict( choices=['postmap','postalias'], default='postmap' ), + instance = dict( required=False ) + ), + add_file_common_args=True, + supports_check_mode=True + ) + + params = module.params + src = params['src'] + cmd = params['cmd'] + + if os.path.isabs(src): + src = src + else: + module.fail_json(msg="absolute paths are required") + + if not os.path.exists(src): + module.fail_json(src=src, msg="no such file") + + try: + dst = "%s.%s" % (src, file_suffix(params['instance'], params['db'])) + params['dest'] = dst + file_args = module.load_file_common_arguments(params) + + changed = False + msg = None + if not os.path.exists(dst) or os.path.getmtime(dst) <= os.path.getmtime(src): + changed = True + if not module.check_mode: + msg = compile( params['cmd'], params['instance'], params['db'], src) + except subprocess.CalledProcessError, e: + module.fail_json(rv=e.returncode, msg=e.output.rstrip()) + + changed = module.set_file_attributes_if_different(file_args, changed) + module.exit_json(changed=changed, msg=msg) + + +# this is magic, see lib/ansible/module_common.py +#<> +main() diff --git a/lib/postmulti b/lib/postmulti new file mode 100644 index 0000000..a6e971c --- /dev/null +++ b/lib/postmulti @@ -0,0 +1,84 @@ +#!/usr/bin/python +# +# Create and manage postfix instances. +# +# Copyright 2013 Guilhem Moulin +# +# Licensed under the GNU GPL version 3 or higher. +# + + +# Look up postfix configuration variable +def postconf(k, instance=None): + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postconf') ] + if instance: + config = os.path.join(os.sep, 'etc', 'postfix-%s' % instance) + cmd.extend([ '-c', config ]) + cmd.extend([ '-h', k ]) + return subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() + + +def main(): + module = AnsibleModule( + argument_spec = dict( + instance = dict( required=True ), + group = dict( required=False ) + ), + supports_check_mode=True + ) + + params = module.params + instance = params['instance'] + group = params['group'] + + changed=False + try: + enable = postconf('multi_instance_enable') + wrapper = postconf('multi_instance_wrapper') + + if enable != "yes" or not wrapper: + # Initiate postmulti + changed = True + if module.check_mode: + module.exit_json(changed=changed, msg="init postmulti") + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] + cmd.extend([ '-e', 'init' ]) + subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() + + instances = postconf('multi_instance_directories').split() + if os.path.join(os.sep, 'etc', 'postfix-%s' % instance) not in instances: + changed = True + # Create the instance + + if module.check_mode: + module.exit_json(changed=changed, msg="create postmulti") + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] + cmd.extend([ '-e', 'create' ]) + if group: + cmd.extend([ '-G', group ]) + cmd.extend([ '-I', 'postfix-%s' % instance ]) + subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() + + elif group != postconf('multi_instance_group', instance): + changed = True + + # Assign a new group, or remove the existing group + if module.check_mode: + module.exit_json(changed=changed, msg="assign group") + cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'postmulti') ] + cmd.extend([ '-e', 'assign', '-i', 'postfix-%s' % instance ]) + if group: + cmd.extend([ '-G', group ]) + else: + cmd.extend([ '-G', '-' ]) + subprocess.check_output(cmd, stderr=subprocess.STDOUT).rstrip() + + module.exit_json(changed=changed) + + except subprocess.CalledProcessError, e: + module.fail_json(rv=e.returncode, msg=e.output.rstrip()) + + +# this is magic, see lib/ansible/module_common.py +#<> +main() diff --git a/roles/common/files/etc/postfix/generic.pcre b/roles/common/files/etc/postfix/generic.pcre new file mode 100644 index 0000000..c46f4b5 --- /dev/null +++ b/roles/common/files/etc/postfix/generic.pcre @@ -0,0 +1 @@ +/^(.+)@([^@.]+)\.[^@]+$/ admin+${1}=${2}@fripost.org diff --git a/roles/common/files/etc/postfix/master.cf b/roles/common/files/etc/postfix/master.cf new file mode 100644 index 0000000..dd49d31 --- /dev/null +++ b/roles/common/files/etc/postfix/master.cf @@ -0,0 +1,35 @@ +# +# Postfix master process configuration file. For details on the format +# of the file, see the master(5) manual page (command: "man 5 master"). +# +# Do not forget to execute "postfix reload" after editing this file. +# +# ========================================================================== +# service type private unpriv chroot wakeup maxproc command + args +# (yes) (yes) (yes) (never) (100) +# ========================================================================== +smtp inet n - - - - smtpd +pickup fifo n - - 60 1 pickup +cleanup unix n - - - 0 cleanup +qmgr fifo n - n 300 1 qmgr +tlsmgr unix - - - 1000? 1 tlsmgr +rewrite unix - - - - - trivial-rewrite +bounce unix - - - - 0 bounce +defer unix - - - - 0 bounce +trace unix - - - - 0 bounce +verify unix - - - - 1 verify +flush unix n - - 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - - - - smtp +relay unix - - - - - smtp +# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 +showq unix n - - - - showq +error unix - - - - - error +retry unix - - - - - error +discard unix - - - - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - - - - lmtp +anvil unix - - - - 1 anvil +scache unix - - - - 1 scache diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 56b37e7..54643ed 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -28,3 +28,10 @@ # it should be "up" whenever ansible has access to the machine, we use # pattern=init as a dummy assumption. service: name=networking pattern=init state=reloaded + +# TODO: should be in a separate file, since it's used by other roles +- name: Restart Postfix + service: name=postfix state=restarted + +- name: Reload Postfix + service: name=postfix state=reloaded diff --git a/roles/common/tasks/ipsec.yml b/roles/common/tasks/ipsec.yml index 619c093..56c8300 100644 --- a/roles/common/tasks/ipsec.yml +++ b/roles/common/tasks/ipsec.yml @@ -52,11 +52,8 @@ notify: - Reload networking -# XXX: As of 1.3.1 ansible doesn't accept relative src. -# See https://github.com/ansible/ansible/issues/4459 - name: Auto-deactivate the dedicated interface for IPSec - file: #src=../if-up.d/ipsec - src=/etc/network/if-up.d/ipsec + file: src=../if-up.d/ipsec dest=/etc/network/if-down.d/ipsec owner=root group=root state=link diff --git a/roles/common/tasks/mail.yml b/roles/common/tasks/mail.yml new file mode 100644 index 0000000..9de0eaa --- /dev/null +++ b/roles/common/tasks/mail.yml @@ -0,0 +1,62 @@ +- name: Install Postfix + apt: pkg={{ item }} + with_items: + # That one is nicer than GNU mailutils' mailx(1) + - heirloom-mailx + - postfix + - postfix-cdb + - postfix-pcre + +- name: Create Postfix instances + postmulti: instance={{ postfix_instance[item].name }} + group={{ postfix_instance[item].group | default('') }} + register: r1 + with_items: postfix_instance.keys() | intersect(group_names) | list + notify: + - Restart Postfix + +- name: Define dynamic maps for children instances + # main.cf and master.cf are configured in dedicated roles, though + file: src=../postfix/dynamicmaps.cf + dest=/etc/postfix-{{ postfix_instance[item].name }}/dynamicmaps.cf + owner=root group=root state=link + register: r2 + with_items: postfix_instance.keys() | intersect(group_names) | list + notify: + - Restart Postfix + +- name: Configure Postfix (1) + copy: src=etc/postfix/{{ item }} + dest=/etc/postfix/{{ item }} + owner=root group=root + mode=0644 + register: r3 + with_items: + - master.cf + - generic.pcre + notify: + - Reload Postfix + +- name: Configure Postfix (2) + template: src=etc/postfix/main.cf.j2 + dest=/etc/postfix/main.cf + owner=root group=root + mode=0644 + register: r4 + notify: + - Restart Postfix + +- name: Update the static local Postfix database + postmap: cmd=postalias src=/etc/aliases db=cdb + owner=root group=root + mode=0644 + +# We're using CDB +- name: Delete /etc/aliases.db + file: path=/etc/aliases.db state=absent + +- name: Start Postfix + service: name=postfix state=started + when: not (r1.changed or r2.changed or r3.changed or r4.changed) + +- meta: flush_handlers diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 3ee4f49..355b2df 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -8,3 +8,4 @@ - include: fail2ban.yml tags=fail2ban - include: ipsec.yml tags=strongswan,ipsec - include: logging.yml tags=logging +- include: mail.yml tags=mail,postfix diff --git a/roles/common/templates/etc/postfix/main.cf.j2 b/roles/common/templates/etc/postfix/main.cf.j2 new file mode 100644 index 0000000..3169ac6 --- /dev/null +++ b/roles/common/templates/etc/postfix/main.cf.j2 @@ -0,0 +1,57 @@ +######################################################################## +# Nullmailer configuration + +smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) +biff = no +readme_directory = no + +myorigin = /etc/mailname +myhostname = {{ ansible_fqdn }} +mydomain = {{ ansible_domain }} +append_dot_mydomain = no + +# This server is for internal use only +mynetworks_style = host +inet_interfaces = loopback-only +inet_protocols = ipv4 +# Tunnel everything through IPSec +smtp_bind_address = 172.16.0.1 + +# No local delivery +mydestination = +local_transport = error:5.1.1 Mailbox unavailable +alias_maps = +local_recipient_maps = + +# All aliases are virtual +default_database_type = cdb +virtual_alias_maps = cdb:/etc/aliases +alias_database = $virtual_alias_maps + +# Transform local FQDN addresses to addresses routable on the internet +smtp_generic_maps = pcre:$config_directory/generic.pcre + +# Forward everything to our internal mailhub +{% if 'MTA-out' in group_names %} +relayhost = [127.0.0.1]:2525 +{% else %} +relayhost = [outgoing.fripost.org]:2525 +{% endif %} + +# This server is for internal use only; external connections are +# protected by IPSec already +smtpd_tls_security_level = none +smtp_tls_security_level = none + +{% set multi_instance = False %} +{%- for g in postfix_instance.keys() | sort -%} + {%- if g in group_names -%} + {%- if not multi_instance -%} + {%- set multi_instance = True -%} +## Other postfix instances +multi_instance_wrapper = $command_directory/postmulti -p -- +multi_instance_enable = yes +multi_instance_directories = + {%- endif %} /etc/postfix-{{ postfix_instance[g].name }} + {%- endif %} +{% endfor %} diff --git a/site.yml b/site.yml deleted file mode 100644 index a232764..0000000 --- a/site.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -# ansible-playbook -i stage_vms site.yml -t rkhunter -- name: all - hosts: all - sudo: True - roles: - - common diff --git a/vars.yml b/vars.yml new file mode 100644 index 0000000..2cd3a42 --- /dev/null +++ b/vars.yml @@ -0,0 +1,9 @@ +--- +postfix_instance: + # The keys are the group names associated with a Postfix role, and the + # values are the name and group (optional) of the instance dedicated + # to that role. + IMAP: { name: mda } + MX: { name: mta-in, group: mta } + MTA-out: { name: mta-out,group: mta } + MSA: { name: msa } -- cgit v1.2.3