matrixsynapse: Add let's encrypt component for certficiates

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
This commit is contained in:
Sunil Mohan Adapa 2019-06-21 11:11:17 -07:00 committed by Joseph Nuthalapati
parent c47a99b25e
commit 9fd1b95244
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
4 changed files with 43 additions and 117 deletions

View File

@ -20,10 +20,6 @@ Configuration helper for Matrix-Synapse server.
""" """
import argparse import argparse
import filecmp
import os
import shutil
import sys
import yaml import yaml
@ -59,67 +55,6 @@ def parse_arguments():
return parser.parse_args() return parser.parse_args()
def _get_certificate_status():
"""Return if the current certificate is an up-to-date LE certificate."""
configured_domain = get_configured_domain_name()
if not configured_domain:
return False
if not os.path.exists(letsencrypt.LIVE_DIRECTORY):
return False
source_dir = os.path.join(letsencrypt.LIVE_DIRECTORY, configured_domain)
source_certificate_path = os.path.join(source_dir, 'fullchain.pem')
source_private_key_path = os.path.join(source_dir, 'privkey.pem')
dest_dir = '/etc/matrix-synapse'
dest_certificate_path = os.path.join(dest_dir, 'homeserver.tls.crt')
dest_private_key_path = os.path.join(dest_dir, 'homeserver.tls.key')
if filecmp.cmp(source_certificate_path, dest_certificate_path) and \
filecmp.cmp(source_private_key_path, dest_private_key_path):
return True
return False
def _update_tls_certificate():
"""Update the TLS certificate and private key used by Matrix Synapse.
A valid certificate is necessary for federation with other instances
starting with version 1.0.
"""
configured_domain = get_configured_domain_name()
if os.path.exists(letsencrypt.LIVE_DIRECTORY) and configured_domain:
# Copy the latest Let's Encrypt certs into Synapse's directory.
src_dir = os.path.join(letsencrypt.LIVE_DIRECTORY, configured_domain)
source_certificate_path = os.path.join(src_dir, 'fullchain.pem')
source_private_key_path = os.path.join(src_dir, 'privkey.pem')
else:
# Copy Apache's snake-oil certificate into Synapse's config directory.
# The self-signed certificate doesn't really work (other Matrix
# Synapse instances do not accept it). It is merely to prevent the
# server from failing to startup because the files are missing.
source_certificate_path = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
source_private_key_path = '/etc/ssl/private/ssl-cert-snakeoil.key'
dest_dir = '/etc/matrix-synapse'
dest_certificate_path = os.path.join(dest_dir, 'homeserver.tls.crt')
dest_private_key_path = os.path.join(dest_dir, 'homeserver.tls.key')
# Private key is only accessible to the user "matrix-synapse"
# Group access is prohibited since it is "nogroup"
old_mask = os.umask(0o133)
shutil.copyfile(source_certificate_path, dest_certificate_path)
os.umask(0o177)
shutil.copyfile(source_private_key_path, dest_private_key_path)
os.umask(old_mask)
shutil.chown(dest_certificate_path, user='matrix-synapse', group='nogroup')
shutil.chown(dest_private_key_path, user='matrix-synapse', group='nogroup')
def subcommand_post_install(_): def subcommand_post_install(_):
"""Perform post installation configuration.""" """Perform post installation configuration."""
with open(CONFIG_FILE_PATH) as config_file: with open(CONFIG_FILE_PATH) as config_file:
@ -152,18 +87,12 @@ def subcommand_post_install(_):
with open(CONFIG_FILE_PATH, 'w') as config_file: with open(CONFIG_FILE_PATH, 'w') as config_file:
yaml.dump(config, config_file) yaml.dump(config, config_file)
_update_tls_certificate()
if action_utils.service_is_running('matrix-synapse'):
action_utils.service_restart('matrix-synapse')
def subcommand_setup(arguments): def subcommand_setup(arguments):
"""Configure the domain name for matrix-synapse package.""" """Configure the domain name for matrix-synapse package."""
domain_name = arguments.domain_name domain_name = arguments.domain_name
action_utils.dpkg_reconfigure('matrix-synapse', action_utils.dpkg_reconfigure('matrix-synapse',
{'server-name': domain_name}) {'server-name': domain_name})
_update_tls_certificate()
def subcommand_public_registration(argument): def subcommand_public_registration(argument):
@ -190,40 +119,6 @@ def subcommand_public_registration(argument):
action_utils.service_restart('matrix-synapse') action_utils.service_restart('matrix-synapse')
def subcommand_letsencrypt(arguments):
"""Add/drop usage of Let's Encrypt cert or show status.
The command 'add' applies only to current domain, will be called by action
'letsencrypt run_renew_hooks', when certbot renews the cert (if
matrix-synapse is selected for cert use). Drop of a cert must be possible
for any domain to respond to domain change.
"""
if arguments.command == 'drop':
print('Dropping certificates is not supported for Matrix Synapse.')
return
if arguments.command == 'get-status':
print('valid' if _get_certificate_status() else 'invalid')
return
configured_domain = get_configured_domain_name()
if arguments.domain is not None and \
arguments.domain != configured_domain:
print('Aborted: Current domain "{}" is not configured.'.format(
arguments.domain))
sys.exit(1)
le_folder = os.path.join(letsencrypt.LIVE_DIRECTORY, configured_domain)
if not os.path.exists(le_folder):
print('Aborted: No certificate directory at %s.' % le_folder)
sys.exit(2)
_update_tls_certificate()
action_utils.service_try_restart('matrix-synapse')
def main(): def main():
arguments = parse_arguments() arguments = parse_arguments()
sub_command = arguments.subcommand.replace('-', '_') sub_command = arguments.subcommand.replace('-', '_')

