Initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| __pycache__ | ||||
| *~ | ||||
| *.swp | ||||
							
								
								
									
										541
									
								
								cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										541
									
								
								cli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
							
								
								
									
										22
									
								
								const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import os | ||||
| import socket | ||||
|  | ||||
| RUN_DIR = "/run/certidude" | ||||
| CONFIG_DIR = "/etc/certidude" | ||||
| CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | ||||
| SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | ||||
|  | ||||
| RE_FQDN =  "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" | ||||
| RE_HOSTNAME =  "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" | ||||
| RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" | ||||
|  | ||||
| try: | ||||
|     FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] | ||||
| except socket.gaierror: | ||||
|     FQDN = socket.gethostname() | ||||
|  | ||||
| try: | ||||
|     HOSTNAME, DOMAIN = FQDN.split(".", 1) | ||||
| except ValueError: # If FQDN is not configured | ||||
|     HOSTNAME = FQDN | ||||
|     DOMAIN = None | ||||
							
								
								
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| asn1crypto | ||||
| certbuilder | ||||
| csrbuilder | ||||
| ipsecparse | ||||
		Reference in New Issue
	
	Block a user