2018-02-14 19:20:16 -05:00
|
|
|
"""certbot installer plugin for postfix."""
|
2016-04-28 19:40:06 -04:00
|
|
|
import logging
|
2017-08-04 12:30:11 -04:00
|
|
|
import os
|
2014-06-10 11:08:17 -04:00
|
|
|
|
2017-08-04 12:18:51 -04:00
|
|
|
import zope.interface
|
2018-02-14 19:20:16 -05:00
|
|
|
import zope.component
|
|
|
|
|
import six
|
2017-08-04 12:18:51 -04:00
|
|
|
|
2017-08-04 12:42:26 -04:00
|
|
|
from certbot import errors
|
2017-08-04 12:18:51 -04:00
|
|
|
from certbot import interfaces
|
2017-08-10 17:19:44 -04:00
|
|
|
from certbot import util as certbot_util
|
2017-08-04 12:18:51 -04:00
|
|
|
from certbot.plugins import common as plugins_common
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
from certbot_postfix import constants
|
|
|
|
|
from certbot_postfix import postconf
|
2017-08-10 17:25:08 -04:00
|
|
|
from certbot_postfix import util
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
# pylint: disable=unused-import, no-name-in-module
|
|
|
|
|
from acme.magic_typing import Callable, Dict, List
|
|
|
|
|
# pylint: enable=unused-import, no-name-in-module
|
2016-03-29 17:19:13 -04:00
|
|
|
|
2016-04-28 19:40:06 -04:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2017-08-04 12:18:51 -04:00
|
|
|
@zope.interface.implementer(interfaces.IInstaller)
|
|
|
|
|
@zope.interface.provider(interfaces.IPluginFactory)
|
2017-08-25 14:41:30 -04:00
|
|
|
class Installer(plugins_common.Installer):
|
2017-08-24 19:08:47 -04:00
|
|
|
"""Certbot installer plugin for Postfix.
|
|
|
|
|
|
|
|
|
|
:ivar str config_dir: Postfix configuration directory to modify
|
|
|
|
|
:ivar list save_notes: documentation for proposed changes. This is
|
|
|
|
|
cleared and stored in Certbot checkpoints when save() is called
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
:ivar postconf: Wrapper for Postfix configuration command-line tool.
|
|
|
|
|
:type postconf: :class: `certbot_postfix.postconf.ConfigMain`
|
|
|
|
|
:ivar postfix: Wrapper for Postfix command-line tool.
|
|
|
|
|
:type postfix: :class: `certbot_postfix.util.PostfixUtil`
|
2017-08-24 19:08:47 -04:00
|
|
|
"""
|
2017-08-04 12:18:51 -04:00
|
|
|
|
|
|
|
|
description = "Configure TLS with the Postfix MTA"
|
|
|
|
|
|
2017-08-09 19:06:06 -04:00
|
|
|
@classmethod
|
|
|
|
|
def add_parser_arguments(cls, add):
|
2018-02-14 19:20:16 -05:00
|
|
|
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
2017-08-25 16:34:03 -04:00
|
|
|
help="Path to the 'postfix' control program.")
|
2018-02-14 19:20:16 -05:00
|
|
|
# This directory points to Postfix's configuration directory.
|
|
|
|
|
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
|
|
|
|
|
help="Path to the directory containing the "
|
2017-08-09 19:06:06 -04:00
|
|
|
"Postfix main.cf file to modify instead of using the "
|
2017-08-25 16:34:03 -04:00
|
|
|
"default configuration paths.")
|
2018-02-14 19:20:16 -05:00
|
|
|
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
|
2017-08-10 14:51:01 -04:00
|
|
|
help="Path to the 'postconf' executable.")
|
2018-02-14 19:20:16 -05:00
|
|
|
add("tls-only", action="store_true", default=constants.CLI_DEFAULTS["tls_only"],
|
|
|
|
|
help="Only set params to enable opportunistic TLS and install certificates.")
|
|
|
|
|
add("server-only", action="store_true", default=constants.CLI_DEFAULTS["server_only"],
|
|
|
|
|
help="Only set server params (prefixed with smtpd*)")
|
|
|
|
|
add("ignore-master-overrides", action="store_true",
|
|
|
|
|
default=constants.CLI_DEFAULTS["ignore_master_overrides"],
|
|
|
|
|
help="Ignore errors reporting overridden TLS parameters in master.cf.")
|
2017-08-09 19:06:06 -04:00
|
|
|
|
2017-08-10 11:50:08 -04:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super(Installer, self).__init__(*args, **kwargs)
|
2018-02-14 19:20:16 -05:00
|
|
|
# Wrapper around postconf commands
|
|
|
|
|
self.postfix = None
|
|
|
|
|
self.postconf = None
|
|
|
|
|
|
|
|
|
|
# Files to save
|
|
|
|
|
self.save_notes = [] # type: List[str]
|
|
|
|
|
|
|
|
|
|
self._enhance_func = {} # type: Dict[str, Callable[[str, str], None]]
|
|
|
|
|
# Since we only need to enable TLS once for all domains,
|
|
|
|
|
# keep track of whether this enhancement was already called.
|
|
|
|
|
self._tls_enabled = False
|
2016-02-17 14:19:02 -05:00
|
|
|
|
2017-08-10 14:51:01 -04:00
|
|
|
def prepare(self):
|
|
|
|
|
"""Prepare the installer.
|
|
|
|
|
|
2017-08-10 19:41:59 -04:00
|
|
|
:raises errors.PluginError: when an unexpected error occurs
|
2017-08-25 17:03:29 -04:00
|
|
|
:raises errors.MisconfigurationError: when the config is invalid
|
2017-08-10 19:41:59 -04:00
|
|
|
:raises errors.NoInstallationError: when can't find installation
|
|
|
|
|
:raises errors.NotSupportedError: when version is not supported
|
2017-08-10 14:51:01 -04:00
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
# Verify postfix and postconf are installed
|
2017-08-24 19:08:47 -04:00
|
|
|
for param in ("ctl", "config_utility",):
|
2018-02-14 19:20:16 -05:00
|
|
|
util.verify_exe_exists(self.conf(param),
|
2017-08-10 19:38:27 -04:00
|
|
|
"Cannot find executable '{0}'. You can provide the "
|
|
|
|
|
"path to this command with --{1}".format(
|
2018-02-14 19:20:16 -05:00
|
|
|
self.conf(param),
|
|
|
|
|
self.option_name(param)))
|
|
|
|
|
|
|
|
|
|
# Set up CLI tools
|
|
|
|
|
self.postfix = util.PostfixUtil(self.conf('config-dir'))
|
|
|
|
|
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
|
|
|
self.conf('ignore-master-overrides'),
|
|
|
|
|
self.conf('config-dir'))
|
|
|
|
|
|
|
|
|
|
# Ensure current configuration is valid.
|
|
|
|
|
self.config_test()
|
2017-08-10 19:38:27 -04:00
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
# Check Postfix version
|
|
|
|
|
self._check_version()
|
|
|
|
|
self._lock_config_dir()
|
|
|
|
|
self.install_ssl_dhparams()
|
2017-08-10 19:38:27 -04:00
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
def config_test(self):
|
|
|
|
|
"""Test to see that the current Postfix configuration is valid.
|
2017-08-10 19:38:27 -04:00
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
:raises errors.MisconfigurationError: If the configuration is invalid.
|
2017-08-10 19:38:27 -04:00
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
self.postfix.test()
|
2017-08-10 19:38:27 -04:00
|
|
|
|
|
|
|
|
def _check_version(self):
|
|
|
|
|
"""Verifies that the installed Postfix version is supported.
|
|
|
|
|
|
|
|
|
|
:raises errors.NotSupportedError: if the version is unsupported
|
|
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
if self._get_version() < constants.MINIMUM_VERSION:
|
|
|
|
|
version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION])
|
|
|
|
|
raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
|
2017-08-10 14:51:01 -04:00
|
|
|
|
2017-08-24 18:03:11 -04:00
|
|
|
def _lock_config_dir(self):
|
|
|
|
|
"""Stop two Postfix plugins from modifying the config at once.
|
|
|
|
|
|
|
|
|
|
:raises .PluginError: if unable to acquire the lock
|
|
|
|
|
"""
|
|
|
|
|
try:
|
2018-02-14 19:20:16 -05:00
|
|
|
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
|
2017-08-24 18:03:11 -04:00
|
|
|
except (OSError, errors.LockError):
|
|
|
|
|
logger.debug("Encountered error:", exc_info=True)
|
|
|
|
|
raise errors.PluginError(
|
2018-02-14 19:20:16 -05:00
|
|
|
"Unable to lock %s" % self.conf('config-dir'))
|
2017-08-24 18:03:11 -04:00
|
|
|
|
2017-08-23 18:16:07 -04:00
|
|
|
def more_info(self):
|
2018-02-14 19:20:16 -05:00
|
|
|
"""Human-readable string to help the user. Describes steps taken and any relevant
|
|
|
|
|
info to help the user decide which plugin to use.
|
|
|
|
|
|
|
|
|
|
:rtype: str
|
2017-08-23 18:16:07 -04:00
|
|
|
"""
|
|
|
|
|
return (
|
|
|
|
|
"Configures Postfix to try to authenticate mail servers, use "
|
|
|
|
|
"installed certificates and disable weak ciphers and protocols.{0}"
|
|
|
|
|
"Server root: {root}{0}"
|
|
|
|
|
"Version: {version}".format(
|
|
|
|
|
os.linesep,
|
2018-02-14 19:20:16 -05:00
|
|
|
root=self.conf('config-dir'),
|
2017-08-23 18:16:07 -04:00
|
|
|
version='.'.join([str(i) for i in self._get_version()]))
|
|
|
|
|
)
|
|
|
|
|
|
2017-08-23 18:13:06 -04:00
|
|
|
def _get_version(self):
|
2018-02-14 19:20:16 -05:00
|
|
|
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
|
2017-08-23 18:13:06 -04:00
|
|
|
|
|
|
|
|
:returns: version
|
|
|
|
|
:rtype: tuple
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
:raises errors.PluginError: Unable to find Postfix version.
|
2017-08-23 18:13:06 -04:00
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
mail_version = self.postconf.get_default("mail_version")
|
2017-08-23 18:13:06 -04:00
|
|
|
return tuple(int(i) for i in mail_version.split('.'))
|
2017-08-10 19:17:46 -04:00
|
|
|
|
2017-08-24 17:59:25 -04:00
|
|
|
def get_all_names(self):
|
|
|
|
|
"""Returns all names that may be authenticated.
|
|
|
|
|
|
|
|
|
|
:rtype: `set` of `str`
|
|
|
|
|
|
|
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
return certbot_util.get_filtered_names(self.postconf.get(var)
|
2017-08-24 17:59:25 -04:00
|
|
|
for var in ('mydomain', 'myhostname', 'myorigin',))
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
def _set_vars(self, var_dict):
|
|
|
|
|
"""Sets all parameters in var_dict to config file. If current value is already set
|
|
|
|
|
as more secure (acceptable), then don't set/overwrite it.
|
|
|
|
|
"""
|
|
|
|
|
for param, acceptable in six.iteritems(var_dict):
|
|
|
|
|
if not util.is_acceptable_value(param, self.postconf.get(param), acceptable):
|
|
|
|
|
self.postconf.set(param, acceptable[0], acceptable)
|
|
|
|
|
|
|
|
|
|
def _confirm_changes(self):
|
|
|
|
|
"""Confirming outstanding updates for configuration parameters.
|
|
|
|
|
|
|
|
|
|
:raises errors.PluginError: when user rejects the configuration changes.
|
|
|
|
|
"""
|
|
|
|
|
updates = self.postconf.get_changes()
|
|
|
|
|
output_string = "Postfix TLS configuration parameters to update in main.cf:\n"
|
|
|
|
|
for name, value in six.iteritems(updates):
|
|
|
|
|
output_string += "{0} = {1}\n".format(name, value)
|
|
|
|
|
output_string += "Is this okay?\n"
|
|
|
|
|
if not zope.component.getUtility(interfaces.IDisplay).yesno(output_string,
|
|
|
|
|
force_interactive=True, default=True):
|
|
|
|
|
raise errors.PluginError(
|
|
|
|
|
"Manually rejected configuration changes.\n"
|
|
|
|
|
"Try using --tls-only or --server-only to change a particular"
|
|
|
|
|
"subset of configuration parameters.")
|
|
|
|
|
|
2017-08-28 17:52:05 -04:00
|
|
|
def deploy_cert(self, domain, cert_path,
|
|
|
|
|
key_path, chain_path, fullchain_path):
|
|
|
|
|
"""Configure the Postfix SMTP server to use the given TLS cert.
|
|
|
|
|
|
|
|
|
|
:param str domain: domain to deploy certificate file
|
|
|
|
|
:param str cert_path: absolute path to the certificate file
|
|
|
|
|
:param str key_path: absolute path to the private key file
|
|
|
|
|
:param str chain_path: absolute path to the certificate chain file
|
|
|
|
|
:param str fullchain_path: absolute path to the certificate fullchain
|
|
|
|
|
file (cert plus chain)
|
|
|
|
|
|
|
|
|
|
:raises .PluginError: when cert cannot be deployed
|
|
|
|
|
|
|
|
|
|
"""
|
2017-08-24 19:08:47 -04:00
|
|
|
# pylint: disable=unused-argument
|
2018-02-14 19:20:16 -05:00
|
|
|
if self._tls_enabled:
|
|
|
|
|
return
|
|
|
|
|
self._tls_enabled = True
|
2017-08-28 20:17:29 -04:00
|
|
|
self.save_notes.append("Configuring TLS for {0}".format(domain))
|
2018-02-14 19:20:16 -05:00
|
|
|
self.postconf.set("smtpd_tls_cert_file", cert_path)
|
|
|
|
|
self.postconf.set("smtpd_tls_key_file", key_path)
|
|
|
|
|
self._set_vars(constants.TLS_SERVER_VARS)
|
|
|
|
|
if not self.conf('server_only'):
|
|
|
|
|
self._set_vars(constants.TLS_CLIENT_VARS)
|
|
|
|
|
if not self.conf('tls_only'):
|
|
|
|
|
self._set_vars(constants.DEFAULT_SERVER_VARS)
|
|
|
|
|
if not self.conf('server_only'):
|
|
|
|
|
self._set_vars(constants.DEFAULT_CLIENT_VARS)
|
|
|
|
|
# Despite the name, this option also supports 2048-bit DH params.
|
|
|
|
|
# http://www.postfix.org/FORWARD_SECRECY_README.html#server_fs
|
|
|
|
|
self.postconf.set("smtpd_tls_dh1024_param_file", self.ssl_dhparams)
|
|
|
|
|
self._confirm_changes()
|
2017-08-28 17:52:05 -04:00
|
|
|
|
2017-08-24 18:26:52 -04:00
|
|
|
def enhance(self, domain, enhancement, options=None):
|
2018-02-14 19:20:16 -05:00
|
|
|
"""Raises an exception since this installer doesn't support any enhancements.
|
2017-08-24 18:26:52 -04:00
|
|
|
"""
|
2017-08-24 19:08:47 -04:00
|
|
|
# pylint: disable=unused-argument
|
2017-08-24 18:26:52 -04:00
|
|
|
raise errors.PluginError(
|
|
|
|
|
"Unsupported enhancement: {0}".format(enhancement))
|
|
|
|
|
|
2017-08-24 18:18:40 -04:00
|
|
|
def supported_enhancements(self):
|
|
|
|
|
"""Returns a list of supported enhancements.
|
|
|
|
|
|
|
|
|
|
:rtype: list
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
return []
|
|
|
|
|
|
2017-08-28 20:17:29 -04:00
|
|
|
def save(self, title=None, temporary=False):
|
|
|
|
|
"""Creates backups and writes changes to configuration files.
|
|
|
|
|
|
|
|
|
|
:param str title: The title of the save. If a title is given, the
|
|
|
|
|
configuration will be saved as a new checkpoint and put in a
|
|
|
|
|
timestamped directory. `title` has no effect if temporary is true.
|
|
|
|
|
|
|
|
|
|
:param bool temporary: Indicates whether the changes made will
|
|
|
|
|
be quickly reversed in the future (challenges)
|
|
|
|
|
|
|
|
|
|
:raises errors.PluginError: when save is unsuccessful
|
|
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
save_files = set((os.path.join(self.conf('config-dir'), "main.cf"),))
|
|
|
|
|
self.add_to_checkpoint(save_files,
|
|
|
|
|
"\n".join(self.save_notes), temporary)
|
|
|
|
|
self.postconf.flush()
|
2017-08-28 20:17:29 -04:00
|
|
|
|
|
|
|
|
del self.save_notes[:]
|
|
|
|
|
|
|
|
|
|
if title and not temporary:
|
|
|
|
|
self.finalize_checkpoint(title)
|
|
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
def recovery_routine(self):
|
|
|
|
|
super(Installer, self).recovery_routine()
|
|
|
|
|
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
|
|
|
self.conf('ignore-master-overrides'),
|
|
|
|
|
self.conf('config-dir'))
|
2017-08-25 16:55:21 -04:00
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
def rollback_checkpoints(self, rollback=1):
|
|
|
|
|
"""Rollback saved checkpoints.
|
2017-08-25 16:55:21 -04:00
|
|
|
|
2018-02-14 19:20:16 -05:00
|
|
|
:param int rollback: Number of checkpoints to revert
|
|
|
|
|
|
|
|
|
|
:raises .errors.PluginError: If there is a problem with the input or
|
|
|
|
|
the function is unable to correctly revert the configuration
|
2017-08-25 16:55:21 -04:00
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
super(Installer, self).rollback_checkpoints(rollback)
|
|
|
|
|
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
|
|
|
self.conf('ignore-master-overrides'),
|
|
|
|
|
self.conf('config-dir'))
|
2017-08-25 16:55:21 -04:00
|
|
|
|
2017-08-25 17:25:22 -04:00
|
|
|
def restart(self):
|
|
|
|
|
"""Restart or refresh the server content.
|
|
|
|
|
|
|
|
|
|
:raises .PluginError: when server cannot be restarted
|
2017-08-28 18:24:51 -04:00
|
|
|
"""
|
2018-02-14 19:20:16 -05:00
|
|
|
self.postfix.restart()
|
2017-08-28 18:24:51 -04:00
|
|
|
|