From 91ba56e3ce5e742d84083d3f881d1ed7a1229a61 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 21 Dec 2018 16:08:04 -0800 Subject: [PATCH] letsencrypt: Use macros for configuring sites Makes it trivial to alter site configuration for all domains at once. Also possible to easily switch to TLS modules other than mod_gnutls. Reviewed-by: James Valleroy --- actions/letsencrypt | 151 +++++++++--------- .../freedombox-tls-site-macro.conf | 37 +++++ plinth/modules/letsencrypt/__init__.py | 5 +- 3 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 data/etc/apache2/conf-available/freedombox-tls-site-macro.conf diff --git a/actions/letsencrypt b/actions/letsencrypt index 2db19f4b7..17b9201fb 100755 --- a/actions/letsencrypt +++ b/actions/letsencrypt @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # - """ Configuration helper for Let's Encrypt. """ @@ -34,48 +33,13 @@ from plinth.errors import ActionError from plinth.modules import config from plinth.modules import letsencrypt as le - TEST_MODE = False RENEWAL_DIRECTORY = '/etc/letsencrypt/renewal/' AUTHENTICATOR = 'webroot' WEB_ROOT_PATH = '/var/www/html' APACHE_PREFIX = '/etc/apache2/sites-available/' APACHE_CONFIGURATION = ''' - - - ServerAdmin webmaster@localhost - ServerName {domain} - DocumentRoot /var/www/html - - Options FollowSymLinks - AllowOverride None - - - Options Indexes FollowSymLinks MultiViews - AllowOverride None - Order allow,deny - allow from all - - ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ - - AllowOverride None - Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch - Order allow,deny - Allow from all - - ErrorLog ${{APACHE_LOG_DIR}}/error.log - # Possible values include: debug, info, notice, warn, error, crit, alert, emerg. - LogLevel warn - CustomLog ${{APACHE_LOG_DIR}}/ssl_access.log combined - # GnuTLS Switch: Enable/Disable SSL/TLS for this virtual host. - GnuTLSEnable On - # Automatically obtained certificates from Let's Encrypt - GnuTLSCertificateFile /etc/letsencrypt/live/{domain}/fullchain.pem - GnuTLSKeyFile /etc/letsencrypt/live/{domain}/privkey.pem - # See http://www.outoforder.cc/projects/apache/mod_gnutls/docs/#GnuTLSPriorities - GnuTLSPriorities NORMAL - - +Use FreedomBoxTLSSiteMacro {domain} ''' @@ -84,8 +48,14 @@ def parse_arguments(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - subparsers.add_parser( - 'get-status', help='Return the status of configured domains.') + setup_parser = subparsers.add_parser( + 'setup', help='Run any setup/upgrade activities.') + setup_parser.add_argument( + '--old-version', type=int, required=True, help= + 'Version number being upgraded from or None if setting up first time.') + + subparsers.add_parser('get-status', + help='Return the status of configured domains.') revoke_parser = subparsers.add_parser( 'revoke', help='Revoke certificate of a domain and disable website.') revoke_parser.add_argument('--domain', required=True, @@ -144,8 +114,8 @@ def parse_arguments(): def get_certificate_expiry(domain): """Return the expiry date of a certificate.""" certificate_file = os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem') - output = subprocess.check_output(['openssl', 'x509', '-enddate', '-noout', - '-in', certificate_file]) + output = subprocess.check_output( + ['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file]) return output.decode().strip().split('=')[1] @@ -175,21 +145,40 @@ def get_status(): except OSError: domains = [] - domains = [domain for domain in domains - if os.path.isdir(os.path.join(le.LIVE_DIRECTORY, domain))] + domains = [ + domain for domain in domains + if os.path.isdir(os.path.join(le.LIVE_DIRECTORY, domain)) + ] domain_status = {} for domain in domains: domain_status[domain] = { - 'certificate_available': True, - 'expiry_date': get_certificate_expiry(domain), + 'certificate_available': + True, + 'expiry_date': + get_certificate_expiry(domain), 'web_enabled': - action_utils.webserver_is_enabled(domain, kind='site'), - 'validity': get_validity_status(domain) + action_utils.webserver_is_enabled(domain, kind='site'), + 'validity': + get_validity_status(domain) } return domain_status +def subcommand_setup(arguments): + """Upgrade old site configuration to new macro based style. + + Nothing to do for first time setup and for newer versions. + """ + if arguments.old_version != 1: + return + + domain_status = get_status() + with action_utils.WebserverChange() as webserver_change: + for domain in domain_status: + setup_webserver_config(domain, webserver_change) + + def subcommand_get_status(_): """Print a JSON dictionary of currently configured domains.""" domain_status = get_status() @@ -200,8 +189,10 @@ def subcommand_revoke(arguments): """Disable a domain and revoke the certificate.""" domain = arguments.domain - command = ['certbot', 'revoke', '--domain', domain, '--cert-path', - os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem')] + command = [ + 'certbot', 'revoke', '--domain', domain, '--cert-path', + os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem') + ] if TEST_MODE: command.append('--staging') @@ -223,7 +214,8 @@ def subcommand_obtain(arguments): 'certbot', 'certonly', '--text', '--agree-tos', '--register-unsafely-without-email', '--domain', arguments.domain, '--authenticator', AUTHENTICATOR, '--webroot-path', WEB_ROOT_PATH, - '--renew-by-default'] + '--renew-by-default' + ] if TEST_MODE: command.append('--staging') @@ -234,9 +226,8 @@ def subcommand_obtain(arguments): print(stderr.decode(), file=sys.stderr) sys.exit(1) - setup_webserver_config(domain) - - action_utils.webserver_enable(domain, kind='site') + with action_utils.WebserverChange() as webserver_change: + setup_webserver_config(domain, webserver_change) def subcommand_manage_hooks(arguments): @@ -280,8 +271,8 @@ def subcommand_manage_hooks(arguments): config_certbot = configobj.ConfigObj(config_path) if 'renewalparams' not in config_certbot: msg, code = ('Aborted', 6) if cmd_is_enable else ('Disabled', 0) - print('%s: No section [renewalparams] in config file at %s.' - % (msg, config_path)) + print('%s: No section [renewalparams] in config file at %s.' % + (msg, config_path)) sys.exit(code) script_path = os.path.realpath(__file__) @@ -289,24 +280,33 @@ def subcommand_manage_hooks(arguments): call_pre = script_path + ' run_pre_hooks --domain ' + arguments.domain call_renew = script_path + ' run_renew_hooks --domain ' + arguments.domain call_post = script_path + ' run_post_hooks --domain ' + arguments.domain - config_plinth = {'renewalparams': - {'authenticator': AUTHENTICATOR, - # 'webroot_path': [WEB_ROOT_PATH], # removed by renew... - 'webroot_map': {arguments.domain: WEB_ROOT_PATH}, - 'installer': 'None', - 'pre_hook': call_pre, - 'renew_hook': call_renew, - 'post_hook': call_post}} + config_plinth = { + 'renewalparams': { + 'authenticator': AUTHENTICATOR, + # 'webroot_path': [WEB_ROOT_PATH], # removed by renew... + 'webroot_map': { + arguments.domain: WEB_ROOT_PATH + }, + 'installer': 'None', + 'pre_hook': call_pre, + 'renew_hook': call_renew, + 'post_hook': call_post + } + } comment_plinth = '# This file was edited by Plinth.' - config_edited_by_plinth = any(['edited by plinth' in line.lower() - for line in config_certbot.initial_comment]) + config_edited_by_plinth = any([ + 'edited by plinth' in line.lower() + for line in config_certbot.initial_comment + ]) if arguments.command == 'status': # check for presence of expected minimal configuration - config_checks = [(entry in config_certbot['renewalparams']) and - (str(config_plinth['renewalparams'][entry]) in - str(config_certbot['renewalparams'][entry])) - for entry in config_plinth['renewalparams'].keys()] + config_checks = [ + (entry in config_certbot['renewalparams']) + and (str(config_plinth['renewalparams'][entry]) in str( + config_certbot['renewalparams'][entry])) + for entry in config_plinth['renewalparams'].keys() + ] if not all(config_checks): print('disabled') @@ -452,9 +452,8 @@ def _run_action(action, action_options=None): # Contract 3C: don't interpret shell escape sequences. # Contract 5 (and 6-ish). - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=False) + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=False) output, error = proc.communicate() output, error = output.decode(), error.decode() @@ -496,18 +495,22 @@ def subcommand_delete(arguments): action_utils.webserver_disable(domain, kind='site') -def setup_webserver_config(domain): +def setup_webserver_config(domain, webserver_change): """Create SSL web server configuration for a domain. Do so only if there is no configuration existing. """ file_name = os.path.join(APACHE_PREFIX, domain + '.conf') if os.path.isfile(file_name): - return + os.rename(file_name, file_name + '.fbx-bak') with open(file_name, 'w') as file_handle: file_handle.write(APACHE_CONFIGURATION.format(domain=domain)) + webserver_change.enable('macro', kind='module') + webserver_change.enable('freedombox-tls-site-macro', kind='config') + webserver_change.enable(domain, kind='site') + def main(): """Parse arguments and perform all duties.""" diff --git a/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf b/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf new file mode 100644 index 000000000..44e5018b3 --- /dev/null +++ b/data/etc/apache2/conf-available/freedombox-tls-site-macro.conf @@ -0,0 +1,37 @@ + + + + ServerAdmin webmaster@localhost + ServerName $domain + DocumentRoot /var/www/html + + Options FollowSymLinks + AllowOverride None + + + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + + ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/ + + AllowOverride None + Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch + Order allow,deny + Allow from all + + ErrorLog ${APACHE_LOG_DIR}/error.log + # Possible values include: debug, info, notice, warn, error, crit, alert, emerg. + LogLevel warn + CustomLog ${APACHE_LOG_DIR}/ssl_access.log combined + # GnuTLS Switch: Enable/Disable SSL/TLS for this virtual host. + GnuTLSEnable On + # Automatically obtained certificates from Let's Encrypt + GnuTLSCertificateFile /etc/letsencrypt/live/$domain/fullchain.pem + GnuTLSKeyFile /etc/letsencrypt/live/$domain/privkey.pem + # See http://www.outoforder.cc/projects/apache/mod_gnutls/docs/#GnuTLSPriorities + GnuTLSPriorities NORMAL + + + diff --git a/plinth/modules/letsencrypt/__init__.py b/plinth/modules/letsencrypt/__init__.py index a94cc6a71..b42b94eca 100644 --- a/plinth/modules/letsencrypt/__init__.py +++ b/plinth/modules/letsencrypt/__init__.py @@ -32,7 +32,7 @@ from plinth.utils import format_lazy from .manifest import backup -version = 1 +version = 2 is_essential = True @@ -81,6 +81,9 @@ def init(): def setup(helper, old_version=None): """Install and configure the module.""" helper.install(managed_packages) + actions.superuser_run( + 'letsencrypt', + ['setup', '--old-version', str(old_version)]) def diagnose():