summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2013-11-24 03:53:39 +0100
committerGuilhem Moulin <guilhem@fripost.org>2015-06-07 02:50:47 +0200
commit6be613d07ddc6d0b1e4b73f93c0fa1c0b1f7ba10 (patch)
tree798df25c6a7c0a9c1e8b01116f454f5d74c94a10
parent099678c6b4fd176d3232984e634d6311f7c83494 (diff)
Postfix master (nullmailer) configuration
We use a dedicated instance for each role: MDA, MTA out, MX, etc.
-rw-r--r--ansible.cfg2
-rw-r--r--common.yml8
-rw-r--r--lib/postmap94
-rw-r--r--lib/postmulti84
-rw-r--r--roles/common/files/etc/postfix/generic.pcre1
-rw-r--r--roles/common/files/etc/postfix/master.cf35
-rw-r--r--roles/common/handlers/main.yml7
-rw-r--r--roles/common/tasks/ipsec.yml5
-rw-r--r--roles/common/tasks/mail.yml62
-rw-r--r--roles/common/tasks/main.yml1
-rw-r--r--roles/common/templates/etc/postfix/main.cf.j257
-rw-r--r--site.yml7
-rw-r--r--vars.yml9
13 files changed, 360 insertions, 12 deletions
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 <guilhem@fripost.org>
+#
+# 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
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+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 <guilhem@fripost.org>
+#
+# 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
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+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 }