|
|
|
@ -0,0 +1,541 @@ |
|
|
|
|
# coding: utf-8 |
|
|
|
|
|
|
|
|
|
import click |
|
|
|
|
import const |
|
|
|
|
import hashlib |
|
|
|
|
import logging |
|
|
|
|
import os |
|
|
|
|
import random |
|
|
|
|
import re |
|
|
|
|
import signal |
|
|
|
|
import string |
|
|
|
|
import socket |
|
|
|
|
import subprocess |
|
|
|
|
import sys |
|
|
|
|
import requests |
|
|
|
|
from jinja2 import Environment, PackageLoader |
|
|
|
|
from ipsecparse import loads |
|
|
|
|
from asn1crypto import pem, x509 |
|
|
|
|
from asn1crypto.csr import CertificationRequest |
|
|
|
|
from certbuilder import CertificateBuilder, pem_armor_certificate |
|
|
|
|
from csrbuilder import CSRBuilder, pem_armor_csr |
|
|
|
|
from configparser import ConfigParser, NoOptionError |
|
|
|
|
from datetime import datetime, timedelta |
|
|
|
|
from oscrypto import asymmetric |
|
|
|
|
|
|
|
|
|
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.read_file(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.read_file(fh) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.command("provision", help="Add endpoint to Certidude client config") |
|
|
|
|
@click.argument("authority") |
|
|
|
|
def certidude_provision(authority): |
|
|
|
|
client_config = ConfigParser() |
|
|
|
|
if os.path.exists(const.CLIENT_CONFIG_PATH): |
|
|
|
|
client_config.read_file(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: |
|
|
|
|
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH)) |
|
|
|
|
b = os.path.join(os.path.join(const.CONFIG_DIR, "authority", authority)) |
|
|
|
|
client_config.add_section(authority) |
|
|
|
|
client_config.set(authority, "trigger", "interface up") |
|
|
|
|
client_config.set(authority, "common name", "$HOSTNAME") |
|
|
|
|
client_config.set(authority, "request path", os.path.join(b, "host_req.pem")) |
|
|
|
|
client_config.set(authority, "key path", os.path.join(b, "host_key.pem")) |
|
|
|
|
client_config.set(authority, "certificate path", os.path.join(b, "host_cert.pem")) |
|
|
|
|
client_config.set(authority, "authority path", os.path.join(b, "ca_cert.pem")) |
|
|
|
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'w') as fh: |
|
|
|
|
client_config.write(fh) |
|
|
|
|
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.command("enroll", help="Run processes for requesting certificates and configuring services") |
|
|
|
|
@click.option("-k", "--kerberos", default=False, is_flag=True, help="Offer system keytab for auth") |
|
|
|
|
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") |
|
|
|
|
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign") |
|
|
|
|
def certidude_enroll(fork, no_wait, kerberos): |
|
|
|
|
try: |
|
|
|
|
os.makedirs(const.RUN_DIR) |
|
|
|
|
except FileExistsError: |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
context = globals() |
|
|
|
|
context.update(locals()) |
|
|
|
|
|
|
|
|
|
if not os.path.exists(const.CLIENT_CONFIG_PATH): |
|
|
|
|
click.echo("Client not configured, so not going to enroll") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
clients = ConfigTreeParser(const.CLIENT_CONFIG_PATH) |
|
|
|
|
service_config = ConfigTreeParser(const.SERVICES_CONFIG_PATH) |
|
|
|
|
|
|
|
|
|
for authority_name in clients.sections(): |
|
|
|
|
try: |
|
|
|
|
trigger = clients.get(authority_name, "trigger") |
|
|
|
|
except NoOptionError: |
|
|
|
|
trigger = "interface up" |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
kerberos = True |
|
|
|
|
elif trigger == "interface up": |
|
|
|
|
pass |
|
|
|
|
else: |
|
|
|
|
raise |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
######################### |
|
|
|
|
### Fork if requested ### |
|
|
|
|
######################### |
|
|
|
|
|
|
|
|
|
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()) |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
authority_path = clients.get(authority_name, "authority path") |
|
|
|
|
except NoOptionError: |
|
|
|
|
authority_path = "/etc/certidude/authority/%s/ca_cert.pem" % authority_name |
|
|
|
|
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) |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
raise |
|
|
|
|
# raise ValueError("Failed to parse PEM: %s" % r.text) |
|
|
|
|
authority_partial = authority_path + ".part" |
|
|
|
|
with open(authority_partial, "wb") as oh: |
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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 |
|
|
|
|
|
|
|
|
|
# Firefox (?) on Debian, Ubuntu |
|
|
|
|
if os.path.exists("/usr/bin/update-ca-certificates") or os.path.exists("/usr/sbin/update-ca-certificates"): |
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
################################ |
|
|
|
|
### 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 |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
############################################## |
|
|
|
|
### 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 |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
autosign = clients.getboolean(authority_name, "autosign") |
|
|
|
|
except NoOptionError: |
|
|
|
|
autosign = True |
|
|
|
|
|
|
|
|
|
if not os.path.exists(certificate_path): |
|
|
|
|
# Set up URL-s |
|
|
|
|
request_params = set() |
|
|
|
|
request_params.add("autosign=%s" % ("yes" if autosign else "no")) |
|
|
|
|
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 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") |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
# 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) |
|
|
|
|
os.unlink(pid_path) |
|
|
|
|
continue |
|
|
|
|
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) |
|
|
|
|
else: |
|
|
|
|
submission.raise_for_status() |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
header, _, certificate_der_bytes = pem.unarmor(submission.content) |
|
|
|
|
cert = x509.Certificate.load(certificate_der_bytes) |
|
|
|
|
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"]) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
else: |
|
|
|
|
click.echo("Certificate found at %s and no renewal requested" % certificate_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
################################## |
|
|
|
|
### Configure related services ### |
|
|
|
|
################################## |
|
|
|
|
|
|
|
|
|
for endpoint in service_config.sections(): |
|
|
|
|
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": |
|
|
|
|
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 |
|
|
|
|
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 |
|
|
|
|
if os.path.exists("/usr/sbin/strongswan"): # wtf fedora |
|
|
|
|
if os.system("strongswan update"): |
|
|
|
|
os.system("strongswan start") |
|
|
|
|
else: |
|
|
|
|
if os.system("ipsec update"): |
|
|
|
|
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") |
|
|
|
|
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", "no") |
|
|
|
|
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")) |
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
if os.path.exists("/run/NetworkManager"): |
|
|
|
|
os.system("nmcli con reload") |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# IPSec set up with NetworkManager |
|
|
|
|
if service_config.get(endpoint, "service") == "network-manager/strongswan": |
|
|
|
|
client_config = ConfigParser() |
|
|
|
|
nm_config = ConfigParser() |
|
|
|
|
nm_config.add_section("connection") |
|
|
|
|
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")) |
|
|
|
|
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") |
|
|
|
|
|
|
|
|
|
# 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) |
|
|
|
|
if os.path.exists("/run/NetworkManager"): |
|
|
|
|
os.system("nmcli con reload") |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
# TODO: Puppet, OpenLDAP, <insert awesomeness here> |
|
|
|
|
click.echo("Unknown service: %s" % service_config.get(endpoint, "service")) |
|
|
|
|
os.unlink(pid_path) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.group() |
|
|
|
|
def entry_point(): pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
entry_point.add_command(certidude_enroll) |
|
|
|
|
entry_point.add_command(certidude_provision) |
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
entry_point() |