new tool to purchase and install a SSL certificate using Gandi.net's API
This commit is contained in:
parent
30c416ff6e
commit
d4ce50de86
|
@ -0,0 +1,155 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
# Helps you purchase a SSL certificate from Gandi.net using
|
||||||
|
# their API.
|
||||||
|
#
|
||||||
|
# Before you begin:
|
||||||
|
# 1) Create an account on Gandi.net.
|
||||||
|
# 2) Pre-pay $16 into your account at https://www.gandi.net/prepaid/operations. Wait until the payment goes through.
|
||||||
|
# 3) Activate your API key first on the test platform (wait a while, refresh the page) and then activate the production API at https://www.gandi.net/admin/api_key.
|
||||||
|
|
||||||
|
import sys, re, os.path, urllib.request
|
||||||
|
import xmlrpc.client
|
||||||
|
import rtyaml
|
||||||
|
|
||||||
|
from utils import load_environment, shell
|
||||||
|
from web_update import get_web_domains, get_domain_ssl_files, get_web_root
|
||||||
|
from whats_next import check_certificate
|
||||||
|
|
||||||
|
def buy_ssl_certificate(api_key, domain, command, env):
|
||||||
|
if domain != env['PUBLIC_HOSTNAME'] \
|
||||||
|
and domain not in get_web_domains(env):
|
||||||
|
raise ValueError("Domain is not %s or a domain we're serving a website for." % env['PUBLIC_HOSTNAME'])
|
||||||
|
|
||||||
|
# Initialize.
|
||||||
|
|
||||||
|
gandi = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing_certs = gandi.cert.list(api_key)
|
||||||
|
except Exception as e:
|
||||||
|
if "Invalid API key" in str(e):
|
||||||
|
print("Invalid API key. Check that you copied the API Key correctly from https://www.gandi.net/admin/api_key.")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Where is the SSL cert stored?
|
||||||
|
|
||||||
|
ssl_key, ssl_certificate, ssl_csr_path = get_domain_ssl_files(domain, env)
|
||||||
|
|
||||||
|
# Have we already created a cert for this domain?
|
||||||
|
|
||||||
|
for cert in existing_certs:
|
||||||
|
if cert['cn'] == domain:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# No existing cert found. Purchase one.
|
||||||
|
if command != 'purchase':
|
||||||
|
print("No certificate or order found yet. If you haven't yet purchased a certificate, run ths script again with the 'purchase' command. Otherwise wait a moment and try again.")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# Start an order for a single standard SSL certificate.
|
||||||
|
# Use DNS validation. Web-based validation won't work because they
|
||||||
|
# require a file on HTTP but not HTTPS w/o redirects and we don't
|
||||||
|
# serve anything plainly over HTTP. Email might be another way but
|
||||||
|
# DNS is easier to automate.
|
||||||
|
op = gandi.cert.create(api_key, {
|
||||||
|
"csr": open(ssl_csr_path).read(),
|
||||||
|
"dcv_method": "dns",
|
||||||
|
"duration": 1, # year?
|
||||||
|
"package": "cert_std_1_0_0",
|
||||||
|
})
|
||||||
|
print("An SSL certificate has been ordered.")
|
||||||
|
print()
|
||||||
|
print(op)
|
||||||
|
print()
|
||||||
|
print("In a moment please run this script again with the 'setup' command.")
|
||||||
|
|
||||||
|
if cert['status'] == 'pending':
|
||||||
|
# Get the information we need to update our DNS with a code so that
|
||||||
|
# Gandi can verify that we own the domain.
|
||||||
|
|
||||||
|
dcv = gandi.cert.get_dcv_params(api_key, {
|
||||||
|
"csr": open(ssl_csr_path).read(),
|
||||||
|
"cert_id": cert['id'],
|
||||||
|
"dcv_method": "dns",
|
||||||
|
"duration": 1, # year?
|
||||||
|
"package": "cert_std_1_0_0",
|
||||||
|
})
|
||||||
|
if dcv["dcv_method"] != "dns":
|
||||||
|
raise Exception("Certificate ordered with an unknown validation method.")
|
||||||
|
|
||||||
|
# Update our DNS data.
|
||||||
|
|
||||||
|
dns_config = env['STORAGE_ROOT'] + '/dns/custom.yaml'
|
||||||
|
if os.path.exists(dns_config):
|
||||||
|
dns_records = rtyaml.load(open(dns_config))
|
||||||
|
else:
|
||||||
|
dns_records = { }
|
||||||
|
|
||||||
|
qname = dcv['md5'] + '.' + domain
|
||||||
|
value = dcv['sha1'] + '.comodoca.com.'
|
||||||
|
dns_records[qname] = { "CNAME": value }
|
||||||
|
|
||||||
|
with open(dns_config, 'w') as f:
|
||||||
|
f.write(rtyaml.dump(dns_records))
|
||||||
|
|
||||||
|
shell('check_call', ['tools/dns_update'])
|
||||||
|
|
||||||
|
# Okay, done with this step.
|
||||||
|
|
||||||
|
print("DNS has been updated. Gandi will check within 60 minutes.")
|
||||||
|
print()
|
||||||
|
print("See https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
|
||||||
|
|
||||||
|
elif cert['status'] == 'valid':
|
||||||
|
# The certificate is ready.
|
||||||
|
|
||||||
|
# Check before we overwrite something we shouldn't.
|
||||||
|
if os.path.exists(ssl_certificate):
|
||||||
|
cert_status = check_certificate(ssl_certificate)
|
||||||
|
if cert_status != "SELF-SIGNED":
|
||||||
|
print("Please back up and delete the file %s so I can save your new certificate." % ssl_certificate)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Form the certificate.
|
||||||
|
|
||||||
|
# The certificate comes as a long base64-encoded string. Break in
|
||||||
|
# into lines in the usual way.
|
||||||
|
pem = "-----BEGIN CERTIFICATE-----\n"
|
||||||
|
pem += "\n".join(chunk for chunk in re.split(r"(.{64})", cert['cert']) if chunk != "")
|
||||||
|
pem += "\n-----END CERTIFICATE-----\n\n"
|
||||||
|
|
||||||
|
# Append intermediary certificates.
|
||||||
|
pem += urllib.request.urlopen("https://www.gandi.net/static/CAs/GandiStandardSSLCA.pem").read().decode("ascii")
|
||||||
|
|
||||||
|
# Write out.
|
||||||
|
|
||||||
|
with open(ssl_certificate, "w") as f:
|
||||||
|
f.write(pem)
|
||||||
|
|
||||||
|
print("The certificate has been installed in %s. Restarting services..." % ssl_certificate)
|
||||||
|
|
||||||
|
# Restart dovecot and if this is for PUBLIC_HOSTNAME.
|
||||||
|
|
||||||
|
if domain == env['PUBLIC_HOSTNAME']:
|
||||||
|
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
|
||||||
|
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
|
||||||
|
|
||||||
|
# Restart nginx in all cases.
|
||||||
|
|
||||||
|
shell('check_call', ["/usr/sbin/service", "nginx", "restart"])
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("The certificate has an unknown status. Please check https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 4:
|
||||||
|
print("Usage: python management/buy_certificate.py gandi_api_key domain_name {purchase, setup}")
|
||||||
|
sys.exit(1)
|
||||||
|
api_key = sys.argv[1]
|
||||||
|
domain_name = sys.argv[2]
|
||||||
|
cmd = sys.argv[3]
|
||||||
|
buy_ssl_certificate(api_key, domain_name, cmd, load_environment())
|
||||||
|
|
|
@ -43,11 +43,9 @@ def do_web_update(env):
|
||||||
def make_domain_config(domain, template, env):
|
def make_domain_config(domain, template, env):
|
||||||
# How will we configure this domain.
|
# How will we configure this domain.
|
||||||
|
|
||||||
# Where will its root directory be for static files? Try STORAGE_ROOT/web/domain_name
|
# Where will its root directory be for static files?
|
||||||
# if it exists, but fall back to STORAGE_ROOT/web/default.
|
|
||||||
for test_domain in (domain, 'default'):
|
root = get_web_root(domain, env)
|
||||||
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
|
|
||||||
if os.path.exists(root): break
|
|
||||||
|
|
||||||
# What private key and SSL certificate will we use for this domain?
|
# What private key and SSL certificate will we use for this domain?
|
||||||
ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env)
|
ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env)
|
||||||
|
@ -64,6 +62,13 @@ def make_domain_config(domain, template, env):
|
||||||
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
|
||||||
return nginx_conf
|
return nginx_conf
|
||||||
|
|
||||||
|
def get_web_root(domain, env):
|
||||||
|
# Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default.
|
||||||
|
for test_domain in (domain, 'default'):
|
||||||
|
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
|
||||||
|
if os.path.exists(root): break
|
||||||
|
return root
|
||||||
|
|
||||||
def get_domain_ssl_files(domain, env):
|
def get_domain_ssl_files(domain, env):
|
||||||
# What SSL private key will we use? Allow the user to override this, but
|
# What SSL private key will we use? Allow the user to override this, but
|
||||||
# in many cases using the same private key for all domains would be fine.
|
# in many cases using the same private key for all domains would be fine.
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
# SSL certificates have been signed, etc., and if not tells the user
|
# SSL certificates have been signed, etc., and if not tells the user
|
||||||
# what to do next.
|
# what to do next.
|
||||||
|
|
||||||
|
__ALL__ = ['check_certificate']
|
||||||
|
|
||||||
import os, os.path, re, subprocess
|
import os, os.path, re, subprocess
|
||||||
|
|
||||||
import dns.reversename, dns.resolver
|
import dns.reversename, dns.resolver
|
||||||
|
@ -175,23 +177,11 @@ def check_ssl_cert(domain, env):
|
||||||
print_error("The SSL certificate file for this domain is missing.")
|
print_error("The SSL certificate file for this domain is missing.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check that the certificate is good. In order to verify with openssl, we need to split out any
|
# Check that the certificate is good.
|
||||||
# intermediary certificates in the chain (if any) from our certificate (at the top).
|
|
||||||
|
|
||||||
cert = open(ssl_certificate).read()
|
cert_status = check_certificate(ssl_certificate)
|
||||||
mycert, chaincerts = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S).groups()
|
|
||||||
|
|
||||||
# This command returns a non-zero exit status in most cases, so trap errors.
|
if cert_status == "SELF-SIGNED":
|
||||||
retcode, verifyoutput = shell('check_output', [
|
|
||||||
"openssl",
|
|
||||||
"verify", "-verbose",
|
|
||||||
"-purpose", "sslserver", "-policy_check",]
|
|
||||||
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
|
|
||||||
+ [ssl_certificate],
|
|
||||||
input=chaincerts.encode('ascii'),
|
|
||||||
trap=True)
|
|
||||||
|
|
||||||
if "self signed" in verifyoutput:
|
|
||||||
fingerprint = shell('check_output', [
|
fingerprint = shell('check_output', [
|
||||||
"openssl",
|
"openssl",
|
||||||
"x509",
|
"x509",
|
||||||
|
@ -216,15 +206,48 @@ def check_ssl_cert(domain, env):
|
||||||
If you receive intermediate certificates, use a text editor and paste your certificate on top and then the intermediate certificates
|
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.""" % ssl_certificate)
|
below it. Save the file and place it onto this machine at %s.""" % ssl_certificate)
|
||||||
|
|
||||||
|
elif cert_status == "OK":
|
||||||
elif retcode == 0:
|
|
||||||
print_ok("SSL certificate is signed.")
|
print_ok("SSL certificate is signed.")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print_error("The SSL certificate has a problem:")
|
print_error("The SSL certificate has a problem:")
|
||||||
print("")
|
print("")
|
||||||
print(verifyoutput.strip())
|
print(cert_status)
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
|
def check_certificate(ssl_certificate):
|
||||||
|
# Use openssl verify to check the status of a certificate.
|
||||||
|
|
||||||
|
# In order to verify with openssl, we need to split out any
|
||||||
|
# intermediary certificates in the chain (if any) from our
|
||||||
|
# certificate (at the top). They need to be passed separately.
|
||||||
|
|
||||||
|
cert = open(ssl_certificate).read()
|
||||||
|
m = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S)
|
||||||
|
if m == None:
|
||||||
|
return "The certificate file is an invalid PEM certificate."
|
||||||
|
mycert, chaincerts = m.groups()
|
||||||
|
|
||||||
|
# This command returns a non-zero exit status in most cases, so trap errors.
|
||||||
|
|
||||||
|
retcode, verifyoutput = shell('check_output', [
|
||||||
|
"openssl",
|
||||||
|
"verify", "-verbose",
|
||||||
|
"-purpose", "sslserver", "-policy_check",]
|
||||||
|
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
|
||||||
|
+ [ssl_certificate],
|
||||||
|
input=chaincerts.encode('ascii'),
|
||||||
|
trap=True)
|
||||||
|
|
||||||
|
if "self signed" in verifyoutput:
|
||||||
|
# Certificate is self-signed.
|
||||||
|
return "SELF-SIGNED"
|
||||||
|
elif retcode == 0:
|
||||||
|
# Certificate is OK.
|
||||||
|
return "OK"
|
||||||
|
else:
|
||||||
|
return verifyoutput.strip()
|
||||||
|
|
||||||
def print_ok(message):
|
def print_ok(message):
|
||||||
print_block(message, first_line="✓ ")
|
print_block(message, first_line="✓ ")
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,8 @@ if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_RO
|
||||||
mkdir -p $STORAGE_ROOT/www/default
|
mkdir -p $STORAGE_ROOT/www/default
|
||||||
if [ ! -f STORAGE_ROOT/www/default/index.html ]; then
|
if [ ! -f STORAGE_ROOT/www/default/index.html ]; then
|
||||||
cp conf/www_default.html $STORAGE_ROOT/www/default/index.html
|
cp conf/www_default.html $STORAGE_ROOT/www/default/index.html
|
||||||
chown -R $STORAGE_USER $STORAGE_ROOT/www/default/index.html
|
|
||||||
fi
|
fi
|
||||||
|
chown -R $STORAGE_USER $STORAGE_ROOT/www
|
||||||
|
|
||||||
# Create an init script to start the PHP FastCGI daemon and keep it
|
# Create an init script to start the PHP FastCGI daemon and keep it
|
||||||
# running after a reboot. Allows us to serve Roundcube for webmail.
|
# running after a reboot. Allows us to serve Roundcube for webmail.
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/dns/update
|
Loading…
Reference in New Issue