Handle DKIM key generation and storage
This commit is contained in:
@@ -20,7 +20,9 @@ default_config = {
|
||||
'HOSTNAME': 'mail.freeposte.io',
|
||||
'DOMAIN': 'freeposte.io',
|
||||
'POSTMASTER': 'postmaster',
|
||||
'DEBUG': False
|
||||
'DEBUG': False,
|
||||
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
|
||||
'DKIM_SELECTOR': 'dkim'
|
||||
}
|
||||
|
||||
# Load configuration from the environment if available
|
||||
|
||||
21
admin/freeposte/admin/dkim.py
Normal file
21
admin/freeposte/admin/dkim.py
Normal file
@@ -0,0 +1,21 @@
|
||||
""" No crypto operation is done on keys.
|
||||
They are thus represented as ASCII armored PEM.
|
||||
"""
|
||||
|
||||
from OpenSSL import crypto
|
||||
|
||||
|
||||
def gen_key(key_type=crypto.TYPE_RSA, bits=1024):
|
||||
""" Generate and return a new RSA key.
|
||||
"""
|
||||
key = crypto.PKey()
|
||||
key.generate_key(key_type, bits)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM, key)
|
||||
|
||||
|
||||
def strip_key(pem):
|
||||
""" Return only the b64 part of the ASCII armored PEM.
|
||||
"""
|
||||
key = crypto.load_privatekey(crypto.FILETYPE_PEM, pem)
|
||||
public_pem = crypto.dump_publickey(crypto.FILETYPE_PEM, key)
|
||||
return public_pem.replace(b"\n", b"").split(b"-----")[2]
|
||||
@@ -1,10 +1,14 @@
|
||||
from freeposte.admin import db
|
||||
from freeposte.admin import db, dkim
|
||||
from freeposte import app
|
||||
|
||||
from sqlalchemy.ext import declarative
|
||||
from passlib import context
|
||||
from datetime import datetime
|
||||
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
# Many-to-many association table for domain managers
|
||||
@@ -34,6 +38,28 @@ class Domain(Base):
|
||||
max_users = db.Column(db.Integer, nullable=False, default=0)
|
||||
max_aliases = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
@property
|
||||
def dkim_key(self):
|
||||
file_path = app.config["DKIM_PATH"].format(
|
||||
domain=self.name, selector=app.config["DKIM_SELECTOR"])
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, "rb") as handle:
|
||||
return handle.read()
|
||||
|
||||
@dkim_key.setter
|
||||
def dkim_key(self, value):
|
||||
file_path = app.config["DKIM_PATH"].format(
|
||||
domain=self.name, selector=app.config["DKIM_SELECTOR"])
|
||||
with open(file_path, "wb") as handle:
|
||||
handle.write(value)
|
||||
|
||||
@property
|
||||
def dkim_publickey(self):
|
||||
return dkim.strip_key(self.dkim_key).decode("utf8")
|
||||
|
||||
def generate_dkim_key(self):
|
||||
self.dkim_key = dkim.gen_key()
|
||||
|
||||
def has_email(self, localpart):
|
||||
for email in self.users + self.aliases:
|
||||
if email.localpart == localpart:
|
||||
|
||||
@@ -10,7 +10,7 @@ Domain details
|
||||
|
||||
{% block main_action %}
|
||||
{% if current_user.global_admin %}
|
||||
<a class="btn btn-primary" href="#">Regenerate keys</a>
|
||||
<a class="btn btn-primary" href="{{ url_for(".domain_genkeys", domain_name=domain.name) }}">Regenerate keys</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,8 +26,18 @@ Domain details
|
||||
<td><pre>{{ domain.name }}. 600 IN MX 10 {{ config["HOSTNAME"] }}.</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DNS SPF entry</th>
|
||||
<td><pre>{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"</pre></td>
|
||||
<th>DNS SPF entries</th>
|
||||
<td><pre>
|
||||
{{ domain.name }}. 600 IN TXT "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"
|
||||
{{ domain.name }}. 600 IN SPF "v=spf1 mx a:{{ config["HOSTNAME"] }} -all"</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DKIM public key</th>
|
||||
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ domain.dkim_publickey }}</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DNS DKIM entry</th>
|
||||
<td><pre style="white-space: pre-wrap; word-wrap: break-word;">{{ config["DKIM_SELECTOR"] }}._domainkey IN 600 TXT "v=DKIM1; k=rsa; p={{ domain.dkim_publickey }}"</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DNS DMARC entry</th>
|
||||
|
||||
@@ -63,3 +63,11 @@ def domain_details(domain_name):
|
||||
domain = utils.get_domain_admin(domain_name)
|
||||
return flask.render_template('domain/details.html', domain=domain,
|
||||
config=flask_app.config)
|
||||
|
||||
|
||||
@app.route('/domain/genkeys/<domain_name>', methods=['GET'])
|
||||
def domain_genkeys(domain_name):
|
||||
domain = utils.get_domain_admin(domain_name)
|
||||
domain.generate_dkim_key()
|
||||
return flask.redirect(
|
||||
flask.url_for(".domain_details", domain_name=domain_name))
|
||||
|
||||
@@ -6,6 +6,7 @@ Flask-migrate
|
||||
Flask-script
|
||||
flask_wtf
|
||||
WTForms-Components
|
||||
PyOpenSSL
|
||||
passlib
|
||||
gunicorn
|
||||
docker-py
|
||||
|
||||
@@ -55,6 +55,7 @@ services:
|
||||
env_file: freeposte.env
|
||||
volumes:
|
||||
- /freeposte/filter:/data
|
||||
- /freeposte/dkim:/dkim
|
||||
|
||||
antispam:
|
||||
build: rspamd
|
||||
@@ -79,6 +80,7 @@ services:
|
||||
env_file: freeposte.env
|
||||
volumes:
|
||||
- /freeposte/freeposte:/data
|
||||
- /freeposte/dkim:/dkim
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
webmail:
|
||||
|
||||
Reference in New Issue
Block a user