diff --git a/basic.sh b/basic.sh index 467486b..cfcc832 100755 --- a/basic.sh +++ b/basic.sh @@ -53,11 +53,9 @@ rc-update -u cp -r srv/vm /srv/vm ln -s /srv/vm/cli.py /usr/bin/vmmgr -# Create a self-signed certificate -vmmgr create-selfsigned - -# Configure nginx +# Configure nginx and create a self-signed certificate cp etc/nginx/nginx.conf /etc/nginx/nginx.conf +vmmgr install # Configure postfix cp etc/postfix/main.cf /etc/postfix/main.cf @@ -74,6 +72,3 @@ if [ ${DEBUG:-0} -eq 1 ]; then rc-update add sshd boot service sshd start fi - -# Generate nginx default.conf -vmmgr update-host spotter.vm 443 diff --git a/basic/srv/vm/cli.py b/basic/srv/vm/cli.py index 477984e..5ffb4a0 100755 --- a/basic/srv/vm/cli.py +++ b/basic/srv/vm/cli.py @@ -11,122 +11,62 @@ from mgr import VMMgr parser = argparse.ArgumentParser(description='VM application manager') subparsers = parser.add_subparsers() -parser_update_login = subparsers.add_parser('update-login', help='Updates application login') +parser_install = subparsers.add_parser('install') +parser_install.set_defaults(action='install') + +parser_update_login = subparsers.add_parser('update-login') parser_update_login.set_defaults(action='update-login') parser_update_login.add_argument('app', help='Application name') parser_update_login.add_argument('login', help='Administrative login') parser_update_login.add_argument('password', help='Administrative password') -parser_show_tiles = subparsers.add_parser('show-tiles', help='Shows application tiles in Portal') -parser_show_tiles.set_defaults(action='show-tiles') -parser_show_tiles.add_argument('app', help='Application name') - -parser_hide_tiles = subparsers.add_parser('hide-tiles', help='Hides application tiles in Portal') -parser_hide_tiles.set_defaults(action='hide-tiles') -parser_hide_tiles.add_argument('app', help='Application name') - -parser_start_app = subparsers.add_parser('start-app', help='Start application including it\'s dependencies') -parser_start_app.set_defaults(action='start-app') -parser_start_app.add_argument('app', help='Application name') - -parser_stop_app = subparsers.add_parser('stop-app', help='Stops application including it\'s dependencies if they are not used by another running application') -parser_stop_app.set_defaults(action='stop-app') -parser_stop_app.add_argument('app', help='Application name') - -parser_enable_autostart = subparsers.add_parser('enable-autostart', help='Enables application autostart') -parser_enable_autostart.set_defaults(action='enable-autostart') -parser_enable_autostart.add_argument('app', help='Application name') - -parser_disable_autostart = subparsers.add_parser('disable-autostart', help='Disables application autostart') -parser_disable_autostart.set_defaults(action='disable-autostart') -parser_disable_autostart.add_argument('app', help='Application name') - -parser_rebuild_issue = subparsers.add_parser('rebuild-issue', help='Rebuilds /etc/issue using current settings - used on VM startup') +parser_rebuild_issue = subparsers.add_parser('rebuild-issue') parser_rebuild_issue.set_defaults(action='rebuild-issue') -parser_prepare_container = subparsers.add_parser('prepare-container', help='Cleans container ephemeral layer and sets common config for the app. Intended to be used with LXC hooks') +parser_prepare_container = subparsers.add_parser('prepare-container') parser_prepare_container.add_argument('lxc', nargs=argparse.REMAINDER) parser_prepare_container.set_defaults(action='prepare-container') -parser_register_container = subparsers.add_parser('register-container', help='Register and assigns IP to an application container. Intended to be used with LXC hooks') +parser_register_container = subparsers.add_parser('register-container') parser_register_container.add_argument('lxc', nargs=argparse.REMAINDER) parser_register_container.set_defaults(action='register-container') -parser_unregister_container = subparsers.add_parser('unregister-container', help='Removes IP assignment for an application container. Intended to be used with LXC hooks') +parser_unregister_container = subparsers.add_parser('unregister-container') parser_unregister_container.add_argument('lxc', nargs=argparse.REMAINDER) parser_unregister_container.set_defaults(action='unregister-container') -parser_register_proxy = subparsers.add_parser('register-proxy', help='Rebuilds nginx proxy target for an application container') +parser_register_proxy = subparsers.add_parser('register-proxy') parser_register_proxy.set_defaults(action='register-proxy') parser_register_proxy.add_argument('app', help='Application name') -parser_unregister_proxy = subparsers.add_parser('unregister-proxy', help='Removes nginx proxy target for an application container') +parser_unregister_proxy = subparsers.add_parser('unregister-proxy') parser_unregister_proxy.set_defaults(action='unregister-proxy') parser_unregister_proxy.add_argument('app', help='Application name') -parser_update_host = subparsers.add_parser('update-host', help='Rebuilds domain structure of VM with new host name and new HTTPS port') -parser_update_host.set_defaults(action='update-host') -parser_update_host.add_argument('domain', help='Domain name') -parser_update_host.add_argument('port', help='HTTPS port') - -parser_update_common = subparsers.add_parser('update-common', help='Updates common configuration properties used by multiple applications') -parser_update_common.set_defaults(action='update-common') -parser_update_common.add_argument('--email', help='Administrative e-mail address') -parser_update_common.add_argument('--gmaps-api-key', help='Google Maps API key') - -parser_update_password = subparsers.add_parser('update-password', help='Updates password for HDD encryption and WSGI administration interface') -parser_update_password.set_defaults(action='update-password') - -parser_create_selfsigned = subparsers.add_parser('create-selfsigned', help='Creates and installs selfsigned certificate for currently set domain') -parser_create_selfsigned.set_defaults(action='create-selfsigned') - -parser_request_cert = subparsers.add_parser('request-cert', help='Requests and installs Let\'s Encrypt certificate for currently set domain') -parser_request_cert.set_defaults(action='request-cert') - -parser_install_cert = subparsers.add_parser('install-cert', help='Installs user supplied certificate') -parser_install_cert.set_defaults(action='install-cert') -parser_install_cert.add_argument('certificate', help='Certificate file') -parser_install_cert.add_argument('key', help='Key file') - args = parser.parse_args() mgr = VMMgr() -if args.action == 'update-login': +if args.action == 'install': + # Used during VM installation + mgr.rebuild_nginx(False) + mgr.create_selfsigned_cert() +elif args.action == 'update-login': + # Used by app install scripts mgr.update_login(args.app, args.login, args.password) -elif args.action == 'show-tiles': - mgr.show_tiles(args.app) -elif args.action == 'hide-tiles': - mgr.hide_tiles(args.app) -elif args.action == 'start-app': - mgr.start_app(args.app) -elif args.action == 'stop-app': - mgr.stop_app(args.app) -elif args.action == 'enable-autostart': - mgr.enable_autostart(args.app) -elif args.action == 'disable-autostart': - mgr.disable_autostart(args.app) elif args.action == 'rebuild-issue': + # Used on VM startup mgr.rebuild_issue() elif args.action == 'prepare-container': + # Used with LXC hooks mgr.prepare_container() elif args.action == 'register-container': + # Used with LXC hooks mgr.register_container() elif args.action == 'unregister-container': + # Used with LXC hooks mgr.unregister_container() elif args.action == 'register-proxy': + # Used in init scripts mgr.register_proxy(args.app) elif args.action == 'unregister-proxy': + # Used in init scripts mgr.unregister_proxy(args.app) -elif args.action == 'update-host': - mgr.update_host(args.domain, args.port) -elif args.action == 'update-common': - mgr.update_common(args.email, args.gmaps_api_key) -elif args.action == 'update-password': - oldpassword = getpass.getpass('Old password: ') - newpassword = getpass.getpass('New password: ') - mgr.update_password(oldpassword, newpassword) -elif args.action == 'create-selfsigned': - mgr.create_selfsigned() -elif args.action == 'request-cert': - mgr.request_cert() -elif args.action == 'install-cert': - mgr.install_cert(args.certificate, args.key) diff --git a/basic/srv/vm/mgr/__init__.py b/basic/srv/vm/mgr/__init__.py index b301316..216b4e2 100644 --- a/basic/srv/vm/mgr/__init__.py +++ b/basic/srv/vm/mgr/__init__.py @@ -253,8 +253,8 @@ class VMMgr: os.unlink(os.path.join(NGINX_DIR, '{}.conf'.format(app))) tools.reload_nginx() - def update_host(self, domain, port, restart_nginx=True): - # Update domain and port and rebuild all configurtion. Defer nginx restart when updating from web interface + def update_host(self, domain, port): + # Update domain and port and rebuild all configuration. Web interface calls tools.restart_nginx() in WSGI close handler if not validator.is_valid_domain(domain): raise validator.InvalidValueException('domain', domain) if not validator.is_valid_port(port): @@ -266,16 +266,13 @@ class VMMgr: for app in self.conf['apps']: if tools.is_service_started(app): tools.restart_service(app) - # Rebuild and restart nginx if it was requested. Web interface calls tools.restart_nginx() in WSGI close handler - self.rebuild_nginx(restart_nginx) + # Rebuild and restart nginx if it was requested. + self.rebuild_nginx() - def rebuild_nginx(self, restart_nginx): - # Rebuild nginx config for the portal app + def rebuild_nginx(self): + # Rebuild nginx config for the portal app. Web interface calls tools.restart_nginx() in WSGI close handler with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f: f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port)) - # Restart nginx to properly bind the new listen port - if restart_nginx: - tools.restart_nginx() def rebuild_issue(self): # Compile the HTTPS host displayed in terminal banner @@ -317,14 +314,17 @@ class VMMgr: # Save config to file self.conf.save() - def create_selfsigned(self): + def create_selfsigned_cert(self): + # Remove acme.sh cronjob + if os.path.exists(ACME_CRON): + os.unlink(ACME_CRON) # Create selfsigned certificate with wildcard alternative subject name with open(os.path.join(CERT_SAN_FILE), 'w') as f: f.write(CERT_SAN.format(domain=self.domain)) subprocess.run(['openssl', 'req', '-config', CERT_SAN_FILE, '-x509', '-new', '-out', CERT_PUB_FILE, '-keyout', CERT_KEY_FILE, '-nodes', '-days', '7305', '-subj', '/CN={}'.format(self.domain)], check=True) os.chmod(CERT_KEY_FILE, 0o640) - def request_cert(self): + def request_acme_cert(self): # Remove all possible conflicting certificates requested in the past certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')] for cert in certs: @@ -352,7 +352,7 @@ class VMMgr: with open(ACME_CRON, 'w') as f: f.write(ACME_CRON_TEMPLATE) - def install_cert(self, public_file, private_file): + def install_manual_cert(self, public_file, private_file): # Remove acme.sh cronjob if os.path.exists(ACME_CRON): os.unlink(ACME_CRON) diff --git a/basic/srv/vm/mgr/tools.py b/basic/srv/vm/mgr/tools.py index 4515351..0005065 100644 --- a/basic/srv/vm/mgr/tools.py +++ b/basic/srv/vm/mgr/tools.py @@ -7,9 +7,12 @@ import os import requests import shutil import socket -import ssl import subprocess +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.x509.oid import NameOID + def compile_url(domain, port, proto='https'): port = '' if (proto == 'https' and port == '443') or (proto == 'http' and port == '80') else ':{}'.format(port) host = '{}{}'.format(domain, port) @@ -94,9 +97,18 @@ def restart_nginx(): restart_service('nginx') def get_cert_info(cert): - data = ssl._ssl._test_decode_cert(cert) - data['subject'] = dict(data['subject'][i][0] for i in range(len(data['subject']))) - data['issuer'] = dict(data['issuer'][i][0] for i in range(len(data['issuer']))) + # Gather certificate data important for setup-host + with open(cert, 'rb') as f: + cert = x509.load_pem_x509_certificate(f.read(), default_backend()) + data = {'subject': cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, + 'issuer': cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value, + 'expires': '{} UTC'.format(cert.not_valid_after), + 'method': 'manual'} + if os.path.exists('/etc/periodic/daily/acme-sh'): + data['method'] = 'letsencrypt' + # This is really naive method of inferring if the cert is selfsigned and should never be used in production :) + elif data['subject'] == data['issuer']: + data['method'] = 'selfsigned' return data def adminpwd_hash(password): diff --git a/basic/srv/vm/mgr/wsgiapp.py b/basic/srv/vm/mgr/wsgiapp.py index d8e91e3..f7f8048 100644 --- a/basic/srv/vm/mgr/wsgiapp.py +++ b/basic/srv/vm/mgr/wsgiapp.py @@ -136,9 +136,8 @@ class WSGIApp(object): ex_ipv6 = tools.get_external_ipv6() in_ipv4 = tools.get_local_ipv4() in_ipv6 = tools.get_local_ipv6() - is_letsencrypt = os.path.exists('/etc/periodic/daily/acme-sh') cert_info = tools.get_cert_info(CERT_PUB_FILE) - return self.render_template('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, is_letsencrypt=is_letsencrypt, cert_info=cert_info) + return self.render_template('setup-host.html', request, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, cert_info=cert_info) def setup_apps_view(self, request): # Application manager view. @@ -208,20 +207,22 @@ class WSGIApp(object): def update_cert_action(self, request): # Update certificate - either request via Let's Encrypt or manually upload files try: - if request.form['method'] not in ['auto', 'manual']: + if request.form['method'] not in ['selfsigned', 'automatic', 'manual']: raise BadRequest() - if request.form['method'] == 'manual': + if request.form['method'] == 'selfsigned': + self.vmmgr.create_selfsigned_cert() + elif request.form['method'] == 'automatic': + self.vmmgr.request_acme_cert() + else: if not request.files['public']: return self.render_json({'error': request.session.lang.cert_file_missing()}) if not request.files['private']: return self.render_json({'error': request.session.lang.key_file_missing()}) request.files['public'].save('/tmp/public.pem') request.files['private'].save('/tmp/private.pem') - self.vmmgr.install_cert('/tmp/public.pem', '/tmp/private.pem') + self.vmmgr.install_manual_cert('/tmp/public.pem', '/tmp/private.pem') os.unlink('/tmp/public.pem') os.unlink('/tmp/private.pem') - else: - self.vmmgr.request_cert() except BadRequest: return self.render_json({'error': request.session.lang.malformed_request()}) except: diff --git a/basic/srv/vm/templates/setup-host.html b/basic/srv/vm/templates/setup-host.html index b7795dd..b9a9d13 100644 --- a/basic/srv/vm/templates/setup-host.html +++ b/basic/srv/vm/templates/setup-host.html @@ -75,25 +75,28 @@

HTTPS certifikát

-

Stávající certifikát je vystaven na jméno {{ cert_info['subject']['commonName'] }} vystavitelem {{ cert_info['issuer']['commonName'] }} a jeho platnost vyprší {{ cert_info['notAfter'] }}.

+

Stávající certifikát je vystaven na jméno {{ cert_info['subject'] }} vystavitelem {{ cert_info['issuer'] }} a jeho platnost vyprší {{ cert_info['expires'] }}.

- + - + - + - +
Způsob správy Volba "Automaticky" způsobí, že systém automaticky zažádá o certifikát certifikační autority Let's Encrypt pro všechny plně kvalifikované doménové názvy (tj. nikoliv wildcard) zmíněné v sekci DNS záznamy a nainstaluje úlohu pro jeho automatickou obnovu. Tato akce může trvat několik minut.
Volba "Ručně" znamená, že soubory certifikátu a jeho soukromého klíče je nutno nahrát a následně obnovovat ručně skrze formulář na této stránce.
Volba "Self-signed" vygeneruje certifikát s vlastním podpisem a platnostÍ 20 let. Tento certifikát je použitelný pro testovací účely, ale většina mobilních aplikací s ním odmítne fungovat. +
Volba "Automaticky" způsobí, že systém automaticky zažádá o certifikát certifikační autority Let's Encrypt pro všechny plně kvalifikované doménové názvy (tj. nikoliv wildcard) zmíněné v sekci DNS záznamy. Počet žádostí o certifikát se stejným doménovým jménem je omezený na 5 týdně, proto je vhodné tento typ certifikátu nastavovat až po instalaci aplikací. Zároveň bude nainstalována úloha pro automatickou obnovu. Proces vyžádání tohoto typu certifikátu může trvat několik minut. +
Volba "Ručně" znamená, že soubory certifikátu a jeho soukromého klíče je nutno nahrát a následně obnovovat ručně skrze formulář na této stránce.