ACME Reprovisioning (#4522)

This commit is contained in:
Amber Brown 2019-02-11 21:36:26 +11:00 committed by Richard van der Hoff
parent 4ffd10f46d
commit 6e2a5aa050
5 changed files with 89 additions and 25 deletions

1
changelog.d/4522.feature Normal file
View File

@ -0,0 +1 @@
Synapse's ACME support will now correctly reprovision a certificate that approaches its expiry while Synapse is running.

View File

@ -23,6 +23,7 @@ import psutil
from daemonize import Daemonize from daemonize import Daemonize
from twisted.internet import error, reactor from twisted.internet import error, reactor
from twisted.protocols.tls import TLSMemoryBIOFactory
from synapse.app import check_bind_error from synapse.app import check_bind_error
from synapse.crypto import context_factory from synapse.crypto import context_factory
@ -220,6 +221,24 @@ def refresh_certificate(hs):
) )
logging.info("Certificate loaded.") logging.info("Certificate loaded.")
if hs._listening_services:
logging.info("Updating context factories...")
for i in hs._listening_services:
# When you listenSSL, it doesn't make an SSL port but a TCP one with
# a TLS wrapping factory around the factory you actually want to get
# requests. This factory attribute is public but missing from
# Twisted's documentation.
if isinstance(i.factory, TLSMemoryBIOFactory):
# We want to replace TLS factories with a new one, with the new
# TLS configuration. We do this by reaching in and pulling out
# the wrappedFactory, and then re-wrapping it.
i.factory = TLSMemoryBIOFactory(
hs.tls_server_context_factory,
False,
i.factory.wrappedFactory
)
logging.info("Context factories updated.")
def start(hs, listeners=None): def start(hs, listeners=None):
""" """

View File

@ -83,7 +83,6 @@ def gz_wrap(r):
class SynapseHomeServer(HomeServer): class SynapseHomeServer(HomeServer):
DATASTORE_CLASS = DataStore DATASTORE_CLASS = DataStore
_listening_services = []
def _listener_http(self, config, listener_config): def _listener_http(self, config, listener_config):
port = listener_config["port"] port = listener_config["port"]
@ -376,42 +375,73 @@ def setup(config_options):
hs.setup() hs.setup()
@defer.inlineCallbacks
def do_acme():
"""
Reprovision an ACME certificate, if it's required.
Returns:
Deferred[bool]: Whether the cert has been updated.
"""
acme = hs.get_acme_handler()
# Check how long the certificate is active for.
cert_days_remaining = hs.config.is_disk_cert_valid(
allow_self_signed=False
)
# We want to reprovision if cert_days_remaining is None (meaning no
# certificate exists), or the days remaining number it returns
# is less than our re-registration threshold.
provision = False
if (cert_days_remaining is None):
provision = True
if cert_days_remaining > hs.config.acme_reprovision_threshold:
provision = True
if provision:
yield acme.provision_certificate()
defer.returnValue(provision)
@defer.inlineCallbacks
def reprovision_acme():
"""
Provision a certificate from ACME, if required, and reload the TLS
certificate if it's renewed.
"""
reprovisioned = yield do_acme()
if reprovisioned:
_base.refresh_certificate(hs)
@defer.inlineCallbacks @defer.inlineCallbacks
def start(): def start():
try: try:
# Check if the certificate is still valid. # Run the ACME provisioning code, if it's enabled.
cert_days_remaining = hs.config.is_disk_cert_valid()
if hs.config.acme_enabled: if hs.config.acme_enabled:
# If ACME is enabled, we might need to provision a certificate
# before starting.
acme = hs.get_acme_handler() acme = hs.get_acme_handler()
# Start up the webservices which we will respond to ACME # Start up the webservices which we will respond to ACME
# challenges with. # challenges with, and then provision.
yield acme.start_listening() yield acme.start_listening()
yield do_acme()
# We want to reprovision if cert_days_remaining is None (meaning no # Check if it needs to be reprovisioned every day.
# certificate exists), or the days remaining number it returns hs.get_clock().looping_call(
# is less than our re-registration threshold. reprovision_acme,
if (cert_days_remaining is None) or ( 24 * 60 * 60 * 1000
not cert_days_remaining > hs.config.acme_reprovision_threshold )
):
yield acme.provision_certificate()
_base.start(hs, config.listeners) _base.start(hs, config.listeners)
hs.get_pusherpool().start() hs.get_pusherpool().start()
hs.get_datastore().start_doing_background_updates() hs.get_datastore().start_doing_background_updates()
except Exception as e: except Exception:
# If a DeferredList failed (like in listening on the ACME listener), # Print the exception and bail out.
# we need to print the subfailure explicitly.
if isinstance(e, defer.FirstError):
e.subFailure.printTraceback(sys.stderr)
sys.exit(1)
# Something else went wrong when starting. Print it and bail out.
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
if reactor.running:
reactor.stop()
sys.exit(1) sys.exit(1)
reactor.callWhenRunning(start) reactor.callWhenRunning(start)
@ -420,7 +450,8 @@ def setup(config_options):
class SynapseService(service.Service): class SynapseService(service.Service):
"""A twisted Service class that will start synapse. Used to run synapse """
A twisted Service class that will start synapse. Used to run synapse
via twistd and a .tac. via twistd and a .tac.
""" """
def __init__(self, config): def __init__(self, config):

View File

@ -64,10 +64,14 @@ class TlsConfig(Config):
self.tls_certificate = None self.tls_certificate = None
self.tls_private_key = None self.tls_private_key = None
def is_disk_cert_valid(self): def is_disk_cert_valid(self, allow_self_signed=True):
""" """
Is the certificate we have on disk valid, and if so, for how long? Is the certificate we have on disk valid, and if so, for how long?
Args:
allow_self_signed (bool): Should we allow the certificate we
read to be self signed?
Returns: Returns:
int: Days remaining of certificate validity. int: Days remaining of certificate validity.
None: No certificate exists. None: No certificate exists.
@ -88,6 +92,12 @@ class TlsConfig(Config):
logger.exception("Failed to parse existing certificate off disk!") logger.exception("Failed to parse existing certificate off disk!")
raise raise
if not allow_self_signed:
if tls_certificate.get_subject() == tls_certificate.get_issuer():
raise ValueError(
"TLS Certificate is self signed, and this is not permitted"
)
# YYYYMMDDhhmmssZ -- in UTC # YYYYMMDDhhmmssZ -- in UTC
expires_on = datetime.strptime( expires_on = datetime.strptime(
tls_certificate.get_notAfter().decode('ascii'), "%Y%m%d%H%M%SZ" tls_certificate.get_notAfter().decode('ascii'), "%Y%m%d%H%M%SZ"

View File

@ -112,6 +112,8 @@ class HomeServer(object):
Attributes: Attributes:
config (synapse.config.homeserver.HomeserverConfig): config (synapse.config.homeserver.HomeserverConfig):
_listening_services (list[twisted.internet.tcp.Port]): TCP ports that
we are listening on to provide HTTP services.
""" """
__metaclass__ = abc.ABCMeta __metaclass__ = abc.ABCMeta
@ -196,6 +198,7 @@ class HomeServer(object):
self._reactor = reactor self._reactor = reactor
self.hostname = hostname self.hostname = hostname
self._building = {} self._building = {}
self._listening_services = []
self.clock = Clock(reactor) self.clock = Clock(reactor)
self.distributor = Distributor() self.distributor = Distributor()