From 9567684225ab2697bb6f9b2b6e63b724876c0256 Mon Sep 17 00:00:00 2001 From: Johannes Löthberg Date: Mon, 21 Nov 2016 11:52:12 +0000 Subject: Initial commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Johannes Löthberg --- _modules/acme.py | 293 +++++++++++++++++++++++++++++++ _states/acme.py | 121 +++++++++++++ theos/certs/git_kyriasis_com.sls | 16 ++ theos/certs/init.sls | 7 + theos/certs/phabricator_kyriasis_com.sls | 16 ++ theos/certs/theos_kyriasis_com.sls | 56 ++++++ theos/certs/xan_kyriasis_com.sls | 16 ++ theos/files/dhparam.pem | 8 + theos/init.sls | 3 + theos/nginx.sls | 13 ++ top.sls | 4 + 11 files changed, 553 insertions(+) create mode 100644 _modules/acme.py create mode 100644 _states/acme.py create mode 100644 theos/certs/git_kyriasis_com.sls create mode 100644 theos/certs/init.sls create mode 100644 theos/certs/phabricator_kyriasis_com.sls create mode 100644 theos/certs/theos_kyriasis_com.sls create mode 100644 theos/certs/xan_kyriasis_com.sls create mode 100644 theos/files/dhparam.pem create mode 100644 theos/init.sls create mode 100644 theos/nginx.sls create mode 100644 top.sls diff --git a/_modules/acme.py b/_modules/acme.py new file mode 100644 index 0000000..43ed1b1 --- /dev/null +++ b/_modules/acme.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +''' +ACME / Let's Encrypt module +=========================== + +.. versionadded: 2016.3 + +This module currently uses letsencrypt-auto, which needs to be available in the path or in /opt/letsencrypt/. + +.. note:: + + Installation & configuration of the Let's Encrypt client can for example be done using + https://github.com/saltstack-formulas/letsencrypt-formula + +.. warning:: + + Be sure to set at least accept-tos = True in cli.ini! + +Most parameters will fall back to cli.ini defaults if None is given. + +''' +# Import python libs +from __future__ import absolute_import +import logging +import datetime +import os + +# Import salt libs +import salt.utils + +log = logging.getLogger(__name__) + +LEA = salt.utils.which_bin(['certbot', 'letsencrypt', + 'certbot-auto', 'letsencrypt-auto', + '/opt/letsencrypt/letsencrypt-auto']) +LE_LIVE = '/etc/letsencrypt/live/' + + +def __virtual__(): + ''' + Only work when letsencrypt-auto is installed + ''' + return LEA is not None, 'The ACME execution module cannot be loaded: letsencrypt-auto not installed.' + + +def _cert_file(name, cert_type): + ''' + Return expected path of a Let's Encrypt live cert + ''' + return os.path.join(LE_LIVE, name, '{0}.pem'.format(cert_type)) + + +def _expires(name): + ''' + Return the expiry date of a cert + + :return datetime object of expiry date + ''' + cert_file = _cert_file(name, 'cert') + # Use the salt module if available + if 'tls.cert_info' in __salt__: + expiry = __salt__['tls.cert_info'](cert_file)['not_after'] + # Cobble it together using the openssl binary + else: + openssl_cmd = 'openssl x509 -in {0} -noout -enddate'.format(cert_file) + # No %e format on my Linux'es here + strptime_sux_cmd = 'date --date="$({0} | cut -d= -f2)" +%s'.format(openssl_cmd) + expiry = float(__salt__['cmd.shell'](strptime_sux_cmd, output_loglevel='quiet')) + # expiry = datetime.datetime.strptime(expiry.split('=', 1)[-1], '%b %e %H:%M:%S %Y %Z') + + return datetime.datetime.fromtimestamp(expiry) + + +def _renew_by(name, window=None): + ''' + Date before a certificate should be renewed + + :param name: Common Name of the certificate (DNS name of certificate) + :param window: days before expiry date to renew + :return datetime object of first renewal date + ''' + expiry = _expires(name) + if window is not None: + expiry = expiry - datetime.timedelta(days=window) + + return expiry + + +def cert(name, + aliases=None, + email=None, + webroot=None, + test_cert=False, + renew=None, + keysize=None, + server=None, + owner='root', + group='root'): + ''' + Obtain/renew a certificate from an ACME CA, probably Let's Encrypt. + + :param name: Common Name of the certificate (DNS name of certificate) + :param aliases: subjectAltNames (Additional DNS names on certificate) + :param email: e-mail address for interaction with ACME provider + :param webroot: True or a full path to use to use webroot. Otherwise use standalone mode + :param test_cert: Request a certificate from the Happy Hacker Fake CA (mutually exclusive with 'server') + :param renew: True/'force' to force a renewal, or a window of renewal before expiry in days + :param keysize: RSA key bits + :param server: API endpoint to talk to + :param owner: owner of private key + :param group: group of private key + :return: dict with 'result' True/False/None, 'comment' and certificate's expiry date ('not_after') + + CLI example: + + .. code-block:: bash + + salt 'gitlab.example.com' acme.cert dev.example.com "[gitlab.example.com]" test_cert=True renew=14 webroot=/opt/gitlab/embedded/service/gitlab-rails/public + ''' + + cmd = [LEA, 'certonly', '--quiet'] + + cert_file = _cert_file(name, 'cert') + if not __salt__['file.file_exists'](cert_file): + log.debug('Certificate {0} does not exist (yet)'.format(cert_file)) + renew = False + elif needs_renewal(name, renew): + log.debug('Certificate {0} will be renewed'.format(cert_file)) + cmd.append('--renew-by-default') + renew = True + else: + return { + 'result': None, + 'comment': 'Certificate {0} does not need renewal'.format(cert_file), + 'not_after': expires(name) + } + + if server: + cmd.append('--server {0}'.format(server)) + + if test_cert: + if server: + return {'result': False, 'comment': 'Use either server or test_cert, not both'} + cmd.append('--test-cert') + + if webroot: + cmd.append('--authenticator webroot') + if webroot is not True: + cmd.append('--webroot-path {0}'.format(webroot)) + else: + cmd.append('--authenticator standalone') + + if email: + cmd.append('--email {0}'.format(email)) + + if keysize: + cmd.append('--rsa-key-size {0}'.format(keysize)) + + cmd.append('--domains {0}'.format(name)) + if aliases is not None: + for dns in aliases: + cmd.append('--domains {0}'.format(dns)) + + res = __salt__['cmd.run_all'](' '.join(cmd)) + + if res['retcode'] != 0: + return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])} + + if renew: + comment = 'Certificate {0} renewed'.format(name) + else: + comment = 'Certificate {0} obtained'.format(name) + ret = {'comment': comment, 'not_after': expires(name)} + + res = __salt__['file.check_perms'](_cert_file(name, 'privkey'), {}, owner, group, '0600', follow_symlinks=True) + + if res is None: + ret['result'] = False + ret['comment'] += ', but setting permissions failed.' + elif not res[0].get('result', False): + ret['result'] = False + ret['comment'] += ', but setting permissions failed with \n{0}'.format(res[0]['comment']) + else: + ret['result'] = True + ret['comment'] += '.' + + return ret + + +def certs(): + ''' + Return a list of active certificates + + CLI example: + + .. code-block:: bash + + salt 'vhost.example.com' acme.certs + ''' + return __salt__['file.readdir'](LE_LIVE)[2:] + + +def info(name): + ''' + Return information about a certificate + + .. note:: + Will output tls.cert_info if that's available, or OpenSSL text if not + + :param name: CommonName of cert + + CLI example: + + .. code-block:: bash + + salt 'gitlab.example.com' acme.info dev.example.com + ''' + cert_file = _cert_file(name, 'cert') + # Use the salt module if available + if 'tls.cert_info' in __salt__: + info = __salt__['tls.cert_info'](cert_file) + # Strip out the extensions object contents; + # these trip over our poor state output + # and they serve no real purpose here anyway + info['extensions'] = info['extensions'].keys() + return info + # Cobble it together using the openssl binary + else: + openssl_cmd = 'openssl x509 -in {0} -noout -text'.format(cert_file) + return __salt__['cmd.run'](openssl_cmd, output_loglevel='quiet') + + +def expires(name): + ''' + The expiry date of a certificate in ISO format + + :param name: CommonName of cert + + CLI example: + + .. code-block:: bash + + salt 'gitlab.example.com' acme.expires dev.example.com + ''' + return _expires(name).isoformat() + + +def has(name): + ''' + Test if a certificate is in the Let's Encrypt Live directory + + :param name: CommonName of cert + + Code example: + + .. code-block:: python + + if __salt__['acme.has']('dev.example.com'): + log.info('That is one nice certificate you have there!') + ''' + return __salt__['file.file_exists'](_cert_file(name, 'cert')) + + +def renew_by(name, window=None): + ''' + Date in ISO format when a certificate should first be renewed + + :param name: CommonName of cert + :param window: number of days before expiry when renewal should take place + ''' + return _renew_by(name, window).isoformat() + + +def needs_renewal(name, window=None): + ''' + Check if a certicate needs renewal + + :param name: CommonName of cert + :param window: Window in days to renew earlier or True/force to just return True + + Code example: + + .. code-block:: python + + if __salt__['acme.needs_renewal']('dev.example.com'): + __salt__['acme.cert']('dev.example.com', **kwargs) + else: + log.info('Your certificate is still good') + ''' + if window is not None and window in ('force', 'Force', True): + return True + + return _renew_by(name, window) <= datetime.datetime.today() diff --git a/_states/acme.py b/_states/acme.py new file mode 100644 index 0000000..4fcf09a --- /dev/null +++ b/_states/acme.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +''' +ACME / Let's Encrypt certificate management state +================================================= + +.. versionadded: 2016.3 + +See also the module documentation + +.. code-block:: yaml + + reload-gitlab: + cmd.run: + - name: gitlab-ctl hup + + dev.example.com: + acme.cert: + - aliases: + - gitlab.example.com + - email: acmemaster@example.com + - webroot: /opt/gitlab/embedded/service/gitlab-rails/public + - renew: 14 + - fire_event: acme/dev.example.com + - onchanges_in: + - cmd: reload-gitlab + +''' +# Import python libs +from __future__ import absolute_import +import logging + +log = logging.getLogger(__name__) + + +def __virtual__(): + ''' + Only work when the ACME module agrees + ''' + return 'acme.cert' in __salt__ + + +def cert(name, + aliases=None, + email=None, + webroot=None, + test_cert=False, + renew=None, + keysize=None, + server=None, + owner='root', + group='root'): + ''' + Obtain/renew a certificate from an ACME CA, probably Let's Encrypt. + + :param name: Common Name of the certificate (DNS name of certificate) + :param aliases: subjectAltNames (Additional DNS names on certificate) + :param email: e-mail address for interaction with ACME provider + :param webroot: True or a full path to use to use webroot. Otherwise use standalone mode + :param test_cert: Request a certificate from the Happy Hacker Fake CA (mutually exclusive with 'server') + :param renew: True/'force' to force a renewal, or a window of renewal before expiry in days + :param keysize: RSA key bits + :param server: API endpoint to talk to + :param owner: owner of private key + :param group: group of private key + ''' + + if __opts__['test']: + ret = { + 'name': name, + 'changes': {}, + 'result': None + } + window = None + try: + window = int(renew) + except: # pylint: disable=bare-except + pass + + comment = 'Certificate {0} '.format(name) + if not __salt__['acme.has'](name): + comment += 'would have been obtained' + elif __salt__['acme.needs_renewal'](name, window): + comment += 'would have been renewed' + else: + comment += 'would not have been touched' + ret['comment'] = comment + return ret + + if not __salt__['acme.has'](name): + old = None + else: + old = __salt__['acme.info'](name) + + res = __salt__['acme.cert']( + name, + aliases=aliases, + email=email, + webroot=webroot, + test_cert=test_cert, + renew=renew, + keysize=keysize, + server=server, + owner=owner, + group=group + ) + + ret = { + 'name': name, + 'result': res['result'] is not False, + 'comment': res['comment'] + } + + if res['result'] is None: + ret['changes'] = {} + else: + ret['changes'] = { + 'old': old, + 'new': __salt__['acme.info'](name) + } + + return ret diff --git a/theos/certs/git_kyriasis_com.sls b/theos/certs/git_kyriasis_com.sls new file mode 100644 index 0000000..47f023b --- /dev/null +++ b/theos/certs/git_kyriasis_com.sls @@ -0,0 +1,16 @@ +include: + - nginx.ng + +git.kyriasis.com: + acme.cert: + - email: johannes@kyriasis.com + - webroot: /srv/http/ + - keysize: 4096 + + - watch_in: + - service: nginx_service + - require_in: + - service: nginx_service + + +# vim: set ft=yaml et: diff --git a/theos/certs/init.sls b/theos/certs/init.sls new file mode 100644 index 0000000..e34d338 --- /dev/null +++ b/theos/certs/init.sls @@ -0,0 +1,7 @@ +include: + - .theos_kyriasis_com + - .xan_kyriasis_com + - .git_kyriasis_com + - .phabricator_kyriasis_com + +# vim: set ft=yaml et: diff --git a/theos/certs/phabricator_kyriasis_com.sls b/theos/certs/phabricator_kyriasis_com.sls new file mode 100644 index 0000000..7f40132 --- /dev/null +++ b/theos/certs/phabricator_kyriasis_com.sls @@ -0,0 +1,16 @@ +include: + - nginx.ng + +phabricator.kyriasis.com: + acme.cert: + - email: johannes@kyriasis.com + - webroot: /srv/http/ + - keysize: 4096 + + - watch_in: + - service: nginx_service + - require_in: + - service: nginx_service + + +# vim: set ft=yaml et: diff --git a/theos/certs/theos_kyriasis_com.sls b/theos/certs/theos_kyriasis_com.sls new file mode 100644 index 0000000..88d0f22 --- /dev/null +++ b/theos/certs/theos_kyriasis_com.sls @@ -0,0 +1,56 @@ +include: + - nginx.ng + +theos.kyriasis.com: + acme.cert: + - email: johannes@kyriasis.com + - webroot: /srv/http/ + - keysize: 4096 + + - watch_in: + - service: nginx_service + - require_in: + - service: nginx_service + +smtpd-access-theos: + acl.present: + - name: /etc/letsencrypt/archive/theos.kyriasis.com/ + - acl_type: user + - acl_name: smtpd + - perms: r + - recurse: True + - require_in: + - acme: theos.kyriasis.com + +ldap-access-theos: + acl.present: + - name: /etc/letsencrypt/archive/theos.kyriasis.com/ + - acl_type: user + - acl_name: ldap + - perms: r + - recurse: True + - require_in: + - acme: theos.kyriasis.com + +znc-access-theos: + acl.present: + - name: /etc/letsencrypt/archive/theos.kyriasis.com/ + - acl_type: user + - acl_name: snc + - perms: r + - recurse: True + - require_in: + - acme: theos.kyriasis.com + +kyrias-access-theos: + acl.present: + - name: /etc/letsencrypt/archive/theos.kyriasis.com/ + - acl_type: user + - acl_name: kyrias + - perms: r + - recurse: True + - require_in: + - acme: theos.kyriasis.com + + +# vim: set ft=yaml et: diff --git a/theos/certs/xan_kyriasis_com.sls b/theos/certs/xan_kyriasis_com.sls new file mode 100644 index 0000000..3a8fbe3 --- /dev/null +++ b/theos/certs/xan_kyriasis_com.sls @@ -0,0 +1,16 @@ +include: + - nginx.ng + +xan.kyriasis.com: + acme.cert: + - email: johannes@kyriasis.com + - webroot: /srv/http/ + - keysize: 4096 + + - watch_in: + - service: nginx_service + - require_in: + - service: nginx_service + + +# vim: set ft=yaml et: diff --git a/theos/files/dhparam.pem b/theos/files/dhparam.pem new file mode 100644 index 0000000..b164654 --- /dev/null +++ b/theos/files/dhparam.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA7tYfJeMSu8u6Z8jbO3eHVQI7MXnt7uegbo0mogT1w0wqvQI4Zie4 +GDbu2xY+yEdW7mb9/kiddcynl9BytXkVNfXqJ1F6h6VP1rKn0jpq3XsVZ9LqI48Z +7qHHx+uTw/reTYqwc/ZKBlj3XlMTVjXpkM3c58HyjFfpGJbFvnqa40hW/boyYOCM +67js4sRmOXm51TlVQw1SSX3K70+sHWJU2TIWirC1WegMQS1Gc9t1rHQMI7BYKGL1 +v3wRDkH5t+5UgxeRzINB5Tf/EZhNqkRo29DHqiCpzCo+vTc68uhOBJY9lI4JdUht +otUORzNf0HWWGJsTegnfDPw8YyZUZCCs+wIBAg== +-----END DH PARAMETERS----- diff --git a/theos/init.sls b/theos/init.sls new file mode 100644 index 0000000..da93cd1 --- /dev/null +++ b/theos/init.sls @@ -0,0 +1,3 @@ +include: + - theos.nginx + - theos.certs diff --git a/theos/nginx.sls b/theos/nginx.sls new file mode 100644 index 0000000..51bfae6 --- /dev/null +++ b/theos/nginx.sls @@ -0,0 +1,13 @@ +include: + - nginx.ng + +dhparam: + file.managed: + - name: /etc/nginx/dhparam.pem + - source: salt://theos/files/dhparam.pem + - require: + - pkg: nginx_install + - require_in: + - service: nginx_service + - watch_in: + - service: nginx_service diff --git a/top.sls b/top.sls new file mode 100644 index 0000000..8aaa91c --- /dev/null +++ b/top.sls @@ -0,0 +1,4 @@ +base: + "theos.kyriasis.com": + - nginx.ng + - theos -- cgit v1.2.3-54-g00ecf