OpenDKIM setup. (#196)
* OpenDKIM setup. see #173 * Fixed unit tests. * Fixed mysql syntax.
This commit is contained in:
@@ -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"
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -60,9 +60,11 @@ class PostgreSQL(Database):
|
|||||||
def _exec_query(self, query, dbname=None, dbuser=None, dbpassword=None):
|
def _exec_query(self, query, dbname=None, dbuser=None, dbpassword=None):
|
||||||
"""Exec a postgresql query."""
|
"""Exec a postgresql query."""
|
||||||
cmd = "psql"
|
cmd = "psql"
|
||||||
if dbname and dbuser:
|
if dbname:
|
||||||
|
cmd += " -d {}".format(dbname)
|
||||||
|
if dbuser:
|
||||||
self._setup_pgpass(dbname, dbuser, dbpassword)
|
self._setup_pgpass(dbname, dbuser, dbpassword)
|
||||||
cmd += " -h {} -d {} -U {} -w".format(self.dbhost, dbname, dbuser)
|
cmd += " -h {} -U {} -w".format(self.dbhost, dbuser)
|
||||||
query = query.replace("'", "'\"'\"'")
|
query = query.replace("'", "'\"'\"'")
|
||||||
cmd = "{} -c '{}' ".format(cmd, query)
|
cmd = "{} -c '{}' ".format(cmd, query)
|
||||||
utils.exec_cmd(cmd, sudo_user=self.dbuser)
|
utils.exec_cmd(cmd, sudo_user=self.dbuser)
|
||||||
@@ -94,6 +96,12 @@ class PostgreSQL(Database):
|
|||||||
query = "GRANT ALL ON DATABASE {} TO {}".format(dbname, user)
|
query = "GRANT ALL ON DATABASE {} TO {}".format(dbname, user)
|
||||||
self._exec_query(query)
|
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):
|
def _setup_pgpass(self, dbname, dbuser, dbpasswd):
|
||||||
"""Setup .pgpass file."""
|
"""Setup .pgpass file."""
|
||||||
if self._pgpass_done:
|
if self._pgpass_done:
|
||||||
@@ -114,10 +122,9 @@ class PostgreSQL(Database):
|
|||||||
def load_sql_file(self, dbname, dbuser, dbpassword, path):
|
def load_sql_file(self, dbname, dbuser, dbpassword, path):
|
||||||
"""Load SQL file."""
|
"""Load SQL file."""
|
||||||
self._setup_pgpass(dbname, dbuser, dbpassword)
|
self._setup_pgpass(dbname, dbuser, dbpassword)
|
||||||
utils.exec_cmd(
|
cmd = "psql -h {} -d {} -U {} -w < {}".format(
|
||||||
"psql -h {} -d {} -U {} -w < {}".format(
|
self.dbhost, dbname, dbuser, path)
|
||||||
self.dbhost, dbname, dbuser, path),
|
utils.exec_cmd(cmd, sudo_user=self.dbuser)
|
||||||
sudo_user=self.dbuser)
|
|
||||||
|
|
||||||
|
|
||||||
class MySQL(Database):
|
class MySQL(Database):
|
||||||
@@ -125,7 +132,7 @@ class MySQL(Database):
|
|||||||
"""MySQL backend."""
|
"""MySQL backend."""
|
||||||
|
|
||||||
packages = {
|
packages = {
|
||||||
"deb": ["mariadb-server", "libmysqlclient-dev"],
|
"deb": ["mariadb-server"],
|
||||||
"rpm": ["mariadb", "mariadb-devel", "mariadb-server"],
|
"rpm": ["mariadb", "mariadb-devel", "mariadb-server"],
|
||||||
}
|
}
|
||||||
service = "mariadb"
|
service = "mariadb"
|
||||||
@@ -140,6 +147,8 @@ class MySQL(Database):
|
|||||||
if name == "debian":
|
if name == "debian":
|
||||||
mysql_name = "mysql" if version.startswith("8") else "mariadb"
|
mysql_name = "mysql" if version.startswith("8") else "mariadb"
|
||||||
self.packages["deb"].append("lib{}client-dev".format(mysql_name))
|
self.packages["deb"].append("lib{}client-dev".format(mysql_name))
|
||||||
|
elif name == "ubuntu":
|
||||||
|
self.packages["deb"].append("libmysqlclient-dev")
|
||||||
super(MySQL, self).install_package()
|
super(MySQL, self).install_package()
|
||||||
if name == "debian" and version.startswith("8"):
|
if name == "debian" and version.startswith("8"):
|
||||||
package.backend.preconfigure(
|
package.backend.preconfigure(
|
||||||
@@ -200,6 +209,12 @@ class MySQL(Database):
|
|||||||
"GRANT ALL PRIVILEGES ON {}.* to '{}'@'localhost'"
|
"GRANT ALL PRIVILEGES ON {}.* to '{}'@'localhost'"
|
||||||
.format(dbname, user))
|
.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):
|
def load_sql_file(self, dbname, dbuser, dbpassword, path):
|
||||||
"""Load SQL file."""
|
"""Load SQL file."""
|
||||||
utils.exec_cmd(
|
utils.exec_cmd(
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ class Installer(object):
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Get configuration."""
|
"""Get configuration."""
|
||||||
self.config = config
|
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")
|
self.dbengine = self.config.get("database", "engine")
|
||||||
# Used to install system packages
|
# Used to install system packages
|
||||||
self.db_driver = (
|
self.db_driver = (
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ INSTANCE=%{instance_path}
|
|||||||
0 * * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
|
0 * * * * root $PYTHON $INSTANCE/manage.py communicate_with_public_api
|
||||||
|
|
||||||
# Generate DKIM keys (they will belong to the user running this job)
|
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
);
|
||||||
89
modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl
Normal file
89
modoboa_installer/scripts/files/opendkim/opendkim.conf.tpl
Normal file
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
127.0.0.1
|
||||||
|
::1
|
||||||
|
localhost
|
||||||
@@ -111,6 +111,12 @@ strict_rfc821_envelopes = yes
|
|||||||
%{dovecot_enabled} $lmtp_sasl_auth_cache_name
|
%{dovecot_enabled} $lmtp_sasl_auth_cache_name
|
||||||
%{dovecot_enabled} $address_verify_map
|
%{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
|
# List of authorized senders
|
||||||
smtpd_sender_login_maps =
|
smtpd_sender_login_maps =
|
||||||
proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf
|
proxy:%{db_driver}:/etc/postfix/sql-sender-login-map.cf
|
||||||
|
|||||||
@@ -177,7 +177,9 @@ class Modoboa(base.Installer):
|
|||||||
),
|
),
|
||||||
"dovecot_mailboxes_owner": (
|
"dovecot_mailboxes_owner": (
|
||||||
self.config.get("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
|
return context
|
||||||
|
|
||||||
@@ -214,6 +216,9 @@ class Modoboa(base.Installer):
|
|||||||
for path in ["/var/log/maillog", "/var/log/mail.log"]:
|
for path in ["/var/log/maillog", "/var/log/mail.log"]:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
settings["modoboa_stats"]["logfile"] = 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)
|
settings = json.dumps(settings)
|
||||||
query = (
|
query = (
|
||||||
"UPDATE core_localconfig SET _parameters='{}'"
|
"UPDATE core_localconfig SET _parameters='{}'"
|
||||||
|
|||||||
78
modoboa_installer/scripts/opendkim.py
Normal file
78
modoboa_installer/scripts/opendkim.py
Normal file
@@ -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")
|
||||||
@@ -60,6 +60,8 @@ class Postfix(base.Installer):
|
|||||||
"modoboa", "venv_path"),
|
"modoboa", "venv_path"),
|
||||||
"modoboa_instance_path": self.config.get(
|
"modoboa_instance_path": self.config.get(
|
||||||
"modoboa", "instance_path"),
|
"modoboa", "instance_path"),
|
||||||
|
"opendkim_port": self.config.get(
|
||||||
|
"opendkim", "port")
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|||||||
1
run.py
1
run.py
@@ -96,6 +96,7 @@ def main(input_args):
|
|||||||
scripts.install("radicale", config)
|
scripts.install("radicale", config)
|
||||||
scripts.install("uwsgi", config)
|
scripts.install("uwsgi", config)
|
||||||
scripts.install("nginx", config)
|
scripts.install("nginx", config)
|
||||||
|
scripts.install("opendkim", config)
|
||||||
scripts.install("postfix", config)
|
scripts.install("postfix", config)
|
||||||
scripts.install("dovecot", config)
|
scripts.install("dovecot", config)
|
||||||
utils.printcolor(
|
utils.printcolor(
|
||||||
|
|||||||
4
tests.py
4
tests.py
@@ -40,7 +40,7 @@ class ConfigFileTestCase(unittest.TestCase):
|
|||||||
def test_interactive_mode(self, mock_user_input):
|
def test_interactive_mode(self, mock_user_input):
|
||||||
"""Check interactive mode."""
|
"""Check interactive mode."""
|
||||||
mock_user_input.side_effect = [
|
mock_user_input.side_effect = [
|
||||||
"0", "0", "", "", "", ""
|
"0", "0", "", "", "", "", ""
|
||||||
]
|
]
|
||||||
with open(os.devnull, "w") as fp:
|
with open(os.devnull, "w") as fp:
|
||||||
sys.stdout = fp
|
sys.stdout = fp
|
||||||
@@ -59,7 +59,7 @@ class ConfigFileTestCase(unittest.TestCase):
|
|||||||
def test_interactive_mode_letsencrypt(self, mock_user_input):
|
def test_interactive_mode_letsencrypt(self, mock_user_input):
|
||||||
"""Check interactive mode."""
|
"""Check interactive mode."""
|
||||||
mock_user_input.side_effect = [
|
mock_user_input.side_effect = [
|
||||||
"1", "admin@example.test", "0", "", "", "", ""
|
"1", "admin@example.test", "0", "", "", "", "", ""
|
||||||
]
|
]
|
||||||
with open(os.devnull, "w") as fp:
|
with open(os.devnull, "w") as fp:
|
||||||
sys.stdout = fp
|
sys.stdout = fp
|
||||||
|
|||||||
Reference in New Issue
Block a user