summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohannes Löthberg <johannes@kyriasis.com>2016-11-21 11:52:12 +0000
committerJohannes Löthberg <johannes@kyriasis.com>2016-11-21 11:52:12 +0000
commit9567684225ab2697bb6f9b2b6e63b724876c0256 (patch)
tree07dca4a01de1356516e708740cf2583661aa450c
downloadfile-9567684225ab2697bb6f9b2b6e63b724876c0256.tar.xz
Initial commit
Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
-rw-r--r--_modules/acme.py293
-rw-r--r--_states/acme.py121
-rw-r--r--theos/certs/git_kyriasis_com.sls16
-rw-r--r--theos/certs/init.sls7
-rw-r--r--theos/certs/phabricator_kyriasis_com.sls16
-rw-r--r--theos/certs/theos_kyriasis_com.sls56
-rw-r--r--theos/certs/xan_kyriasis_com.sls16
-rw-r--r--theos/files/dhparam.pem8
-rw-r--r--theos/init.sls3
-rw-r--r--theos/nginx.sls13
-rw-r--r--top.sls4
11 files changed, 553 insertions, 0 deletions
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