diff --git a/brutus/generate.py b/brutus/generate.py index 3c8544e..b40dac9 100644 --- a/brutus/generate.py +++ b/brutus/generate.py @@ -8,6 +8,9 @@ import os import jinja2 import yaml +import base64 +import subprocess +import tempfile from . import utils @@ -22,9 +25,10 @@ def register(cls): class Generate: template_env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath="templates"), trim_blocks=True) - def __init__(self, db, rootdir): + def __init__(self, db, rootdir, generate_keys): self.db = db self.rootdir = rootdir + self.generate_keys = generate_keys def generate_file(self, name, **kargs): template = "{}/{}.j2".format(self.name, name) @@ -171,8 +175,19 @@ def generate(self): basedir = os.path.join(self.rootdir, "knot", "etc", "knot") template = self.template_env.get_template('knot/knot.conf.j2') + + # for LE + for domain in self.db['domains']: + if 'tsigid' not in self.db["domains"][domain]: + self.db["domains"][domain]["tsigid"] = domain + ".LE.KEY" + self.db["domains"][domain]["aclid"] = domain + ".LE.ACL" + randomtext = os.urandom(int(int(256)/8)) + tsigsecret = base64.b64encode(randomtext).decode("utf-8") + self.db["domains"][domain]["tsigsecret"] = tsigsecret if self.generate_keys else "" + variables = {} variables['domains'] = self.db["domains"] + filename = os.path.join(basedir, "knot.conf") utils.makedirs(os.path.dirname(filename)) with open(filename, "w") as stream: @@ -180,10 +195,69 @@ def generate(self): print(output, file=stream) -def generate_all(db, rootdir): +@register +class LetsencryptGenerate(Generate): + service = "certificates" + name = "letsencrypt" + + def create_RSA_key(self): + return subprocess.check_output(["openssl", "genrsa", "4096"]) + + def create_CSR(self, san, key): + with open("/etc/ssl/openssl.cnf") as sslconf: + openssl_csr_config = sslconf.read() + san_config = "[SAN]\nsubjectAltName=" + ",".join(["DNS:" + domain for domain in san]) + openssl_csr_config = openssl_csr_config + san_config + + tempdir = tempfile.mkdtemp() + keyfifo = os.path.join(tempdir, 'keyfifo') + conffifo = os.path.join(tempdir, 'conffifo') + keyfd = os.open(keyfifo, os.O_WRONLY|os.O_CREAT) + os.write(keyfd, key) + os.close(keyfd) + with open(conffifo, 'w') as pipe: + pipe.write(openssl_csr_config) + ossl = subprocess.Popen(['openssl', 'req', '-new', '-sha256', '-key', keyfifo, '-subj', '/', '-reqexts', 'SAN', '-config', conffifo], stdout=subprocess.PIPE) + out, err = ossl.communicate() + return out + + def generate(self): + if self.generate_keys and 'letsencrypt_account_key' not in self.db["instances"][None]: + self.db["instances"][None]["letsencrypt_account_key"] = self.create_RSA_key() + + basedir = os.path.join(self.rootdir, self.name, "etc", "letsencrypt") + template = self.template_env.get_template('letsencrypt/le.ini.j2') + for name, value in sorted(self.db["domains"].items()): + if ('dns' not in value['services']) or ('tsigsecret' not in value): + continue + variables = { + 'domain': name, + 'tsigid': value['tsigid'], + 'tsigsecret': value['tsigsecret'] if self.generate_keys else "", + 'admin_mail': self.db["instances"][None].get("admin_mail", ""), + 'acme_dir': 'https://acme-staging.api.letsencrypt.org/directory', + 'dns': 'localhost', + } + if self.generate_keys and 'letsencrypt_domain_key' not in value: + self.db["domains"][name]["letsencrypt_domain_key"] = self.create_RSA_key() + san = [domain for domain in self.db["websites"]] + csr = self.create_CSR(san, self.db["domains"][name]["letsencrypt_domain_key"]) + utils.makedirs(basedir) + keyfd = os.open(os.path.join(basedir, name + '.csr'), os.O_WRONLY | os.O_CREAT) + os.write(keyfd, csr) + os.close(keyfd) + + filename = os.path.join(basedir, name + ".ini") + utils.makedirs(os.path.dirname(filename)) + with open(filename, "w") as stream: + output = template.render(variables) + print(output, file=stream) + + +def generate_all(db, rootdir, generate_keys=True): for cls in registered_classes: services = db["instances"][None]["services"] # Skip disabled services if not services.get(cls.service): continue - cls(db, rootdir).generate() + cls(db, rootdir, generate_keys).generate() diff --git a/examples/instance.yaml b/examples/instance.yaml index ea02b4c..b2efd16 100644 --- a/examples/instance.yaml +++ b/examples/instance.yaml @@ -1,5 +1,6 @@ catalog: instances id: ~ +admin_mail: admin@example.net services: mailserver: enabled: yes @@ -8,3 +9,5 @@ services: enabled: yes dnsserver: enabled: yes + certificates: + enabled: yes diff --git a/templates/knot/knot.conf.j2 b/templates/knot/knot.conf.j2 index 1cbc2bb..24da091 100644 --- a/templates/knot/knot.conf.j2 +++ b/templates/knot/knot.conf.j2 @@ -18,8 +18,22 @@ log: remote: key: +{% for item in domains %} +{% if domains[item].services and domains[item].services.dns and domains[item].tsigid %} + - id: {{ domains[item].tsigid }} + algorithm: hmac-sha256 + secret: {{ domains[item].tsigsecret }} +{% endif %} +{% endfor %} acl: +{% for item in domains %} +{% if domains[item].services and domains[item].services.dns and domains[item].tsigid %} + - id: {{ domains[item].aclid }} + key: {{ domains[item].tsigid }} + action: update +{% endif %} +{% endfor %} template: - id: default @@ -33,10 +47,14 @@ template: dnssec-signing: off semantic-checks: on -zone:{% for item in domains %}{% if domains[item].services and domains[item].services.dns and domains[item].services.dns.enabled %} - - domain: {{item}}{% if domains[item].services.dns.dnssec %} - template: default{% else %} +zone: +{% for item in domains %} +{% if domains[item].services and domains[item].services.dns and domains[item].services.dns.enabled %} + - domain: {{item}} +{% if domains[item].services.dns.dnssec %} + template: default +{% else %} template: unsigned {% endif %} {% endif %} -{% endfor%} +{% endfor %} diff --git a/templates/letsencrypt/le.ini.j2 b/templates/letsencrypt/le.ini.j2 new file mode 100644 index 0000000..11aa461 --- /dev/null +++ b/templates/letsencrypt/le.ini.j2 @@ -0,0 +1,27 @@ +[acmednstiny] +# Required readable ACME account key +AccountKeyFile = /srv/letsencrypt/account.key +# Required readable CSR file +CSRFile = /srv/letsencrypt/{{ domain }}.csr +# Optional ACME directory url (default: https://acme-staging.api.letsencrypt.org/directory) +ACMEDirectory = {{ acme_dir }} +# Optional time in seconds to wait between DNS update and challenge check (default: 3) +CheckChallengeDelay = 3 +# Optional Contact info to send to the ACME provider +MailContact = {{ admin_mail }} + +[TSIGKeyring] +# Required TSIG key name +KeyName = {{ tsigid }} +# Required TSIG key value in base64 +KeyValue = {{ tsigsecret }} +# Required TSIG algorithm +Algorithm = hmac-sha256 + +[DNS] +# Required name of zone to update +Zone = {{ domain }} +# Required name or IP of DNS server +Host = {{ dns }} +# Optional port to connect on DNS server (default: 53) +Port = 53 diff --git a/tests/output/knot/etc/knot/knot.conf b/tests/output/knot/etc/knot/knot.conf index e91e44f..16377a1 100644 --- a/tests/output/knot/etc/knot/knot.conf +++ b/tests/output/knot/etc/knot/knot.conf @@ -18,8 +18,14 @@ log: remote: key: + - id: example.net.LE.KEY + algorithm: hmac-sha256 + secret: acl: + - id: example.net.LE.ACL + key: example.net.LE.KEY + action: update template: - id: default @@ -33,4 +39,7 @@ template: dnssec-signing: off semantic-checks: on -zone: - domain: example.net template: default +zone: + - domain: example.net + template: default + diff --git a/tests/output/letsencrypt/etc/letsencrypt/example.net.ini b/tests/output/letsencrypt/etc/letsencrypt/example.net.ini new file mode 100644 index 0000000..8200d2a --- /dev/null +++ b/tests/output/letsencrypt/etc/letsencrypt/example.net.ini @@ -0,0 +1,27 @@ +[acmednstiny] +# Required readable ACME account key +AccountKeyFile = /srv/letsencrypt/account.key +# Required readable CSR file +CSRFile = /srv/letsencrypt/example.net.csr +# Optional ACME directory url (default: https://acme-staging.api.letsencrypt.org/directory) +ACMEDirectory = https://acme-staging.api.letsencrypt.org/directory +# Optional time in seconds to wait between DNS update and challenge check (default: 3) +CheckChallengeDelay = 3 +# Optional Contact info to send to the ACME provider +MailContact = admin@example.net + +[TSIGKeyring] +# Required TSIG key name +KeyName = example.net.LE.KEY +# Required TSIG key value in base64 +KeyValue = +# Required TSIG algorithm +Algorithm = hmac-sha256 + +[DNS] +# Required name of zone to update +Zone = example.net +# Required name or IP of DNS server +Host = localhost +# Optional port to connect on DNS server (default: 53) +Port = 53 diff --git a/tests/testgenerate.py b/tests/testgenerate.py index 1210aa1..48acf57 100644 --- a/tests/testgenerate.py +++ b/tests/testgenerate.py @@ -38,6 +38,6 @@ def test_services(): with open(example) as stream: db.add(yaml.load(stream)) - generate_all(db, rootdir) + generate_all(db, rootdir, generate_keys=False) subprocess.check_call(["diff", "-ru", rootdir, "tests/output"])