]> git.wh0rd.org - home.git/blame - .bin/le-renew
vunshare
[home.git] / .bin / le-renew
CommitLineData
ab74211f
MF
1#!/usr/bin/python2
2# We need to force py2 until M2Crypto is ported:
3# https://gitlab.com/m2crypto/m2crypto/issues/114
4# pylint: disable=invalid-name
5
6"""Renew Let's Encrypt certs!
7
8To generate a new set of certs:
9$ certbot certonly --webroot \\
10 --webroot-path /var/www/wh0rd/ -d wh0rd.org -d www.wh0rd.org \\
11 --webroot-path /var/www/rss/ -d rss.wh0rd.org
12"""
13
14from __future__ import print_function
15
16import argparse
17try:
18 import configparser
19except ImportError:
20 import ConfigParser as configparser
21try:
22 from cStringIO import StringIO
23except ImportError:
24 from io import StringIO
25import datetime
26import logging
27import logging.handlers
28import M2Crypto
29import os
30import pytz
31import subprocess
32import sys
33
34
35LE_BASE = '/etc/letsencrypt'
36
37
38def get_parser():
39 """Return an ArgumentParser() for this module."""
40 parser = argparse.ArgumentParser(description=__doc__,
41 formatter_class=argparse.RawTextHelpFormatter)
42 parser.add_argument('-n', '--dry-run', default=False,
43 action='store_true',
44 help='Do not actually update certs')
45 parser.add_argument('--cronjob', default=False,
46 action='store_true',
47 help='Exit non-zero if no certs were changed')
48 return parser
49
50
51def setup_logging(debug=False, syslog=None):
52 """Setup the logging module just the way we like it."""
53 if syslog is None:
54 syslog = not os.isatty(sys.stdin.fileno())
55
56 if syslog:
57 handler = logging.handlers.SysLogHandler(address='/dev/log')
58 else:
59 handler = logging.StreamHandler(stream=sys.stdout)
60
61 fmt = '%(asctime)s: %(levelname)-7s: %(message)s'
62
63 datefmt = '%a, %d %b %Y %H:%M:%S letsencrypt'
64
65 level = logging.DEBUG if debug else logging.INFO
66
67 formatter = logging.Formatter(fmt, datefmt)
68 handler.setFormatter(formatter)
69
70 logger = logging.getLogger()
71 logger.addHandler(handler)
72 logger.setLevel(level)
73
74
75def load_cert(path):
76 """Load the cert at |path|"""
77 with open(path) as f:
78 data = f.read()
79 return M2Crypto.X509.load_cert_string(data)
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)
110 webroot_path = conf.get('[webroot_map', domain)
111
112 cert_path = os.path.realpath(conf.get('globals', 'cert'))
113
114 cert = load_cert(cert_path)
115 stamp = cert.get_not_after()
116 now = pytz.timezone('UTC').localize(datetime.datetime.now())
117 delta = stamp.get_datetime() - now
118 logging.info('%s: expires in %2s days', domain, delta.days)
119
120 cmd = [
121 'certbot',
122 'certonly', '--webroot',
123 '--webroot-path', webroot_path,
124 '-d', domain,
125 ]
126 san = cert.get_ext('subjectAltName').get_value()
127 domains = [x.strip()[4:] for x in san.split(',')]
128 domains.remove(domain)
129 for d in domains:
130 cmd += ['-d', d]
131 if delta.days < 30:
132 logging.info('%s: renewing', domain)
133 logging.info('%s: %s', domain, cmd)
134 if not dry_run:
135 subprocess.check_call(cmd)
136 ret = 1
137 # Try to revoke the old one.
138 cmd = ['certbot', 'revoke', '--cert-path', cert_path]
139 logging.info('%s: revoking old cert', domain)
140 logging.info('%s: %s', domain, cmd)
141 if not dry_run:
142 subprocess.check_call(cmd)
143 else:
144 logging.info('%s: up-to-date!', domain)
145
146 return ret
147
148
149def main(argv):
150 """The main() entry point!"""
151 parser = get_parser()
152 opts = parser.parse_args(argv)
153 setup_logging()
154
155 cnt = 0
156 domains = [x[:-5] for x in os.listdir('/etc/letsencrypt/renewal')]
157 for domain in domains:
158 cnt += process_domain(domain, dry_run=opts.dry_run)
159
160 if opts.cronjob:
161 if cnt:
162 return 0
163 else:
164 return 1
165
166
167if __name__ == '__main__':
168 sys.exit(main(sys.argv[1:]))