From 8a650e699843889c607319161357030886d838de Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 2 Apr 2018 16:25:58 +0200 Subject: [PATCH] OpenDKIM setup. (#196) * OpenDKIM setup. see #173 * Fixed unit tests. * Fixed mysql syntax. --- modoboa_installer/config_dict_template.py | 36 ++++++++ modoboa_installer/database.py | 31 +++++-- modoboa_installer/scripts/base.py | 2 + .../scripts/files/modoboa/crontab.tpl | 2 +- .../files/opendkim/dkim_view_mysql.sql | 5 ++ .../files/opendkim/dkim_view_postgres.sql | 5 ++ .../scripts/files/opendkim/opendkim.conf.tpl | 89 +++++++++++++++++++ .../scripts/files/opendkim/opendkim.hosts.tpl | 3 + .../scripts/files/postfix/main.cf.tpl | 6 ++ modoboa_installer/scripts/modoboa.py | 7 +- modoboa_installer/scripts/opendkim.py | 78 ++++++++++++++++ modoboa_installer/scripts/postfix.py | 2 + run.py | 1 + tests.py | 4 +- 14 files changed, 259 insertions(+), 12 deletions(-) create mode 100644 modoboa_installer/scripts/files/opendkim/dkim_view_mysql.sql create mode 100644 modoboa_installer/scripts/files/opendkim/dkim_view_postgres.sql create mode 100644 modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl create mode 100644 modoboa_installer/scripts/files/opendkim/opendkim.hosts.tpl create mode 100644 modoboa_installer/scripts/opendkim.py diff --git a/modoboa_installer/config_dict_template.py b/modoboa_installer/config_dict_template.py index 7e15a74..f300364 100644 --- a/modoboa_installer/config_dict_template.py +++ b/modoboa_installer/config_dict_template.py @@ -403,4 +403,40 @@ ConfigDictTemplate = [ } ] }, + { + "name": "opendkim", + "values": [ + { + "option": "enabled", + "default": "true", + }, + { + "option": "user", + "default": "opendkim", + }, + { + "option": "config_dir", + "default": "/etc", + }, + { + "option": "port", + "default": "12345" + }, + { + "option": "keys_storage_dir", + "default": "/var/lib/dkim" + }, + { + "option": "dbuser", + "default": "opendkim", + }, + { + "option": "dbpassword", + "default": make_password, + "customizable": True, + "question": "Please enter OpenDKIM db password" + }, + + ] + }, ] diff --git a/modoboa_installer/database.py b/modoboa_installer/database.py index 2a9db0e..e1a5ed3 100644 --- a/modoboa_installer/database.py +++ b/modoboa_installer/database.py @@ -60,9 +60,11 @@ class PostgreSQL(Database): def _exec_query(self, query, dbname=None, dbuser=None, dbpassword=None): """Exec a postgresql query.""" cmd = "psql" - if dbname and dbuser: - self._setup_pgpass(dbname, dbuser, dbpassword) - cmd += " -h {} -d {} -U {} -w".format(self.dbhost, dbname, dbuser) + if dbname: + cmd += " -d {}".format(dbname) + if dbuser: + self._setup_pgpass(dbname, dbuser, dbpassword) + cmd += " -h {} -U {} -w".format(self.dbhost, dbuser) query = query.replace("'", "'\"'\"'") cmd = "{} -c '{}' ".format(cmd, query) utils.exec_cmd(cmd, sudo_user=self.dbuser) @@ -94,6 +96,12 @@ class PostgreSQL(Database): query = "GRANT ALL ON DATABASE {} TO {}".format(dbname, user) self._exec_query(query) + def grant_right_on_table(self, dbname, table, user, right): + """Grant specific right to user on table.""" + query = "GRANT {} ON {} TO {}".format( + right.upper(), table, user) + self._exec_query(query, dbname=dbname) + def _setup_pgpass(self, dbname, dbuser, dbpasswd): """Setup .pgpass file.""" if self._pgpass_done: @@ -114,10 +122,9 @@ class PostgreSQL(Database): def load_sql_file(self, dbname, dbuser, dbpassword, path): """Load SQL file.""" self._setup_pgpass(dbname, dbuser, dbpassword) - utils.exec_cmd( - "psql -h {} -d {} -U {} -w < {}".format( - self.dbhost, dbname, dbuser, path), - sudo_user=self.dbuser) + cmd = "psql -h {} -d {} -U {} -w < {}".format( + self.dbhost, dbname, dbuser, path) + utils.exec_cmd(cmd, sudo_user=self.dbuser) class MySQL(Database): @@ -125,7 +132,7 @@ class MySQL(Database): """MySQL backend.""" packages = { - "deb": ["mariadb-server", "libmysqlclient-dev"], + "deb": ["mariadb-server"], "rpm": ["mariadb", "mariadb-devel", "mariadb-server"], } service = "mariadb" @@ -140,6 +147,8 @@ class MySQL(Database): if name == "debian": mysql_name = "mysql" if version.startswith("8") else "mariadb" self.packages["deb"].append("lib{}client-dev".format(mysql_name)) + elif name == "ubuntu": + self.packages["deb"].append("libmysqlclient-dev") super(MySQL, self).install_package() if name == "debian" and version.startswith("8"): package.backend.preconfigure( @@ -200,6 +209,12 @@ class MySQL(Database): "GRANT ALL PRIVILEGES ON {}.* to '{}'@'localhost'" .format(dbname, user)) + def grant_right_on_table(self, dbname, table, user, right): + """Grant specific right to user on table.""" + query = "GRANT {} ON {}.{} TO '{}'@'%'".format( + right.upper(), dbname, table, user) + self._exec_query(query) + def load_sql_file(self, dbname, dbuser, dbpassword, path): """Load SQL file.""" utils.exec_cmd( diff --git a/modoboa_installer/scripts/base.py b/modoboa_installer/scripts/base.py index 0b897fb..a0f084e 100644 --- a/modoboa_installer/scripts/base.py +++ b/modoboa_installer/scripts/base.py @@ -22,6 +22,8 @@ class Installer(object): def __init__(self, config): """Get configuration.""" self.config = config + if self.config.has_section(self.appname): + self.app_config = dict(self.config.items(self.appname)) self.dbengine = self.config.get("database", "engine") # Used to install system packages self.db_driver = ( diff --git a/modoboa_installer/scripts/files/modoboa/crontab.tpl b/modoboa_installer/scripts/files/modoboa/crontab.tpl index 9967dc6..45fcf23 100644 --- a/modoboa_installer/scripts/files/modoboa/crontab.tpl +++ b/modoboa_installer/scripts/files/modoboa/crontab.tpl @@ -33,4 +33,4 @@ INSTANCE=%{instance_path} 0 * * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api # Generate DKIM keys (they will belong to the user running this job) -* * * * * root $PYTHON $INSTANCE/manage.py modo manage_dkim_keys +%{opendkim_enabled}* * * * * %{opendkim_user} $PYTHON $INSTANCE/manage.py modo manage_dkim_keys diff --git a/modoboa_installer/scripts/files/opendkim/dkim_view_mysql.sql b/modoboa_installer/scripts/files/opendkim/dkim_view_mysql.sql new file mode 100644 index 0000000..7f1ed25 --- /dev/null +++ b/modoboa_installer/scripts/files/opendkim/dkim_view_mysql.sql @@ -0,0 +1,5 @@ +CREATE OR REPLACE VIEW dkim AS ( + SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, + dkim_key_selector AS selector + FROM admin_domain WHERE enable_dkim=1 +); diff --git a/modoboa_installer/scripts/files/opendkim/dkim_view_postgres.sql b/modoboa_installer/scripts/files/opendkim/dkim_view_postgres.sql new file mode 100644 index 0000000..f3e7d41 --- /dev/null +++ b/modoboa_installer/scripts/files/opendkim/dkim_view_postgres.sql @@ -0,0 +1,5 @@ +CREATE OR REPLACE VIEW dkim AS ( + SELECT id, name as domain_name, dkim_private_key_path AS private_key_path, + dkim_key_selector AS selector + FROM admin_domain WHERE enable_dkim +); diff --git a/modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl b/modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl new file mode 100644 index 0000000..890d25c --- /dev/null +++ b/modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl @@ -0,0 +1,89 @@ +# This is a basic configuration that can easily be adapted to suit a standard +# installation. For more advanced options, see opendkim.conf(5) and/or +# /usr/share/doc/opendkim/examples/opendkim.conf.sample. + +# Log to syslog +Syslog yes +LogWhy Yes +SyslogSuccess Yes +# Required to use local socket with MTAs that access the socket as a non- +# privileged user (e.g. Postfix) +UMask 007 + +# Sign for example.com with key in /etc/dkimkeys/dkim.key using +# selector '2007' (e.g. 2007._domainkey.example.com) +#Domain example.com +#KeyFile /etc/dkimkeys/dkim.key +#Selector 2007 + +KeyTable dsn:%{db_driver}://%{db_user}:%{db_password}@%{dbhost}/%{db_name}/table=dkim?keycol=id?datacol=domain_name,selector,private_key_path +SigningTable dsn:%db_driver://%{db_user}:%{db_password}@%{dbhost}/%{db_name}/table=dkim?keycol=domain_name?datacol=id + +# Commonly-used options; the commented-out versions show the defaults. +#Canonicalization simple +#Mode sv +SubDomains yes +Canonicalization relaxed/relaxed + +# Socket smtp://localhost +# +# ## Socket socketspec +# ## +# ## Names the socket where this filter should listen for milter connections +# ## from the MTA. Required. Should be in one of these forms: +# ## +# ## inet:port@address to listen on a specific interface +# ## inet:port to listen on all interfaces +# ## local:/path/to/socket to listen on a UNIX domain socket +# +Socket inet:%{port}@localhost +#Socket local:/var/run/opendkim/opendkim.sock + +## PidFile filename +### default (none) +### +### Name of the file where the filter should write its pid before beginning +### normal operations. +# +PidFile /var/run/opendkim/opendkim.pid + + +# Always oversign From (sign using actual From and a null From to prevent +# malicious signatures header fields (From and/or others) between the signer +# and the verifier. From is oversigned by default in the Debian pacakge +# because it is often the identity key used by reputation systems and thus +# somewhat security sensitive. +OversignHeaders From + +## ResolverConfiguration filename +## default (none) +## +## Specifies a configuration file to be passed to the Unbound library that +## performs DNS queries applying the DNSSEC protocol. See the Unbound +## documentation at http://unbound.net for the expected content of this file. +## The results of using this and the TrustAnchorFile setting at the same +## time are undefined. +## In Debian, /etc/unbound/unbound.conf is shipped as part of the Suggested +## unbound package + +# ResolverConfiguration /etc/unbound/unbound.conf + +## TrustAnchorFile filename +## default (none) +## +## Specifies a file from which trust anchor data should be read when doing +## DNS queries and applying the DNSSEC protocol. See the Unbound documentation +## at http://unbound.net for the expected format of this file. + +# TrustAnchorFile /usr/share/dns/root.key + +## Userid userid +### default (none) +### +### Change to user "userid" before starting normal operation? May include +### a group ID as well, separated from the userid by a colon. +# +UserID %{user} + +ExternalIgnoreList /etc/opendkim.hosts +InternalHosts /etc/opendkim.hosts diff --git a/modoboa_installer/scripts/files/opendkim/opendkim.hosts.tpl b/modoboa_installer/scripts/files/opendkim/opendkim.hosts.tpl new file mode 100644 index 0000000..46a0ab8 --- /dev/null +++ b/modoboa_installer/scripts/files/opendkim/opendkim.hosts.tpl @@ -0,0 +1,3 @@ +127.0.0.1 +::1 +localhost diff --git a/modoboa_installer/scripts/files/postfix/main.cf.tpl b/modoboa_installer/scripts/files/postfix/main.cf.tpl index ae57d3c..7fc38c5 100644 --- a/modoboa_installer/scripts/files/postfix/main.cf.tpl +++ b/modoboa_installer/scripts/files/postfix/main.cf.tpl @@ -111,6 +111,12 @@ strict_rfc821_envelopes = yes %{dovecot_enabled} $lmtp_sasl_auth_cache_name %{dovecot_enabled} $address_verify_map +# OpenDKIM setup +%{opendkim_enabled}smtpd_milters = inet:127.0.0.1:%{opendkim_port} +%{opendkim_enabled}non_smtpd_milters = inet:127.0.0.1:%{opendkim_port} +%{opendkim_enabled}milter_default_action = accept +%{opendkim_enabled}milter_content_timeout = 30s + # List of authorized senders smtpd_sender_login_maps = proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf diff --git a/modoboa_installer/scripts/modoboa.py b/modoboa_installer/scripts/modoboa.py index 97a7a74..f27db93 100644 --- a/modoboa_installer/scripts/modoboa.py +++ b/modoboa_installer/scripts/modoboa.py @@ -177,7 +177,9 @@ class Modoboa(base.Installer): ), "dovecot_mailboxes_owner": ( self.config.get("dovecot", "mailboxes_owner")), - "radicale_enabled": "" if "modoboa-radicale" in extensions else "#" + "radicale_enabled": ( + "" if "modoboa-radicale" in extensions else "#"), + "opendkim_user": self.config.get("opendkim", "user"), }) return context @@ -214,6 +216,9 @@ class Modoboa(base.Installer): for path in ["/var/log/maillog", "/var/log/mail.log"]: if os.path.exists(path): settings["modoboa_stats"]["logfile"] = path + if self.config.getboolean("opendkim", "enabled"): + settings["admin"]["dkim_keys_storage_dir"] = ( + self.config.get("opendkim", "keys_storage_dir")) settings = json.dumps(settings) query = ( "UPDATE core_localconfig SET _parameters='{}'" diff --git a/modoboa_installer/scripts/opendkim.py b/modoboa_installer/scripts/opendkim.py new file mode 100644 index 0000000..6d904af --- /dev/null +++ b/modoboa_installer/scripts/opendkim.py @@ -0,0 +1,78 @@ +"""OpenDKIM related tools.""" + +import os +import pwd +import stat + +from .. import database +from .. import package +from .. import utils + +from . import base + + +class Opendkim(base.Installer): + """OpenDKIM installer.""" + + appname = "opendkim" + packages = { + "deb": ["opendkim"], + "rpm": ["opendkim"] + } + config_files = ["opendkim.conf", "opendkim.hosts"] + + def get_packages(self): + """Additional packages.""" + packages = super(Opendkim, self).get_packages() + if package.backend.FORMAT == "deb": + packages += ["libopendbx1-{}".format(self.db_driver)] + else: + dbengine = "postgresql" if self.dbengine == "postgres" else "mysql" + packages += ["opendbx-{}".format(dbengine)] + return packages + + def install_config_files(self): + """Make sure config directory exists.""" + user = self.config.get("opendkim", "user") + pw = pwd.getpwnam(user) + targets = [ + [self.app_config["keys_storage_dir"], pw[2], pw[3]] + ] + for target in targets: + if not os.path.exists(target[0]): + utils.mkdir( + target[0], + stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | + stat.S_IROTH | stat.S_IXOTH, + target[1], target[2] + ) + super(Opendkim, self).install_config_files() + + def get_template_context(self): + """Additional variables.""" + context = super(Opendkim, self).get_template_context() + context.update({ + "db_driver": self.db_driver, + "db_name": self.config.get("modoboa", "dbname"), + "db_user": self.app_config["dbuser"], + "db_password": self.app_config["dbpassword"], + "port": self.app_config["port"], + "user": self.app_config["user"] + }) + return context + + def setup_database(self): + """Setup database.""" + self.backend = database.get_backend(self.config) + self.backend.create_user( + self.app_config["dbuser"], self.app_config["dbpassword"] + ) + dbname = self.config.get("modoboa", "dbname") + dbuser = self.config.get("modoboa", "dbuser") + dbpassword = self.config.get("modoboa", "dbpassword") + self.backend.load_sql_file( + dbname, dbuser, dbpassword, + self.get_file_path("dkim_view_{}.sql".format(self.dbengine)) + ) + self.backend.grant_right_on_table( + dbname, "dkim", self.app_config["dbuser"], "SELECT") diff --git a/modoboa_installer/scripts/postfix.py b/modoboa_installer/scripts/postfix.py index 5c95c84..b6c6955 100644 --- a/modoboa_installer/scripts/postfix.py +++ b/modoboa_installer/scripts/postfix.py @@ -60,6 +60,8 @@ class Postfix(base.Installer): "modoboa", "venv_path"), "modoboa_instance_path": self.config.get( "modoboa", "instance_path"), + "opendkim_port": self.config.get( + "opendkim", "port") }) return context diff --git a/run.py b/run.py index d796bba..f370f9e 100755 --- a/run.py +++ b/run.py @@ -96,6 +96,7 @@ def main(input_args): scripts.install("radicale", config) scripts.install("uwsgi", config) scripts.install("nginx", config) + scripts.install("opendkim", config) scripts.install("postfix", config) scripts.install("dovecot", config) utils.printcolor( diff --git a/tests.py b/tests.py index 67068bd..4b30735 100644 --- a/tests.py +++ b/tests.py @@ -40,7 +40,7 @@ class ConfigFileTestCase(unittest.TestCase): def test_interactive_mode(self, mock_user_input): """Check interactive mode.""" mock_user_input.side_effect = [ - "0", "0", "", "", "", "" + "0", "0", "", "", "", "", "" ] with open(os.devnull, "w") as fp: sys.stdout = fp @@ -59,7 +59,7 @@ class ConfigFileTestCase(unittest.TestCase): def test_interactive_mode_letsencrypt(self, mock_user_input): """Check interactive mode.""" mock_user_input.side_effect = [ - "1", "admin@example.test", "0", "", "", "", "" + "1", "admin@example.test", "0", "", "", "", "", "" ] with open(os.devnull, "w") as fp: sys.stdout = fp