mirror of
https://github.com/certbot/certbot.git
synced 2026-08-02 19:22:16 +02:00
Merge pull request #3466 from certbot/all-together-now
DNS challenge support in the manual plugin and general purpose --preferred-challenges flag
This commit is contained in:
@@ -234,8 +234,8 @@ class DNS01Response(KeyAuthorizationChallengeResponse):
|
|||||||
try:
|
try:
|
||||||
from acme import dns_resolver
|
from acme import dns_resolver
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
raise errors.Error("Local validation for 'dns-01' challenges "
|
raise errors.DependencyError("Local validation for 'dns-01' "
|
||||||
"requires 'dnspython'")
|
"challenges requires 'dnspython'")
|
||||||
txt_records = dns_resolver.txt_records_for_name(validation_domain_name)
|
txt_records = dns_resolver.txt_records_for_name(validation_domain_name)
|
||||||
exists = validation in txt_records
|
exists = validation in txt_records
|
||||||
if not exists:
|
if not exists:
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class Error(Exception):
|
|||||||
"""Generic ACME error."""
|
"""Generic ACME error."""
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyError(Error):
|
||||||
|
"""Dependency error"""
|
||||||
|
|
||||||
|
|
||||||
class SchemaValidationError(jose_errors.DeserializationError):
|
class SchemaValidationError(jose_errors.DeserializationError):
|
||||||
"""JSON schema ACME object validation error."""
|
"""JSON schema ACME object validation error."""
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -33,14 +33,18 @@ class AuthHandler(object):
|
|||||||
and values are :class:`acme.messages.AuthorizationResource`
|
and values are :class:`acme.messages.AuthorizationResource`
|
||||||
:ivar list achalls: DV challenges in the form of
|
:ivar list achalls: DV challenges in the form of
|
||||||
:class:`certbot.achallenges.AnnotatedChallenge`
|
:class:`certbot.achallenges.AnnotatedChallenge`
|
||||||
|
:ivar list pref_challs: sorted user specified preferred challenges
|
||||||
|
in the form of subclasses of :class:`acme.challenges.Challenge`
|
||||||
|
with the most preferred challenge listed first
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, auth, acme, account):
|
def __init__(self, auth, acme, account, pref_challs):
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.acme = acme
|
self.acme = acme
|
||||||
|
|
||||||
self.account = account
|
self.account = account
|
||||||
self.authzr = dict()
|
self.authzr = dict()
|
||||||
|
self.pref_challs = pref_challs
|
||||||
|
|
||||||
# List must be used to keep responses straight.
|
# List must be used to keep responses straight.
|
||||||
self.achalls = []
|
self.achalls = []
|
||||||
@@ -244,9 +248,18 @@ class AuthHandler(object):
|
|||||||
:param str domain: domain for which you are requesting preferences
|
:param str domain: domain for which you are requesting preferences
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Make sure to make a copy...
|
|
||||||
chall_prefs = []
|
chall_prefs = []
|
||||||
chall_prefs.extend(self.auth.get_chall_pref(domain))
|
# Make sure to make a copy...
|
||||||
|
plugin_pref = self.auth.get_chall_pref(domain)
|
||||||
|
if self.pref_challs:
|
||||||
|
chall_prefs.extend(pref for pref in self.pref_challs
|
||||||
|
if pref in plugin_pref)
|
||||||
|
if chall_prefs:
|
||||||
|
return chall_prefs
|
||||||
|
raise errors.AuthorizationError(
|
||||||
|
"None of the preferred challenges "
|
||||||
|
"are supported by the selected plugin")
|
||||||
|
chall_prefs.extend(plugin_pref)
|
||||||
return chall_prefs
|
return chall_prefs
|
||||||
|
|
||||||
def _cleanup_challenges(self, achall_list=None):
|
def _cleanup_challenges(self, achall_list=None):
|
||||||
|
|||||||
+32
-2
@@ -11,6 +11,8 @@ import sys
|
|||||||
import configargparse
|
import configargparse
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from acme import challenges
|
||||||
|
|
||||||
import certbot
|
import certbot
|
||||||
|
|
||||||
from certbot import constants
|
from certbot import constants
|
||||||
@@ -788,11 +790,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||||||
help=config_help("no_verify_ssl"),
|
help=config_help("no_verify_ssl"),
|
||||||
default=flag_default("no_verify_ssl"))
|
default=flag_default("no_verify_ssl"))
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"testing", "--tls-sni-01-port", type=int,
|
["certonly", "renew", "run"], "--tls-sni-01-port", type=int,
|
||||||
default=flag_default("tls_sni_01_port"),
|
default=flag_default("tls_sni_01_port"),
|
||||||
help=config_help("tls_sni_01_port"))
|
help=config_help("tls_sni_01_port"))
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"testing", "--http-01-port", type=int, dest="http01_port",
|
["certonly", "renew", "run", "manual"], "--http-01-port", type=int,
|
||||||
|
dest="http01_port",
|
||||||
default=flag_default("http01_port"), help=config_help("http01_port"))
|
default=flag_default("http01_port"), help=config_help("http01_port"))
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"testing", "--break-my-certs", action="store_true",
|
"testing", "--break-my-certs", action="store_true",
|
||||||
@@ -844,6 +847,18 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis
|
|||||||
"security", "--strict-permissions", action="store_true",
|
"security", "--strict-permissions", action="store_true",
|
||||||
help="Require that all configuration files are owned by the current "
|
help="Require that all configuration files are owned by the current "
|
||||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||||
|
helpful.add(
|
||||||
|
["manual", "standalone", "certonly", "renew", "run"],
|
||||||
|
"--preferred-challenges", dest="pref_challs",
|
||||||
|
action=_PrefChallAction, default=[],
|
||||||
|
help='A sorted, comma delimited list of the preferred challenge to '
|
||||||
|
'use during authorization with the most preferred challenge '
|
||||||
|
'listed first (Eg, "dns" or "tls-sni-01,http,dns"). '
|
||||||
|
'Not all plugins support all challenges. See '
|
||||||
|
'https://certbot.eff.org/docs/using.html#plugins for details. '
|
||||||
|
'ACME Challenges are versioned, but if you pick "http" rather '
|
||||||
|
'than "http-01", Certbot will select the latest version '
|
||||||
|
'automatically.')
|
||||||
helpful.add(
|
helpful.add(
|
||||||
"renew", "--pre-hook",
|
"renew", "--pre-hook",
|
||||||
help="Command to be run in a shell before obtaining any certificates."
|
help="Command to be run in a shell before obtaining any certificates."
|
||||||
@@ -1032,3 +1047,18 @@ def add_domains(args_or_config, domains):
|
|||||||
args_or_config.domains.append(domain)
|
args_or_config.domains.append(domain)
|
||||||
|
|
||||||
return validated_domains
|
return validated_domains
|
||||||
|
|
||||||
|
class _PrefChallAction(argparse.Action):
|
||||||
|
"""Action class for parsing preferred challenges."""
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, pref_challs, option_string=None):
|
||||||
|
aliases = {"dns": "dns-01", "http": "http-01", "tls-sni": "tls-sni-01"}
|
||||||
|
challs = [c.strip() for c in pref_challs.split(",")]
|
||||||
|
challs = [aliases[c] if c in aliases else c for c in challs]
|
||||||
|
unrecognized = ", ".join(name for name in challs
|
||||||
|
if name not in challenges.Challenge.TYPES)
|
||||||
|
if unrecognized:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"Unrecognized challenges: {0}".format(unrecognized))
|
||||||
|
namespace.pref_challs.extend(challenges.Challenge.TYPES[name]
|
||||||
|
for name in challs)
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ class Client(object):
|
|||||||
|
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
self.auth_handler = auth_handler.AuthHandler(
|
self.auth_handler = auth_handler.AuthHandler(
|
||||||
auth, self.acme, self.account)
|
auth, self.acme, self.account, self.config.pref_challs)
|
||||||
else:
|
else:
|
||||||
self.auth_handler = None
|
self.auth_handler = None
|
||||||
|
|
||||||
|
|||||||
@@ -229,11 +229,14 @@ class IConfig(zope.interface.Interface):
|
|||||||
no_verify_ssl = zope.interface.Attribute(
|
no_verify_ssl = zope.interface.Attribute(
|
||||||
"Disable verification of the ACME server's certificate.")
|
"Disable verification of the ACME server's certificate.")
|
||||||
tls_sni_01_port = zope.interface.Attribute(
|
tls_sni_01_port = zope.interface.Attribute(
|
||||||
"Port number to perform tls-sni-01 challenge. "
|
"Port used during tls-sni-01 challenge. "
|
||||||
"Boulder in testing mode defaults to 5001.")
|
"This only affects the port Certbot listens on. "
|
||||||
|
"A conforming ACME server will still attempt to connect on port 443.")
|
||||||
|
|
||||||
http01_port = zope.interface.Attribute(
|
http01_port = zope.interface.Attribute(
|
||||||
"Port used in the SimpleHttp challenge.")
|
"Port used in the http-01 challenge."
|
||||||
|
"This only affects the port Certbot listens on. "
|
||||||
|
"A conforming ACME server will still attempt to connect on port 80.")
|
||||||
|
|
||||||
|
|
||||||
class IInstaller(IPlugin):
|
class IInstaller(IPlugin):
|
||||||
|
|||||||
+86
-28
@@ -15,6 +15,7 @@ import zope.component
|
|||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
|
from acme import errors as acme_errors
|
||||||
|
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot import interfaces
|
from certbot import interfaces
|
||||||
@@ -41,7 +42,16 @@ class Authenticator(common.Plugin):
|
|||||||
|
|
||||||
description = "Manually configure an HTTP server"
|
description = "Manually configure an HTTP server"
|
||||||
|
|
||||||
MESSAGE_TEMPLATE = """\
|
MESSAGE_TEMPLATE = {
|
||||||
|
"dns-01": """\
|
||||||
|
Please deploy a DNS TXT record under the name
|
||||||
|
{domain} with the following value:
|
||||||
|
|
||||||
|
{validation}
|
||||||
|
|
||||||
|
Once this is deployed,
|
||||||
|
""",
|
||||||
|
"http-01": """\
|
||||||
Make sure your web server displays the following content at
|
Make sure your web server displays the following content at
|
||||||
{uri} before continuing:
|
{uri} before continuing:
|
||||||
|
|
||||||
@@ -51,7 +61,7 @@ If you don't have HTTP server configured, you can run the following
|
|||||||
command on the target server (as root):
|
command on the target server (as root):
|
||||||
|
|
||||||
{command}
|
{command}
|
||||||
"""
|
"""}
|
||||||
|
|
||||||
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
|
# a disclaimer about your current IP being transmitted to Let's Encrypt's servers.
|
||||||
IP_DISCLAIMER = """\
|
IP_DISCLAIMER = """\
|
||||||
@@ -97,21 +107,29 @@ s.serve_forever()" """
|
|||||||
|
|
||||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||||
return ("This plugin requires user's manual intervention in setting "
|
return ("This plugin requires user's manual intervention in setting "
|
||||||
"up an HTTP server for solving http-01 challenges and thus "
|
"up challenges to prove control of a domain and does not need "
|
||||||
"does not need to be run as a privileged process. "
|
"to be run as a privileged process. When solving "
|
||||||
"Alternatively shows instructions on how to use Python's "
|
"http-01 challenges, the user is responsible for setting up "
|
||||||
"built-in HTTP server.")
|
"an HTTP server. Alternatively, instructions are shown on how "
|
||||||
|
"to use Python's built-in HTTP server. The user is "
|
||||||
|
"responsible for configuration of a domain's DNS when solving "
|
||||||
|
"dns-01 challenges. The type of challenges used can be "
|
||||||
|
"controlled through the --preferred-challenges flag.")
|
||||||
|
|
||||||
def get_chall_pref(self, domain):
|
def get_chall_pref(self, domain):
|
||||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||||
return [challenges.HTTP01]
|
return [challenges.HTTP01, challenges.DNS01]
|
||||||
|
|
||||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
def perform(self, achalls):
|
||||||
|
# pylint: disable=missing-docstring
|
||||||
|
self._get_ip_logging_permission()
|
||||||
|
mapping = {"http-01": self._perform_http01_challenge,
|
||||||
|
"dns-01": self._perform_dns01_challenge}
|
||||||
responses = []
|
responses = []
|
||||||
# TODO: group achalls by the same socket.gethostbyname(_ex)
|
# TODO: group achalls by the same socket.gethostbyname(_ex)
|
||||||
# and prompt only once per server (one "echo -n" per domain)
|
# and prompt only once per server (one "echo -n" per domain)
|
||||||
for achall in achalls:
|
for achall in achalls:
|
||||||
responses.append(self._perform_single(achall))
|
responses.append(mapping[achall.typ](achall))
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -128,7 +146,13 @@ s.serve_forever()" """
|
|||||||
finally:
|
finally:
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
def _perform_single(self, achall):
|
def cleanup(self, achalls):
|
||||||
|
# pylint: disable=missing-docstring
|
||||||
|
for achall in achalls:
|
||||||
|
if isinstance(achall.chall, challenges.HTTP01):
|
||||||
|
self._cleanup_http01_challenge(achall)
|
||||||
|
|
||||||
|
def _perform_http01_challenge(self, achall):
|
||||||
# same path for each challenge response would be easier for
|
# same path for each challenge response would be easier for
|
||||||
# users, but will not work if multiple domains point at the
|
# users, but will not work if multiple domains point at the
|
||||||
# same server: default command doesn't support virtual hosts
|
# same server: default command doesn't support virtual hosts
|
||||||
@@ -162,19 +186,16 @@ s.serve_forever()" """
|
|||||||
# give it some time to bootstrap, before we try to verify
|
# give it some time to bootstrap, before we try to verify
|
||||||
# (cert generation in case of simpleHttpS might take time)
|
# (cert generation in case of simpleHttpS might take time)
|
||||||
self._test_mode_busy_wait(port)
|
self._test_mode_busy_wait(port)
|
||||||
|
|
||||||
if self._httpd.poll() is not None:
|
if self._httpd.poll() is not None:
|
||||||
raise errors.Error("Couldn't execute manual command")
|
raise errors.Error("Couldn't execute manual command")
|
||||||
else:
|
else:
|
||||||
if not self.conf("public-ip-logging-ok"):
|
self._notify_and_wait(
|
||||||
if not zope.component.getUtility(interfaces.IDisplay).yesno(
|
self._get_message(achall).format(
|
||||||
self.IP_DISCLAIMER, "Yes", "No",
|
validation=validation,
|
||||||
cli_flag="--manual-public-ip-logging-ok"):
|
response=response,
|
||||||
raise errors.PluginError("Must agree to IP logging to proceed")
|
uri=achall.chall.uri(achall.domain),
|
||||||
|
command=command))
|
||||||
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
|
|
||||||
validation=validation, response=response,
|
|
||||||
uri=achall.chall.uri(achall.domain),
|
|
||||||
command=command))
|
|
||||||
|
|
||||||
if not response.simple_verify(
|
if not response.simple_verify(
|
||||||
achall.chall, achall.domain,
|
achall.chall, achall.domain,
|
||||||
@@ -183,15 +204,30 @@ s.serve_forever()" """
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _notify_and_wait(self, message): # pylint: disable=no-self-use
|
def _perform_dns01_challenge(self, achall):
|
||||||
# TODO: IDisplay wraps messages, breaking the command
|
response, validation = achall.response_and_validation()
|
||||||
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
if not self.conf("test-mode"):
|
||||||
# message=message, height=25, pause=True)
|
self._notify_and_wait(
|
||||||
sys.stdout.write(message)
|
self._get_message(achall).format(
|
||||||
six.moves.input("Press ENTER to continue")
|
validation=validation,
|
||||||
|
domain=achall.validation_domain_name(achall.domain),
|
||||||
|
response=response))
|
||||||
|
|
||||||
def cleanup(self, achalls):
|
try:
|
||||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
verification_status = response.simple_verify(
|
||||||
|
achall.chall, achall.domain,
|
||||||
|
achall.account_key.public_key())
|
||||||
|
except acme_errors.DependencyError:
|
||||||
|
logger.warning("Self verification requires optional "
|
||||||
|
"dependency `dnspython` to be installed.")
|
||||||
|
else:
|
||||||
|
if not verification_status:
|
||||||
|
logger.warning("Self-verify of challenge failed.")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _cleanup_http01_challenge(self, achall):
|
||||||
|
# pylint: disable=missing-docstring,unused-argument
|
||||||
if self.conf("test-mode"):
|
if self.conf("test-mode"):
|
||||||
assert self._httpd is not None, (
|
assert self._httpd is not None, (
|
||||||
"cleanup() must be called after perform()")
|
"cleanup() must be called after perform()")
|
||||||
@@ -202,3 +238,25 @@ s.serve_forever()" """
|
|||||||
logger.debug("Manual command process already terminated "
|
logger.debug("Manual command process already terminated "
|
||||||
"with %s code", self._httpd.returncode)
|
"with %s code", self._httpd.returncode)
|
||||||
shutil.rmtree(self._root)
|
shutil.rmtree(self._root)
|
||||||
|
|
||||||
|
def _notify_and_wait(self, message):
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
# TODO: IDisplay wraps messages, breaking the command
|
||||||
|
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
||||||
|
# message=message, height=25, pause=True)
|
||||||
|
sys.stdout.write(message)
|
||||||
|
six.moves.input("Press ENTER to continue")
|
||||||
|
|
||||||
|
def _get_ip_logging_permission(self):
|
||||||
|
# pylint: disable=missing-docstring
|
||||||
|
if not (self.conf("test-mode") or self.conf("public-ip-logging-ok")):
|
||||||
|
if not zope.component.getUtility(interfaces.IDisplay).yesno(
|
||||||
|
self.IP_DISCLAIMER, "Yes", "No",
|
||||||
|
cli_flag="--manual-public-ip-logging-ok"):
|
||||||
|
raise errors.PluginError("Must agree to IP logging to proceed")
|
||||||
|
else:
|
||||||
|
self.config.namespace.manual_public_ip_logging_ok = True
|
||||||
|
|
||||||
|
def _get_message(self, achall):
|
||||||
|
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||||
|
return self.MESSAGE_TEMPLATE.get(achall.chall.typ, "")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import unittest
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
from acme import challenges
|
from acme import challenges
|
||||||
|
from acme import errors as acme_errors
|
||||||
from acme import jose
|
from acme import jose
|
||||||
|
|
||||||
from certbot import achallenges
|
from certbot import achallenges
|
||||||
@@ -26,8 +27,13 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
http01_port=8080, manual_test_mode=False,
|
http01_port=8080, manual_test_mode=False,
|
||||||
manual_public_ip_logging_ok=False, noninteractive_mode=True)
|
manual_public_ip_logging_ok=False, noninteractive_mode=True)
|
||||||
self.auth = Authenticator(config=self.config, name="manual")
|
self.auth = Authenticator(config=self.config, name="manual")
|
||||||
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
|
|
||||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
|
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
|
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)
|
||||||
|
self.dns01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||||
|
challb=acme_util.DNS01_P, domain="foo.com", account_key=KEY)
|
||||||
|
|
||||||
|
self.achalls = [self.http01, self.dns01]
|
||||||
|
|
||||||
config_test_mode = mock.MagicMock(
|
config_test_mode = mock.MagicMock(
|
||||||
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
|
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
|
||||||
@@ -45,7 +51,9 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
||||||
for pref in self.auth.get_chall_pref("foo.com")))
|
for pref in self.auth.get_chall_pref("foo.com")))
|
||||||
|
|
||||||
def test_perform_empty(self):
|
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||||
|
def test_perform_empty(self, mock_interaction):
|
||||||
|
mock_interaction().yesno.return_value = True
|
||||||
self.assertEqual([], self.auth.perform([]))
|
self.assertEqual([], self.auth.perform([]))
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||||
@@ -56,19 +64,34 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
mock_verify.return_value = True
|
mock_verify.return_value = True
|
||||||
mock_interaction().yesno.return_value = True
|
mock_interaction().yesno.return_value = True
|
||||||
|
|
||||||
resp = self.achalls[0].response(KEY)
|
resp_http = self.http01.response(KEY)
|
||||||
self.assertEqual([resp], self.auth.perform(self.achalls))
|
resp_dns = self.dns01.response(KEY)
|
||||||
self.assertEqual(1, mock_raw_input.call_count)
|
|
||||||
|
self.assertEqual([resp_http, resp_dns], self.auth.perform(self.achalls))
|
||||||
|
self.assertEqual(2, mock_raw_input.call_count)
|
||||||
mock_verify.assert_called_with(
|
mock_verify.assert_called_with(
|
||||||
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 8080)
|
self.http01.challb.chall, "foo.com", KEY.public_key(), 8080)
|
||||||
|
|
||||||
message = mock_stdout.write.mock_calls[0][1][0]
|
message = mock_stdout.write.mock_calls[0][1][0]
|
||||||
self.assertTrue(self.achalls[0].chall.encode("token") in message)
|
self.assertTrue(self.http01.chall.encode("token") in message)
|
||||||
|
|
||||||
mock_verify.return_value = False
|
mock_verify.return_value = False
|
||||||
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
||||||
self.auth.perform(self.achalls)
|
self.auth.perform(self.achalls)
|
||||||
mock_logger.warning.assert_called_once_with(mock.ANY)
|
self.assertEqual(2, mock_logger.warning.call_count)
|
||||||
|
|
||||||
|
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||||
|
@mock.patch("acme.challenges.DNS01Response.simple_verify")
|
||||||
|
@mock.patch("six.moves.input")
|
||||||
|
def test_perform_missing_dependency(self, mock_raw_input, mock_verify, mock_interaction):
|
||||||
|
mock_interaction().yesno.return_value = True
|
||||||
|
mock_verify.side_effect = acme_errors.DependencyError()
|
||||||
|
|
||||||
|
with mock.patch("certbot.plugins.manual.logger") as mock_logger:
|
||||||
|
self.auth.perform([self.dns01])
|
||||||
|
self.assertEqual(1, mock_logger.warning.call_count)
|
||||||
|
|
||||||
|
mock_raw_input.assert_called_once_with("Press ENTER to continue")
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
@mock.patch("certbot.plugins.manual.zope.component.getUtility")
|
||||||
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
|
@mock.patch("certbot.plugins.manual.Authenticator._notify_and_wait")
|
||||||
@@ -82,7 +105,7 @@ class AuthenticatorTest(unittest.TestCase):
|
|||||||
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
@mock.patch("certbot.plugins.manual.subprocess.Popen", autospec=True)
|
||||||
def test_perform_test_command_oserror(self, mock_popen):
|
def test_perform_test_command_oserror(self, mock_popen):
|
||||||
mock_popen.side_effect = OSError
|
mock_popen.side_effect = OSError
|
||||||
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
|
self.assertEqual([False], self.auth_test_mode.perform([self.http01]))
|
||||||
|
|
||||||
@mock.patch("certbot.plugins.manual.socket.socket")
|
@mock.patch("certbot.plugins.manual.socket.socket")
|
||||||
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
|
@mock.patch("certbot.plugins.manual.time.sleep", autospec=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import argparse
|
|||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
@@ -12,6 +13,7 @@ import zope.interface
|
|||||||
from acme import challenges
|
from acme import challenges
|
||||||
from acme import standalone as acme_standalone
|
from acme import standalone as acme_standalone
|
||||||
|
|
||||||
|
from certbot import cli
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot import interfaces
|
from certbot import interfaces
|
||||||
|
|
||||||
@@ -119,6 +121,11 @@ def supported_challenges_validator(data):
|
|||||||
It should be passed as `type` argument to `add_argument`.
|
It should be passed as `type` argument to `add_argument`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if cli.set_by_cli("standalone_supported_challenges"):
|
||||||
|
sys.stderr.write(
|
||||||
|
"WARNING: The standalone specific "
|
||||||
|
"supported challenges flag is deprecated.\n"
|
||||||
|
"Please use the --preferred-challenges flag instead.\n")
|
||||||
challs = data.split(",")
|
challs = data.split(",")
|
||||||
|
|
||||||
# tls-sni-01 was dvsni during private beta
|
# tls-sni-01 was dvsni during private beta
|
||||||
@@ -177,7 +184,7 @@ class Authenticator(common.Plugin):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def add_parser_arguments(cls, add):
|
def add_parser_arguments(cls, add):
|
||||||
add("supported-challenges",
|
add("supported-challenges",
|
||||||
help="Supported challenges. Preferred in the order they are listed.",
|
help=argparse.SUPPRESS,
|
||||||
type=supported_challenges_validator,
|
type=supported_challenges_validator,
|
||||||
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
|
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
|
||||||
|
|
||||||
|
|||||||
@@ -67,10 +67,25 @@ class ServerManagerTest(unittest.TestCase):
|
|||||||
class SupportedChallengesValidatorTest(unittest.TestCase):
|
class SupportedChallengesValidatorTest(unittest.TestCase):
|
||||||
"""Tests for plugins.standalone.supported_challenges_validator."""
|
"""Tests for plugins.standalone.supported_challenges_validator."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.set_by_cli_patch = mock.patch(
|
||||||
|
"certbot.plugins.standalone.cli.set_by_cli")
|
||||||
|
self.stderr_patch = mock.patch("certbot.plugins.standalone.sys.stderr")
|
||||||
|
|
||||||
|
self.set_by_cli_patch.start().return_value = True
|
||||||
|
self.stderr = self.stderr_patch.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.set_by_cli_patch.stop()
|
||||||
|
self.stderr_patch.stop()
|
||||||
|
|
||||||
def _call(self, data):
|
def _call(self, data):
|
||||||
from certbot.plugins.standalone import (
|
from certbot.plugins.standalone import (
|
||||||
supported_challenges_validator)
|
supported_challenges_validator)
|
||||||
return supported_challenges_validator(data)
|
return_value = supported_challenges_validator(data)
|
||||||
|
self.assertTrue(self.stderr.write.called) # pylint: disable=no-member
|
||||||
|
self.stderr.write.reset_mock() # pylint: disable=no-member
|
||||||
|
return return_value
|
||||||
|
|
||||||
def test_correct(self):
|
def test_correct(self):
|
||||||
self.assertEqual("tls-sni-01", self._call("tls-sni-01"))
|
self.assertEqual("tls-sni-01", self._call("tls-sni-01"))
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ HTTP01 = challenges.HTTP01(
|
|||||||
token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
|
token=b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
|
||||||
TLSSNI01 = challenges.TLSSNI01(
|
TLSSNI01 = challenges.TLSSNI01(
|
||||||
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
|
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
|
||||||
DNS = challenges.DNS(token=b"17817c66b60ce2e4012dfad92657527a")
|
DNS01 = challenges.DNS01(token=b"17817c66b60ce2e4012dfad92657527a")
|
||||||
|
|
||||||
CHALLENGES = [HTTP01, TLSSNI01, DNS]
|
CHALLENGES = [HTTP01, TLSSNI01, DNS01]
|
||||||
|
|
||||||
|
|
||||||
def gen_combos(challbs):
|
def gen_combos(challbs):
|
||||||
@@ -45,9 +45,9 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
|
|||||||
# Pending ChallengeBody objects
|
# Pending ChallengeBody objects
|
||||||
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
|
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
|
||||||
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
|
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
|
||||||
DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
|
DNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)
|
||||||
|
|
||||||
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P]
|
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS01_P]
|
||||||
|
|
||||||
|
|
||||||
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ChallengeFactoryTest(unittest.TestCase):
|
|||||||
from certbot.auth_handler import AuthHandler
|
from certbot.auth_handler import AuthHandler
|
||||||
|
|
||||||
# Account is mocked...
|
# Account is mocked...
|
||||||
self.handler = AuthHandler(None, None, mock.Mock(key="mock_key"))
|
self.handler = AuthHandler(None, None, mock.Mock(key="mock_key"), [])
|
||||||
|
|
||||||
self.dom = "test"
|
self.dom = "test"
|
||||||
self.handler.authzr[self.dom] = acme_util.gen_authzr(
|
self.handler.authzr[self.dom] = acme_util.gen_authzr(
|
||||||
@@ -74,7 +74,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||||||
self.mock_net = mock.MagicMock(spec=acme_client.Client)
|
self.mock_net = mock.MagicMock(spec=acme_client.Client)
|
||||||
|
|
||||||
self.handler = AuthHandler(
|
self.handler = AuthHandler(
|
||||||
self.mock_auth, self.mock_net, self.mock_account)
|
self.mock_auth, self.mock_net, self.mock_account, [])
|
||||||
|
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||||||
|
|
||||||
mock_poll.side_effect = self._validate_all
|
mock_poll.side_effect = self._validate_all
|
||||||
self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01)
|
self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01)
|
||||||
self.mock_auth.get_chall_pref.return_value.append(challenges.DNS)
|
self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01)
|
||||||
|
|
||||||
authzr = self.handler.get_authorizations(["0"])
|
authzr = self.handler.get_authorizations(["0"])
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||||||
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
|
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
|
||||||
# Test if list first element is TLSSNI01, use typ because it is an achall
|
# Test if list first element is TLSSNI01, use typ because it is an achall
|
||||||
for achall in self.mock_auth.cleanup.call_args[0][0]:
|
for achall in self.mock_auth.cleanup.call_args[0][0]:
|
||||||
self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns"])
|
self.assertTrue(achall.typ in ["tls-sni-01", "http-01", "dns-01"])
|
||||||
|
|
||||||
# Length of authorizations list
|
# Length of authorizations list
|
||||||
self.assertEqual(len(authzr), 1)
|
self.assertEqual(len(authzr), 1)
|
||||||
@@ -167,6 +167,29 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||||||
def test_no_domains(self):
|
def test_no_domains(self):
|
||||||
self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, [])
|
self.assertRaises(errors.AuthorizationError, self.handler.get_authorizations, [])
|
||||||
|
|
||||||
|
@mock.patch("certbot.auth_handler.AuthHandler._poll_challenges")
|
||||||
|
def test_preferred_challenge_choice(self, mock_poll):
|
||||||
|
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||||
|
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||||
|
|
||||||
|
mock_poll.side_effect = self._validate_all
|
||||||
|
self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01)
|
||||||
|
|
||||||
|
self.handler.pref_challs.extend((challenges.HTTP01, challenges.DNS01,))
|
||||||
|
|
||||||
|
self.handler.get_authorizations(["0"])
|
||||||
|
|
||||||
|
self.assertEqual(self.mock_auth.cleanup.call_count, 1)
|
||||||
|
self.assertEqual(
|
||||||
|
self.mock_auth.cleanup.call_args[0][0][0].typ, "http-01")
|
||||||
|
|
||||||
|
def test_preferred_challenges_not_supported(self):
|
||||||
|
self.mock_net.request_domain_challenges.side_effect = functools.partial(
|
||||||
|
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||||
|
self.handler.pref_challs.append(challenges.HTTP01)
|
||||||
|
self.assertRaises(
|
||||||
|
errors.AuthorizationError, self.handler.get_authorizations, ["0"])
|
||||||
|
|
||||||
def _validate_all(self, unused_1, unused_2):
|
def _validate_all(self, unused_1, unused_2):
|
||||||
for dom in six.iterkeys(self.handler.authzr):
|
for dom in six.iterkeys(self.handler.authzr):
|
||||||
azr = self.handler.authzr[dom]
|
azr = self.handler.authzr[dom]
|
||||||
@@ -189,7 +212,7 @@ class PollChallengesTest(unittest.TestCase):
|
|||||||
# Account and network are mocked...
|
# Account and network are mocked...
|
||||||
self.mock_net = mock.MagicMock()
|
self.mock_net = mock.MagicMock()
|
||||||
self.handler = AuthHandler(
|
self.handler = AuthHandler(
|
||||||
None, self.mock_net, mock.Mock(key="mock_key"))
|
None, self.mock_net, mock.Mock(key="mock_key"), [])
|
||||||
|
|
||||||
self.doms = ["0", "1", "2"]
|
self.doms = ["0", "1", "2"]
|
||||||
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr(
|
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr(
|
||||||
@@ -240,7 +263,7 @@ class PollChallengesTest(unittest.TestCase):
|
|||||||
from certbot.auth_handler import challb_to_achall
|
from certbot.auth_handler import challb_to_achall
|
||||||
self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid
|
self.mock_net.poll.side_effect = self._mock_poll_solve_one_valid
|
||||||
self.chall_update[self.doms[0]].append(
|
self.chall_update[self.doms[0]].append(
|
||||||
challb_to_achall(acme_util.DNS_P, "key", self.doms[0]))
|
challb_to_achall(acme_util.DNS01_P, "key", self.doms[0]))
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
errors.AuthorizationError, self.handler._poll_challenges,
|
errors.AuthorizationError, self.handler._poll_challenges,
|
||||||
self.chall_update, False)
|
self.chall_update, False)
|
||||||
@@ -342,7 +365,7 @@ class GenChallengePathTest(unittest.TestCase):
|
|||||||
self.assertTrue(self._call(challbs[::-1], prefs, None))
|
self.assertTrue(self._call(challbs[::-1], prefs, None))
|
||||||
|
|
||||||
def test_not_supported(self):
|
def test_not_supported(self):
|
||||||
challbs = (acme_util.DNS_P, acme_util.TLSSNI01_P)
|
challbs = (acme_util.DNS01_P, acme_util.TLSSNI01_P)
|
||||||
prefs = [challenges.TLSSNI01]
|
prefs = [challenges.TLSSNI01]
|
||||||
combos = ((0, 1),)
|
combos = ((0, 1),)
|
||||||
|
|
||||||
|
|||||||
@@ -423,6 +423,18 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
namespace = parse(long_args)
|
namespace = parse(long_args)
|
||||||
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
|
||||||
|
|
||||||
|
def test_preferred_challenges(self):
|
||||||
|
from acme.challenges import HTTP01, TLSSNI01, DNS01
|
||||||
|
parse = self._get_argument_parser()
|
||||||
|
|
||||||
|
short_args = ['--preferred-challenges', 'http, tls-sni-01, dns']
|
||||||
|
namespace = parse(short_args)
|
||||||
|
|
||||||
|
self.assertEqual(namespace.pref_challs, [HTTP01, TLSSNI01, DNS01])
|
||||||
|
|
||||||
|
short_args = ['--preferred-challenges', 'jumping-over-the-moon']
|
||||||
|
self.assertRaises(argparse.ArgumentTypeError, parse, short_args)
|
||||||
|
|
||||||
def test_server_flag(self):
|
def test_server_flag(self):
|
||||||
parse = self._get_argument_parser()
|
parse = self._get_argument_parser()
|
||||||
namespace = parse('--server example.com'.split())
|
namespace = parse('--server example.com'.split())
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ class FaiiledChallengesTest(unittest.TestCase):
|
|||||||
from certbot.errors import FailedChallenges
|
from certbot.errors import FailedChallenges
|
||||||
self.error = FailedChallenges(set([achallenges.DNS(
|
self.error = FailedChallenges(set([achallenges.DNS(
|
||||||
domain="example.com", challb=messages.ChallengeBody(
|
domain="example.com", challb=messages.ChallengeBody(
|
||||||
chall=acme_util.DNS, uri=None,
|
chall=acme_util.DNS01, uri=None,
|
||||||
error=messages.Error(typ="tls", detail="detail")))]))
|
error=messages.Error(typ="tls", detail="detail")))]))
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
self.assertTrue(str(self.error).startswith(
|
self.assertTrue(str(self.error).startswith(
|
||||||
"Failed authorization procedure. example.com (dns): tls"))
|
"Failed authorization procedure. example.com (dns-01): tls"))
|
||||||
|
|
||||||
|
|
||||||
class StandaloneBindErrorTest(unittest.TestCase):
|
class StandaloneBindErrorTest(unittest.TestCase):
|
||||||
|
|||||||
+40
-28
@@ -60,7 +60,7 @@ an alternate method fo install ``certbot``.
|
|||||||
|
|
||||||
Certbot-Auto
|
Certbot-Auto
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies
|
The ``certbot-auto`` wrapper script installs Certbot, obtaining some dependencies
|
||||||
from your web server OS and putting others in a python virtual environment. You can
|
from your web server OS and putting others in a python virtual environment. You can
|
||||||
download and run it as follows::
|
download and run it as follows::
|
||||||
|
|
||||||
@@ -77,8 +77,8 @@ download and run it as follows::
|
|||||||
|
|
||||||
The ``certbot-auto`` command updates to the latest client release automatically.
|
The ``certbot-auto`` command updates to the latest client release automatically.
|
||||||
Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly
|
Since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly
|
||||||
the same command line flags and arguments. For more information, see
|
the same command line flags and arguments. For more information, see
|
||||||
`Certbot command-line options <https://certbot.eff.org/docs/using.html#command-line-options>`_.
|
`Certbot command-line options <https://certbot.eff.org/docs/using.html#command-line-options>`_.
|
||||||
|
|
||||||
Running with Docker
|
Running with Docker
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -88,8 +88,8 @@ certificate. However, this mode of operation is unable to install
|
|||||||
certificates or configure your webserver, because our installer
|
certificates or configure your webserver, because our installer
|
||||||
plugins cannot reach your webserver from inside the Docker container.
|
plugins cannot reach your webserver from inside the Docker container.
|
||||||
|
|
||||||
Most users should use the operating system packages (see instructions at
|
Most users should use the operating system packages (see instructions at
|
||||||
certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only
|
certbot.eff.org_) or, as a fallback, ``certbot-auto``. You should only
|
||||||
use Docker if you are sure you know what you are doing and have a
|
use Docker if you are sure you know what you are doing and have a
|
||||||
good reason to do so.
|
good reason to do so.
|
||||||
|
|
||||||
@@ -113,12 +113,12 @@ to, `install Docker`_, then issue the following command:
|
|||||||
quay.io/letsencrypt/letsencrypt:latest certonly
|
quay.io/letsencrypt/letsencrypt:latest certonly
|
||||||
|
|
||||||
Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory
|
Running Certbot with the ``certonly`` command will obtain a certificate and place it in the directory
|
||||||
``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from
|
``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from
|
||||||
within Docker, you must install the certificate manually according to the procedure
|
within Docker, you must install the certificate manually according to the procedure
|
||||||
recommended by the provider of your webserver.
|
recommended by the provider of your webserver.
|
||||||
|
|
||||||
For more information about the layout
|
For more information about the layout
|
||||||
of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.
|
of the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.
|
||||||
|
|
||||||
.. _Docker: https://docker.com
|
.. _Docker: https://docker.com
|
||||||
.. _`install Docker`: https://docs.docker.com/userguide/
|
.. _`install Docker`: https://docs.docker.com/userguide/
|
||||||
@@ -242,8 +242,8 @@ whole process is described in the :doc:`contributing`.
|
|||||||
|
|
||||||
.. _plugins:
|
.. _plugins:
|
||||||
|
|
||||||
Getting certificates
|
Getting certificates (and chosing plugins)
|
||||||
====================
|
==========================================
|
||||||
|
|
||||||
The Certbot client supports a number of different "plugins" that can be
|
The Certbot client supports a number of different "plugins" that can be
|
||||||
used to obtain and/or install certificates.
|
used to obtain and/or install certificates.
|
||||||
@@ -252,38 +252,50 @@ Plugins that can obtain a cert are called "authenticators" and can be used with
|
|||||||
the "certonly" command. This will carry out the steps needed to validate that you
|
the "certonly" command. This will carry out the steps needed to validate that you
|
||||||
control the domain(s) you are requesting a cert for, obtain a cert for the specified
|
control the domain(s) you are requesting a cert for, obtain a cert for the specified
|
||||||
domain(s), and place it in the ``/etc/letsencrypt`` directory on your
|
domain(s), and place it in the ``/etc/letsencrypt`` directory on your
|
||||||
machine - without editing any of your server's configuration files to serve the
|
machine - without editing any of your server's configuration files to serve the
|
||||||
obtained certificate. If you specify multiple domains to authenticate, they will
|
obtained certificate. If you specify multiple domains to authenticate, they will
|
||||||
all be listed in a single certificate. To obtain multiple seperate certificates
|
all be listed in a single certificate. To obtain multiple seperate certificates
|
||||||
you will need to run Certbot multiple times.
|
you will need to run Certbot multiple times.
|
||||||
|
|
||||||
Plugins that can install a cert are called "installers" and can be used with the
|
Plugins that can install a cert are called "installers" and can be used with the
|
||||||
"install" command. These plugins can modify your webserver's configuration to
|
"install" command. These plugins can modify your webserver's configuration to
|
||||||
serve your website over HTTPS using certificates obtained by certbot.
|
serve your website over HTTPS using certificates obtained by certbot.
|
||||||
|
|
||||||
Plugins that do both can be used with the "certbot run" command, which is the default
|
Plugins that do both can be used with the "certbot run" command, which is the default
|
||||||
when no command is specified. The "run" subcommand can also be used to specify
|
when no command is specified. The "run" subcommand can also be used to specify
|
||||||
a combination of distinct authenticator and installer plugins.
|
a combination of distinct authenticator and installer plugins.
|
||||||
|
|
||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== =============================================================== =============================
|
||||||
Plugin Auth Inst Notes
|
Plugin Auth Inst Notes Challenge types (and port)
|
||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== =============================================================== =============================
|
||||||
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
|
apache_ Y Y | Automates obtaining and installing a cert with Apache 2.4 on tls-sni-01_ (443)
|
||||||
Debian-based distributions with ``libaugeas0`` 1.0+.
|
| Debian-based distributions with ``libaugeas0`` 1.0+.
|
||||||
webroot_ Y N Obtains a cert by writing to the webroot directory of an
|
webroot_ Y N | Obtains a cert by writing to the webroot directory of an http-01_ (80)
|
||||||
already running webserver.
|
| already running webserver.
|
||||||
standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires
|
standalone_ Y N | Uses a "standalone" webserver to obtain a cert. Requires http-01_ (80) or
|
||||||
port 80 or 443 to be available. This is useful on systems
|
| port 80 or 443 to be available. This is useful on systems tls-sni-01_ (443)
|
||||||
with no webserver, or when direct integration with the local
|
| with no webserver, or when direct integration with the local
|
||||||
webserver is not supported or not desired.
|
| webserver is not supported or not desired.
|
||||||
manual_ Y N Helps you obtain a cert by giving you instructions to perform
|
manual_ Y N | Helps you obtain a cert by giving you instructions to perform http-01_ (80) or
|
||||||
domain validation yourself.
|
| domain validation yourself. dns-01_ (53)
|
||||||
nginx_ Y Y Very experimental and not included in certbot-auto_.
|
nginx_ Y Y | Very experimental and not included in certbot-auto_. tls-sni-01_ (443)
|
||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== =============================================================== =============================
|
||||||
|
|
||||||
|
Under the hood, plugins use one of several ACME protocol "Challenges_" to
|
||||||
|
prove you control a domain. The options are http-01_ (which uses port 80),
|
||||||
|
tls-sni-01_ (port 443) and dns-01_ (requring configuration of a DNS server on
|
||||||
|
port 53, thought that's often not the same machine as your webserver). A few
|
||||||
|
plugins support more than one challenge type, in which case you can choose one
|
||||||
|
with ``--preferred-challenges``.
|
||||||
|
|
||||||
There are also many third-party-plugins_ available. Below we describe in more detail
|
There are also many third-party-plugins_ available. Below we describe in more detail
|
||||||
the circumstances in which each plugin can be used, and how to use it.
|
the circumstances in which each plugin can be used, and how to use it.
|
||||||
|
|
||||||
|
.. _Challenges: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7
|
||||||
|
.. _tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3
|
||||||
|
.. _http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.2
|
||||||
|
.. _dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.4
|
||||||
|
|
||||||
Apache
|
Apache
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user