View File

@ -20,6 +20,7 @@ FreedomBox app to configure matrix-synapse server.
import logging import logging
import os import os
import pathlib
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -31,6 +32,7 @@ from plinth import frontpage, menu
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt
from .manifest import backup, clients from .manifest import backup, clients
@ -40,6 +42,8 @@ managed_services = ['matrix-synapse']
managed_packages = ['matrix-synapse', 'matrix-synapse-ldap3'] managed_packages = ['matrix-synapse', 'matrix-synapse-ldap3']
managed_paths = [pathlib.Path('/etc/matrix-synapse/')]
name = _('Matrix Synapse') name = _('Matrix Synapse')
short_description = _('Chat Server') short_description = _('Chat Server')
@ -101,6 +105,16 @@ class MatrixSynapseApp(app_module.App):
'matrix-synapse-plinth') 'matrix-synapse-plinth')
self.add(webserver) self.add(webserver)
letsencrypt = LetsEncrypt(
'letsencrypt-matrixsynapse', domains=get_domains, daemons=[
managed_services[0]
], should_copy_certificates=True,
private_key_path='/etc/matrix-synapse/homeserver.tls.key',
certificate_path='/etc/matrix-synapse/homeserver.tls.crt',
user_owner='matrix-synapse', group_owner='nogroup',
managing_app='matrixsynapse')
self.add(letsencrypt)
daemon = Daemon('daemon-matrixsynapse', managed_services[0]) daemon = Daemon('daemon-matrixsynapse', managed_services[0])
self.add(daemon) self.add(daemon)
@ -121,6 +135,15 @@ def setup(helper, old_version=None):
helper.call('post', actions.superuser_run, 'matrixsynapse', helper.call('post', actions.superuser_run, 'matrixsynapse',
['post-install']) ['post-install'])
helper.call('post', app.enable) helper.call('post', app.enable)
app.get_component('letsencrypt-matrixsynapse').setup_certificates()
def setup_domain(domain_name):
"""Configure a domain name for matrixsynapse."""
app.get_component('letsencrypt-matrixsynapse').setup_certificates(
[domain_name])
actions.superuser_run('matrixsynapse',
['setup', '--domain-name', domain_name])
def is_setup(): def is_setup():
@ -141,6 +164,15 @@ def diagnose():
return results return results
def get_domains():
"""Return a list of domains this app is interested in."""
domain = get_configured_domain_name()
if domain:
return [domain]
return []
def get_configured_domain_name(): def get_configured_domain_name():
"""Return the currently configured domain name.""" """Return the currently configured domain name."""
if not is_setup(): if not is_setup():
@ -159,8 +191,10 @@ def get_public_registration_status():
return output.strip() == 'enabled' return output.strip() == 'enabled'
def has_valid_certificate(): def get_certificate_status():
"""Return whether the configured domain name has a valid certificate.""" """Return the status of certificate for the configured domain."""
status = actions.superuser_run('matrixsynapse', status = app.get_component('letsencrypt-matrixsynapse').get_status()
['letsencrypt', 'get-status']) if not status:
return status.startswith('valid') return 'no-domains'
return list(status.values())[0]

View File

@ -39,7 +39,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% if not has_valid_certificate %} {% if certificate_status != "valid" %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
{% url 'letsencrypt:index' as letsencrypt_url %} {% url 'letsencrypt:index' as letsencrypt_url %}
{% blocktrans %} {% blocktrans %}

View File

@ -30,7 +30,7 @@ from plinth.modules import matrixsynapse
from plinth.utils import get_domain_names from plinth.utils import get_domain_names
from plinth.views import AppView from plinth.views import AppView
from . import get_public_registration_status, has_valid_certificate from . import get_public_registration_status
from .forms import MatrixSynapseForm from .forms import MatrixSynapseForm
@ -42,10 +42,7 @@ class SetupView(FormView):
def form_valid(self, form): def form_valid(self, form):
"""Handle valid form submission.""" """Handle valid form submission."""
domain_name = form.cleaned_data['domain_name'] matrixsynapse.setup_domain(form.cleaned_data['domain_name'])
actions.superuser_run('matrixsynapse',
['setup', '--domain-name', domain_name])
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
@ -82,7 +79,7 @@ class MatrixSynapseAppView(AppView):
context['domain_name'] = matrixsynapse.get_configured_domain_name() context['domain_name'] = matrixsynapse.get_configured_domain_name()
context['clients'] = matrixsynapse.clients context['clients'] = matrixsynapse.clients
context['manual_page'] = matrixsynapse.manual_page context['manual_page'] = matrixsynapse.manual_page
context['has_valid_certificate'] = has_valid_certificate() context['certificate_status'] = matrixsynapse.get_certificate_status()
return context return context
def get_initial(self): def get_initial(self):