mirror of
https://github.com/certbot/certbot.git
synced 2026-04-21 22:26:59 -04:00
- Finishing refactor of postconf/postfix command-line utilities - Plugin uses starttls_policy plugin to specify per-domain policies Cleaning up TLS policy code. Print warning when setting configuration parameter that is overridden by master. Update client to use new policy API Cleanup and test fixes Documentation fix smaller fixes Policy is now an enhancement and reverting works Added a README, and small documentation fixes throughout Moving testing infra from starttls repo to certbot-postfix fixing tests and lint Changes against new policy API starttls-everywhere => starttls-policy testing(postfix): Added more varieties of certificates to test against. Moar fixes against policy API. Address comments on README and setup.py Address small comments on postconf and util Address comments in installer Python 3 fixes and Postconf tester extends TempDir test class Mock out postconf calls from tests and test coverage for master overrides More various fixes. Everything minus testing done Remove STARTTLS policy enhancement from this branch. sphinx quickstart 99% test coverage some cleanup and testfixing cleanup leftover files Remove print statement testfix for python 3.4 Revert dockerfile change mypy fix fix(postfix): brad's comments test(postfix): coverage to 100 test(postfix): mypy import mypy types fix(postfix docs): add .rst files and fix build fix(postfix): tls_only and server_only params behave nicely together some cleanup lint fix more comments bump version number
288 lines
12 KiB
Python
288 lines
12 KiB
Python
"""certbot installer plugin for postfix."""
|
|
import logging
|
|
import os
|
|
|
|
import zope.interface
|
|
import zope.component
|
|
import six
|
|
|
|
from certbot import errors
|
|
from certbot import interfaces
|
|
from certbot import util as certbot_util
|
|
from certbot.plugins import common as plugins_common
|
|
|
|
from certbot_postfix import constants
|
|
from certbot_postfix import postconf
|
|
from certbot_postfix import util
|
|
|
|
# pylint: disable=unused-import, no-name-in-module
|
|
from acme.magic_typing import Callable, Dict, List
|
|
# pylint: enable=unused-import, no-name-in-module
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@zope.interface.implementer(interfaces.IInstaller)
|
|
@zope.interface.provider(interfaces.IPluginFactory)
|
|
class Installer(plugins_common.Installer):
|
|
"""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
|
|
|
|
: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`
|
|
"""
|
|
|
|
description = "Configure TLS with the Postfix MTA"
|
|
|
|
@classmethod
|
|
def add_parser_arguments(cls, add):
|
|
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
|
help="Path to the 'postfix' control program.")
|
|
# This directory points to Postfix's configuration directory.
|
|
add("config-dir", default=constants.CLI_DEFAULTS["config_dir"],
|
|
help="Path to the directory containing the "
|
|
"Postfix main.cf file to modify instead of using the "
|
|
"default configuration paths.")
|
|
add("config-utility", default=constants.CLI_DEFAULTS["config_utility"],
|
|
help="Path to the 'postconf' executable.")
|
|
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.")
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Installer, self).__init__(*args, **kwargs)
|
|
# 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
|
|
|
|
def prepare(self):
|
|
"""Prepare the installer.
|
|
|
|
:raises errors.PluginError: when an unexpected error occurs
|
|
:raises errors.MisconfigurationError: when the config is invalid
|
|
:raises errors.NoInstallationError: when can't find installation
|
|
:raises errors.NotSupportedError: when version is not supported
|
|
"""
|
|
# Verify postfix and postconf are installed
|
|
for param in ("ctl", "config_utility",):
|
|
util.verify_exe_exists(self.conf(param),
|
|
"Cannot find executable '{0}'. You can provide the "
|
|
"path to this command with --{1}".format(
|
|
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()
|
|
|
|
# Check Postfix version
|
|
self._check_version()
|
|
self._lock_config_dir()
|
|
self.install_ssl_dhparams()
|
|
|
|
def config_test(self):
|
|
"""Test to see that the current Postfix configuration is valid.
|
|
|
|
:raises errors.MisconfigurationError: If the configuration is invalid.
|
|
"""
|
|
self.postfix.test()
|
|
|
|
def _check_version(self):
|
|
"""Verifies that the installed Postfix version is supported.
|
|
|
|
:raises errors.NotSupportedError: if the version is unsupported
|
|
"""
|
|
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)
|
|
|
|
def _lock_config_dir(self):
|
|
"""Stop two Postfix plugins from modifying the config at once.
|
|
|
|
:raises .PluginError: if unable to acquire the lock
|
|
"""
|
|
try:
|
|
certbot_util.lock_dir_until_exit(self.conf('config-dir'))
|
|
except (OSError, errors.LockError):
|
|
logger.debug("Encountered error:", exc_info=True)
|
|
raise errors.PluginError(
|
|
"Unable to lock %s" % self.conf('config-dir'))
|
|
|
|
def more_info(self):
|
|
"""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
|
|
"""
|
|
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,
|
|
root=self.conf('config-dir'),
|
|
version='.'.join([str(i) for i in self._get_version()]))
|
|
)
|
|
|
|
def _get_version(self):
|
|
"""Return the version of Postfix, as a tuple. (e.g. '2.11.3' is (2, 11, 3))
|
|
|
|
:returns: version
|
|
:rtype: tuple
|
|
|
|
:raises errors.PluginError: Unable to find Postfix version.
|
|
"""
|
|
mail_version = self.postconf.get_default("mail_version")
|
|
return tuple(int(i) for i in mail_version.split('.'))
|
|
|
|
def get_all_names(self):
|
|
"""Returns all names that may be authenticated.
|
|
|
|
:rtype: `set` of `str`
|
|
|
|
"""
|
|
return certbot_util.get_filtered_names(self.postconf.get(var)
|
|
for var in ('mydomain', 'myhostname', 'myorigin',))
|
|
|
|
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.")
|
|
|
|
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
|
|
|
|
"""
|
|
# pylint: disable=unused-argument
|
|
if self._tls_enabled:
|
|
return
|
|
self._tls_enabled = True
|
|
self.save_notes.append("Configuring TLS for {0}".format(domain))
|
|
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()
|
|
|
|
def enhance(self, domain, enhancement, options=None):
|
|
"""Raises an exception since this installer doesn't support any enhancements.
|
|
"""
|
|
# pylint: disable=unused-argument
|
|
raise errors.PluginError(
|
|
"Unsupported enhancement: {0}".format(enhancement))
|
|
|
|
def supported_enhancements(self):
|
|
"""Returns a list of supported enhancements.
|
|
|
|
:rtype: list
|
|
|
|
"""
|
|
return []
|
|
|
|
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
|
|
"""
|
|
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()
|
|
|
|
del self.save_notes[:]
|
|
|
|
if title and not temporary:
|
|
self.finalize_checkpoint(title)
|
|
|
|
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'))
|
|
|
|
def rollback_checkpoints(self, rollback=1):
|
|
"""Rollback saved checkpoints.
|
|
|
|
: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
|
|
"""
|
|
super(Installer, self).rollback_checkpoints(rollback)
|
|
self.postconf = postconf.ConfigMain(self.conf('config-utility'),
|
|
self.conf('ignore-master-overrides'),
|
|
self.conf('config-dir'))
|
|
|
|
def restart(self):
|
|
"""Restart or refresh the server content.
|
|
|
|
:raises .PluginError: when server cannot be restarted
|
|
"""
|
|
self.postfix.restart()
|
|
|