ACME Reprovisioning (#4522)
This commit is contained in:
parent
4ffd10f46d
commit
6e2a5aa050
|
@ -0,0 +1 @@
|
||||||
|
Synapse's ACME support will now correctly reprovision a certificate that approaches its expiry while Synapse is running.
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue