OTcerts.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import os
  2. import sys
  3. import errno
  4. import argparse
  5. import configparser
  6. import logging
  7. import mysql.connector
  8. import subprocess
  9. from pwd import getpwnam
  10. from grp import getgrnam
  11. # Query for IMAP/POP3 certificate
  12. mbox_list_stmt = "SELECT DISTINCT(name) FROM records WHERE content in ({}) and (name LIKE 'imap.%' or name LIKE 'pop3.%' or name LIKE 'mail.%')"
  13. # Query for SMTP certificate
  14. smtp_list_stmt = "SELECT DISTINCT(name) FROM records WHERE content in ({}) and (name LIKE 'smtp.%' or name LIKE 'mail.%')"
  15. # Get list of defined domains in vhosts configuration database
  16. domains_list_stmt = """SELECT DISTINCT(SUBSTRING_INDEX(urls.dns_name, '.', -2)) AS domain_names
  17. FROM hosts INNER JOIN (hosts_urls, urls, vhosts_features, vhosts)
  18. ON (urls.url_id = hosts_urls.url_id and urls.url_id = vhosts.url_id
  19. and vhosts.vhost_id = vhosts_features.vhost_id and hosts.host_id = hosts_urls.host_id)
  20. WHERE (hosts_urls.http = 'Y' and hosts.hostname = %(webserver)s)
  21. """
  22. # Get list of defined urls for specific webserver
  23. urls_list_stmt = """SELECT DISTINCT(urls.dns_name) AS urls
  24. FROM hosts INNER JOIN (hosts_urls, urls, vhosts_features, vhosts)
  25. ON (urls.url_id = hosts_urls.url_id and urls.url_id = vhosts.url_id
  26. and vhosts.vhost_id = vhosts_features.vhost_id and hosts.host_id = hosts_urls.host_id)
  27. WHERE (hosts_urls.http = 'Y' and hosts.hostname = %(webserver)s)
  28. """
  29. # Get domain_id if defined in nameserver database
  30. domain_id_stmt="SELECT domains.id as domain_id FROM domains WHERE domains.name=%(domain)s"
  31. subdomains_list_stmt = "SELECT DISTINCT(urls.dns_name) AS domain_names "\
  32. "FROM urls INNER JOIN (hosts_urls, hosts, vhosts_features, vhosts ) "\
  33. "ON (urls.url_id = hosts_urls.url_id and urls.url_id = vhosts.url_id and vhosts.vhost_id = vhosts_features.vhost_id) "\
  34. "WHERE (hosts_urls.http = 'Y' and hosts.hostname = %(webserver)s and "\
  35. "urls.dns_name LIKE %(domain)s)"
  36. default_conf_file="./etc/ot_certs.ini"
  37. logging.basicConfig(level=logging.INFO)
  38. logger = logging.getLogger()
  39. def init_prog(argv):
  40. """
  41. Parse command line args and config file
  42. """
  43. parser = argparse.ArgumentParser(
  44. description="Manage LetsEncrypt certificates")
  45. parser.add_argument("-c", "--config", type=open,
  46. required=False,
  47. default=default_conf_file,
  48. help="Specifity config file (default: {})".format(default_conf_file))
  49. parser.add_argument("--renew", default=False, action='store_true', required=False,
  50. help="Invoca solamente il renew per i certificati gia' presenti")
  51. service_group = parser.add_mutually_exclusive_group(required=True)
  52. service_group.add_argument("--proxy", default=False, action='store_true', required=False,
  53. help="Richiedi i certificati per i siti proxaty")
  54. service_group.add_argument("--liste", default=False, action='store_true', required=False,
  55. help="Richiedi i certificati per liste.indivia.net")
  56. service_group.add_argument("--hosting", default=False, action='store_true', required=False,
  57. help="Richiedi i certificati per i siti in hosting")
  58. service_group.add_argument("--webmail", default=False, action='store_true', required=False,
  59. help="Richiedi i certificati per le webmail")
  60. service_group.add_argument("--smtp", default=False, action='store_true', required=False,
  61. help="Richiedi i certificati per il server SMTP")
  62. service_group.add_argument("--mbox", default=False, action='store_true', required=False,
  63. help="Richiedi i certificati per il server POP/IMAP")
  64. args = parser.parse_args()
  65. try:
  66. config = configparser.ConfigParser()
  67. config.read_file(args.config)
  68. except Exception as e:
  69. logger.error("Error parsing configuration {}".format(e))
  70. exit(-1)
  71. return args, config
  72. def connect_db(conf_dict):
  73. try:
  74. cnx = mysql.connector.connect(**conf_dict)
  75. except mysql.connector.Error as err:
  76. if err.errno == mysql.connector.errorcode.ER_ACCESS_DENIED_ERROR:
  77. logger.error("Something is wrong with your user name or password")
  78. elif err.errno == mysql.connector.errorcode.ER_BAD_DB_ERROR:
  79. logger.error("Database does not exist")
  80. else:
  81. logger.error(err)
  82. return None
  83. return cnx
  84. def get_subdomain_list(config, domain, ot_conn, ex_subdomains=tuple()):
  85. """
  86. Return a Python list containing subdomain of domain parameter
  87. eg: ['app.arkiwi.org']
  88. """
  89. result_dict=dict()
  90. ot_cursor=ot_conn.cursor()
  91. ot_cursor.execute(subdomains_list_stmt, {'webserver':config['main']['webserver'].strip(" '\""),
  92. 'domain':"%{}%".format(domain)})
  93. subdomains_res = ot_cursor.fetchall()
  94. ot_cursor.close()
  95. try:
  96. subdomains_filtered = [s[0].decode('utf-8') for s in subdomains_res
  97. if not(s[0].decode('utf-8').startswith(ex_subdomains))]
  98. except AttributeError:
  99. subdomains_filtered = [s[0] for s in subdomains_res
  100. if not(s[0].startswith(ex_subdomains))]
  101. return subdomains_filtered
  102. def get_domain_list(config, ot_conn, dns_conn):
  103. """
  104. Return a dictionary of domains and properties
  105. eg:{'indivia.net': {manage_ns=True, domain_id=92},
  106. 'metalabs.org': {managed_ns=False}}
  107. """
  108. result_dict=dict()
  109. ot_cursor=ot_conn.cursor()
  110. ot_cursor.execute(domains_list_stmt, {'webserver':config['main']['webserver'].strip(" '\"")})
  111. dns_cursor=dns_conn.cursor()
  112. for domain_barr, in ot_cursor:
  113. try:
  114. domain_name = domain_barr.decode("utf-8")
  115. except AttributeError:
  116. domain_name = domain_barr;
  117. logger.debug(domain_barr)
  118. try:
  119. dns_cursor.execute(domain_id_stmt, {'domain':domain_name})
  120. except Exception as e:
  121. logger.error(e)
  122. exit(-1)
  123. dns_res = dns_cursor.fetchall()
  124. if dns_cursor.rowcount == 1 :
  125. result_dict.update({domain_name: {'managed_ns':True, 'domain_id':dns_res[0][0]}})
  126. elif dns_cursor.rowcount == 0:
  127. result_dict.update({domain_name: {'managed_ns':False, 'domain_id':None}})
  128. else:
  129. logger.error('Unexpected result for domain {}'.format(domain_name))
  130. dns_cursor.close()
  131. ot_cursor.close()
  132. return result_dict
  133. def get_url_list(config_section, server_name, ot_conn, dns_conn):
  134. """
  135. Ritorna la lista delle url configurate per uno specifico server_name
  136. NB: il questo momento il dato viene estratto dal db di ortiche, ma non viene
  137. controllato se il dns e' configurato in maniera coerente. Questo potrebbe generare
  138. errori in momenti successivi (es, durante il challenge HTTP-01)
  139. """
  140. urls_list = []
  141. ot_cursor=ot_conn.cursor()
  142. ot_cursor.execute(urls_list_stmt, {'webserver':server_name})
  143. ot_res = ot_cursor.fetchall()
  144. logger.debug(ot_res)
  145. urls_list = [t[0] for t in ot_res]
  146. ot_cursor.close()
  147. return urls_list
  148. def get_alias_list(config, dns_conn, query, aliases):
  149. """
  150. Return a list of domains to get the certificate for
  151. """
  152. result_list = list()
  153. dns_cursor=dns_conn.cursor()
  154. try:
  155. dns_cursor.execute(query, aliases)
  156. except Exception as e:
  157. logger.error(e)
  158. exit(-1)
  159. dns_res = dns_cursor.fetchall()
  160. result_list = [name[0].decode('utf-8') for name in dns_res]
  161. dns_cursor.close()
  162. return result_list
  163. def acme_renew(config, pre_hook_cmd, post_hook_cmd, dryrun=False):
  164. args = config['certbot']['base_args']
  165. # args += " -m {} ".format(config['certbot']['email'])
  166. # args += "--server {} ".format(config['certbot']['server'])
  167. if dryrun:
  168. args += "--dry-run "
  169. if not pre_hook_cmd is None:
  170. args +=' --pre-hook "{}"'.format(pre_hook_cmd)
  171. if not post_hook_cmd is None:
  172. args +=' --post-hook "{}"'.format(post_hook_cmd)
  173. args += " renew"
  174. if dryrun:
  175. logging.info("{} {}".format(config['certbot']['bin'], args))
  176. else:
  177. os.system("{} {}".format(config['certbot']['bin'], args))
  178. return True
  179. def acme_request(config, domain_name, acme_test='DNS-01', webroot=None, dryrun=False, domains_list=None):
  180. args = config['certbot']['base_args']
  181. args += " -m {} ".format(config['certbot']['email'])
  182. args += "--server {} ".format(config['certbot']['server'])
  183. if dryrun:
  184. args += "--dry-run "
  185. if acme_test == 'DNS-01':
  186. args += " --manual certonly "
  187. args += "--preferred-challenges dns-01 "
  188. args += "--manual-auth-hook {} ".format(config['certbot']['auth_hook'])
  189. args += "--manual-cleanup-hook {} ".format(config['certbot']['cleanup_hook'])
  190. args += "-d {},*.{}".format(domain_name, domain_name)
  191. elif acme_test == 'HTTP-01':
  192. args += " --webroot certonly "
  193. args += "--preferred-challenges http-01 "
  194. if webroot is None:
  195. args += "-w {}/{}/htdocs ".format(config['apache']['webroot'], domain_name)
  196. else:
  197. args += "-w {} ".format(webroot)
  198. if domains_list is None:
  199. args += "-d {}".format(domain_name)
  200. else:
  201. args += "--expand --cert-name {} ".format(domain_name)
  202. args += "-d {}".format(",".join(domains_list))
  203. else:
  204. logger.error('acme test {} not supported'.format(acme_test))
  205. return False
  206. if dryrun:
  207. logging.info("{} {}".format(config['certbot']['bin'], args))
  208. else:
  209. os.system("{} {}".format(config['certbot']['bin'], args))
  210. return True
  211. def symlink_force(target, link_name):
  212. try:
  213. os.symlink(target, link_name)
  214. except Exception as e:
  215. if e.errno == errno.EEXIST:
  216. os.remove(link_name)
  217. os.symlink(target, link_name)
  218. else:
  219. raise e
  220. def link_cert(config, source, dest, dryrun=False):
  221. src_name = os.path.join(config['certbot']['live_certificates_dir'], source)
  222. link_name = os.path.join(config['apache']['certificates_root'], dest)
  223. if dryrun:
  224. logger.info('{} -> {}'.format(link_name,src_name))
  225. return
  226. else:
  227. symlink_force(src_name, link_name)
  228. def fix_permissions(config):
  229. """
  230. Sistema i permessi dei certificati affinche' risultino leggibili dai demoni interessati
  231. """
  232. archive_dir = config['certbot']['archive_certificates_dir']
  233. uid = getpwnam(config['certbot']['certificates_user'])[2]
  234. gid = getgrnam(config['certbot']['certificates_group'])[2]
  235. for root, dirs, files in os.walk(archive_dir):
  236. for momo in dirs:
  237. logger.debug('Fixing user/group and permissions on {}'.format(os.path.join(root, momo)))
  238. os.chown(os.path.join(root, momo), uid, gid)
  239. os.chmod(os.path.join(root, momo), 0o755)
  240. for momo in files:
  241. logger.debug('Fixing user/group and permissions on {}'.format(os.path.join(root, momo)))
  242. os.chown(os.path.join(root, momo), uid, gid)
  243. if momo.startswith('privkey'):
  244. os.chmod(os.path.join(root, momo), 0o640)
  245. else:
  246. os.chmod(os.path.join(root, momo), 0o644)
  247. if __name__ == '__main__':
  248. args, config = init_prog(sys.argv)
  249. dryrun=config['main'].getboolean('dryrun')
  250. service_reload = dict()
  251. if dryrun:
  252. print("DRYRUN, nessun certificato verra' richiesto, nessun link/file creato o modificato")
  253. if args.renew:
  254. pre_hook_cmd = None
  255. post_hook_cmd = None
  256. logging.info('Renewing certificates ')
  257. if args.webmail or args.hosting or args.liste:
  258. post_hook_cmd = "systemctl reload apache2"
  259. elif args.smtp:
  260. post_hook_cmd = "systemctl reload postfix"
  261. elif args.mbox:
  262. post_hook_cmd = "systemctl restart dovecot"
  263. elif args.proxy:
  264. post_hook_cmd = "systemctl reload nginx"
  265. logger.debug("post_hook_cmd: {}".format(post_hook_cmd))
  266. if acme_renew(config, pre_hook_cmd, post_hook_cmd, dryrun=dryrun):
  267. logger.info("Done renew")
  268. else:
  269. # Fai le nuove richieste per i certificati
  270. # Caso speciale per le webmail
  271. if args.webmail:
  272. logging.info('Asking certificates for webmail')
  273. vhost_name = config['webmail']['vhost'].strip()
  274. webmails_list = ["webmail.{}".format(d.strip()) for d in config['webmail']['domains'].split(',') if len(d.strip())>0]
  275. logging.info('vhost {}, domains_list {}'.format(vhost_name, webmails_list))
  276. if acme_request(config, vhost_name, acme_test='HTTP-01', dryrun=dryrun, domains_list=webmails_list):
  277. link_cert(config, vhost_name, vhost_name, dryrun=dryrun)
  278. else:
  279. logger.error('Error asking certificate for {}'.format(vhost_name))
  280. # reload apache
  281. logger.info("Reloading apache")
  282. # ret = subprocess.run("systemctl reload apache2")
  283. ret = os.system("systemctl reload apache2")
  284. logger.info(ret)
  285. # Caso speciale per il proxy
  286. if args.proxy:
  287. logging.info('Asking certificates for proxy web domains')
  288. try:
  289. proxy_conf = config['nginx']
  290. except KeyError as e:
  291. logger.error("Error parsing configuration, KeyError {}".format(e))
  292. exit(-1)
  293. ot_conn=connect_db(dict(config['ot_db']))
  294. upstream_servers = [s.strip() for s in proxy_conf['upstream_servers'].split(',') if len(s.strip())>0]
  295. for server_name in upstream_servers:
  296. logger.debug("Upstream server {}".format(server_name))
  297. url_list = get_url_list(proxy_conf, server_name,
  298. ot_conn, None)
  299. logger.debug(url_list)
  300. for url in url_list:
  301. acme_request(config, url, acme_test='HTTP-01', webroot=proxy_conf['http-01_webroot'],
  302. dryrun=dryrun, domains_list=[url])
  303. ot_conn.close()
  304. if not dryrun:
  305. fix_permissions(config)
  306. logger.info("Reloading nginx")
  307. ret = os.system("systemctl reload nginx")
  308. logger.info(ret)
  309. # Caso speciale per l'hosting
  310. if args.hosting:
  311. logging.info('Asking certificates for hosted web domains')
  312. try:
  313. hosting_conf = config['apache']
  314. except KeyError as e:
  315. logger.error("Error parsing configuration, KeyError {}".format(e))
  316. exit(-1)
  317. ot_conn=connect_db(dict(config['ot_db']))
  318. dns_conn=connect_db(dict(config['dns_db']))
  319. # Subdomains da escludere
  320. ex_subdomains = tuple([s.strip() for s in config['main']['special_subdomains'].split(',') if len(s.strip())>0])
  321. domains_dict = get_domain_list(config, ot_conn, dns_conn)
  322. logger.debug(domains_dict)
  323. for domain_name, domain_feat in domains_dict.items():
  324. domain_feat['subdomains']=get_subdomain_list(config, domain_name, ot_conn, ex_subdomains=ex_subdomains)
  325. # Controlla se i nameserver sono gestiti da noi
  326. if domain_feat['managed_ns']:
  327. # Nel caso il nameserver sia gestito, chiedi certificati per il dominio e la wildcard
  328. logger.info('Get certificates for {}, *.{}'.format(domain_name, domain_name))
  329. if acme_request(config, domain_name, acme_test='DNS-01', dryrun=dryrun):
  330. link_cert(config, domain_name, domain_name, dryrun=dryrun)
  331. # Crea il link per ogni subdomain
  332. for subdomain in domain_feat['subdomains']:
  333. link_cert(config, domain_name, subdomain, dryrun=dryrun)
  334. else:
  335. # Nel caso i nameserver NON siano gestiti, allora chiedi un certificato per ogni sottodominio
  336. # Crea il link per ogni subdomain
  337. for subdomain in domain_feat['subdomains']:
  338. logger.info('Get certificates for {}'.format(subdomain))
  339. if acme_request(config, subdomain, acme_test='HTTP-01', dryrun=dryrun):
  340. link_cert(config, subdomain, subdomain, dryrun=dryrun)
  341. ot_conn.close()
  342. dns_conn.close()
  343. # reload apache
  344. logger.info("Reloading apache")
  345. # ret = subprocess.run("systemctl reload apache2")
  346. ret = os.system("systemctl reload apache2")
  347. logger.info(ret)
  348. # Caso speciale per l'interfaccia di mailman
  349. if args.liste:
  350. logging.info('Asking certificates for liste.indivia.net')
  351. vhost_name = config['mailman']['vhost'].strip()
  352. liste_list = ["liste.{}".format(d.strip()) for d in config['mailman']['domains'].split(',') if len(d.strip())>0]
  353. if acme_request(config, vhost_name, acme_test='HTTP-01', dryrun=dryrun, domains_list=liste_list):
  354. link_cert(config, vhost_name, vhost_name, dryrun=dryrun)
  355. else:
  356. logger.error('Error asking certificate for {}'.format(vhost_name))
  357. # reload apache
  358. logger.info("Reloading apache")
  359. # ret = subprocess.run("systemctl reload apache2")
  360. ret = os.system("systemctl reload apache2")
  361. logger.info(ret)
  362. # Caso speciale per il server POP/IMAP
  363. if args.mbox:
  364. dns_conn=connect_db(dict(config['dns_db']))
  365. logging.info('Asking certificates for POP/IMAP server')
  366. vhost_name = config['mail']['mbox_vhost'].strip()
  367. server_addresses = [s.strip() for s in config['mail']['mbox_server_addresses'].split(',') if len(s.strip())>0]
  368. mbox_fmt = ','.join(['%s'] * len(server_addresses))
  369. mbox_query = mbox_list_stmt.format(mbox_fmt)
  370. alias_list = get_alias_list(config, dns_conn, mbox_query, server_addresses)
  371. # Per usi futuri, aggiungo l'alias 'mail.indivia.net'
  372. alias_list.append('mail.indivia.net')
  373. logging.info('vhost {}, domains_list {}'.format(vhost_name, alias_list))
  374. if acme_request(config, vhost_name, acme_test='HTTP-01', webroot=config['mail']['mbox_webroot'].strip(),
  375. dryrun=dryrun, domains_list=alias_list):
  376. # non e' richiesto il link, punto direttamente le configurazioni alle dir di letsencrypt
  377. # link_cert(config, vhost_name, vhost_name, dryrun=dryrun)
  378. service_reload['mbox'] = True
  379. pass
  380. else:
  381. logger.error('Error asking certificate for {}'.format(vhost_name))
  382. dns_conn.close()
  383. # restart dovecot
  384. logger.info("Restarting dovecot")
  385. # ret = subprocess.run("systemctl restart dovecot")
  386. ret = os.system("systemctl restart dovecot")
  387. logger.info(ret)
  388. # Caso speciale per il server SMTP
  389. if args.smtp:
  390. logging.info('Asking certificates for SMTP server')
  391. dns_conn=connect_db(dict(config['dns_db']))
  392. vhost_name = config['mail']['smtp_vhost'].strip()
  393. server_addresses = [s.strip() for s in config['mail']['smtp_server_addresses'].split(',') if len(s.strip())>0]
  394. smtp_fmt = ','.join(['%s'] * len(server_addresses))
  395. smtp_query = smtp_list_stmt.format(smtp_fmt)
  396. alias_list = get_alias_list(config, dns_conn, smtp_query, server_addresses)
  397. logging.info('vhost {}, domains_list {}'.format(vhost_name, alias_list))
  398. if acme_request(config, vhost_name, acme_test='HTTP-01', webroot=config['mail']['smtp_webroot'].strip(),
  399. dryrun=dryrun, domains_list=alias_list):
  400. # non e' richiesto il link, punto direttamente le configurazioni alle dir di letsencrypt
  401. # link_cert(config, vhost_name, vhost_name, dryrun=dryrun)
  402. service_reload['smtp'] = True
  403. pass
  404. else:
  405. logger.error('Error asking certificate for {}'.format(vhost_name))
  406. dns_conn.close()
  407. # reload postfix
  408. logger.info("Restarting postfix")
  409. # ret = subprocess.run("systemctl reload postfix")
  410. ret = os.system("systemctl reload postfix")
  411. logger.info(ret)