Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions brutus/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import os
import jinja2
import yaml
import base64
import subprocess
import tempfile

from . import utils

Expand All @@ -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)
Expand Down Expand Up @@ -171,19 +175,89 @@ 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:
output = template.render(variables)
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()
3 changes: 3 additions & 0 deletions examples/instance.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
catalog: instances
id: ~
admin_mail: admin@example.net
services:
mailserver:
enabled: yes
Expand All @@ -8,3 +9,5 @@ services:
enabled: yes
dnsserver:
enabled: yes
certificates:
enabled: yes
26 changes: 22 additions & 4 deletions templates/knot/knot.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 %}
27 changes: 27 additions & 0 deletions templates/letsencrypt/le.ini.j2
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion tests/output/knot/etc/knot/knot.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,4 +39,7 @@ template:
dnssec-signing: off
semantic-checks: on

zone: - domain: example.net template: default
zone:
- domain: example.net
template: default

27 changes: 27 additions & 0 deletions tests/output/letsencrypt/etc/letsencrypt/example.net.ini
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion tests/testgenerate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])