Add pgp keyring management (#5)

This PR adds into the admin panel a front-end to manage PGP keys. Possibilities are many.
This commit is contained in:
David Duque 2020-10-04 16:35:59 +01:00 committed by GitHub
parent 03a1e57de6
commit 8519b7fc0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 634 additions and 4 deletions

View file

@ -580,6 +580,63 @@ def smtp_relay_set():
except Exception as e: except Exception as e:
return (str(e), 500) return (str(e), 500)
# PGP
@app.route('/system/pgp/', methods=["GET"])
@authorized_personnel_only
def get_keys():
from pgp import get_daemon_key, get_imported_keys, key_representation
return {
"daemon": key_representation(get_daemon_key()),
"imported": list(map(key_representation, get_imported_keys()))
}
@app.route('/system/pgp/<fpr>', methods=["GET"])
@authorized_personnel_only
def get_key(fpr):
from pgp import get_key, key_representation
k = get_key(fpr)
if k is None:
abort(404)
return key_representation(k)
@app.route('/system/pgp/<fpr>', methods=["DELETE"])
@authorized_personnel_only
def delete_key(fpr):
from pgp import delete_key
try:
if delete_key(fpr) is None:
abort(404)
return "OK"
except ValueError as e:
return (str(e), 400)
@app.route('/system/pgp/<fpr>/export', methods=["GET"])
@authorized_personnel_only
def export_key(fpr):
from pgp import export_key
exp = export_key(fpr)
if exp is None:
abort(404)
return exp
@app.route('/system/pgp/import', methods=["POST"])
@authorized_personnel_only
def import_key():
from pgp import import_key
k = request.form.get('key')
try:
result = import_key(k)
return {
"keys_read": result.considered,
"keys_added": result.imported,
"keys_unchanged": result.unchanged,
"uids_added": result.new_user_ids,
"sigs_added": result.new_signatures,
"revs_added": result.new_revocations
}
except ValueError as e:
return (str(e), 400)
# MUNIN # MUNIN

View file

@ -21,5 +21,8 @@ management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
# Provision any new certificates for new domains or domains with expiring certificates. # Provision any new certificates for new domains or domains with expiring certificates.
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result" management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
# Renew the daemon's PGP key if about to expire
management/pgp.py 2>&1 | management/email_administrator.py "PGP Key Renewal Result"
# Run status checks and email the administrator if anything changed. # Run status checks and email the administrator if anything changed.
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice" management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"

View file

@ -7,9 +7,12 @@ import sys
import html import html
import smtplib import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from pgp import create_signature
# In Python 3.6: # In Python 3.6:
#from email.message import Message #from email.message import Message
@ -45,6 +48,7 @@ content_html = "<html><body><pre>{}</pre></body></html>".format(html.escape(cont
msg.attach(MIMEText(content, 'plain')) msg.attach(MIMEText(content, 'plain'))
msg.attach(MIMEText(content_html, 'html')) msg.attach(MIMEText(content_html, 'html'))
msg.attach(MIMEApplication(create_signature(content.encode()), Name="signed.asc"))
# In Python 3.6: # In Python 3.6:
#msg.set_content(content) #msg.set_content(content)

125
management/pgp.py Executable file
View file

@ -0,0 +1,125 @@
#!/usr/local/lib/mailinabox/env/bin/python
# Tools to manipulate PGP keys
import gpg, utils, datetime
env = utils.load_environment()
# Import daemon's keyring - usually in /home/user-data/.gnupg/
gpghome = env['GNUPGHOME']
daemon_key_fpr = env['PGPKEY']
context = gpg.Context(armor=True, home_dir=gpghome)
# Global auxiliary lookup tables
crpyt_algos = {
0: "Unknown",
gpg.constants.PK_RSA: "RSA",
gpg.constants.PK_RSA_E: "RSA-E",
gpg.constants.PK_RSA_S: "RSA-S",
gpg.constants.PK_ELG_E: "ELG-E",
gpg.constants.PK_DSA: "DSA",
gpg.constants.PK_ECC: "ECC",
gpg.constants.PK_ELG: "ELG",
gpg.constants.PK_ECDSA: "ECDSA",
gpg.constants.PK_ECDH: "ECDH",
gpg.constants.PK_EDDSA: "EDDSA"
}
# Auxiliary function to process the key in order to be read more conveniently
def key_representation(key):
if key is None:
return None
key_rep = {
"master_fpr": key.fpr,
"revoked": key.revoked != 0,
"ids": [],
"subkeys": []
}
now = datetime.datetime.utcnow()
key_rep["ids"] = [ id.uid for id in key.uids ]
key_rep["subkeys"] = [{
"master": skey.fpr == key.fpr,
"sign": skey.can_sign == 1,
"cert": skey.can_certify == 1,
"encr": skey.can_encrypt == 1,
"auth": skey.can_authenticate == 1,
"fpr": skey.fpr,
"expires": skey.expires if skey.expires != 0 else None,
"expires_date": datetime.datetime.utcfromtimestamp(skey.expires).strftime("%x") if skey.expires != 0 else None,
"expires_days": (datetime.datetime.utcfromtimestamp(skey.expires) - now).days if skey.expires != 0 else None,
"expired": skey.expired == 1,
"algorithm": crpyt_algos[skey.pubkey_algo] if skey.pubkey_algo in crpyt_algos.keys() else crpyt_algos[0],
"bits": skey.length
} for skey in key.subkeys ]
return key_rep
# Tests an import as for whether we have any sort of private key material in our import
def contains_private_keys(imports):
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
with gpg.Context(home_dir=tmpdir, armor=True) as tmp:
result = tmp.key_import(imports)
return result.secret_read != 0
def get_key(fingerprint):
try:
return context.get_key(fingerprint, secret=False)
except KeyError:
return None
def get_daemon_key():
if daemon_key_fpr is None or daemon_key_fpr == "":
return None
return context.get_key(daemon_key_fpr, secret=True)
def get_imported_keys():
# All the keys in the keyring, except for the daemon's key
return list(
filter(
lambda k: k.fpr != daemon_key_fpr,
context.keylist(secret=False)
)
)
def import_key(key):
data = str.encode(key)
if contains_private_keys(data):
raise ValueError("Import cannot contain private keys!")
return context.key_import(data)
def export_key(fingerprint):
if get_key(fingerprint) is None:
return None
return context.key_export(pattern=fingerprint) # Key does exist, export it!
def delete_key(fingerprint):
key = get_key(fingerprint)
if fingerprint == daemon_key_fpr:
raise ValueError("You cannot delete the daemon's key!")
elif key is None:
return None
context.op_delete_ext(key, gpg.constants.DELETE_ALLOW_SECRET | gpg.constants.DELETE_FORCE)
return True
# Key usage
# Uses the daemon key to sign the provided message. If 'detached' is True, only the signature will be returned
def create_signature(data, detached=False):
signed_data, _ = context.sign(data, mode=gpg.constants.SIG_MODE_DETACH if detached else gpg.constants.SIG_MODE_CLEAR)
return signed_data
if __name__ == "__main__":
import sys, utils
# Check if we should renew the key
daemon_key = get_daemon_key()
exp = daemon_key.subkeys[0].expires
now = datetime.datetime.utcnow()
days_left = (datetime.datetime.utcfromtimestamp(exp) - now).days
if days_left > 14:
sys.exit(0)
else:
utils.shell("check_output", ["management/pgp_renew.sh"])

12
management/pgp_renew.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
# Renews the daemon's PGP key, if needed.
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
gpg --batch --command-fd=0 --edit-key "${PGPKEY-}" << EOF;
key 0
expire
180d
save
EOF

View file

@ -17,6 +17,7 @@ from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config,
from web_update import get_web_domains, get_domains_with_a_records from web_update import get_web_domains, get_domains_with_a_records
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
from mailconfig import get_mail_domains, get_mail_aliases from mailconfig import get_mail_domains, get_mail_aliases
from pgp import get_daemon_key, get_imported_keys
from utils import shell, sort_domains, load_env_vars_from_file, load_settings from utils import shell, sort_domains, load_env_vars_from_file, load_settings
@ -59,6 +60,7 @@ def run_checks(rounded_values, env, output, pool):
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True) shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
run_system_checks(rounded_values, env, output) run_system_checks(rounded_values, env, output)
run_pgp_checks(env, output)
# perform other checks asynchronously # perform other checks asynchronously
@ -266,6 +268,75 @@ def check_free_memory(rounded_values, env, output):
if rounded_values: memory_msg = "System free memory is below 10%." if rounded_values: memory_msg = "System free memory is below 10%."
output.print_error(memory_msg) output.print_error(memory_msg)
def run_pgp_checks(env, output):
now = datetime.datetime.utcnow()
output.add_heading("PGP Keyring")
# Check daemon key
k = None
sk = None
try:
k = get_daemon_key()
sk = k.subkeys[0]
except KeyError:
pass
if k is None:
output.print_error("The daemon's key does not exist!")
elif sk.expired == 1:
output.print_error(f"The daemon's key ({k.fpr}) expired.")
elif k.revoked == 1:
output.print_error(f"The daemon's key ({k.fpr}) has been revoked.")
else:
exp = datetime.datetime.utcfromtimestamp(sk.expires) # Our daemon key only has one subkey
if (exp - now).days < 10 and sk.expires != 0:
output.print_warning(f"The daemon's key ({k.fpr}) will expire soon, in {(exp - now).days} days on {exp.strftime('%x')}.")
else:
output.print_ok(f"The daemon's key ({k.fpr}) is good. It expires in {(exp - now).days} days on {exp.strftime('%x')}.")
# Check imported keys
keys = get_imported_keys()
if len(keys) == 0:
output.print_warning("There are no imported keys here.")
else:
about_to_expire = []
expired = []
revoked = []
for key in keys:
if key.revoked == 1:
revoked.append(key)
continue
else:
for skey in key.subkeys:
exp = datetime.datetime.utcfromtimestamp(skey.expires)
if skey.expired == 1:
expired.append((key, skey))
elif (exp - now).days < 10 and skey.expires != 0:
about_to_expire.append((key, skey))
all_good = True
def printpair(keytuple):
key, skey = keytuple
output.print_line(f"Key {key.fpr}, subkey {skey.keyid}")
if len(about_to_expire) != 0:
all_good = False
output.print_warning(f"There {'is 1 subkey' if len(about_to_expire) == 1 else f'are {len(about_to_expire)} subkeys'} about to expire.")
list(map(printpair, about_to_expire))
if len(expired) != 0:
all_good = False
output.print_error(f"There {'is 1 expired subkey' if len(expired) == 1 else f'are {len(expired)} expired subkeys'}.")
list(map(printpair, expired))
if len(revoked) != 0:
all_good = False
output.print_error(f"There {'is 1 revoked key' if len(revoked) == 1 else f'are {len(revoked)} revoked keys'}.")
list(map(lambda k: output.print_line(k.fpr), revoked))
if all_good:
output.print_ok("All imported keys are good.")
def run_network_checks(env, output): def run_network_checks(env, output):
# Also see setup/network-checks.sh. # Also see setup/network-checks.sh.

View file

@ -105,6 +105,8 @@
DNS</a></li> DNS</a></li>
<li class="dropdown-item"><a href="#external_dns" <li class="dropdown-item"><a href="#external_dns"
onclick="return show_panel(this);">External DNS</a></li> onclick="return show_panel(this);">External DNS</a></li>
<li class="dropdown-item"><a href="#pgp_keyring"
onclick="return show_panel(this);">PGP Keyring Management</a></li>
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li> <li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
</ul> </ul>
</li> </li>
@ -153,6 +155,10 @@
{% include "custom-dns.html" %} {% include "custom-dns.html" %}
</div> </div>
<div id="panel_pgp_keyring" class="admin_panel">
{% include "pgp-keyring.html" %}
</div>
<div id="panel_login" class="admin_panel"> <div id="panel_login" class="admin_panel">
{% include "login.html" %} {% include "login.html" %}
</div> </div>
@ -199,7 +205,7 @@
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle" <div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm" style="max-width: 600px;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="errorModalTitle"> </h4> <h4 class="modal-title" id="errorModalTitle"> </h4>

View file

@ -0,0 +1,279 @@
<style>
#pgp_keyring_config .status-error {
color: rgb(140, 0, 0);
}
#pgp_keyring_config .status-warning {
color: rgb(170, 120, 0);
}
#pgp_keyring_config .status-ok {
color: rgb(0, 140, 0);
}
#pgp_keyring_config .status-none {
color: rgb(190, 190, 190);
}
#pgp_keyring_config #uids {
white-space: pre-line;
}
</style>
<h2>PGP Keyring Management</h2>
<template id="pgpkey-template">
<tr>
<td>
<div id="trustlevel" style="font-size: 14pt;"><b>Trust Level:</b> Ultimate</div>
<code id="uids">
🤖 Power Mail-in-a-Box Management Daemon &lt;administrator@mailinabox.lan&gt;
</code>
<h3 style="font-size: 12pt;">Subkeys</h3>
<table id="subkeys">
<tr id="subkey-template">
<td id="ismaster">🔑</td>
<td>
<b>
<a id="sign">S</a>
<a id="cert">C</a>
<a id="encr">E</a>
<a id="auth">A</a>
</b>
</td>
<td style="width: 120pt;">
<b id="algo">RSA, 3072 bit</b>
</td>
<td>
<pre id="fpr">1756 6B81 D8A4 24C7 0098 659E 6872 2633 F692 52C6</pre>
</td>
<td id="expiration">
12/12/20 (119 days)
</td>
</tr>
</table>
</td>
<td id="options" style="width: 140pt;">
</td>
</tr>
</template>
<div id="pgp_keyring_config">
<h3>Daemon's Private Key</h3>
<table id="privatekey" class="table container">
</table>
<h3>Imported Public Keys</h3>
<table id="pubkeys" class="table container">
</table>
<h3>Import Key</h3>
<p>
You can upload your <b>public</b> key/keychain here. Keys <b>must</b> be submitted in ASCII-armored format.
<br>
If you're using <code>gpg</code>, you can export your public key by following this example:
<pre>
# Get all the keys in the ring
<b>$ gpg --list-keys</b>
/home/you/.gnupg/pubring.kbx
----------------------------
pub rsa4096 1970-01-01 [SC]
247C3553B4B36107BA0490C3CAFCCF3B4965761A
uid [ full ] Someone That I Used to Know &lt;someone@example.com&gt;
sub rsa2048 2020-01-01 [E] [expires: 2021-01-01]
pub rsa4096 2020-05-24 [SC] [expires: 2021-02-12]
52661092E5CD9EEFD7796B19E85F540C9318B69F
uid [ultimate] Me, Myself and I &lt;me@mydomain.com&gt;
sub rsa2048 2020-05-24 [E] [expires: 2021-02-12]
# Say that we want to export our own key, this is - "Me, Myself and I". That's the fingerprint "52661..."
<b>$ gpg --export --armor 52661092E5CD9EEFD7796B19E85F540C9318B69F</b> # Replace with your key's fingerprint
<b>-----BEGIN PGP PUBLIC KEY BLOCK-----
copy and paste this block in the area below
-----END PGP PUBLIC KEY BLOCK-----</b>
</pre>
</p>
<p><textarea id="pgp_paste_key" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#xA;stuff here&#xA;-----END PGP PUBLIC KEY BLOCK-----"></textarea></p>
<button class="btn btn-primary" onclick="importkey()">Import Key</button>
</div>
<script>
function pretty_fpr(fpr) {
let pfpr = ""
for (let n = 0; n < 2; ++n) {
for (let i = 0; i < 5; ++i) {
pfpr += `${fpr.substring(n * 20 + i * 4, n * 20 + (i + 1) * 4)} `
}
pfpr += " "
}
return pfpr.substring(0, pfpr.length - 2)
}
function key_html(key, darken_bg, daemon) {
let keyrep = $("#pgpkey-template").html()
keyrep = $(keyrep)
keyrep.attr("id", key.master_fpr)
// Main key config
if (darken_bg) {
keyrep.addClass("bg-light")
}
const tlevel = keyrep.find("#trustlevel")
if (key.revoked) {
tlevel.html("<b class='status-error'>This key was revoked by it's owner.</b>")
} else {
tlevel.html("<b>Key is not revoked.</b>")
}
let uidtxt = ""
if (daemon) {
key.ids.forEach(id => {
uidtxt += "🤖 " + id + "\n"
});
} else {
key.ids.forEach(id => {
uidtxt += "🕵 " + id + "\n"
});
}
keyrep.find("#uids").text(uidtxt.substring(0, uidtxt.length - 1))
// Subkeys
const keyflags = ["sign", "cert", "encr", "auth"]
let subkeys = keyrep.find("#subkeys")
let subkeytemplate = subkeys.html()
keyrep.find("#subkey-template").remove()
key.subkeys.forEach(skey => {
let skeyrep = $(subkeytemplate)
skeyrep.attr("id", `sub${skey.fpr}`)
// Master key?
if (skey.master) {
skeyrep.find("#ismaster").html("🔑")
} else {
skeyrep.find("#ismaster").html("")
}
// Usage flags
keyflags.forEach(flag => {
if (!skey[flag]) {
skeyrep.find(`#${flag}`).addClass("status-none")
}
});
// Algorithm and fingerprint
skeyrep.find("#algo").html(`${skey.algorithm}, ${skey.bits} bits`)
skeyrep.find("#fpr").html(pretty_fpr(skey.fpr))
let expiration = skeyrep.find("#expiration")
// Expiration
if (key.revoked) {
skeyrep.addClass("status-error")
expiration.html(`Revoked`)
} else if (skey.expired) {
skeyrep.addClass("status-error")
expiration.html(`${skey.expires_date} (expired)`)
} else if (skey.expires && skey.expires_days <= 14) {
skeyrep.addClass("status-warning")
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
} else if (skey.expires) {
skeyrep.addClass("status-ok")
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
} else {
skeyrep.addClass("status-ok")
expiration.html("Does not expire")
}
skeyrep.appendTo(subkeys)
});
// Options
if (daemon) {
keyrep.find("#options").html(`<button class="btn btn-primary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button>`)
} else {
keyrep.find("#options").html(`<button class="btn btn-secondary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button><button class="btn btn-danger btn-block" onclick="rmkey('${key.master_fpr}')">Remove Key</button>`)
}
return keyrep
}
function show_pgp_keyring() {
$('#privatekey').html("<tr><td class='text-muted'>Loading...</td></tr>")
$('#pubkeys').html("<tr><td class='text-muted'>Loading...</td></tr>")
api(
"/system/pgp/",
"GET",
{},
function(r) {
$('#privatekey').html("")
$('#pubkeys').html("")
key_html(r.daemon, true, true).appendTo("#privatekey")
let pendulum = 1
r.imported.forEach(k => {
key_html(k, pendulum > 0, false).appendTo("#pubkeys")
pendulum *= -1
});
}
)
}
function exportkey(fpr) {
api(
`/system/pgp/${fpr}/export`,
"GET",
{},
function(r) {
show_modal_error("PGP Key", `Key export for <b>${fpr}</b>:<br><br><pre>${r}</pre>`)
},
function(_ ,xhr) {
if (xhr.status == 404) {
show_modal_error("Error", `The key you asked for (<b>${fpr}</b>) does not exist!`)
} else {
// Fallback to the default error modal
show_modal_error("Error", "Something went wrong, sorry.")
}
}
)
}
function rmkey(fpr) {
show_modal_confirm("Delete key", `Are you sure you wish to remove the key with the fingerprint ${pretty_fpr(fpr)}?`, "Yes, remove it", () => {
api(
`/system/pgp/${fpr}`,
"DELETE",
{},
function(r) {
show_modal_error("Delete key", r, show_pgp_keyring)
},
function(r) {
show_modal_error("Key deletion error", r)
}
)
}, ()=>{})
}
function importkey() {
api(
"/system/pgp/import",
"POST",
{
key: $("#pgp_paste_key").val()
},
function(r) {
show_modal_error("Import Results", `<ul>
<li><b>Keys read:</b> ${r.keys_read}</li>
<li><b>Keys added:</b> ${r.keys_added}</li>
<li><b>Keys not changed:</b> ${r.keys_unchanged}</li>
<li><b>User id's added:</b> ${r.uids_added}</li>
<li><b>Signatures added:</b> ${r.sigs_added}</li>
<li><b>Revocations added:</b> ${r.revs_added}</li>
</ul>`, show_pgp_keyring)
},
function(r) {
show_modal_error("Import Error", r)
}
)
}
</script>

View file

@ -225,3 +225,7 @@ function git_clone {
function php_version { function php_version {
php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3 php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3
} }
function python_version {
python3 --version | cut -d " " -f 2 | cut -c 1-3
}

View file

@ -29,7 +29,7 @@ done
# #
# certbot installs EFF's certbot which we use to # certbot installs EFF's certbot which we use to
# provision free TLS certificates. # provision free TLS certificates.
apt_install duplicity python3-pip virtualenv certbot apt_install duplicity python3-pip python3-gpg virtualenv certbot
hide_output pip3 install --upgrade boto hide_output pip3 install --upgrade boto
# Create a virtualenv for the installation of Python 3 packages # Create a virtualenv for the installation of Python 3 packages
@ -52,6 +52,11 @@ hide_output $venv/bin/pip install --upgrade \
flask dnspython python-dateutil \ flask dnspython python-dateutil \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver "idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
# Make the venv use the packaged gpgme bindings (the ones pip provides are severely out-of-date)
if [ ! -d $venv/lib/python$(python_version)/site-packages/gpg/ ]; then
ln -s /usr/lib/python3/dist-packages/gpg/ $venv/lib/python$(python_version)/site-packages/
fi
# CONFIGURATION # CONFIGURATION
# Create a backup directory and a random key for encrypting backups. # Create a backup directory and a random key for encrypting backups.

32
setup/pgp.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Daemon PGP Keyring
# ------------------
#
# Initializes the PGP keyring at /home/user-data/.gnupg
# For this, we will generate a new PGP keypair (if one isn't already present)
source setup/functions.sh # load our functions
source /etc/mailinabox.conf # load global vars
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
# Install gnupg
apt_install gnupg
if [ "$(gpg --list-secret-keys 2> /dev/null)" = "" -o "${PGPKEY-}" = "" ]; then
echo "No keypair found. Generating daemon's PGP keypair..."
gpg --generate-key --batch << EOF;
%no-protection
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign,encrypt,auth
Name-Real: Power Mail-in-a-Box Management Daemon
Name-Email: administrator@${PRIMARY_HOSTNAME}
Expire-Date: 180d
%commit
EOF
chown -R root:root $GNUPGHOME
# Remove the old key fingerprint if it exists, and add the new one
echo "$(cat /etc/mailinabox.conf | grep -v "PGPKEY")" > /etc/mailinabox.conf
echo "PGPKEY=$(gpg --list-secret-keys --with-colons | grep fpr | head -n 1 | sed 's/fpr//g' | sed 's/://g')" >> /etc/mailinabox.conf
fi

View file

@ -100,12 +100,15 @@ PUBLIC_IPV6=$PUBLIC_IPV6
PRIVATE_IP=$PRIVATE_IP PRIVATE_IP=$PRIVATE_IP
PRIVATE_IPV6=$PRIVATE_IPV6 PRIVATE_IPV6=$PRIVATE_IPV6
MTA_STS_MODE=${MTA_STS_MODE-} MTA_STS_MODE=${MTA_STS_MODE-}
GNUPGHOME=${STORAGE_ROOT}/.gnupg/
PGPKEY=${DEFAULT_PGPKEY-}
EOF EOF
# Start service configuration. # Start service configuration.
source setup/system.sh source setup/system.sh
source setup/ssl.sh source setup/ssl.sh
source setup/dns.sh source setup/dns.sh
source setup/pgp.sh
source setup/mail-postfix.sh source setup/mail-postfix.sh
source setup/mail-dovecot.sh source setup/mail-dovecot.sh
source setup/mail-users.sh source setup/mail-users.sh

View file

@ -23,7 +23,7 @@ echo "Installing Roundcube (webmail)..."
apt_install \ apt_install \
dbconfig-common \ dbconfig-common \
php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \ php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring php-gnupg
# Install Roundcube from source if it is not already present or if it is out of date. # Install Roundcube from source if it is not already present or if it is out of date.
# Combine the Roundcube version number with the commit hash of plugins to track # Combine the Roundcube version number with the commit hash of plugins to track
@ -126,7 +126,7 @@ cat > $RCM_CONFIG <<EOF;
\$config['support_url'] = 'https://mailinabox.email/'; \$config['support_url'] = 'https://mailinabox.email/';
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail'; \$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
\$config['des_key'] = '$SECRET_KEY'; \$config['des_key'] = '$SECRET_KEY';
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav'); \$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav', 'enigma');
\$config['skin'] = 'elastic'; \$config['skin'] = 'elastic';
\$config['login_autocomplete'] = 2; \$config['login_autocomplete'] = 2;
\$config['password_charset'] = 'UTF-8'; \$config['password_charset'] = 'UTF-8';
@ -134,6 +134,35 @@ cat > $RCM_CONFIG <<EOF;
?> ?>
EOF EOF
mkdir -p ${STORAGE_ROOT}/userkeys/
chmod 700 ${STORAGE_ROOT}/userkeys/
chown www-data:www-data ${STORAGE_ROOT}/userkeys/
# Configure Enigma
cat > ${RCM_PLUGIN_DIR}/enigma/config.inc.php <<EOF;
<?php
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
\$config['enigma_pgp_driver'] = 'gnupg';
\$config['enigma_smime_driver'] = 'phpssl';
\$config['enigma_debug'] = false;
\$config['enigma_pgp_homedir'] = '${STORAGE_ROOT}/.enigma/';
\$config['enigma_pgp_binary'] = '';
\$config['enigma_pgp_agent'] = '';
\$config['enigma_pgp_gpgconf'] = '';
\$config['enigma_pgp_cipher_algo'] = null;
\$config['enigma_pgp_digest_algo'] = null;
\$config['enigma_multihost'] = false;
\$config['enigma_signatures'] = true;
\$config['enigma_decryption'] = true;
\$config['enigma_encryption'] = true;
\$config['enigma_sign_all'] = false;
\$config['enigma_encrypt_all'] = false;
\$config['enigma_attach_pubkey'] = false;
\$config['enigma_password_time'] = 5;
\$config['enigma_options_lock'] = array();
?>
EOF
# Configure CardDav # Configure CardDav
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF; cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
<?php <?php