Commit 254b8a98 authored by Sunil Mohan Adapa's avatar Sunil Mohan Adapa Committed by Joseph Nuthalapati

letsencrypt: Handling certificate renewals when daemon is offline

During boot or in other situations when FreedomBox Service is offline, Let's
Encrypt certificate renewals might happen. When FreedomBox Service starts, check
on such certificates and run certificate setup mechanism in each app to use the
latest renewed certificate.
Signed-off-by: Sunil Mohan Adapa's avatarSunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati's avatarJoseph Nuthalapati <njoseph@thoughtworks.com>
parent 9c6efad5
......@@ -61,6 +61,11 @@ def parse_arguments():
subparsers.add_parser('get-status',
help='Return the status of configured domains.')
subparser = subparsers.add_parser(
'get-modified-time',
help='Return the modified time for a certificate.')
subparser.add_argument('--domain', required=True,
help='Domain name to get modified time for')
revoke_parser = subparsers.add_parser(
'revoke', help='Revoke certificate of a domain and disable website.')
revoke_parser.add_argument('--domain', required=True,
......@@ -133,6 +138,12 @@ def get_certificate_expiry(domain):
return output.decode().strip().split('=')[1]
def get_modified_time(domain):
"""Return the last modified time of a certificate."""
certificate_file = pathlib.Path(le.LIVE_DIRECTORY) / domain / 'cert.pem'
return int(certificate_file.stat().st_mtime)
def get_validity_status(domain):
"""Return validity status of a certificate, e.g. valid, revoked, expired"""
output = subprocess.check_output(['certbot', 'certificates', '-d', domain])
......@@ -176,7 +187,9 @@ def get_status():
'validity':
get_validity_status(domain),
'lineage':
str(pathlib.Path(le.LIVE_DIRECTORY) / domain)
str(pathlib.Path(le.LIVE_DIRECTORY) / domain),
'modified_time':
get_modified_time(domain)
}
return domain_status
......@@ -205,6 +218,11 @@ def subcommand_get_status(_):
print(json.dumps({'domains': domain_status}))
def subcommand_get_modified_time(arguments):
"""Print the modified time of a certificate as integer."""
print(get_modified_time(arguments.domain))
def subcommand_revoke(arguments):
"""Disable a domain and revoke the certificate."""
domain = arguments.domain
......
......@@ -20,6 +20,7 @@ FreedomBox app for using Let's Encrypt.
import json
import logging
import pathlib
from django.utils.translation import ugettext_lazy as _
......@@ -28,7 +29,8 @@ from plinth import app as app_module
from plinth import cfg, menu
from plinth.errors import ActionError
from plinth.modules import names
from plinth.signals import domain_added, domain_removed, domainname_change
from plinth.signals import (domain_added, domain_removed, domainname_change,
post_module_loading)
from plinth.utils import format_lazy
from . import components
......@@ -64,6 +66,7 @@ description = [
manual_page = 'LetsEncrypt'
LIVE_DIRECTORY = '/etc/letsencrypt/live/'
CERTIFICATE_CHECK_DELAY = 120
logger = logging.getLogger(__name__)
app = None
......@@ -93,6 +96,8 @@ def init():
domain_added.connect(on_domain_added)
domain_removed.connect(on_domain_removed)
post_module_loading.connect(_certificate_handle_modified)
def setup(helper, old_version=None):
"""Install and configure the module."""
......@@ -207,3 +212,65 @@ def get_status():
status['domains'].setdefault(domain, {})
return status
def _certificate_handle_modified(**kwargs):
"""Generate events for certificates that got modified during downtime.
This runs as a synchronous method soon after initializing the apps. After
this is done, remaining initialization happens.
This method is a wrapper over the read method to catch and print
exceptions.
"""
logger.info('Checking if any Let\'s Encrypt certificates got renewed.')
try:
_certificate_handle_modified_internal()
except Exception:
logger.exception('Error triggering certificate events.')
def _certificate_handle_modified_internal():
"""Generate events for certificates that got modified during downtime."""
status = get_status()
for domain, domain_status in status['domains'].items():
if not domain_status:
continue
lineage = domain_status['lineage']
modified_time = domain_status['modified_time']
if certificate_get_last_seen_modified_time(lineage) < modified_time:
logger.info('Certificate for %s got renewed offline.', domain)
components.on_certificate_event_sync('renewed', domain, lineage)
else:
logger.info('Certificate for %s is already the latest known.',
domain)
def certificate_get_last_seen_modified_time(lineage):
"""Return the last seen expiry date of a certificate."""
from plinth import kvstore
info = kvstore.get_default('letsencrypt_certificate_info', '{}')
info = json.loads(info)
try:
return info[str(lineage)]['last_seen_modified_time']
except KeyError:
return 0
def certificate_set_last_seen_modified_time(lineage):
"""Write to store a certificate's last seen expiry date."""
lineage = pathlib.Path(lineage)
output = actions.superuser_run(
'letsencrypt', ['get-modified-time', '--domain', lineage.name])
modified_time = int(output)
from plinth import kvstore
info = kvstore.get_default('letsencrypt_certificate_info', '{}')
info = json.loads(info)
certificate_info = info.setdefault(str(lineage), {})
certificate_info['last_seen_modified_time'] = modified_time
kvstore.set('letsencrypt_certificate_info', json.dumps(info))
......@@ -398,3 +398,7 @@ def on_certificate_event_sync(event, domains, lineage):
logger.exception(
'Error executing certificate hook for %s: %s, %s, %s: %s',
component.component_id, event, domains, lineage, exception)
if event in ('obtained', 'renewed'):
from plinth.modules import letsencrypt
letsencrypt.certificate_set_last_seen_modified_time(lineage)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment