adding a really slick ssl certificate installation form in the control panel
This commit is contained in:
parent
5130b279d8
commit
17331e7d82
|
@ -226,6 +226,24 @@ def dns_get_dump():
|
||||||
from dns_update import build_recommended_dns
|
from dns_update import build_recommended_dns
|
||||||
return json_response(build_recommended_dns(env))
|
return json_response(build_recommended_dns(env))
|
||||||
|
|
||||||
|
# SSL
|
||||||
|
|
||||||
|
@app.route('/ssl/csr/<domain>', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_get_csr(domain):
|
||||||
|
from web_update import get_domain_ssl_files, create_csr
|
||||||
|
ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env)
|
||||||
|
return create_csr(domain, ssl_key, env)
|
||||||
|
|
||||||
|
@app.route('/ssl/install', methods=['POST'])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def ssl_install_cert():
|
||||||
|
from web_update import install_cert
|
||||||
|
domain = request.form.get('domain')
|
||||||
|
ssl_cert = request.form.get('cert')
|
||||||
|
ssl_chain = request.form.get('chain')
|
||||||
|
return install_cert(domain, ssl_cert, ssl_chain, env)
|
||||||
|
|
||||||
# WEB
|
# WEB
|
||||||
|
|
||||||
@app.route('/web/domains')
|
@app.route('/web/domains')
|
||||||
|
|
|
@ -381,23 +381,16 @@ def check_ssl_cert(domain, env):
|
||||||
if domain == env['PRIMARY_HOSTNAME']:
|
if domain == env['PRIMARY_HOSTNAME']:
|
||||||
env['out'].print_error("""The SSL certificate for this domain is currently self-signed. You will get a security
|
env['out'].print_error("""The SSL certificate for this domain is currently self-signed. You will get a security
|
||||||
warning when you check or send email and when visiting this domain in a web browser (for webmail or
|
warning when you check or send email and when visiting this domain in a web browser (for webmail or
|
||||||
static site hosting). You may choose to confirm the security exception, but check that the certificate
|
static site hosting). Use the SSL Certificates page in this control panel to install a signed SSL certificate.
|
||||||
fingerprint matches the following:""")
|
You may choose to leave the self-signed certificate in place and confirm the security exception, but check that
|
||||||
|
the certificate fingerprint matches the following:""")
|
||||||
env['out'].print_line("")
|
env['out'].print_line("")
|
||||||
env['out'].print_line(" " + fingerprint, monospace=True)
|
env['out'].print_line(" " + fingerprint, monospace=True)
|
||||||
else:
|
else:
|
||||||
env['out'].print_warning("""The SSL certificate for this domain is currently self-signed. Visitors to a website on
|
env['out'].print_warning("""The SSL certificate for this domain is currently self-signed. Visitors to a website on
|
||||||
this domain will get a security warning. If you are not serving a website on this domain, then it is
|
this domain will get a security warning. If you are not serving a website on this domain, then it is
|
||||||
safe to leave the self-signed certificate in place.""")
|
safe to leave the self-signed certificate in place. Use the SSL Certificates page in this control panel to
|
||||||
env['out'].print_line("")
|
install a signed SSL certificate.""")
|
||||||
env['out'].print_line("""You can purchase a signed certificate from many places. You will need to provide this Certificate Signing Request (CSR)
|
|
||||||
to whoever you purchase the SSL certificate from:""")
|
|
||||||
env['out'].print_line("")
|
|
||||||
env['out'].print_line(open(ssl_csr_path).read().strip(), monospace=True)
|
|
||||||
env['out'].print_line("")
|
|
||||||
env['out'].print_line("""When you purchase an SSL certificate you will receive a certificate in PEM format and possibly a file containing intermediate certificates in PEM format.
|
|
||||||
If you receive intermediate certificates, use a text editor and paste your certificate on top and then the intermediate certificates
|
|
||||||
below it. Save the file and place it onto this machine at %s. Then run "service nginx restart".""" % ssl_certificate)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
env['out'].print_error("The SSL certificate has a problem: " + cert_status)
|
env['out'].print_error("The SSL certificate has a problem: " + cert_status)
|
||||||
|
@ -423,7 +416,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key):
|
||||||
# More information was probably written to stderr (which we aren't capturing),
|
# More information was probably written to stderr (which we aren't capturing),
|
||||||
# but it is probably not helpful to the user anyway.
|
# but it is probably not helpful to the user anyway.
|
||||||
if retcode != 0:
|
if retcode != 0:
|
||||||
return ("The SSL certificate file at %s appears to be corrupted or not a PEM-formatted SSL certificate file." % ssl_certificate, None)
|
return ("The SSL certificate appears to be corrupted or not a PEM-formatted SSL certificate file. (%s)" % ssl_certificate, None)
|
||||||
|
|
||||||
cert_dump = cert_dump.split("\n")
|
cert_dump = cert_dump.split("\n")
|
||||||
certificate_names = set()
|
certificate_names = set()
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">System <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
<li><a href="#system_status" onclick="return show_panel(this);">Status Checks</a></li>
|
||||||
|
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
||||||
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
<li class="dropdown-header">Super Advanced Options</li>
|
<li class="dropdown-header">Super Advanced Options</li>
|
||||||
|
@ -155,6 +156,10 @@
|
||||||
{% include "web.html" %}
|
{% include "web.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="panel_ssl" class="container panel">
|
||||||
|
{% include "ssl.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h2>SSL Certificates</h2>
|
||||||
|
|
||||||
|
<h3>Certificate Status</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<table id="ssl_domains" class="table" style="margin-bottom: 2em; width: auto;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Domain</th>
|
||||||
|
<th>Certificate Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 id="ssl_install_header">Install SSL Certificate</h3>
|
||||||
|
|
||||||
|
<p>There are many places where you can get a free or cheap SSL certificate. We recommend <a href="https://www.namecheap.com/cart/remove.aspx?itemid=47016639&i=i2">Namecheap’s $9 certificate</a> or <a href="https://www.startssl.com/">StartSSL’s free express lane</a>.</p>
|
||||||
|
|
||||||
|
<p>Which domain are you getting an SSL certificate for?</p>
|
||||||
|
|
||||||
|
<p><select id="ssldomain" onchange="show_csr()" class="form-control" style="width: auto"></select></p>
|
||||||
|
|
||||||
|
<div id="csr_info" style="display: none">
|
||||||
|
<p>You will need to provide the SSL certificate provider this Certificate Signing Request (CSR):</p>
|
||||||
|
|
||||||
|
<pre id="ssl_csr"></pre>
|
||||||
|
|
||||||
|
<p><small>The CSR is safe to share. It can only be used in combination with a secret key stored on this machine.</small></p>
|
||||||
|
|
||||||
|
<p>The SSL certificate provider will then provide you with an SSL certificate. They may also provide you with an intermediate chain. Paste each separately into the boxes below:</p>
|
||||||
|
|
||||||
|
<p style="margin-bottom: .5em">SSL certificate:</p>
|
||||||
|
<p><textarea id="ssl_paste_cert" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
|
<p style="margin-bottom: .5em">SSL intermediate chain (if provided):</p>
|
||||||
|
<p><textarea id="ssl_paste_chain" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN CERTIFICATE-----
stuff here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
more stuff here
-----END CERTIFICATE-----"></textarea></p>
|
||||||
|
|
||||||
|
<p>After you paste in the information, click the install button.</p>
|
||||||
|
|
||||||
|
<button class="btn-primary" onclick="install_cert()">Install</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function show_ssl() {
|
||||||
|
api(
|
||||||
|
"/web/domains",
|
||||||
|
"GET",
|
||||||
|
{
|
||||||
|
},
|
||||||
|
function(domains) {
|
||||||
|
var tb = $('#ssl_domains tbody');
|
||||||
|
tb.text('');
|
||||||
|
$('#ssldomain').html('<option value="">(select)</option>');
|
||||||
|
|
||||||
|
for (var i = 0; i < domains.length; i++) {
|
||||||
|
var row = $("<tr><th class='domain'><a href=''></a></th><td class='status'></td> <td class='actions'><a href='#' onclick='return ssl_install(this);' class='btn btn-xs'>Install Certificate</a></td></tr>");
|
||||||
|
tb.append(row);
|
||||||
|
row.attr('data-domain', domains[i].domain);
|
||||||
|
row.find('.domain a').text(domains[i].domain);
|
||||||
|
row.find('.domain a').attr('href', 'https://' + domains[i].domain);
|
||||||
|
row.addClass("text-" + domains[i].ssl_certificate[0]);
|
||||||
|
row.find('.status').text(domains[i].ssl_certificate[1]);
|
||||||
|
if (domains[i].ssl_certificate[0] == "success") {
|
||||||
|
row.find('.actions a').addClass('btn-default').text('Replace Certificate');
|
||||||
|
} else {
|
||||||
|
row.find('.actions a').addClass('btn-primary').text('Install Certificate');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#ssldomain').append($('<option>').text(domains[i].domain));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ssl_install(elem) {
|
||||||
|
var domain = $(elem).parents('tr').attr('data-domain');
|
||||||
|
$('#ssldomain').val(domain);
|
||||||
|
$('#csr_info').slideDown();
|
||||||
|
$('#ssl_csr').text('Loading...');
|
||||||
|
show_csr();
|
||||||
|
$('html, body').animate({ scrollTop: $('#ssl_install_header').offset().top })
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_csr() {
|
||||||
|
api(
|
||||||
|
"/ssl/csr/" + $('#ssldomain').val(),
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
},
|
||||||
|
function(data) {
|
||||||
|
$('#ssl_csr').text(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function install_cert() {
|
||||||
|
api(
|
||||||
|
"/ssl/install",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
domain: $('#ssldomain').val(),
|
||||||
|
cert: $('#ssl_paste_cert').val(),
|
||||||
|
chain: $('#ssl_paste_chain').val()
|
||||||
|
},
|
||||||
|
function(status) {
|
||||||
|
if (status == "") {
|
||||||
|
show_modal_error("SSL Certificate Installation", "Certificate has been installed. Check that you have no connection problems to the domain.", function() { show_ssl(); $('#csr_info').slideUp(); });
|
||||||
|
} else {
|
||||||
|
show_modal_error("SSL Certificate Installation", status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -38,7 +38,7 @@ def get_web_domains(env):
|
||||||
|
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
def do_web_update(env):
|
def do_web_update(env, ok_status="web updated\n"):
|
||||||
# Build an nginx configuration file.
|
# Build an nginx configuration file.
|
||||||
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ def do_web_update(env):
|
||||||
# enough and doesn't break any open connections.
|
# enough and doesn't break any open connections.
|
||||||
shell('check_call', ["/usr/sbin/service", "nginx", "reload"])
|
shell('check_call', ["/usr/sbin/service", "nginx", "reload"])
|
||||||
|
|
||||||
return "web updated\n"
|
return ok_status
|
||||||
|
|
||||||
def make_domain_config(domain, template, template_for_primaryhost, env):
|
def make_domain_config(domain, template, template_for_primaryhost, env):
|
||||||
# How will we configure this domain.
|
# How will we configure this domain.
|
||||||
|
@ -94,6 +94,19 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
||||||
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
|
nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key)
|
||||||
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
||||||
|
|
||||||
|
# Because the certificate may change, we should recognize this so we
|
||||||
|
# can trigger an nginx update.
|
||||||
|
def hashfile(filepath):
|
||||||
|
import hashlib
|
||||||
|
sha1 = hashlib.sha1()
|
||||||
|
f = open(filepath, 'rb')
|
||||||
|
try:
|
||||||
|
sha1.update(f.read())
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
return sha1.hexdigest()
|
||||||
|
nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
||||||
|
|
||||||
# Add in any user customizations in YAML format.
|
# Add in any user customizations in YAML format.
|
||||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||||
if os.path.exists(nginx_conf_custom_fn):
|
if os.path.exists(nginx_conf_custom_fn):
|
||||||
|
@ -178,12 +191,8 @@ def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, en
|
||||||
# Generate a new self-signed certificate using the same private key that we already have.
|
# Generate a new self-signed certificate using the same private key that we already have.
|
||||||
|
|
||||||
# Start with a CSR.
|
# Start with a CSR.
|
||||||
shell("check_call", [
|
with open(csr_path, "w") as f:
|
||||||
"openssl", "req", "-new",
|
f.write(create_csr(domain, ssl_key, env))
|
||||||
"-key", ssl_key,
|
|
||||||
"-out", csr_path,
|
|
||||||
"-sha256",
|
|
||||||
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
|
||||||
|
|
||||||
# And then make the certificate.
|
# And then make the certificate.
|
||||||
shell("check_call", [
|
shell("check_call", [
|
||||||
|
@ -193,12 +202,62 @@ def ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, csr_path, en
|
||||||
"-signkey", ssl_key,
|
"-signkey", ssl_key,
|
||||||
"-out", ssl_certificate])
|
"-out", ssl_certificate])
|
||||||
|
|
||||||
|
def create_csr(domain, ssl_key, env):
|
||||||
|
return shell("check_output", [
|
||||||
|
"openssl", "req", "-new",
|
||||||
|
"-key", ssl_key,
|
||||||
|
"-out", "/dev/stdout",
|
||||||
|
"-sha256",
|
||||||
|
"-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)])
|
||||||
|
|
||||||
|
def install_cert(domain, ssl_cert, ssl_chain, env):
|
||||||
|
if domain not in get_web_domains(env):
|
||||||
|
return "Invalid domain name."
|
||||||
|
|
||||||
|
# Write the combined cert+chain to a temporary path and validate that it is OK.
|
||||||
|
# The certificate always goes above the chain.
|
||||||
|
import tempfile, os
|
||||||
|
fd, fn = tempfile.mkstemp('.pem')
|
||||||
|
os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii"))
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
# Do validation on the certificate before installing it.
|
||||||
|
from status_checks import check_certificate
|
||||||
|
ssl_key, ssl_certificate, ssl_csr_path = get_domain_ssl_files(domain, env)
|
||||||
|
cert_status, cert_status_details = check_certificate(domain, fn, ssl_key)
|
||||||
|
if cert_status != "OK":
|
||||||
|
if cert_status == "SELF-SIGNED":
|
||||||
|
cert_status = "This is a self-signed certificate. I can't install that."
|
||||||
|
os.unlink(fn)
|
||||||
|
return cert_status
|
||||||
|
|
||||||
|
# Copy the certificate to its expected location.
|
||||||
|
os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True)
|
||||||
|
os.rename(fn, ssl_certificate)
|
||||||
|
|
||||||
|
# Kick nginx so it sees the cert.
|
||||||
|
return do_web_update(env, ok_status="")
|
||||||
|
|
||||||
def get_web_domains_info(env):
|
def get_web_domains_info(env):
|
||||||
|
def check_cert(domain):
|
||||||
|
from status_checks import check_certificate
|
||||||
|
ssl_key, ssl_certificate, ssl_csr_path = get_domain_ssl_files(domain, env)
|
||||||
|
if not os.path.exists(ssl_certificate):
|
||||||
|
return ("danger", "No Certificate Installed")
|
||||||
|
cert_status, cert_status_details = check_certificate(domain, ssl_certificate, ssl_key)
|
||||||
|
if cert_status == "OK":
|
||||||
|
return ("success", "Signed & valid. " + cert_status_details)
|
||||||
|
elif cert_status == "SELF-SIGNED":
|
||||||
|
return ("warning", "Self-signed. Get a signed certificate to stop warnings.")
|
||||||
|
else:
|
||||||
|
return ("danger", "Certificate has a problem: " + cert_status)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
"root": get_web_root(domain, env),
|
"root": get_web_root(domain, env),
|
||||||
"custom_root": get_web_root(domain, env, test_exists=False),
|
"custom_root": get_web_root(domain, env, test_exists=False),
|
||||||
|
"ssl_certificate": check_cert(domain),
|
||||||
}
|
}
|
||||||
for domain in get_web_domains(env)
|
for domain in get_web_domains(env)
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue