]> git.wh0rd.org - home.git/blame - .bin/le-renew
cros-board: update
[home.git] / .bin / le-renew
CommitLineData
6dbeb0d6
MF
1#!/usr/bin/python
2#-*- coding:utf-8 -*-
ab74211f
MF
3# pylint: disable=invalid-name
4
5"""Renew Let's Encrypt certs!
6
7To generate a new set of certs:
8$ certbot certonly --webroot \\
9 --webroot-path /var/www/wh0rd/ -d wh0rd.org -d www.wh0rd.org \\
10 --webroot-path /var/www/rss/ -d rss.wh0rd.org
11"""
12
13from __future__ import print_function
14
15import argparse
16try:
17 import configparser
18except ImportError:
19 import ConfigParser as configparser
6dbeb0d6
MF
20import cryptography.hazmat.backends
21from cryptography import x509
ab74211f
MF
22try:
23 from cStringIO import StringIO
24except ImportError:
25 from io import StringIO
26import datetime
27import logging
28import logging.handlers
ab74211f 29import os
ab74211f
MF
30import subprocess
31import sys
32
33
34LE_BASE = '/etc/letsencrypt'
35
36
37def get_parser():
38 """Return an ArgumentParser() for this module."""
39 parser = argparse.ArgumentParser(description=__doc__,
40 formatter_class=argparse.RawTextHelpFormatter)
41 parser.add_argument('-n', '--dry-run', default=False,
42 action='store_true',
43 help='Do not actually update certs')
44 parser.add_argument('--cronjob', default=False,
45 action='store_true',
46 help='Exit non-zero if no certs were changed')
47 return parser
48
49
50def setup_logging(debug=False, syslog=None):
51 """Setup the logging module just the way we like it."""
52 if syslog is None:
53 syslog = not os.isatty(sys.stdin.fileno())
54
55 if syslog:
56 handler = logging.handlers.SysLogHandler(address='/dev/log')
57 else:
58 handler = logging.StreamHandler(stream=sys.stdout)
59
60 fmt = '%(asctime)s: %(levelname)-7s: %(message)s'
61
62 datefmt = '%a, %d %b %Y %H:%M:%S letsencrypt'
63
64 level = logging.DEBUG if debug else logging.INFO
65
66 formatter = logging.Formatter(fmt, datefmt)
67 handler.setFormatter(formatter)
68
69 logger = logging.getLogger()
70 logger.addHandler(handler)
71 logger.setLevel(level)
72
73
74def load_cert(path):
75 """Load the cert at |path|"""
6dbeb0d6 76 with open(path, 'rb') as f:
ab74211f 77 data = f.read()
6dbeb0d6
MF
78 return x509.load_pem_x509_certificate(
79 data, cryptography.hazmat.backends.default_backend())
ab74211f
MF
80
81
82def load_live_cert(domain):
83 """Load the live cert for |domain|"""
84 path = os.path.join(LE_BASE, 'live', domain, 'cert.pem')
85 return load_cert(path)
86
87
88def load_conf(domain):
89 """Load the LE config file for |domain|"""
90 path = os.path.join(LE_BASE, 'renewal', domain + '.conf')
91 # The config file format is almost enough for the configparser.
92 # We need to insert a section header for the first few items.
93 fp = StringIO()
94 fp.write('[globals]\n')
95 fp.write(open(path).read())
96 fp.seek(0)
97
98 conf = configparser.RawConfigParser()
99 conf.readfp(fp)
100 return conf
101
102
103def process_domain(domain, dry_run=False):
104 """Update |domain|'s certs as needed."""
105 ret = 0
106
107 logging.info('%s: checking', domain)
108
109 conf = load_conf(domain)
881cee1e
MF
110 try:
111 webroot_path = conf.get('[webroot_map', domain)
112 except configparser.NoOptionError:
113 webroot_path = conf.get('renewalparams', 'webroot_path')
114 # The conf writing has a bug here where it appends a comma.
115 webroot_path = webroot_path.rstrip(',')
ab74211f
MF
116
117 cert_path = os.path.realpath(conf.get('globals', 'cert'))
118
119 cert = load_cert(cert_path)
6dbeb0d6 120 delta = cert.not_valid_after - datetime.datetime.utcnow()
ab74211f
MF
121 logging.info('%s: expires in %2s days', domain, delta.days)
122
123 cmd = [
124 'certbot',
125 'certonly', '--webroot',
126 '--webroot-path', webroot_path,
127 '-d', domain,
128 ]
6dbeb0d6
MF
129 domains = []
130 try:
131 san = cert.extensions.get_extension_for_oid(
132 x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
133 domains = san.value.get_values_for_type(x509.DNSName)
134 except x509.ExtensionNotFound:
135 pass
ab74211f
MF
136 for d in domains:
137 cmd += ['-d', d]
138 if delta.days < 30:
139 logging.info('%s: renewing', domain)
462d58bb 140 logging.info('%s: %s', domain, ' '.join(cmd))
ab74211f 141 if not dry_run:
462d58bb
MF
142 try:
143 subprocess.check_call(cmd)
144 except subprocess.CalledProcessError:
145 logging.error('failed', exc_info=True)
146 return 0
ab74211f
MF
147 ret = 1
148 # Try to revoke the old one.
96e9a04a
MF
149 cmd = ['certbot', 'revoke', '--no-delete-after-revoke', '--cert-path',
150 cert_path]
ab74211f 151 logging.info('%s: revoking old cert', domain)
f33e57fb 152 logging.info('%s: %s', domain, ' '.join(cmd))
ab74211f 153 if not dry_run:
462d58bb 154 try:
f33e57fb 155 subprocess.check_call(cmd, stdin=open('/dev/null', 'r'))
462d58bb
MF
156 except subprocess.CalledProcessError:
157 logging.error('failed', exc_info=True)
ab74211f
MF
158 else:
159 logging.info('%s: up-to-date!', domain)
160
161 return ret
162
163
164def main(argv):
165 """The main() entry point!"""
166 parser = get_parser()
167 opts = parser.parse_args(argv)
168 setup_logging()
169
170 cnt = 0
171 domains = [x[:-5] for x in os.listdir('/etc/letsencrypt/renewal')]
172 for domain in domains:
173 cnt += process_domain(domain, dry_run=opts.dry_run)
174
175 if opts.cronjob:
176 if cnt:
177 return 0
178 else:
179 return 1
180
181
182if __name__ == '__main__':
183 sys.exit(main(sys.argv[1:]))