certidude/certidude/cli.py

1761 lines
82 KiB
Python
Raw Normal View History

2015-07-12 19:22:10 +00:00
# coding: utf-8
2015-08-13 08:11:08 +00:00
import click
import hashlib
2015-08-13 08:11:08 +00:00
import logging
2015-07-12 19:22:10 +00:00
import os
import random
import re
2015-07-26 20:34:46 +00:00
import signal
import string
2015-07-27 15:49:50 +00:00
import subprocess
2015-08-13 08:11:08 +00:00
import sys
from asn1crypto import pem, x509
from asn1crypto.csr import CertificationRequest
from asn1crypto.crl import CertificateList
2017-05-27 18:17:21 +00:00
from base64 import b64encode
from certbuilder import CertificateBuilder, pem_armor_certificate
from certidude import const
from csrbuilder import CSRBuilder, pem_armor_csr
from configparser import ConfigParser, NoOptionError
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial
from datetime import datetime, timedelta
from glob import glob
from ipaddress import ip_network
from oscrypto import asymmetric
try:
import coverage
cov = coverage.process_startup()
if cov:
click.echo("Enabling coverage tracking")
else:
click.echo("Coverage tracking not requested")
except ImportError:
pass
2017-04-04 05:02:08 +00:00
logger = logging.getLogger(__name__)
2015-07-26 20:34:46 +00:00
2015-07-12 19:22:10 +00:00
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
2015-07-26 20:34:46 +00:00
# https://kjur.github.io/jsrsasign/
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
2015-08-13 08:11:08 +00:00
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
2015-07-12 19:22:10 +00:00
NOW = datetime.utcnow()
2015-08-13 08:11:08 +00:00
2017-05-04 06:40:47 +00:00
def fqdn_required(func):
def wrapped(**args):
common_name = args.get("common_name")
if "." in common_name:
logger.info("Using fully qualified hostname %s" % common_name)
2017-05-04 06:40:47 +00:00
else:
raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works")
return func(**args)
return wrapped
def setup_client(prefix="client_", dh=False):
2017-05-03 21:03:51 +00:00
# Create section in /etc/certidude/client.conf
def wrapper(func):
def wrapped(**arguments):
common_name = arguments.get("common_name")
authority = arguments.get("authority")
b = os.path.join("/etc/certidude/authority", authority)
if dh:
path = os.path.join("/etc/ssl/dhparam.pem")
if not os.path.exists(path):
rpm("openssl")
apt("openssl")
cmd = "openssl", "dhparam", "-out", path, str(const.KEY_SIZE)
subprocess.check_call(cmd)
arguments["dhparam_path"] = path
2017-05-03 21:03:51 +00:00
# Create corresponding section in Certidude client configuration file
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
if client_config.has_section(authority):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
else:
client_config.add_section(authority)
client_config.set(authority, "trigger", "interface up")
client_config.set(authority, "common name", common_name)
client_config.set(authority, "request path", os.path.join(b, prefix + "req.pem"))
client_config.set(authority, "key path", os.path.join(b, prefix + "key.pem"))
client_config.set(authority, "certificate path", os.path.join(b, prefix + "cert.pem"))
client_config.set(authority, "authority path", os.path.join(b, "ca_cert.pem"))
client_config.set(authority, "revocations path", os.path.join(b, "ca_crl.pem"))
with open(const.CLIENT_CONFIG_PATH + ".part", 'w') as fh:
2017-05-03 21:03:51 +00:00
client_config.write(fh)
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
for j in ("key", "request", "certificate", "authority", "revocations"):
arguments["%s_path" % j] = client_config.get(authority, "%s path" % j)
return func(**arguments)
return wrapped
return wrapper
class ConfigTreeParser(ConfigParser):
def __init__(self, path, *args, **kwargs):
ConfigParser.__init__(self, *args, **kwargs)
if os.path.exists(path):
with open(path) as fh:
click.echo("Parsing: %s" % fh.name)
self.readfp(fh)
if os.path.exists(path + ".d"):
for filename in os.listdir(path + ".d"):
if not filename.endswith(".conf"):
continue
with open(os.path.join(path + ".d", filename)) as fh:
click.echo("Parsing: %s" % fh.name)
self.readfp(fh)
@click.command("enroll", help="Run processes for requesting certificates and configuring services")
2017-06-04 14:19:29 +00:00
@click.option("-k", "--kerberos", default=False, is_flag=True, help="Offer system keytab for auth")
@click.option("-r", "--renew", default=False, is_flag=True, help="Renew now")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
assert os.getuid() == 0 and os.getgid() == 0, "Can enroll only as root"
2018-01-03 22:12:02 +00:00
if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Self-enrolling authority's web interface certificate")
from certidude import authority
authority.self_enroll()
from jinja2 import Environment, PackageLoader
context = globals()
context.update(locals())
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
if not os.path.exists("/etc/systemd/system/certidude-enroll.timer"):
click.echo("Creating systemd timer...")
with open("/etc/systemd/system/certidude-enroll.timer", "w") as fh:
fh.write(env.get_template("client/certidude.timer").render(context))
if not os.path.exists("/etc/systemd/system/certidude-enroll.service"):
click.echo("Creating systemd service...")
with open("/etc/systemd/system/certidude-enroll.service", "w") as fh:
fh.write(env.get_template("client/certidude.service").render(context))
os.system("systemctl daemon-reload")
os.system("systemctl enable certidude-enroll.timer")
os.system("systemctl start certidude-enroll.timer")
2017-04-13 20:30:28 +00:00
if not os.path.exists(const.CLIENT_CONFIG_PATH):
click.echo("Client not configured, so not going to enroll")
return
import requests
clients = ConfigTreeParser(const.CLIENT_CONFIG_PATH)
service_config = ConfigTreeParser(const.SERVICES_CONFIG_PATH)
# Process directories
if not os.path.exists(const.RUN_DIR):
click.echo("Creating: %s" % const.RUN_DIR)
os.makedirs(const.RUN_DIR)
2017-05-27 18:17:21 +00:00
for authority_name in clients.sections():
# TODO: Create directories automatically
2017-05-27 18:17:21 +00:00
try:
trigger = clients.get(authority_name, "trigger")
except NoOptionError:
trigger = "interface up"
2017-05-27 18:17:21 +00:00
if trigger == "domain joined":
# Stop further processing if command line argument said so or trigger expects domain membership
if not os.path.exists("/etc/krb5.keytab"):
continue
2017-06-04 14:19:29 +00:00
kerberos = True
2017-05-27 18:17:21 +00:00
elif trigger == "interface up":
pass
else:
raise
#########################
### Fork if requested ###
#########################
2017-05-27 18:17:21 +00:00
pid_path = os.path.join(const.RUN_DIR, authority_name + ".pid")
try:
with open(pid_path) as fh:
pid = int(fh.readline())
os.kill(pid, signal.SIGTERM)
click.echo("Terminated process %d" % pid)
os.unlink(pid_path)
except EnvironmentError:
pass
if fork:
child_pid = os.fork()
else:
child_pid = None
if child_pid:
click.echo("Spawned certificate request process with PID %d" % (child_pid))
continue
with open(pid_path, "w") as fh:
fh.write("%d\n" % os.getpid())
2017-05-27 18:17:21 +00:00
try:
authority_path = clients.get(authority_name, "authority path")
except NoOptionError:
authority_path = "/etc/certidude/authority/%s/ca_cert.pem" % authority_name
2017-05-27 18:17:21 +00:00
finally:
if os.path.exists(authority_path):
click.echo("Found authority certificate in: %s" % authority_path)
with open(authority_path, "rb") as fh:
header, _, certificate_der_bytes = pem.unarmor(fh.read())
authority_certificate = x509.Certificate.load(certificate_der_bytes)
2017-05-27 18:17:21 +00:00
else:
if not os.path.exists(os.path.dirname(authority_path)):
os.makedirs(os.path.dirname(authority_path))
authority_url = "http://%s/api/certificate/" % authority_name
2017-05-27 18:17:21 +00:00
click.echo("Attempting to fetch authority certificate from %s" % authority_url)
try:
r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
header, _, certificate_der_bytes = pem.unarmor(r.content)
authority_certificate = x509.Certificate.load(certificate_der_bytes)
except: # TODO: catch correct exceptions
2017-05-27 18:17:21 +00:00
raise
# raise ValueError("Failed to parse PEM: %s" % r.text)
authority_partial = authority_path + ".part"
with open(authority_partial, "wb") as oh:
2017-05-27 18:17:21 +00:00
oh.write(r.content)
click.echo("Writing authority certificate to: %s" % authority_path)
selinux_fixup(authority_partial)
os.rename(authority_partial, authority_path)
authority_public_key = asymmetric.load_public_key(
authority_certificate["tbs_certificate"]["subject_public_key_info"])
2017-05-27 18:17:21 +00:00
# Attempt to install CA certificates system wide
try:
authority_system_wide = clients.getboolean(authority_name, "system wide")
except NoOptionError:
authority_system_wide = False
finally:
if authority_system_wide:
# Firefox, Chromium, wget, curl on Fedora
# Note that if ~/.pki/nssdb has been customized before, curl breaks
if os.path.exists("/usr/bin/update-ca-trust"):
link_path = "/etc/pki/ca-trust/source/anchors/%s" % authority_name
if not os.path.lexists(link_path):
os.symlink(authority_path, link_path)
os.system("update-ca-trust")
# curl on Fedora ?
# pip
2017-07-06 09:29:02 +00:00
# Firefox (?) on Debian, Ubuntu
if os.path.exists("/usr/bin/update-ca-certificates") or os.path.exists("/usr/sbin/update-ca-certificates"):
2017-07-06 09:29:02 +00:00
link_path = "/usr/local/share/ca-certificates/%s" % authority_name
if not os.path.lexists(link_path):
os.symlink(authority_path, link_path)
os.system("update-ca-certificates")
# TODO: test for curl, wget
2017-05-27 18:17:21 +00:00
###############
### Get CRL ###
###############
try:
revocations_path = clients.get(authority_name, "revocations path")
except NoOptionError:
revocations_path = None
else:
# Fetch certificate revocation list
revoked_url = "http://%s/api/revoked/" % authority_name
2017-05-27 18:17:21 +00:00
click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path))
r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})
if r.status_code == 200:
header, _, crl_der_bytes = pem.unarmor(r.content)
revocations = CertificateList.load(crl_der_bytes)
# TODO: check signature, parse reasons, remove keys if revoked
revocations_partial = revocations_path + ".part"
with open(revocations_partial, 'wb') as f:
f.write(r.content)
os.rename(revocations_partial, revocations_path)
elif r.status_code == 404:
click.echo("CRL disabled, server said 404")
else:
click.echo("Failed to fetch CRL from %s, got %s" % (revoked_url, r.text))
2017-05-27 18:17:21 +00:00
try:
common_name = clients.get(authority_name, "common name")
except NoOptionError:
click.echo("No common name specified for %s, not requesting a certificate" % authority_name)
continue
# If deriving common name from *current* hostname is preferred
if common_name == "$HOSTNAME":
common_name = const.HOSTNAME
elif common_name == "$FQDN":
common_name = const.FQDN
elif "$" in common_name:
raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name)
if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Supplied common name %s doesn't match the expression %s" % (common_name, const.RE_COMMON_NAME))
2017-05-27 18:17:21 +00:00
################################
### Generate keypair and CSR ###
################################
try:
key_path = clients.get(authority_name, "key path")
request_path = clients.get(authority_name, "request path")
except NoOptionError:
key_path = "/etc/certidude/authority/%s/host_key.pem" % authority_name
request_path = "/etc/certidude/authority/%s/host_csr.pem" % authority_name
2017-05-27 18:17:21 +00:00
if os.path.exists(request_path):
with open(request_path, "rb") as fh:
header, _, der_bytes = pem.unarmor(fh.read())
csr = CertificationRequest.load(der_bytes)
if csr["certification_request_info"]["subject"].native["common_name"] != common_name:
click.echo("Stored request's common name differs from currently requested one, deleting old request")
os.remove(request_path)
2017-05-27 18:17:21 +00:00
if not os.path.exists(request_path):
key_partial = key_path + ".part"
request_partial = request_path + ".part"
if authority_public_key.algorithm == "ec":
self_public_key, private_key = asymmetric.generate_pair("ec", curve=authority_public_key.curve)
elif authority_public_key.algorithm == "rsa":
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=authority_public_key.bit_size)
else:
NotImplemented
builder = CSRBuilder({"common_name": common_name}, self_public_key)
2017-05-27 18:17:21 +00:00
request = builder.build(private_key)
with open(key_partial, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None))
with open(request_partial, 'wb') as f:
f.write(pem_armor_csr(request))
selinux_fixup(key_partial)
selinux_fixup(request_partial)
os.rename(key_partial, key_path)
os.rename(request_partial, request_path)
2017-05-27 18:17:21 +00:00
##############################################
### Submit CSR and save signed certificate ###
##############################################
try:
certificate_path = clients.get(authority_name, "certificate path")
except NoOptionError:
certificate_path = "/etc/certidude/authority/%s/host_cert.pem" % authority_name
2017-05-27 18:17:21 +00:00
try:
renewal_overlap = clients.getint(authority_name, "renewal overlap")
except NoOptionError: # Renewal not configured
2017-06-04 14:19:29 +00:00
renewal_overlap = None
try:
with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh:
2017-05-27 18:17:21 +00:00
cert_buf = ch.read()
cert = asymmetric.load_certificate(cert_buf)
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap):
2017-05-27 18:17:21 +00:00
click.echo("Certificate will expire %s, will attempt to renew" % expires)
renew = True
2017-06-04 14:19:29 +00:00
except EnvironmentError: # Certificate missing, can't renew
2017-05-27 18:17:21 +00:00
pass
try:
autosign = clients.getboolean(authority_name, "autosign")
except NoOptionError:
autosign = True
2017-05-27 18:17:21 +00:00
if not os.path.exists(certificate_path) or renew:
# Set up URL-s
request_params = set()
request_params.add("autosign=%s" % ("yes" if autosign else "no"))
2017-05-27 18:17:21 +00:00
if not no_wait:
request_params.add("wait=forever")
kwargs = {
"data": open(request_path),
"verify": authority_path,
"headers": {
"Content-Type": "application/pkcs10",
"Accept": "application/x-x509-user-cert,application/x-pem-file"
}
}
if renew: # Do mutually authenticated TLS handshake
kwargs["cert"] = certificate_path, key_path
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
2017-05-27 18:17:21 +00:00
else:
# If machine is joined to domain attempt to present machine credentials for authentication
if kerberos:
try:
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
except ImportError:
click.echo("Kerberos bindings not available, please install requests-kerberos")
else:
os.environ["KRB5CCNAME"]="/tmp/ca.ticket"
# Mac OS X has keytab with lowercase hostname
cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.lower())
click.echo("Executing: %s" % cmd)
if os.system(cmd):
# Fedora /w SSSD has keytab with uppercase hostname
cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.upper())
if os.system(cmd):
# Failed, probably /etc/krb5.keytab contains spaghetti
raise ValueError("Failed to initialize Kerberos service ticket using machine keytab")
assert os.path.exists("/tmp/ca.ticket"), "Ticket not created!"
click.echo("Initialized Kerberos service ticket using machine keytab")
kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
else:
click.echo("Not using machine keytab")
2017-05-27 18:17:21 +00:00
request_url = "https://%s:8443/api/request/" % authority_name
if request_params:
request_url = request_url + "?" + "&".join(request_params)
submission = requests.post(request_url, **kwargs)
2017-05-27 18:17:21 +00:00
# Destroy service ticket
if os.path.exists("/tmp/ca.ticket"):
os.system("kdestroy")
if submission.status_code == requests.codes.ok:
pass
if submission.status_code == requests.codes.accepted:
click.echo("Server accepted the request, but refused to sign immideately (%s). Waiting was not requested, hence quitting for now" % submission.text)
2017-06-04 14:19:29 +00:00
os.unlink(pid_path)
continue
2017-05-27 18:17:21 +00:00
if submission.status_code == requests.codes.conflict:
raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite")
elif submission.status_code == requests.codes.gone:
# Should the client retry or disable request submission?
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
elif submission.status_code == requests.codes.bad_request:
raise ValueError("Server said following, likely current certificate expired/revoked? %s" % submission.text)
2017-05-27 18:17:21 +00:00
else:
submission.raise_for_status()
try:
header, _, certificate_der_bytes = pem.unarmor(submission.content)
cert = x509.Certificate.load(certificate_der_bytes)
2017-05-27 18:17:21 +00:00
except: # TODO: catch correct exceptions
raise ValueError("Failed to parse PEM: %s" % submission.text)
assert cert.subject.native["common_name"] == common_name, \
"Expected certificate with common name %s, but got %s instead" % \
(common_name, cert.subject.native["common_name"])
2017-05-27 18:17:21 +00:00
os.umask(0o022)
certificate_partial = certificate_path + ".part"
with open(certificate_partial, "w") as fh:
# Dump certificate
fh.write(submission.text)
click.echo("Writing certificate to: %s" % certificate_path)
selinux_fixup(certificate_partial)
os.rename(certificate_partial, certificate_path)
# Nginx requires bundle
try:
bundle_path = clients.get(authority_name, "bundle path")
except NoOptionError:
pass
else:
bundle_partial = bundle_path + ".part"
with open(bundle_partial, "w") as fh:
fh.write(submission.text)
with open(authority_path) as ch:
fh.write(ch.read())
click.echo("Writing bundle to: %s" % bundle_path)
os.rename(bundle_partial, bundle_path)
else:
click.echo("Certificate found at %s and no renewal requested" % certificate_path)
2017-05-27 18:17:21 +00:00
##################################
### Configure related services ###
##################################
for endpoint in service_config.sections():
2017-05-27 18:17:21 +00:00
if service_config.get(endpoint, "authority") != authority_name:
continue
click.echo("Configuring '%s'" % endpoint)
csummer = hashlib.sha1()
csummer.update(endpoint.encode("ascii"))
csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
# Intranet HTTPS handled by PKCS#12 bundle generation,
# so it will not be implemented here
# OpenVPN set up with initscripts
if service_config.get(endpoint, "service") == "init/openvpn":
if os.path.exists("/etc/openvpn/%s.disabled" % endpoint) and not os.path.exists("/etc/openvpn/%s.conf" % endpoint):
os.rename("/etc/openvpn/%s.disabled" % endpoint, "/etc/openvpn/%s.conf" % endpoint)
if os.path.exists("/bin/systemctl"):
click.echo("Re-running systemd generators for OpenVPN...")
os.system("systemctl daemon-reload")
if not os.path.exists("/etc/systemd/system/openvpn-reconnect.service"):
with open("/etc/systemd/system/openvpn-reconnect.service.part", "w") as fh:
fh.write(env.get_template("client/openvpn-reconnect.service").render(context))
os.rename("/etc/systemd/system/openvpn-reconnect.service.part",
"/etc/systemd/system/openvpn-reconnect.service")
click.echo("Created /etc/systemd/system/openvpn-reconnect.service")
click.echo("Starting OpenVPN...")
os.system("service openvpn start")
continue
# IPSec set up with initscripts
if service_config.get(endpoint, "service") == "init/strongswan":
from ipsecparse import loads
2017-04-13 23:49:11 +00:00
config = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
for section_type, section_name in config:
# Identify correct ipsec.conf section by leftcert
if section_type != "conn":
continue
2017-05-27 18:17:21 +00:00
if config[section_type,section_name]["leftcert"] != certificate_path:
continue
if config[section_type,section_name].get("left", "") == "%defaultroute":
config[section_type,section_name]["auto"] = "start" # This is client
elif config[section_type,section_name].get("leftsourceip", ""):
config[section_type,section_name]["auto"] = "add" # This is server
else:
config[section_type,section_name]["auto"] = "route" # This is site-to-site tunnel
with open("%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, "w") as fh:
fh.write(config.dumps())
os.rename(
"%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX,
"%s/ipsec.conf" % const.STRONGSWAN_PREFIX)
break
# Tune AppArmor profile, TODO: retain contents
if os.path.exists("/etc/apparmor.d/local"):
with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh:
fh.write(key_path + " r,\n")
fh.write(authority_path + " r,\n")
fh.write(certificate_path + " r,\n")
# Attempt to reload config or start if it's not running
2017-04-13 23:49:11 +00:00
if os.path.exists("/usr/sbin/strongswan"): # wtf fedora
if os.system("strongswan update"):
2017-04-13 23:49:11 +00:00
os.system("strongswan start")
else:
if os.system("ipsec update"):
2017-04-13 23:49:11 +00:00
os.system("ipsec start")
continue
# OpenVPN set up with NetworkManager
if service_config.get(endpoint, "service") == "network-manager/openvpn":
# NetworkManager-strongswan-gnome
nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint)
if os.path.exists(nm_config_path):
click.echo("Not creating %s, remove to regenerate" % nm_config_path)
continue
nm_config = ConfigParser()
nm_config.add_section("connection")
2017-05-27 18:17:21 +00:00
nm_config.set("connection", "certidude managed", "true")
nm_config.set("connection", "id", endpoint)
nm_config.set("connection", "uuid", uuid)
nm_config.set("connection", "type", "vpn")
nm_config.add_section("vpn")
nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn")
nm_config.set("vpn", "connection-type", "tls")
nm_config.set("vpn", "comp-lzo", "yes")
nm_config.set("vpn", "cert-pass-flags", "0")
nm_config.set("vpn", "tap-dev", "no")
nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
nm_config.set("vpn", "remote", service_config.get(endpoint, "remote"))
2017-05-27 18:17:21 +00:00
nm_config.set("vpn", "key", key_path)
nm_config.set("vpn", "cert", certificate_path)
nm_config.set("vpn", "ca", authority_path)
nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % (
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
nm_config.set("vpn", "cipher", "AES-128-GCM")
nm_config.set("vpn", "auth", "SHA384")
nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto")
nm_config.set("ipv4", "never-default", "true")
nm_config.add_section("ipv6")
nm_config.set("ipv6", "method", "auto")
2017-05-27 18:17:21 +00:00
try:
nm_config.set("vpn", "port", str(service_config.getint(endpoint, "port")))
except NoOptionError:
nm_config.set("vpn", "port", "1194")
try:
if service_config.get(endpoint, "proto") == "tcp":
nm_config.set("vpn", "proto-tcp", "yes")
except NoOptionError:
pass
# Prevent creation of files with liberal permissions
os.umask(0o177)
# Write NetworkManager configuration
with open(nm_config_path, "w") as fh:
nm_config.write(fh)
click.echo("Created %s" % nm_config_path)
2017-05-03 21:03:51 +00:00
if os.path.exists("/run/NetworkManager"):
os.system("nmcli con reload")
continue
2016-01-10 17:51:54 +00:00
2015-07-26 20:34:46 +00:00
# IPSec set up with NetworkManager
2017-05-27 18:17:21 +00:00
if service_config.get(endpoint, "service") == "network-manager/strongswan":
client_config = ConfigParser()
2017-04-12 13:56:29 +00:00
nm_config = ConfigParser()
nm_config.add_section("connection")
2017-05-27 18:17:21 +00:00
nm_config.set("connection", "certidude managed", "true")
nm_config.set("connection", "id", endpoint)
nm_config.set("connection", "uuid", uuid)
nm_config.set("connection", "type", "vpn")
nm_config.add_section("vpn")
nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
nm_config.set("vpn", "encap", "no")
nm_config.set("vpn", "virtual", "yes")
nm_config.set("vpn", "method", "key")
nm_config.set("vpn", "ipcomp", "no")
nm_config.set("vpn", "address", service_config.get(endpoint, "remote"))
2017-05-27 18:17:21 +00:00
nm_config.set("vpn", "userkey", key_path)
nm_config.set("vpn", "usercert", certificate_path)
nm_config.set("vpn", "certificate", authority_path)
dhgroup = "ecp384" if authority_public_key.algorithm == "ec" else "modp2048"
nm_config.set("vpn", "ike", "aes256-sha384-prfsha384-" + dhgroup)
nm_config.set("vpn", "esp", "aes128gcm16-aes128gmac-" + dhgroup)
nm_config.set("vpn", "proposal", "yes")
nm_config.add_section("ipv4")
nm_config.set("ipv4", "method", "auto")
2015-07-26 20:34:46 +00:00
# Add routes, may need some more tweaking
if service_config.has_option(endpoint, "route"):
for index, subnet in enumerate(service_config.get(endpoint, "route").split(","), start=1):
nm_config.set("ipv4", "route%d" % index, subnet)
2015-07-26 20:34:46 +00:00
# Prevent creation of files with liberal permissions
os.umask(0o177)
# Write NetworkManager configuration
with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as fh:
nm_config.write(fh)
click.echo("Created %s" % fh.name)
2017-05-03 21:03:51 +00:00
if os.path.exists("/run/NetworkManager"):
os.system("nmcli con reload")
continue
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
2017-04-13 23:49:11 +00:00
click.echo("Unknown service: %s" % service_config.get(endpoint, "service"))
os.unlink(pid_path)
2015-07-26 20:34:46 +00:00
@click.command("server", help="Set up OpenVPN server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
2015-07-26 20:34:46 +00:00
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
2017-04-12 13:21:49 +00:00
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
2015-07-26 20:34:46 +00:00
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
2015-08-13 08:11:08 +00:00
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
2015-07-26 20:34:46 +00:00
@click.option("--config", "-o",
default="/etc/openvpn/site-to-client.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
2017-05-04 06:40:47 +00:00
@fqdn_required
@setup_client(prefix="server_", dh=True)
2017-05-03 21:03:51 +00:00
def certidude_setup_openvpn_server(authority, common_name, config, subnet, route, local, proto, port, **paths):
2017-04-20 05:20:10 +00:00
# Install dependencies
apt("openvpn")
rpm("openvpn")
2015-07-26 20:34:46 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = "OpenVPN server %s of %s" % (common_name, authority)
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "service", "init/openvpn")
with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
authority_hostname = authority.split(".")[0]
config.write("server %s %s\n" % (subnet.network_address, subnet.netmask))
config.write("dev tun-%s\n" % authority_hostname)
config.write("proto %s\n" % proto)
config.write("port %d\n" % port)
config.write("local %s\n" % local)
2017-05-03 21:03:51 +00:00
config.write("key %s\n" % paths.get("key_path"))
config.write("cert %s\n" % paths.get("certificate_path"))
config.write("ca %s\n" % paths.get("authority_path"))
config.write("crl-verify %s\n" % paths.get("revocations_path"))
config.write("dh %s\n" % paths.get("dhparam_path"))
config.write("comp-lzo\n")
config.write("user nobody\n")
config.write("group nogroup\n")
config.write("persist-tun\n")
config.write("persist-key\n")
config.write("#ifconfig-pool-persist /tmp/openvpn-leases.txt\n")
2017-05-03 21:03:51 +00:00
2015-07-26 20:34:46 +00:00
click.echo("Generated %s" % config.name)
click.echo("Inspect generated files and issue following to request certificate:")
2015-07-26 20:34:46 +00:00
click.echo()
click.echo(" certidude enroll")
2015-07-26 20:34:46 +00:00
@click.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--site-config", "-o",
default="/etc/nginx/sites-available/%s.conf" % const.HOSTNAME,
type=click.File(mode="w", atomic=True, lazy=True),
help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % const.HOSTNAME)
@click.option("--verify-client", "-vc", default="optional", type=click.Choice(['optional', 'on', 'off']))
2017-05-04 06:40:47 +00:00
@fqdn_required
@setup_client(prefix="server_", dh=True)
2017-05-03 21:03:51 +00:00
def certidude_setup_nginx(authority, common_name, site_config, tls_config, verify_client, **paths):
2017-05-04 06:40:47 +00:00
2017-05-03 21:03:51 +00:00
apt("nginx")
rpm("nginx")
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
context = globals() # Grab const.BLAH
context.update(locals())
2017-05-03 21:03:51 +00:00
context.update(paths)
if os.path.exists(site_config.name):
click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
else:
site_config.write(env.get_template("nginx-https-site.conf").render(context))
click.echo("Generated %s" % site_config.name)
if os.path.exists(tls_config.name):
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
else:
tls_config.write(env.get_template("nginx-tls.conf").render(context))
click.echo("Generated %s" % tls_config.name)
click.echo()
click.echo("Inspect configuration files, enable it and start nginx service:")
click.echo()
click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % (
os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"),
os.path.basename(site_config.name)))
click.echo(" service nginx restart")
click.echo()
2015-07-26 20:34:46 +00:00
@click.command("client", help="Set up OpenVPN client")
@click.argument("authority")
2015-07-26 20:34:46 +00:00
@click.argument("remote")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
2015-07-26 20:34:46 +00:00
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf", # TODO: created initially disabled conf
2015-07-26 20:34:46 +00:00
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
2017-05-03 21:03:51 +00:00
@setup_client()
2017-05-04 06:40:47 +00:00
def certidude_setup_openvpn_client(authority, remote, common_name, config, proto, **paths):
2017-04-20 05:20:10 +00:00
# Install dependencies
apt("openvpn")
rpm("openvpn")
# Create corresponding section in /etc/certidude/services.conf
2017-04-20 05:20:10 +00:00
endpoint = "OpenVPN to %s" % remote
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "service", "init/openvpn")
2017-04-13 23:49:11 +00:00
service_config.set(endpoint, "remote", remote)
with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
config.write("client\n")
config.write("remote %s\n" % remote)
config.write("remote-cert-tls server\n")
config.write("proto %s\n" % proto)
2017-05-03 21:03:51 +00:00
config.write("dev tun-%s\n" % remote.split(".")[0])
config.write("nobind\n")
config.write("key %s\n" % paths.get("key_path"))
config.write("cert %s\n" % paths.get("certificate_path"))
config.write("ca %s\n" % paths.get("authority_path"))
config.write("crl-verify %s\n" % paths.get("revocations_path"))
config.write("comp-lzo\n")
config.write("user nobody\n")
config.write("group nogroup\n")
config.write("persist-tun\n")
config.write("persist-key\n")
config.write("up /etc/openvpn/update-resolv-conf\n")
config.write("down /etc/openvpn/update-resolv-conf\n")
2015-07-26 20:34:46 +00:00
click.echo("Generated %s" % config.name)
click.echo("Inspect generated files and issue following to request certificate:")
2015-07-26 20:34:46 +00:00
click.echo()
click.echo(" certidude enroll")
2015-07-26 20:34:46 +00:00
2015-08-13 08:11:08 +00:00
@click.command("server", help="Set up strongSwan server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--subnet", "-sn", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
2015-08-13 08:11:08 +00:00
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
2017-05-04 06:40:47 +00:00
@fqdn_required
2017-05-03 21:03:51 +00:00
@setup_client(prefix="server_")
def certidude_setup_strongswan_server(authority, common_name, subnet, route, **paths):
2017-04-20 05:20:10 +00:00
# Install dependencies
apt("strongswan")
rpm("strongswan")
2017-05-03 21:03:51 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = "IPsec gateway for %s" % authority
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
2017-05-03 21:03:51 +00:00
service_config.add_section(endpoint)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "service", "init/strongswan")
with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
2017-05-03 21:03:51 +00:00
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
# Create corresponding section to /etc/ipsec.conf
from ipsecparse import loads
ipsec_conf = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
ipsec_conf["ca", authority] = dict(
auto="add",
cacert=paths.get("authority_path"))
ipsec_conf["conn", authority] = dict(
2017-05-03 21:03:51 +00:00
leftcert=paths.get("certificate_path"),
leftsubnet=",".join(route),
right="%any",
rightsourceip=str(subnet),
closeaction="restart",
auto="ignore")
with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh:
fh.write(ipsec_conf.dumps())
with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh:
fh.write(": RSA %s\n" % paths.get("key_path"))
2015-08-13 08:11:08 +00:00
click.echo()
2016-03-29 15:37:28 +00:00
click.echo("If you're running Ubuntu make sure you're not affected by #1505222")
click.echo("https://bugs.launchpad.net/ubuntu/+source/strongswan/+bug/1505222")
2015-08-13 08:11:08 +00:00
@click.command("client", help="Set up strongSwan client")
@click.argument("authority")
2015-08-13 08:11:08 +00:00
@click.argument("remote")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
2017-05-03 21:03:51 +00:00
@setup_client()
def certidude_setup_strongswan_client(authority, remote, common_name, **paths):
2017-04-20 05:20:10 +00:00
# Install dependencies
apt("strongswan") or rpm("strongswan")
2017-04-20 05:20:10 +00:00
2017-04-13 23:49:11 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = "IPsec connection to %s" % remote
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "service", "init/strongswan")
service_config.set(endpoint, "remote", remote)
with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
2017-04-13 23:49:11 +00:00
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
# Create corresponding section in /etc/ipsec.conf
from ipsecparse import loads
ipsec_conf = loads(open('%s/ipsec.conf' % const.STRONGSWAN_PREFIX).read())
ipsec_conf["ca", authority] = dict(
auto="add",
cacert=paths.get("authority_path"))
ipsec_conf["conn", remote] = dict(
leftsourceip="%config",
left="%defaultroute",
2017-05-03 21:03:51 +00:00
leftcert=paths.get("certificate_path"),
rightid="%any",
right=remote,
rightsubnet="0.0.0.0/0", # To allow anything suggested by gateway
keyexchange="ikev2",
keyingtries="300",
dpdaction="restart",
closeaction="restart",
auto="ignore")
with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh:
fh.write(ipsec_conf.dumps())
with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh:
fh.write(": RSA %s\n" % paths.get("key_path"))
if os.path.exists("/etc/apparmor.d/local"):
with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh:
fh.write(os.path.join(const.STORAGE_PATH, "**") + " r,\n")
2017-04-13 23:49:11 +00:00
click.echo("Generated section %s in %s" % (authority, const.CLIENT_CONFIG_PATH))
click.echo("Run 'certidude enroll' to request certificates and to enable services")
2015-08-13 08:11:08 +00:00
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("authority") # Certidude server
@click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
2017-05-03 21:03:51 +00:00
@setup_client()
def certidude_setup_strongswan_networkmanager(authority, remote, common_name, **paths):
2017-04-20 05:20:10 +00:00
# Install dependencies
apt("network-manager strongswan-nm")
rpm("NetworkManager NetworkManager-tui NetworkManager-strongswan-gnome")
# Create corresponding section in /etc/certidude/services.conf
endpoint = "IPSec to %s" % remote
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
2017-04-13 23:49:11 +00:00
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "remote", remote)
service_config.set(endpoint, "service", "network-manager/strongswan")
with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
service_config.write(fh)
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
@click.argument("authority")
@click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
2017-05-03 21:03:51 +00:00
@setup_client()
def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **paths):
apt("network-manager network-manager-openvpn-gnome")
rpm("NetworkManager NetworkManager-tui NetworkManager-openvpn-gnome")
2017-05-03 21:03:51 +00:00
# Create corresponding section in /etc/certidude/services.conf
endpoint = "OpenVPN to %s" % remote
service_config = ConfigParser()
if os.path.exists(const.SERVICES_CONFIG_PATH):
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
if service_config.has_section(endpoint):
click.echo("Section '%s' already exists in %s, remove to regenerate" % (endpoint, const.SERVICES_CONFIG_PATH))
else:
service_config.add_section(endpoint)
2017-05-03 21:03:51 +00:00
service_config.set(endpoint, "authority", authority)
service_config.set(endpoint, "remote", remote)
service_config.set(endpoint, "service", "network-manager/openvpn")
service_config.write(open("/etc/certidude/services.conf", "w"))
click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)
@click.command("authority", help="Set up Certificate Authority in a directory")
2015-08-13 08:11:08 +00:00
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default")
2015-08-13 08:11:08 +00:00
@click.option("--nginx-config", "-n",
default="/etc/nginx/sites-available/certidude.conf",
2015-08-13 08:11:08 +00:00
type=click.File(mode="w", atomic=True, lazy=True),
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
@click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
@click.option("--organization", "-o", default=None, help="Company or organization name")
@click.option("--organizational-unit", "-ou", default="Certificate Authority")
2017-04-13 20:30:28 +00:00
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", default="/var/lib/certidude", help="Directory for authority files")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-assets", is_flag=True, help="Don't attempt to assemble JS/CSS/font assets")
2018-01-03 22:12:02 +00:00
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@click.option("--packages-only", is_flag=True, help="Install only apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate, packages_only):
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
import pwd
from jinja2 import Environment, PackageLoader
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
2018-01-03 22:12:02 +00:00
if skip_packages:
2018-05-17 09:40:36 +00:00
click.echo("Not attempting to install packages as requested...")
else:
2018-01-03 22:12:02 +00:00
click.echo("Installing packages...")
cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
cython3 python3-dev \
2018-01-03 22:12:02 +00:00
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
rsync attr wget unzip"
click.echo("Running: %s" % cmd)
if os.system(cmd):
raise click.ClickException("Failed to install APT packages")
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"):
raise click.ClickException("Failed to install Python packages")
if os.system("pip3 install -q --pre --upgrade python-ldap"):
raise click.ClickException("Failed to install python-ldap")
2018-01-03 22:12:02 +00:00
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
click.echo("Enabling nginx PPA")
if os.system("add-apt-repository -y ppa:nginx/stable"):
raise click.ClickException("Failed to add nginx PPA")
if os.system("apt-get update -q"):
raise click.ClickException("Failed to update package lists")
if os.system("apt-get install -y -q libnginx-mod-nchan"):
raise click.ClickException("Failed to install nchan")
2018-01-03 22:12:02 +00:00
else:
click.echo("PPA for nginx already enabled")
2018-01-03 22:12:02 +00:00
if not os.path.exists("/usr/sbin/nginx"):
click.echo("Installing nginx from PPA")
if os.system("apt-get install -y -q nginx"):
raise click.ClickException("Failed to install nginx")
2018-01-03 22:12:02 +00:00
else:
click.echo("Web server nginx already installed")
2018-05-17 09:40:36 +00:00
cmd = "npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
click.echo("Installing JavaScript packages: %s" % cmd)
if os.system(cmd):
raise click.ClickException("Failed to install JavaScript packages")
if not os.path.exists("/usr/bin/node"):
os.symlink("/usr/bin/nodejs", "/usr/bin/node")
if packages_only:
return
2017-04-20 05:20:10 +00:00
if "." in common_name:
logger.info("Using fully qualified hostname %s" % common_name)
else:
raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works")
# Generate secret for tokens
token_url = "https://" + common_name + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn"
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
click.echo("Using templates from %s" % template_path)
click.echo("Placing authority files in %s" % directory)
certificate_url = "http://%s/api/certificate/" % common_name
click.echo("Setting CA certificate URL to %s" % certificate_url)
revoked_url = "http://%s/api/revoked/" % common_name
click.echo("Setting revocation list URL to %s" % revoked_url)
responder_url = "http://%s/api/ocsp/" % common_name
click.echo("Setting OCSP responder URL to %s" % responder_url)
# Expand variables
assets_dir = os.path.join(directory, "assets")
ca_key = os.path.join(directory, "ca_key.pem")
ca_req = os.path.join(directory, "ca_req.pem")
ca_cert = os.path.join(directory, "ca_cert.pem")
self_key = os.path.join(directory, "self_key.pem")
2017-05-25 19:20:29 +00:00
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
distinguished_name = cn_to_dn(title, common_name, o=organization, ou=organizational_unit)
dhparam_path = "/etc/ssl/dhparam.pem"
# Builder variables
dhgroup = "ecp384" if elliptic_curve else "modp2048"
2017-05-03 07:04:52 +00:00
try:
pwd.getpwnam("certidude")
click.echo("User 'certidude' already exists")
except KeyError:
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
if subprocess.call(cmd):
raise click.ClickException("Failed to create system user 'certidude'")
2017-05-03 07:04:52 +00:00
if os.path.exists(kerberos_keytab):
click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
else:
2018-05-17 20:18:19 +00:00
click.echo("To use 'kerberos' authentication backend join the domain , create service principal and provision authority again:")
2017-05-03 07:04:52 +00:00
click.echo()
2018-05-17 20:18:19 +00:00
click.echo(" kinit administrator@EXAMPLE.LAN")
click.echo(" net ads join -k")
2017-05-03 07:04:52 +00:00
click.echo(" KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab)
2018-05-17 20:18:19 +00:00
click.echo(" kdestroy")
2017-05-03 07:04:52 +00:00
click.echo(" chown %s %s" % (username, kerberos_keytab))
2018-05-17 20:18:19 +00:00
click.echo(" mv /etc/certidude/server.conf /etc/certidude/server.backup")
click.echo(" certidude setup authority")
2017-05-03 07:04:52 +00:00
click.echo()
2015-08-13 08:11:08 +00:00
2017-05-03 07:04:52 +00:00
if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"):
# Fetch Kerberos ticket for system account
cp = ConfigParser()
cp.read("/etc/samba/smb.conf")
realm = cp.get("global", "realm")
domain = realm.lower()
name = cp.get("global", "netbios name")
base = ",".join(["dc=" + j for j in domain.split(".")])
if not os.path.exists("/etc/cron.hourly/certidude"):
with open("/etc/cron.hourly/certidude", "w") as fh:
fh.write(env.get_template("server/cronjob").render(vars()))
os.chmod("/etc/cron.hourly/certidude", 0o755)
click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly")
os.system("/etc/cron.hourly/certidude")
else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
2015-08-13 08:11:08 +00:00
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
letsencrypt = os.path.exists(letsencrypt_fullchain)
2018-05-17 20:18:19 +00:00
builder_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "builder")
2018-01-03 22:12:02 +00:00
script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")
2017-05-03 07:04:52 +00:00
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
certidude_path = sys.argv[0]
2015-08-13 08:11:08 +00:00
click.echo("Generating: %s" % nginx_config.name)
nginx_config.write(env.get_template("server/nginx.conf").render(vars()))
nginx_config.close()
if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
if os.path.exists("/etc/nginx/sites-enabled/default"):
os.unlink("/etc/nginx/sites-enabled/default")
2017-05-03 07:04:52 +00:00
if os.path.exists("/etc/systemd"):
if os.path.exists("/etc/systemd/system/certidude.service"):
click.echo("File /etc/systemd/system/certidude.service already exists, remove to regenerate")
else:
2017-05-03 07:04:52 +00:00
with open("/etc/systemd/system/certidude.service", "w") as fh:
fh.write(env.get_template("server/systemd.service").render(vars()))
click.echo("File /etc/systemd/system/certidude.service created")
os.system("systemctl daemon-reload")
else:
raise NotImplementedError("Not systemd based OS, don't know how to set up initscripts")
2017-05-03 07:04:52 +00:00
# Set umask to 0022
os.umask(0o022)
assert os.getuid() == 0 and os.getgid() == 0
bootstrap_pid = os.fork()
if not bootstrap_pid:
# Create what's usually /var/lib/certidude
if not os.path.exists(directory):
os.makedirs(directory)
assert os.stat(directory).st_mode == 0o40755
# Create bundle directories
bundle_js = os.path.join(assets_dir, "js", "bundle.js")
bundle_css = os.path.join(assets_dir, "css", "bundle.css")
for path in bundle_js, bundle_css:
subdir = os.path.dirname(path)
if not os.path.exists(subdir):
2018-01-03 22:34:52 +00:00
click.echo("Creating directory %s" % subdir)
os.makedirs(subdir)
if skip_assets:
click.echo("Not attempting to assemble assets as requested...")
else:
# Copy fonts
click.echo("Copying fonts...")
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir):
raise click.ClickException("Failed to copy fonts")
# Compile nunjucks templates
cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js)
click.echo("Compiling templates: %s" % cmd)
if os.system(cmd):
raise click.ClickException("Failed to compile nunjucks templates")
# Assemble bundle.js
click.echo("Assembling %s" % bundle_js)
with open(bundle_js + ".part", "a") as fh:
for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js":
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
click.echo("- Merging: %s" % j)
with open(j) as ih:
fh.write(ih.read())
# Assemble bundle.css
click.echo("Assembling %s" % bundle_css)
with open(bundle_css + ".part", "w") as fh:
for pkg in "tether/dist/css/*.min.css", "bootstrap/dist/css/*.min.*css", "font-awesome/css/font-awesome.min.css":
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
click.echo("- Merging: %s" % j)
with open(j) as ih:
fh.write(ih.read())
os.rename(bundle_css + ".part", bundle_css)
os.rename(bundle_js + ".part", bundle_js)
assert os.getuid() == 0 and os.getgid() == 0
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.setgid(gid)
# Generate Certidude server config
if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR)
if not os.path.exists(const.SCRIPT_DIR):
click.echo("Creating %s" % const.SCRIPT_DIR)
os.makedirs(const.SCRIPT_DIR)
os.umask(0o177) # 600
if not os.path.exists(dhparam_path):
cmd = "openssl", "dhparam", "-out", dhparam_path, str(const.KEY_SIZE)
subprocess.check_call(cmd)
if os.path.exists(tls_config.name):
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
else:
tls_config.write(env.get_template("nginx-tls.conf").render(locals()))
click.echo("Generated %s" % tls_config.name)
2018-01-03 22:12:02 +00:00
if os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
2017-05-25 19:20:29 +00:00
else:
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
2018-01-03 22:12:02 +00:00
with open(const.SERVER_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/server.conf").render(vars()))
2018-01-03 22:12:02 +00:00
click.echo("Generated %s" % const.SERVER_CONFIG_PATH)
2018-01-03 22:34:52 +00:00
# Create image builder config
if os.path.exists(const.BUILDER_CONFIG_PATH):
click.echo("Image builder config %s already exists, remove to regenerate" % const.BUILDER_CONFIG_PATH)
else:
with open(const.BUILDER_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/builder.conf").render(vars()))
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
# Create image builder site script
if os.path.exists(const.BUILDER_SITE_SCRIPT):
click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT)
else:
with open(const.BUILDER_SITE_SCRIPT, "w") as fh:
fh.write(env.get_template("server/site.sh").render(vars()))
click.echo("File %s created" % const.BUILDER_SITE_SCRIPT)
# Create signature profile config
if os.path.exists(const.PROFILE_CONFIG_PATH):
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
else:
with open(const.PROFILE_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/profile.conf").render(vars()))
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
# Create subdirectories with 770 permissions
os.umask(0o007)
for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"):
path = os.path.join(directory, subdir)
if not os.path.exists(path):
click.echo("Creating directory %s" % path)
os.mkdir(path)
else:
click.echo("Directory already exists %s" % path)
assert os.stat(path).st_mode == 0o40770, path
2017-07-27 21:53:11 +00:00
# Create SQLite database file with correct permissions
os.umask(0o117)
if not os.path.exists(sqlite_path):
with open(sqlite_path, "wb") as fh:
pass
2015-07-26 20:34:46 +00:00
# Generate and sign CA key
if not os.path.exists(ca_key) or subordinate and not os.path.exists(ca_req):
if elliptic_curve:
click.echo("Generating %s EC key for CA ..." % const.CURVE_NAME)
public_key, private_key = asymmetric.generate_pair("ec", curve=const.CURVE_NAME)
else:
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
# Set permission bits to 600
os.umask(0o177)
with open(ca_key, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None))
if subordinate:
builder = CSRBuilder(distinguished_name, public_key)
request = builder.build(private_key)
with open(ca_req + ".part", 'wb') as f:
f.write(pem_armor_csr(request))
os.rename(ca_req + ".part", ca_req)
if not os.path.exists(ca_cert):
if subordinate:
click.echo("Request has been written to %s" % ca_req)
click.echo()
click.echo(open(ca_req).read())
click.echo()
click.echo("Get it signed and insert signed certificate into %s" % ca_cert)
click.echo()
click.echo(" cat > %s" % ca_cert)
click.echo()
click.echo("Paste contents and press Ctrl-D, adjust permissions:")
click.echo()
click.echo(" chown root:root %s" % ca_cert)
click.echo(" chmod 0644 %s" % ca_cert)
click.echo()
click.echo("To finish setup procedure run 'certidude setup authority' again")
sys.exit(1) # stop this fork here with error
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
builder = CertificateBuilder(distinguished_name, public_key)
builder.self_signed = True
builder.ca = True
builder.serial_number = generate_serial()
builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE
builder.end_date = NOW + timedelta(days=authority_lifetime)
certificate = builder.build(private_key)
# Set permission bits to 640
os.umask(0o137)
with open(ca_cert, 'wb') as f:
f.write(pem_armor_certificate(certificate))
click.echo("Authority certificate written to: %s" % ca_cert)
sys.exit(0) # stop this fork here
else:
_, exitcode = os.waitpid(bootstrap_pid, 0)
if exitcode:
return 0
from certidude import authority
authority.self_enroll(skip_notify=True)
assert os.path.exists(self_key)
assert os.path.exists(os.path.join(directory, "signed", common_name) + ".pem")
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
assert os.stat(sqlite_path).st_mode == 0o100660
assert os.stat(ca_cert).st_mode == 0o100640
assert os.stat(ca_key).st_mode == 0o100600
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600
assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600
2018-01-03 22:12:02 +00:00
click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo()
click.echo("Use following commands to inspect the newly created files:")
click.echo()
click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert)
click.echo(" openssl rsa -check -in %s" % ca_key)
click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
click.echo()
click.echo("To inspect logs and issued tokens:")
click.echo()
click.echo(" echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
click.echo(" echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
click.echo()
click.echo("Enabling Certidude backend and nginx...")
os.system("systemctl enable certidude")
os.system("systemctl enable nginx")
click.echo("To (re)start services:")
click.echo()
click.echo(" systemctl restart certidude")
click.echo(" systemctl restart nginx")
click.echo()
return 0
2015-07-26 20:34:46 +00:00
@click.command("users", help="List users")
def certidude_users():
from certidude.user import User
admins = set(User.objects.filter_admins())
for user in User.objects.all():
click.echo("%s;%s;%s;%s;%s" % (
"admin" if user in admins else "user",
user.name, user.given_name, user.surname, user.mail))
2015-07-26 20:34:46 +00:00
@click.command("list", help="List certificates")
2015-10-28 08:51:52 +00:00
@click.option("--verbose", "-v", default=False, is_flag=True, help="Verbose output")
2015-07-26 20:34:46 +00:00
@click.option("--show-key-type", "-k", default=False, is_flag=True, help="Show key type and length")
@click.option("--show-path", "-p", default=False, is_flag=True, help="Show filesystem paths")
@click.option("--show-extensions", "-e", default=False, is_flag=True, help="Show X.509 Certificate Extensions")
2015-10-28 08:51:52 +00:00
@click.option("--hide-requests", "-h", default=False, is_flag=True, help="Hide signing requests")
@click.option("--show-signed", "-s", default=False, is_flag=True, help="Show signed certificates")
@click.option("--show-revoked", "-r", default=False, is_flag=True, help="Show revoked certificates")
def certidude_list(verbose, show_key_type, show_extensions, show_path, show_signed, show_revoked, hide_requests):
2015-10-28 08:51:52 +00:00
# Statuses:
# s - submitted
# v - valid
# e - expired
# y - not valid yet
# r - revoked
2017-04-13 20:52:09 +00:00
from humanize import naturaltime
from certidude import authority
def dump_common(common_name, path, cert):
click.echo("certidude revoke %s" % common_name)
with open(path, "rb") as fh:
buf = fh.read()
click.echo("md5sum: %s" % hashlib.md5(buf).hexdigest())
click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest())
click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
click.echo()
2015-10-28 08:51:52 +00:00
if not hide_requests:
for common_name, path, buf, csr, submitted, server in authority.list_requests():
created = 0
if not verbose:
click.echo("s " + path)
continue
click.echo()
click.echo(click.style(common_name, fg="blue"))
click.echo("=" * len(common_name))
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created, fg="white"))
click.echo("openssl req -in %s -text -noout" % path)
2017-03-28 09:24:51 +00:00
dump_common(common_name, path, csr)
if show_signed:
for common_name, path, buf, cert, signed, expires in authority.list_signed():
if not verbose:
if signed < NOW and NOW < expires:
click.echo("v " + path)
elif expires < NOW:
click.echo("e " + path)
2015-10-28 08:51:52 +00:00
else:
click.echo("y " + path)
continue
click.echo()
click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60))
if signed < NOW and NOW < expires:
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
elif NOW > expires:
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
else:
click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" % expires, fg="white"))
click.echo()
click.echo("openssl x509 -in %s -text -noout" % path)
dump_common(common_name, path, cert)
for ext in cert["tbs_certificate"]["extensions"]:
click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))
if show_revoked:
for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
if not verbose:
click.echo("r " + path)
continue
click.echo()
click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60))
click.echo("Status: " + click.style("revoked", fg="red") + " due to " + reason + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, fg="white")))
click.echo("openssl x509 -in %s -text -noout" % path)
dump_common(common_name, path, cert)
for ext in cert["tbs_certificate"]["extensions"]:
click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))
2015-07-12 19:22:10 +00:00
@click.command("sign", help="Sign certificate")
2015-07-26 20:34:46 +00:00
@click.argument("common_name")
@click.option("--profile", "-p", default="rw", help="Profile")
2015-07-26 20:34:46 +00:00
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
2018-03-03 13:54:31 +00:00
def certidude_sign(common_name, overwrite, profile):
from certidude import authority, config
drop_privileges()
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
@click.command("revoke", help="Revoke certificate")
@click.option("--reason", "-r", default="key_compromise", help="Revocation reason, one of: key_compromise affiliation_changed superseded cessation_of_operation privilege_withdrawn")
@click.argument("common_name")
def certidude_revoke(common_name, reason):
from certidude import authority
drop_privileges()
authority.revoke(common_name, reason)
2015-07-26 20:34:46 +00:00
@click.command("expire", help="Move expired certificates")
def certidude_expire():
from certidude import authority, config
threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE
for common_name, path, buf, cert, signed, expires in authority.list_signed():
if expires < threshold:
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
click.echo("Moving %s to %s" % (path, expired_path))
os.rename(path, expired_path)
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
if expires < threshold:
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
click.echo("Moving %s to %s" % (path, expired_path))
os.rename(path, expired_path)
# TODO: Send e-mail
2017-05-03 07:04:52 +00:00
@click.command("serve", help="Run server")
@click.option("-p", "--port", default=8080, help="Listen port")
2017-07-05 21:22:02 +00:00
@click.option("-l", "--listen", default="127.0.1.1", help="Listen address")
2017-03-13 15:20:41 +00:00
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
2017-07-11 18:57:19 +00:00
def certidude_serve(port, listen, fork):
from certidude import authority, const, push
if port == 80:
click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")
2018-01-03 22:12:02 +00:00
click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH)
2017-05-03 07:04:52 +00:00
2017-04-04 05:02:08 +00:00
log_handlers = []
from certidude import config
click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS)
click.echo("CRL subnets: %s" % config.CRL_SUBNETS)
click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS)
click.echo("Loading signature profiles:")
for profile in config.PROFILES.values():
click.echo("- %s" % profile)
click.echo()
2017-05-25 19:20:29 +00:00
# Rebuild reverse mapping
for cn, path, buf, cert, signed, expires in authority.list_signed():
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)
2017-05-25 19:20:29 +00:00
if not os.path.exists(by_serial):
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
os.symlink("../%s.pem" % cn, by_serial)
2017-05-01 22:32:55 +00:00
# Process directories
if not os.path.exists(const.RUN_DIR):
click.echo("Creating: %s" % const.RUN_DIR)
os.makedirs(const.RUN_DIR)
os.chmod(const.RUN_DIR, 0o755)
2017-05-01 23:06:45 +00:00
click.echo("Users subnets: %s" %
", ".join([str(j) for j in config.USER_SUBNETS]))
click.echo("Administrative subnets: %s" %
", ".join([str(j) for j in config.ADMIN_SUBNETS]))
click.echo("Auto-sign enabled for following subnets: %s" %
", ".join([str(j) for j in config.AUTOSIGN_SUBNETS]))
click.echo("Request submissions allowed from following subnets: %s" %
", ".join([str(j) for j in config.REQUEST_SUBNETS]))
2015-07-26 20:34:46 +00:00
2015-07-12 19:22:10 +00:00
click.echo("Serving API at %s:%d" % (listen, port))
from wsgiref.simple_server import make_server, WSGIServer
from certidude.api import certidude_app
2015-07-12 19:22:10 +00:00
2015-07-27 12:30:50 +00:00
2015-07-12 19:22:10 +00:00
click.echo("Listening on %s:%d" % (listen, port))
2015-07-26 20:34:46 +00:00
2017-04-25 21:10:12 +00:00
app = certidude_app(log_handlers)
httpd = make_server(listen, port, app, WSGIServer)
2015-09-03 09:00:45 +00:00
"""
Drop privileges
"""
# Initialize LDAP service ticket
if os.path.exists("/etc/cron.hourly/certidude"):
os.system("/etc/cron.hourly/certidude")
from certidude.push import EventSourceLogHandler
log_handlers.append(EventSourceLogHandler())
2017-04-04 05:02:08 +00:00
for j in logging.Logger.manager.loggerDict.values():
if isinstance(j, logging.Logger): # PlaceHolder is what?
if j.name.startswith("certidude."):
j.setLevel(logging.DEBUG)
for handler in log_handlers:
j.addHandler(handler)
2017-03-13 15:20:41 +00:00
if not fork or not os.fork():
pid = os.getpid()
with open(const.SERVER_PID_PATH, "w") as pidfile:
pidfile.write("%d\n" % pid)
2017-07-11 18:57:19 +00:00
push.publish("server-started")
logger.debug("Started Certidude at %s", const.FQDN)
2017-07-11 18:57:19 +00:00
drop_privileges()
try:
httpd.serve_forever()
except KeyboardInterrupt:
click.echo("Caught Ctrl-C, exiting...")
push.publish("server-stopped")
logger.debug("Shutting down Certidude")
return
2017-04-07 07:57:25 +00:00
@click.command("yubikey", help="Set up Yubikey as client authentication token")
@click.argument("authority")
@click.option("-p", "--pin", default="123456", help="Slot pincode, 123456 by default")
@click.option("-s", "--slot", default="9a", help="Yubikey slot to use, 9a by default")
@click.option("-u", "--username", default=os.getenv("USER"), help="Username to use, %s by default" % os.getenv("USER"))
def certidude_setup_yubikey(authority, slot, username, pin):
2017-04-13 20:30:28 +00:00
import requests
2017-04-07 07:57:25 +00:00
cmd = "ykinfo", "-q", "-s"
click.echo("Executing: %s" % " ".join(cmd))
serial = subprocess.check_output(cmd).strip()
dn = "/CN=%s@yk-%s-%s" % (username, slot, serial)
cmd = "yubico-piv-tool", "-a", "generate", "-s", slot, "-o", "/tmp/pk.pem"
click.echo("Executing: %s" % " ".join(cmd))
subprocess.call(cmd)
cmd = "yubico-piv-tool", \
"-i", "/tmp/pk.pem", "-o", "/tmp/req.pem", \
"-P", pin, \
"-S", dn, \
"-a", "verify", "-a", "request", \
"-s", slot
click.echo("Executing: %s" % " ".join(cmd))
scheme = "http"
request_url = "%s://%s/api/request/?wait=true" % (scheme, authority)
subprocess.check_output(cmd)
click.echo("Submitting to %s, waiting for response..." % request_url)
headers={
"Content-Type": "application/pkcs10",
"Accept": "application/x-x509-user-cert,application/x-pem-file"
}
submission = requests.post(request_url, data=open("/tmp/req.pem"), headers=headers)
with open("/tmp/cert.pem", "w") as fh:
fh.write(submission.text)
cmd = "yubico-piv-tool", "-a", "import-certificate", "-s", slot, "-i", "/tmp/cert.pem"
click.echo("Executing: %s" % " ".join(cmd))
subprocess.call(cmd)
2017-04-21 16:58:01 +00:00
@click.command("test", help="Test mailer")
@click.argument("recipient")
def certidude_test(recipient):
from certidude import mailer
mailer.send(
"test.md",
to=recipient
)
@click.command("list", help="List tokens")
def certidude_token_list():
from certidude import config
from certidude.tokens import TokenManager
token_manager = TokenManager(config.TOKEN_DATABASE)
cols = "uuid", "expires", "subject", "state"
now = datetime.utcnow()
for token in token_manager.list(expired=True, used=True):
token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now else "expired")
print(";".join([str(token.get(col)) for col in cols]))
@click.command("purge", help="Purge tokens")
@click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens")
def certidude_token_purge(all):
from certidude import config
from certidude.tokens import TokenManager
token_manager = TokenManager(config.TOKEN_DATABASE)
print(token_manager.purge(all))
@click.command("issue", help="Issue token")
@click.option("-m", "--subject-mail", default=None, help="Subject e-mail override")
@click.argument("subject")
def certidude_token_issue(subject, subject_mail):
from certidude import config
from certidude.tokens import TokenManager
from certidude.user import User
token_manager = TokenManager(config.TOKEN_DATABASE)
token_manager.issue(None, User.objects.get(subject), subject_mail)
2017-04-21 16:58:01 +00:00
2015-08-13 08:11:08 +00:00
@click.group("strongswan", help="strongSwan helpers")
def certidude_setup_strongswan(): pass
2015-07-26 20:34:46 +00:00
@click.group("openvpn", help="OpenVPN helpers")
def certidude_setup_openvpn(): pass
2015-07-12 19:22:10 +00:00
2015-07-26 20:34:46 +00:00
@click.group("setup", help="Getting started section")
def certidude_setup(): pass
2015-07-12 19:22:10 +00:00
@click.group("token", help="Token management")
def certidude_token(): pass
2015-07-12 19:22:10 +00:00
@click.group()
def entry_point(): pass
2015-08-13 08:11:08 +00:00
certidude_setup_strongswan.add_command(certidude_setup_strongswan_server)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_client)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager)
2015-07-26 20:34:46 +00:00
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
2015-07-26 20:34:46 +00:00
certidude_setup.add_command(certidude_setup_authority)
certidude_setup.add_command(certidude_setup_openvpn)
2015-08-13 08:11:08 +00:00
certidude_setup.add_command(certidude_setup_strongswan)
certidude_setup.add_command(certidude_setup_nginx)
2017-04-07 07:57:25 +00:00
certidude_setup.add_command(certidude_setup_yubikey)
certidude_token.add_command(certidude_token_list)
certidude_token.add_command(certidude_token_purge)
certidude_token.add_command(certidude_token_issue)
entry_point.add_command(certidude_token)
2015-07-26 20:34:46 +00:00
entry_point.add_command(certidude_setup)
entry_point.add_command(certidude_serve)
entry_point.add_command(certidude_enroll)
2015-07-26 20:34:46 +00:00
entry_point.add_command(certidude_sign)
entry_point.add_command(certidude_revoke)
2015-07-26 20:34:46 +00:00
entry_point.add_command(certidude_list)
entry_point.add_command(certidude_expire)
entry_point.add_command(certidude_users)
2017-04-21 16:58:01 +00:00
entry_point.add_command(certidude_test)
if __name__ == "__main__":
entry_point()