1344 lines
62 KiB
Python
Executable File
1344 lines
62 KiB
Python
Executable File
# coding: utf-8
|
|
|
|
import asyncore
|
|
import click
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import pwd
|
|
import random
|
|
import re
|
|
import requests
|
|
import signal
|
|
import socket
|
|
import string
|
|
import subprocess
|
|
import sys
|
|
from configparser import ConfigParser, NoOptionError, NoSectionError
|
|
from certidude.helpers import certidude_request_certificate
|
|
from certidude.common import expand_paths, ip_address, ip_network
|
|
from cryptography import x509
|
|
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from datetime import datetime, timedelta
|
|
from humanize import naturaltime
|
|
from jinja2 import Environment, PackageLoader
|
|
from time import sleep
|
|
from setproctitle import setproctitle
|
|
import const
|
|
|
|
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
|
|
|
|
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
|
|
# https://kjur.github.io/jsrsasign/
|
|
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
|
|
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
|
|
|
|
# Parse command-line argument defaults from environment
|
|
|
|
USERNAME = os.environ.get("USER")
|
|
NOW = datetime.utcnow().replace(tzinfo=None)
|
|
FIRST_NAME = None
|
|
SURNAME = None
|
|
EMAIL = None
|
|
|
|
if USERNAME:
|
|
EMAIL = USERNAME + "@" + const.FQDN
|
|
|
|
if os.getuid() >= 1000:
|
|
_, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME)
|
|
if " " in gecos:
|
|
FIRST_NAME, SURNAME = gecos.split(" ", 1)
|
|
else:
|
|
FIRST_NAME = gecos
|
|
|
|
|
|
@click.command("request", help="Run processes for requesting certificates and configuring services")
|
|
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
|
|
def certidude_request(fork):
|
|
if not os.path.exists(const.CLIENT_CONFIG_PATH):
|
|
click.echo("No %s!" % const.CLIENT_CONFIG_PATH)
|
|
return 1
|
|
|
|
if not os.path.exists(const.SERVICES_CONFIG_PATH):
|
|
click.echo("No %s!" % const.SERVICES_CONFIG_PATH)
|
|
return 1
|
|
|
|
clients = ConfigParser()
|
|
clients.readfp(open(const.CLIENT_CONFIG_PATH))
|
|
|
|
service_config = ConfigParser()
|
|
service_config.readfp(open(const.SERVICES_CONFIG_PATH))
|
|
|
|
# Process directories
|
|
run_dir = "/run/certidude"
|
|
|
|
# Prepare signer PID-s directory
|
|
if not os.path.exists(run_dir):
|
|
click.echo("Creating: %s" % run_dir)
|
|
os.makedirs(run_dir)
|
|
|
|
for authority in clients.sections():
|
|
try:
|
|
endpoint_common_name = clients.get(authority, "common name")
|
|
except NoOptionError:
|
|
endpoint_common_name = const.HOSTNAME
|
|
try:
|
|
endpoint_key_path = clients.get(authority, "key path")
|
|
except NoOptionError:
|
|
endpoint_key_path = "/var/lib/certidude/%s/keys/%s.pem" % (authority, const.HOSTNAME)
|
|
try:
|
|
endpoint_request_path = clients.get(authority, "request path")
|
|
except NoOptionError:
|
|
endpoint_request_path = "/var/lib/certidude/%s/requests/%s.pem" % (authority, const.HOSTNAME)
|
|
try:
|
|
endpoint_certificate_path = clients.get(authority, "certificate path")
|
|
except NoOptionError:
|
|
endpoint_certificate_path = "/var/lib/certidude/%s/signed/%s.pem" % (authority, const.HOSTNAME)
|
|
try:
|
|
endpoint_authority_path = clients.get(authority, "authority path")
|
|
except NoOptionError:
|
|
endpoint_authority_path = "/var/lib/certidude/%s/ca_crt.pem" % authority
|
|
try:
|
|
endpoint_revocations_path = clients.get(authority, "revocations path")
|
|
except NoOptionError:
|
|
endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority
|
|
# TODO: Create directories automatically
|
|
|
|
extended_key_usage_flags=[]
|
|
try:
|
|
endpoint_key_flags = set([j.strip() for j in clients.get(authority, "extended key usage flags").lower().split(",") if j.strip()])
|
|
except NoOptionError:
|
|
pass
|
|
else:
|
|
if "server auth" in endpoint_key_flags:
|
|
endpoint_key_flags -= set(["server auth"])
|
|
extended_key_usage_flags.append(ExtendedKeyUsageOID.SERVER_AUTH)
|
|
if "ike intermediate" in endpoint_key_flags:
|
|
endpoint_key_flags -= set(["ike intermediate"])
|
|
extended_key_usage_flags.append(x509.ObjectIdentifier("1.3.6.1.5.5.8.2.2"))
|
|
if endpoint_key_flags:
|
|
raise ValueError("Extended key usage flags %s not understood!" % endpoint_key_flags)
|
|
# TODO: IKE Intermediate
|
|
|
|
if clients.get(authority, "trigger") == "domain joined":
|
|
if not os.path.exists("/etc/krb5.keytab"):
|
|
continue
|
|
elif clients.get(authority, "trigger") != "interface up":
|
|
continue
|
|
|
|
pid_path = os.path.join(run_dir, authority + ".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())
|
|
retries = 30
|
|
|
|
while retries > 0:
|
|
try:
|
|
certidude_request_certificate(
|
|
authority,
|
|
endpoint_key_path,
|
|
endpoint_request_path,
|
|
endpoint_certificate_path,
|
|
endpoint_authority_path,
|
|
endpoint_revocations_path,
|
|
endpoint_common_name,
|
|
extended_key_usage_flags,
|
|
None,
|
|
autosign=True,
|
|
wait=True)
|
|
break
|
|
except requests.exceptions.Timeout:
|
|
retries -= 1
|
|
continue
|
|
|
|
for endpoint in service_config.sections():
|
|
if service_config.get(endpoint, "authority") != authority:
|
|
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")
|
|
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
|
|
config = loads(open('/etc/ipsec.conf').read())
|
|
if config["conn"][server]["left"] == "%defaultroute":
|
|
config["conn"][server]["auto"] = "start" # This is client
|
|
else:
|
|
config["conn"][server]["auto"] = "add" # This is server
|
|
with open("/etc/ipsec.conf.part", "w") as fh:
|
|
fh.write(config.dumps())
|
|
os.rename("/etc/ipsec.conf.part", "/etc/ipsec.conf")
|
|
|
|
# Regenerate /etc/ipsec.secrets
|
|
with open("/etc/ipsec.secrets.part", "w") as fh:
|
|
for filename in os.listdir("/etc/ipsec.d/private"):
|
|
if not filename.endswith(".pem"):
|
|
continue
|
|
fh.write(": RSA /etc/ipsec.d/private/%s\n" % filename)
|
|
os.rename("/etc/ipsec.secrets.part", "/etc/ipsec.secrets")
|
|
|
|
# Attempt to reload config or start if it's not running
|
|
if os.system("ipsec update") == 130:
|
|
os.system("ipsec start")
|
|
continue
|
|
|
|
# OpenVPN set up with NetworkManager
|
|
if service_config.get(endpoint, "service") == "network-manager/openvpn":
|
|
nm_config = ConfigParser()
|
|
nm_config.add_section("connection")
|
|
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", "yes")
|
|
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"))
|
|
nm_config.set("vpn", "key", endpoint_key_path)
|
|
nm_config.set("vpn", "cert", endpoint_certificate_path)
|
|
nm_config.set("vpn", "ca", endpoint_authority_path)
|
|
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")
|
|
|
|
# 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)
|
|
continue
|
|
|
|
|
|
# IPSec set up with NetworkManager
|
|
if service_config.get(endpoint, "service") == "network-manager/strongswan":
|
|
client_config = ConfigParser()
|
|
nm_config.add_section("connection")
|
|
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"))
|
|
nm_config.set("vpn", "userkey", endpoint_key_path)
|
|
nm_config.set("vpn", "usercert", endpoint_certificate_path)
|
|
nm_config.set("vpn", "certificate", endpoint_authority_path)
|
|
nm_config.add_section("ipv4")
|
|
nm_config.set("ipv4", "method", "auto")
|
|
|
|
# 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)
|
|
|
|
# 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)
|
|
continue
|
|
|
|
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
|
|
|
|
os.unlink(pid_path)
|
|
|
|
|
|
@click.command("client", help="Setup X.509 certificates for application")
|
|
@click.argument("server")
|
|
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, '%s' by default" % const.HOSTNAME)
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
|
|
@click.option("--given-name", "-gn", default=FIRST_NAME, help="Given name of the person associted with the certificate, '%s' by default" % FIRST_NAME)
|
|
@click.option("--surname", "-sn", default=SURNAME, help="Surname of the person associted with the certificate, '%s' by default" % SURNAME)
|
|
@click.option("--key-usage", "-ku", help="Key usage attributes, none requested by default")
|
|
@click.option("--extended-key-usage", "-eku", help="Extended key usage attributes, none requested by default")
|
|
@click.option("--quiet", "-q", default=False, is_flag=True, help="Disable verbose output")
|
|
@click.option("--autosign", "-s", default=False, is_flag=True, help="Request for automatic signing if available")
|
|
@click.option("--wait", "-w", default=False, is_flag=True, help="Wait for certificate, by default return immideately")
|
|
@click.option("--key-path", "-k", default=const.HOSTNAME + ".key", help="Key path, %s.key by default" % const.HOSTNAME)
|
|
@click.option("--request-path", "-r", default=const.HOSTNAME + ".csr", help="Request path, %s.csr by default" % const.HOSTNAME)
|
|
@click.option("--certificate-path", "-c", default=const.HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % const.HOSTNAME)
|
|
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default")
|
|
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
|
|
def certidude_setup_client(quiet, **kwargs):
|
|
return certidude_request_certificate(**kwargs)
|
|
|
|
|
|
@click.command("server", help="Set up OpenVPN server")
|
|
@click.argument("authority")
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
|
|
@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")
|
|
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
|
|
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
|
|
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
|
@click.option("--config", "-o",
|
|
default="/etc/openvpn/site-to-client.conf",
|
|
type=click.File(mode="w", atomic=True, lazy=True),
|
|
help="OpenVPN configuration file")
|
|
def certidude_setup_openvpn_server(authority, config, subnet, route, email_address, org_unit, local, proto, port):
|
|
|
|
# TODO: Make dirs
|
|
# TODO: Intelligent way of getting last IP address in the subnet
|
|
subnet_first = None
|
|
subnet_last = None
|
|
subnet_second = None
|
|
for addr in subnet.hosts():
|
|
if not subnet_first:
|
|
subnet_first = addr
|
|
continue
|
|
if not subnet_second:
|
|
subnet_second = addr
|
|
subnet_last = addr
|
|
|
|
# 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.set(authority, "trigger", "interface up")
|
|
client_config.set(authority, "common name", const.HOSTNAME)
|
|
client_config.set(authority, "subject alternative name dns", const.FQDN)
|
|
client_config.set(authority, "extended key usage flags", "server auth")
|
|
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
|
|
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
|
|
# Create corresponding section in /etc/certidude/services.conf
|
|
endpoint = "OpenVPN server %s of %s" % (const.FQDN, 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", 'wb') 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))
|
|
|
|
dhparam_path = "/etc/openvpn/keys/dhparam.pem"
|
|
if not os.path.exists(dhparam_path):
|
|
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
|
|
subprocess.check_call(cmd)
|
|
|
|
config.write("mode server\n")
|
|
config.write("tls-server\n")
|
|
config.write("proto %s\n" % proto)
|
|
config.write("port %d\n" % port)
|
|
config.write("dev tap\n")
|
|
config.write("local %s\n" % local)
|
|
config.write("key %s\n" % client_config.get(authority, "key path"))
|
|
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
|
|
config.write("ca %s\n" % client_config.get(authority, "authority path"))
|
|
config.write("dh %s\n" % 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")
|
|
config.write("ifconfig %s 255.255.255.0\n" % subnet_first)
|
|
config.write("server-bridge %s 255.255.255.0 %s %s\n" % (subnet_first, subnet_second, subnet_last))
|
|
config.write("#crl-verify %s\n" % client_config.get(authority, "revocations path"))
|
|
|
|
click.echo("Generated %s" % config.name)
|
|
click.echo("Inspect generated files and issue following to request certificate:")
|
|
click.echo()
|
|
click.echo(" certidude request")
|
|
click.echo()
|
|
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
|
click.echo("use following command to sign on Certidude server instead of web interface:")
|
|
click.echo()
|
|
click.echo(" certidude sign %s" % const.HOSTNAME)
|
|
|
|
|
|
@click.command("nginx", help="Set up nginx as HTTPS server")
|
|
@click.argument("server")
|
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@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("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
|
|
@click.option("--key-path", "-key", default=const.HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % const.HOSTNAME)
|
|
@click.option("--request-path", "-csr", default=const.HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % const.HOSTNAME)
|
|
@click.option("--certificate-path", "-crt", default=const.HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % const.HOSTNAME)
|
|
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
|
|
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
|
|
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
|
|
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
|
|
@expand_paths()
|
|
def certidude_setup_nginx(authority, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
|
|
# TODO: Intelligent way of getting last IP address in the subnet
|
|
|
|
if not os.path.exists(certificate_path):
|
|
click.echo("As HTTPS server certificate needs specific key usage extensions please")
|
|
click.echo("use following command to sign on Certidude server instead of web interface:")
|
|
click.echo()
|
|
click.echo(" certidude sign %s" % common_name)
|
|
click.echo()
|
|
retval = certidude_request_certificate(authority, key_path, request_path,
|
|
certificate_path, authority_path, revocations_path, common_name, org_unit,
|
|
extended_key_usage_flags = [ExtendedKeyUsageOID.SERVER_AUTH],
|
|
dns = const.FQDN, wait=True, bundle=True)
|
|
|
|
if not os.path.exists(dhparam_path):
|
|
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
|
|
subprocess.check_call(cmd)
|
|
|
|
if retval:
|
|
return retval
|
|
|
|
context = globals() # Grab const.BLAH
|
|
context.update(locals())
|
|
|
|
if os.path.exists(site_client_config.name):
|
|
click.echo("Configuration file %s already exists, not overwriting" % site_client_config.name)
|
|
else:
|
|
site_client_config.write(env.get_template("nginx-https-site.conf").render(context))
|
|
click.echo("Generated %s" % site_client_config.name)
|
|
|
|
if os.path.exists(tls_client_config.name):
|
|
click.echo("Configuration file %s already exists, not overwriting" % tls_client_config.name)
|
|
else:
|
|
tls_client_config.write(env.get_template("nginx-tls.conf").render(context))
|
|
click.echo("Generated %s" % tls_client_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_client_config.name, "/etc/nginx/sites-enabled"),
|
|
os.path.basename(site_client_config.name)))
|
|
click.secho(" service nginx restart", bold=True)
|
|
click.echo()
|
|
|
|
|
|
@click.command("client", help="Set up OpenVPN client")
|
|
@click.argument("authority")
|
|
@click.argument("remote")
|
|
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@click.option("--config", "-o",
|
|
default="/etc/openvpn/client-to-site.conf",
|
|
type=click.File(mode="w", atomic=True, lazy=True),
|
|
help="OpenVPN configuration file")
|
|
def certidude_setup_openvpn_client(authority, remote, config, org_unit, proto):
|
|
|
|
# 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", const.HOSTNAME)
|
|
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
|
|
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
# Create corresponding section in /etc/certidude/services.conf
|
|
endpoint = "OpenVPN 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/openvpn")
|
|
with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') 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)
|
|
config.write("dev tap\n")
|
|
config.write("nobind\n")
|
|
config.write("key %s\n" % client_config.get(authority, "key path"))
|
|
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
|
|
config.write("ca %s\n" % client_config.get(authority, "authority path"))
|
|
config.write("crl-verify %s\n" % client_config.get(authority, "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")
|
|
|
|
click.echo("Generated %s" % config.name)
|
|
click.echo("Inspect generated files and issue following to request certificate:")
|
|
click.echo()
|
|
click.echo(" certidude request")
|
|
click.echo()
|
|
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
|
click.echo("use following command to sign on Certidude server instead of web interface:")
|
|
click.echo()
|
|
click.echo(" certidude sign %s" % const.HOSTNAME)
|
|
|
|
|
|
@click.command("server", help="Set up strongSwan server")
|
|
@click.argument("server")
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
|
|
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
|
@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
|
|
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
|
def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, email_address, org_unit, local, fqdn):
|
|
if "." not in common_name:
|
|
raise ValueError("Hostname has to be fully qualified!")
|
|
if not local:
|
|
raise ValueError("Please specify local IP address")
|
|
|
|
# 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(server):
|
|
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
|
|
else:
|
|
client_config.set(authority, "trigger", "interface up")
|
|
client_config.set(authority, "common name", const.FQDN)
|
|
client_config.set(authority, "subject alternative name dns", const.FQDN)
|
|
client_config.set(authority, "extended key usage flags", "server auth, ike intermediate")
|
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/crls/ca.pem")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
# Create corresponding section to /etc/ipsec.conf
|
|
from ipsecparse import loads
|
|
config = loads(open('/etc/ipsec.conf').read())
|
|
config["conn", server] = dict(
|
|
leftsourceip="%config",
|
|
left=common_name,
|
|
leftcert=certificate_path,
|
|
leftsubnet=route.join(", "),
|
|
right="%any",
|
|
rightsourceip=subnet,
|
|
keyexchange="ikev2",
|
|
keyingtries="300",
|
|
dpdaction=dpdaction,
|
|
closeaction="restart",
|
|
auto="ignore")
|
|
with open("/etc/ipsec.conf.part", "w") as fh:
|
|
fh.write(client_config.dumps())
|
|
os.rename("/etc/ipsec.conf.part", "/etc/ipsec.conf")
|
|
|
|
click.echo()
|
|
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")
|
|
|
|
|
|
@click.command("client", help="Set up strongSwan client")
|
|
@click.argument("server")
|
|
@click.argument("remote")
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction):
|
|
# Create corresponding section in /etc/certidude/client.conf
|
|
client_config = ConfigParser()
|
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
|
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
|
|
if client_config.has_section(server):
|
|
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", const.HOSTNAME)
|
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/crls/ca.pem")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
# Create corresponding section in /etc/ipsec.conf
|
|
from ipsecparse import loads
|
|
config = loads(open('/etc/ipsec.conf').read())
|
|
config["conn", server] = dict(
|
|
leftsourceip="%config",
|
|
left="%defaultroute",
|
|
leftcert=certificate_path,
|
|
rightid="%any",
|
|
right=remote,
|
|
rightsubnet=route,
|
|
keyexchange="ikev2",
|
|
keyingtries="300",
|
|
dpdaction="restart",
|
|
closeaction="restart",
|
|
auto="ignore")
|
|
with open("/etc/ipsec.conf.part", "w") as fh:
|
|
fh.write(client_config.dumps())
|
|
os.rename("/etc/ipsec.conf.part", "/etc/ipsec.conf")
|
|
|
|
click.echo("Generated section %s in %s" % (authority, client_config.name))
|
|
click.echo("Run 'certidude request' to request certificates and to enable services")
|
|
|
|
|
|
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
|
|
@click.argument("server") # Certidude server
|
|
@click.argument("remote") # StrongSwan gateway
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
|
|
endpoint = "IPSec to %s" % remote
|
|
|
|
# Create corresponding section in /etc/certidude/client.conf
|
|
client_config = ConfigParser()
|
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
|
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
|
|
if client_config.has_section(server):
|
|
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", const.HOSTNAME)
|
|
client_config.set(authority, "org unit", org_unit)
|
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/crls/ca.pem")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
# Create corresponding section in /etc/certidude/services.conf
|
|
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)
|
|
service_config.set(authority, "authority", server)
|
|
service_config.set(authority, "remote", remote)
|
|
service_config.set(authority, "service", "network-manager/strongswan-client")
|
|
with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') 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("server") # Certidude server
|
|
@click.argument("remote") # OpenVPN gateway
|
|
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
|
|
@click.option("--org-unit", "-ou", help="Organizational unit")
|
|
@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
|
|
def certidude_setup_openvpn_networkmanager(authority, email_address, org_unit, remote):
|
|
# Create corresponding section in /etc/certidude/client.conf
|
|
client_config = ConfigParser()
|
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
|
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
|
|
if client_config.has_section(server):
|
|
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", const.HOSTNAME)
|
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/cacerts/ca.pem")
|
|
client_config.set(authority, "authority path", "/etc/ipsec.d/crls/ca.pem")
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
|
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))
|
|
|
|
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)
|
|
service_config.set(authority, "authority", server)
|
|
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")
|
|
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
|
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Path to Certidude's static JS/CSS/etc")
|
|
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default")
|
|
@click.option("--nginx-config", "-n",
|
|
default="/etc/nginx/sites-available/certidude.conf",
|
|
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("--parent", "-p", help="Parent CA, none by default")
|
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, fully qualified hostname by default")
|
|
@click.option("--country", "-c", default=None, help="Country, none by default")
|
|
@click.option("--state", "-s", default=None, help="State or country, none by default")
|
|
@click.option("--locality", "-l", default=None, help="City or locality, none by default")
|
|
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 days (20 years) by default")
|
|
@click.option("--certificate-lifetime", default=5*365, help="Certificate lifetime in days, 1825 days (5 years) by default")
|
|
@click.option("--revocation-list-lifetime", default=20*60, help="Revocation list lifetime in days, 1200 seconds (20 minutes) by default")
|
|
@click.option("--organization", "-o", default=None, help="Company or organization name")
|
|
@click.option("--organizational-unit", "-ou", default=None)
|
|
@click.option("--revoked-url", default=None, help="CRL distribution URL")
|
|
@click.option("--certificate-url", default=None, help="Authority certificate URL")
|
|
@click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN)
|
|
@click.option("--email-address", default="certidude@" + const.FQDN, help="E-mail address of the CA")
|
|
@click.option("--directory", help="Directory for authority files")
|
|
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
|
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
|
def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags):
|
|
|
|
if not directory:
|
|
if os.getuid():
|
|
directory = os.path.join(os.path.expanduser("~/.certidude"), const.FQDN)
|
|
else:
|
|
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
|
|
|
click.echo("Using fully qualified hostname: %s" % common_name)
|
|
|
|
# Expand variables
|
|
if not revoked_url:
|
|
revoked_url = "http://%s/api/revoked/" % common_name
|
|
if not certificate_url:
|
|
certificate_url = "http://%s/api/certificate/" % common_name
|
|
ca_key = os.path.join(directory, "ca_key.pem")
|
|
ca_crt = os.path.join(directory, "ca_crt.pem")
|
|
|
|
if not static_path.endswith("/"):
|
|
static_path += "/"
|
|
|
|
if os.getuid() == 0:
|
|
try:
|
|
pwd.getpwnam("certidude")
|
|
except KeyError:
|
|
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
|
|
if subprocess.call(cmd):
|
|
click.echo("Failed to create system user 'certidude'")
|
|
return 255
|
|
|
|
if os.path.exists(kerberos_keytab):
|
|
click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
|
|
else:
|
|
click.echo("To use 'kerberos' authentication backend join the domain and create service principal with:")
|
|
click.echo()
|
|
click.echo(" KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab)
|
|
click.echo(" chown %s %s" % (username, kerberos_keytab))
|
|
click.echo()
|
|
|
|
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(".")])
|
|
with open("/etc/cron.hourly/certidude", "w") as fh:
|
|
fh.write(env.get_template("ldap-ticket-renewal.sh").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")
|
|
|
|
|
|
working_directory = os.path.realpath(os.path.dirname(__file__))
|
|
certidude_path = sys.argv[0]
|
|
|
|
if not os.path.exists("/etc/nginx"):
|
|
click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration")
|
|
listen = "0.0.0.0"
|
|
port = "80"
|
|
else:
|
|
nginx_client_config.write(env.get_template("nginx.conf").render(vars()))
|
|
click.echo("Generated: %s" % nginx_client_config.name)
|
|
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_client_config.name)
|
|
if os.path.exists("/etc/nginx/sites-enabled/default"):
|
|
os.unlink("/etc/nginx/sites-enabled/default")
|
|
if not push_server:
|
|
click.echo("Remember to install nchan instead of regular nginx!")
|
|
|
|
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:
|
|
with open("/etc/systemd/system/certidude.service", "w") as fh:
|
|
fh.write(env.get_template("systemd.service").render(vars()))
|
|
click.echo("File /etc/systemd/system/certidude.service created")
|
|
else:
|
|
NotImplemented # No systemd
|
|
|
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
|
os.setgid(gid)
|
|
else:
|
|
click.echo("Not root, skipping user and system config creation")
|
|
|
|
if not os.path.exists(const.CONFIG_DIR):
|
|
click.echo("Creating %s" % const.CONFIG_DIR)
|
|
os.makedirs(const.CONFIG_DIR)
|
|
|
|
if os.path.exists(const.CONFIG_PATH):
|
|
click.echo("Configuration file %s already exists, remove to regenerate" % const.CONFIG_PATH)
|
|
else:
|
|
os.umask(0o137)
|
|
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
|
|
with open(const.CONFIG_PATH, "w") as fh:
|
|
fh.write(env.get_template("certidude-server.conf").render(vars()))
|
|
click.echo("Generated %s" % const.CONFIG_PATH)
|
|
|
|
if os.path.lexists(directory):
|
|
click.echo("CA directory %s already exists, remove to regenerate" % directory)
|
|
else:
|
|
click.echo("CA configuration files are saved to: {}".format(directory))
|
|
|
|
click.echo("Generating 4096-bit RSA key...")
|
|
|
|
key = rsa.generate_private_key(
|
|
public_exponent=65537,
|
|
key_size=4096,
|
|
backend=default_backend()
|
|
)
|
|
|
|
subject = issuer = x509.Name([
|
|
x509.NameAttribute(o, value) for o, value in (
|
|
(NameOID.COUNTRY_NAME, country),
|
|
(NameOID.STATE_OR_PROVINCE_NAME, state),
|
|
(NameOID.LOCALITY_NAME, locality),
|
|
(NameOID.ORGANIZATION_NAME, organization),
|
|
(NameOID.COMMON_NAME, common_name),
|
|
) if value
|
|
])
|
|
|
|
builder = x509.CertificateBuilder(
|
|
).subject_name(subject
|
|
).issuer_name(issuer
|
|
).public_key(key.public_key()
|
|
).not_valid_before(datetime.utcnow()
|
|
).not_valid_after(
|
|
datetime.utcnow() + timedelta(days=authority_lifetime)
|
|
).serial_number(1
|
|
).add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True,
|
|
).add_extension(x509.KeyUsage(
|
|
digital_signature=True,
|
|
key_encipherment=False,
|
|
content_commitment=False,
|
|
data_encipherment=False,
|
|
key_agreement=False,
|
|
key_cert_sign=True,
|
|
crl_sign=True,
|
|
encipher_only=False,
|
|
decipher_only=False), critical=True,
|
|
).add_extension(
|
|
x509.SubjectAlternativeName([
|
|
x509.DNSName(common_name),
|
|
x509.RFC822Name(email_address)
|
|
]),
|
|
critical=False,
|
|
).add_extension(
|
|
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
|
|
critical=False
|
|
).add_extension(
|
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()),
|
|
critical=False
|
|
|
|
)
|
|
|
|
if server_flags:
|
|
builder = builder.add_extension(x509.ExtendedKeyUsage([
|
|
ExtendedKeyUsageOID.SERVER_AUTH,
|
|
x509.ObjectIdentifier("1.3.6.1.5.5.8.2.2")]), critical=False)
|
|
|
|
cert = builder.sign(key, hashes.SHA512(), default_backend())
|
|
|
|
click.echo("Signing %s..." % cert.subject)
|
|
|
|
# Create authority directory with 750 permissions
|
|
os.umask(0o027)
|
|
if not os.path.exists(directory):
|
|
os.makedirs(directory)
|
|
|
|
# Create subdirectories with 770 permissions
|
|
os.umask(0o007)
|
|
for subdir in ("signed", "requests", "revoked", "expired"):
|
|
if not os.path.exists(os.path.join(directory, subdir)):
|
|
os.mkdir(os.path.join(directory, subdir))
|
|
|
|
# Set permission bits to 640
|
|
os.umask(0o137)
|
|
with open(ca_crt, "wb") as fh:
|
|
fh.write(cert.public_bytes(serialization.Encoding.PEM))
|
|
|
|
# Set permission bits to 600
|
|
os.umask(0o177)
|
|
with open(ca_key, "wb") as fh:
|
|
fh.write(key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption() # TODO: Implement passphrase
|
|
))
|
|
|
|
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_crt)
|
|
click.echo(" openssl rsa -check -in %s" % ca_key)
|
|
click.echo(" openssl verify -CAfile %s %s" % (ca_crt, ca_crt))
|
|
click.echo()
|
|
click.echo("Use following command to serve CA read-only:")
|
|
click.echo()
|
|
click.echo(" certidude serve")
|
|
|
|
|
|
@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():
|
|
print "%s;%s;%s;%s;%s" % (
|
|
"admin" if user in admins else "user",
|
|
user.name, user.given_name, user.surname, user.mail)
|
|
|
|
|
|
@click.command("list", help="List certificates")
|
|
@click.option("--verbose", "-v", default=False, is_flag=True, help="Verbose output")
|
|
@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")
|
|
@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):
|
|
# Statuses:
|
|
# s - submitted
|
|
# v - valid
|
|
# e - expired
|
|
# y - not valid yet
|
|
# r - revoked
|
|
|
|
from certidude import authority
|
|
from pycountry import countries
|
|
|
|
def dump_common(j):
|
|
|
|
person = [j for j in (j.given_name, j.surname) if j]
|
|
if person:
|
|
click.echo("Associated person: %s" % " ".join(person) + (" <%s>" % j.email_address if j.email_address else ""))
|
|
elif j.email_address:
|
|
click.echo("Associated e-mail: " + j.email_address)
|
|
|
|
bits = [j for j in (
|
|
countries.get(alpha2=j.country_code.upper()).name if
|
|
j.country_code else "",
|
|
j.state_or_county,
|
|
j.city,
|
|
j.organization,
|
|
j.organizational_unit) if j]
|
|
if bits:
|
|
click.echo("Organization: %s" % ", ".join(bits))
|
|
|
|
if show_key_type:
|
|
click.echo("Key type: %s-bit %s" % (j.key_length, j.key_type))
|
|
|
|
if show_extensions:
|
|
for key, value, data in j.extensions:
|
|
click.echo(("Extension " + key + ":").ljust(50) + " " + value)
|
|
else:
|
|
if j.key_usage:
|
|
click.echo("Key usage: " + j.key_usage)
|
|
if j.fqdn:
|
|
click.echo("Associated hostname: " + j.fqdn)
|
|
|
|
|
|
if not hide_requests:
|
|
for j in authority.list_requests():
|
|
|
|
if not verbose:
|
|
click.echo("s " + j.path + " " + j.identity)
|
|
continue
|
|
click.echo(click.style(j.common_name, fg="blue"))
|
|
click.echo("=" * len(j.common_name))
|
|
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(j.created) + click.style(", %s" %j.created, fg="white"))
|
|
|
|
dump_common(j)
|
|
|
|
# Calculate checksums for cross-checking
|
|
import hashlib
|
|
md5sum = hashlib.md5()
|
|
sha1sum = hashlib.sha1()
|
|
sha256sum = hashlib.sha256()
|
|
with open(j.path, "rb") as fh:
|
|
buf = fh.read()
|
|
md5sum.update(buf)
|
|
sha1sum.update(buf)
|
|
sha256sum.update(buf)
|
|
click.echo("MD5 checksum: %s" % md5sum.hexdigest())
|
|
click.echo("SHA-1 checksum: %s" % sha1sum.hexdigest())
|
|
click.echo("SHA-256 checksum: %s" % sha256sum.hexdigest())
|
|
|
|
if show_path:
|
|
click.echo("Details: openssl req -in %s -text -noout" % j.path)
|
|
click.echo("Sign: certidude sign %s" % j.path)
|
|
click.echo()
|
|
|
|
if show_signed:
|
|
for j in authority.list_signed():
|
|
if not verbose:
|
|
if j.signed < NOW and j.expires > NOW:
|
|
click.echo("v " + j.path + " " + j.identity)
|
|
elif NOW > j.expires:
|
|
click.echo("e " + j.path + " " + j.identity)
|
|
else:
|
|
click.echo("y " + j.path + " " + j.identity)
|
|
continue
|
|
|
|
click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white"))
|
|
click.echo("="*(len(j.common_name)+60))
|
|
|
|
if j.signed < NOW and j.expires > NOW:
|
|
click.echo("Status: \u2713 " + click.style("valid", fg="green") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires, fg="white"))
|
|
elif NOW > j.expires:
|
|
click.echo("Status: \u2717 " + click.style("expired", fg="red") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires, fg="white"))
|
|
else:
|
|
click.echo("Status: \u2717 " + click.style("not valid yet", fg="red") + click.style(", %s" %j.expires, fg="white"))
|
|
dump_common(j)
|
|
|
|
if show_path:
|
|
click.echo("Details: openssl x509 -in %s -text -noout" % j.path)
|
|
click.echo("Revoke: certidude revoke %s" % j.path)
|
|
click.echo()
|
|
|
|
if show_revoked:
|
|
for j in authority.list_revoked():
|
|
if not verbose:
|
|
click.echo("r " + j.path + " " + j.identity)
|
|
continue
|
|
click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white"))
|
|
click.echo("="*(len(j.common_name)+60))
|
|
click.echo("Status: \u2717 " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-j.changed), click.style(", %s" % j.changed, fg="white")))
|
|
dump_common(j)
|
|
if show_path:
|
|
click.echo("Details: openssl x509 -in %s -text -noout" % j.path)
|
|
click.echo()
|
|
|
|
click.echo()
|
|
|
|
|
|
@click.command("sign", help="Sign certificates")
|
|
@click.argument("common_name")
|
|
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
|
@click.option("--lifetime", "-l", help="Lifetime")
|
|
def certidude_sign(common_name, overwrite, lifetime):
|
|
from certidude import authority, config
|
|
request = authority.get_request(common_name)
|
|
|
|
# Use signer if this is regular client CSR
|
|
if request.signable:
|
|
# Sign via signer process
|
|
cert = authority.sign(request)
|
|
|
|
# Sign directly if it's eg. TLS server CSR
|
|
else:
|
|
# Load CA private key and certificate
|
|
private_key = serialization.load_pem_private_key(
|
|
open(config.AUTHORITY_PRIVATE_KEY_PATH).read(),
|
|
password=None, # TODO: Ask password for private key?
|
|
backend=default_backend())
|
|
authority_certificate = x509.load_pem_x509_certificate(
|
|
open(config.AUTHORITY_CERTIFICATE_PATH).read(),
|
|
backend=default_backend())
|
|
|
|
# Drop privileges
|
|
# to use LDAP service ticket to read usernames of the admins group
|
|
# in order to send e-mail
|
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
|
os.setgroups([])
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
|
|
# Sign directly using private key
|
|
cert = authority.sign2(request, private_key, authority_certificate,
|
|
overwrite, True, lifetime)
|
|
|
|
|
|
@click.command("serve", help="Run server")
|
|
@click.option("-p", "--port", default=8080 if os.getuid() else 80, help="Listen port")
|
|
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
|
def certidude_serve(port, listen):
|
|
from certidude.signer import SignServer
|
|
from certidude import const
|
|
click.echo("Using configuration from: %s" % const.CONFIG_PATH)
|
|
|
|
|
|
from certidude import config
|
|
|
|
# Fetch UID, GID of certidude user
|
|
import pwd
|
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
|
restricted_groups = []
|
|
restricted_groups.append(gid)
|
|
|
|
"""
|
|
Spawn signer process
|
|
"""
|
|
|
|
child_pid = os.fork()
|
|
|
|
if child_pid:
|
|
pass
|
|
else:
|
|
click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH))
|
|
setproctitle("[signer]")
|
|
|
|
with open(const.SIGNER_PID_PATH, "w") as fh:
|
|
fh.write("%d\n" % os.getpid())
|
|
|
|
logging.basicConfig(
|
|
filename=const.SIGNER_LOG_PATH,
|
|
level=logging.INFO)
|
|
|
|
os.umask(0o007)
|
|
server = SignServer()
|
|
|
|
# Drop privileges
|
|
if not os.getuid():
|
|
os.chown(const.SIGNER_SOCKET_PATH, uid, gid)
|
|
os.chmod(const.SIGNER_SOCKET_PATH, 0770)
|
|
|
|
click.echo("Dropping privileges of signer")
|
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody")
|
|
os.setgroups([])
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
else:
|
|
click.echo("Not dropping privileges of signer process")
|
|
|
|
asyncore.loop()
|
|
return
|
|
|
|
|
|
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]))
|
|
|
|
logging.basicConfig(
|
|
filename=const.SERVER_LOG_PATH,
|
|
level=logging.DEBUG)
|
|
|
|
click.echo("Serving API at %s:%d" % (listen, port))
|
|
from wsgiref.simple_server import make_server, WSGIServer
|
|
from SocketServer import ThreadingMixIn, ForkingMixIn
|
|
from certidude.api import certidude_app, StaticResource
|
|
|
|
class ThreadingWSGIServer(ForkingMixIn, WSGIServer):
|
|
pass
|
|
|
|
click.echo("Listening on %s:%d" % (listen, port))
|
|
|
|
app = certidude_app()
|
|
app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static")))
|
|
|
|
httpd = make_server(listen, port, app, ThreadingWSGIServer)
|
|
|
|
|
|
"""
|
|
Drop privileges
|
|
"""
|
|
|
|
if os.getuid() == 0:
|
|
|
|
# Initialize LDAP service ticket
|
|
if os.path.exists("/etc/cron.hourly/certidude"):
|
|
os.system("/etc/cron.hourly/certidude")
|
|
|
|
# Drop privileges
|
|
if config.AUTHENTICATION_BACKENDS == {"pam"}:
|
|
# PAM needs access to /etc/shadow
|
|
import grp
|
|
name, passwd, num, mem = grp.getgrnam("shadow")
|
|
click.echo("Adding current user to shadow group due to PAM authentication backend")
|
|
restricted_groups.append(num)
|
|
|
|
os.setgroups(restricted_groups)
|
|
os.setgid(gid)
|
|
os.setuid(uid)
|
|
|
|
click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" %
|
|
("certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()])))
|
|
|
|
os.umask(0o007)
|
|
|
|
|
|
# Set up log handlers
|
|
log_handlers = []
|
|
|
|
if config.LOGGING_BACKEND == "sql":
|
|
from certidude.mysqllog import LogHandler
|
|
from certidude.api.log import LogResource
|
|
uri = config.cp.get("logging", "database")
|
|
log_handlers.append(LogHandler(uri))
|
|
app.add_route("/api/log/", LogResource(uri))
|
|
elif config.LOGGING_BACKEND == "syslog":
|
|
from logging.handlers import SyslogHandler
|
|
log_handlers.append(SysLogHandler())
|
|
# Browsing syslog via HTTP is obviously not possible out of the box
|
|
elif config.LOGGING_BACKEND:
|
|
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
|
|
|
|
if config.PUSH_PUBLISH:
|
|
from certidude.push import PushLogHandler
|
|
log_handlers.append(PushLogHandler())
|
|
|
|
for facility in "api", "cli":
|
|
logger = logging.getLogger(facility)
|
|
logger.setLevel(logging.DEBUG)
|
|
for handler in log_handlers:
|
|
logger.addHandler(handler)
|
|
|
|
import atexit
|
|
|
|
def exit_handler():
|
|
logging.getLogger("cli").debug("Shutting down Certidude")
|
|
|
|
atexit.register(exit_handler)
|
|
|
|
logging.getLogger("cli").debug("Started Certidude at %s", const.FQDN)
|
|
print "Ready"
|
|
httpd.serve_forever()
|
|
|
|
@click.group("strongswan", help="strongSwan helpers")
|
|
def certidude_setup_strongswan(): pass
|
|
|
|
@click.group("openvpn", help="OpenVPN helpers")
|
|
def certidude_setup_openvpn(): pass
|
|
|
|
@click.group("setup", help="Getting started section")
|
|
def certidude_setup(): pass
|
|
|
|
@click.group("signer", help="Signer process management")
|
|
def certidude_signer(): pass
|
|
|
|
@click.group()
|
|
def entry_point(): pass
|
|
|
|
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)
|
|
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)
|
|
certidude_setup.add_command(certidude_setup_authority)
|
|
certidude_setup.add_command(certidude_setup_openvpn)
|
|
certidude_setup.add_command(certidude_setup_strongswan)
|
|
certidude_setup.add_command(certidude_setup_client)
|
|
certidude_setup.add_command(certidude_setup_nginx)
|
|
entry_point.add_command(certidude_setup)
|
|
entry_point.add_command(certidude_serve)
|
|
entry_point.add_command(certidude_signer)
|
|
entry_point.add_command(certidude_request)
|
|
entry_point.add_command(certidude_sign)
|
|
entry_point.add_command(certidude_list)
|
|
entry_point.add_command(certidude_users)
|
|
|
|
if __name__ == "__main__":
|
|
entry_point()
|