X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;f=debian%2Fovs-monitor-ipsec;h=17f399767c60a5475245bb47e8c88b73411e48a3;hb=e977ba19df6210a5ccd46dd61151c804eeeafc78;hp=1cea8009b2ec45cde2a4ad96c4dc03c116b570c3;hpb=e97a10342018f992634fa90d25c007eb60c25662;p=sliver-openvswitch.git diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec index 1cea8009b..17f399767 100755 --- a/debian/ovs-monitor-ipsec +++ b/debian/ovs-monitor-ipsec @@ -20,13 +20,15 @@ # xxx To-do: # - Doesn't actually check that Interface is connected to bridge -# - Doesn't support cert authentication +# - If a certificate is badly formed, Racoon will refuse to start. We +# should do a better job of verifying certificates are valid before +# adding an interface to racoon.conf. import getopt +import glob import logging, logging.handlers import os -import stat import subprocess import sys @@ -53,75 +55,202 @@ setkey = "/usr/sbin/setkey" class Racoon: # Default locations for files conf_file = "/etc/racoon/racoon.conf" - cert_file = "/etc/racoon/certs" + cert_dir = "/etc/racoon/certs" psk_file = "/etc/racoon/psk.txt" - # Default racoon configuration file we use for IKE - conf_template = """# Configuration file generated by Open vSwitch + # Racoon configuration header we use for IKE + conf_header = """# Configuration file generated by Open vSwitch # # Do not modify by hand! -path pre_shared_key "/etc/racoon/psk.txt"; -path certificate "/etc/racoon/certs"; +path pre_shared_key "%s"; +path certificate "%s"; -remote anonymous { +""" + + # Racoon configuration footer we use for IKE + conf_footer = """sainfo anonymous { + pfs_group 2; + lifetime time 1 hour; + encryption_algorithm aes; + authentication_algorithm hmac_sha1, hmac_md5; + compression_algorithm deflate; +} + +""" + + # Certificate entry template. + cert_entry = """remote %s { exchange_mode main; nat_traversal on; + certificate_type x509 "%s" "%s"; + my_identifier asn1dn; + peers_identifier asn1dn; + peers_certfile x509 "%s"; + verify_identifier on; proposal { encryption_algorithm aes; hash_algorithm sha1; - authentication_method pre_shared_key; + authentication_method rsasig; dh_group 2; } } -sainfo anonymous { - pfs_group 2; - lifetime time 1 hour; - encryption_algorithm aes; - authentication_algorithm hmac_sha1, hmac_md5; - compression_algorithm deflate; +""" + + # Pre-shared key template. + psk_entry = """remote %s { + exchange_mode main; + nat_traversal on; + proposal { + encryption_algorithm aes; + hash_algorithm sha1; + authentication_method pre_shared_key; + dh_group 2; + } } + """ def __init__(self): self.psk_hosts = {} self.cert_hosts = {} - # Replace racoon's conf file with our template - f = open(Racoon.conf_file, "w") - f.write(Racoon.conf_template) - f.close() + if not os.path.isdir(self.cert_dir): + os.mkdir(self.cert_dir) - # Clear out any pre-shared keys - self.commit_psk() + # Clean out stale peer certs from previous runs + for ovs_cert in glob.glob("%s/ovs-*.pem" % self.cert_dir): + try: + os.remove(ovs_cert) + except OSError: + s_log.warning("couldn't remove %s" % ovs_cert) - self.reload() + # Replace racoon's conf file with our template + self.commit() def reload(self): exitcode = subprocess.call(["/etc/init.d/racoon", "reload"]) if exitcode != 0: - s_log.warning("couldn't reload racoon") + # Racoon is finicky about it's configuration file and will + # refuse to start if it sees something it doesn't like + # (e.g., a certificate file doesn't exist). Try restarting + # the process before giving up. + s_log.warning("attempting to restart racoon") + exitcode = subprocess.call(["/etc/init.d/racoon", "restart"]) + if exitcode != 0: + s_log.warning("couldn't reload racoon") + + def commit(self): + # Rewrite the Racoon configuration file + conf_file = open(self.conf_file, 'w') + conf_file.write(Racoon.conf_header % (self.psk_file, self.cert_dir)) + + for host, vals in self.cert_hosts.iteritems(): + conf_file.write(Racoon.cert_entry % (host, vals["certificate"], + vals["private_key"], vals["peer_cert_file"])) + + for host in self.psk_hosts: + conf_file.write(Racoon.psk_entry % host) + + conf_file.write(Racoon.conf_footer) + conf_file.close() + + # Rewrite the pre-shared keys file; it must only be readable by root. + orig_umask = os.umask(0077) + psk_file = open(Racoon.psk_file, 'w') + os.umask(orig_umask) + + psk_file.write("# Generated by Open vSwitch...do not modify by hand!") + psk_file.write("\n\n") + for host, vals in self.psk_hosts.iteritems(): + psk_file.write("%s %s\n" % (host, vals["psk"])) + psk_file.close() - def commit_psk(self): - f = open(Racoon.psk_file, 'w') - - # The file must only be accessible by root - os.chmod(Racoon.psk_file, stat.S_IRUSR | stat.S_IWUSR) + self.reload() - f.write("# Generated by Open vSwitch...do not modify by hand!\n\n") - for host, psk in self.psk_hosts.iteritems(): - f.write("%s %s\n" % (host, psk)) - f.close() + def _add_psk(self, host, psk): + if host in self.cert_hosts: + raise error.Error("host %s already defined for cert" % host) - def add_psk(self, host, psk): self.psk_hosts[host] = psk - self.commit_psk() - - def del_psk(self, host): + self.commit() + + def _verify_certs(self, vals): + # Racoon will refuse to start if the certificate files don't + # exist, so verify that they're there. + if not os.path.isfile(vals["certificate"]): + raise error.Error("'certificate' file does not exist: %s" + % vals["certificate"]) + elif not os.path.isfile(vals["private_key"]): + raise error.Error("'private_key' file does not exist: %s" + % vals["private_key"]) + + # Racoon won't start if a given certificate or private key isn't + # valid. This is a weak test, but will detect the most flagrant + # errors. + if vals["peer_cert"].find("-----BEGIN CERTIFICATE-----") == -1: + raise error.Error("'peer_cert' is not in valid PEM format") + + cert = open(vals["certificate"]).read() + if cert.find("-----BEGIN CERTIFICATE-----") == -1: + raise error.Error("'certificate' is not in valid PEM format") + + cert = open(vals["private_key"]).read() + if cert.find("-----BEGIN RSA PRIVATE KEY-----") == -1: + raise error.Error("'private_key' is not in valid PEM format") + + + def _add_cert(self, host, vals): if host in self.psk_hosts: + raise error.Error("host %s already defined for psk" % host) + + if vals["certificate"] == None: + raise error.Error("'certificate' not defined for %s" % host) + elif vals["private_key"] == None: + # Assume the private key is stored in the same PEM file as + # the certificate. We make a copy of "vals" so that we don't + # modify the original "vals", which would cause the script + # to constantly think that the configuration has changed + # in the database. + vals = vals.copy() + vals["private_key"] = vals["certificate"] + + self._verify_certs(vals) + + # The peer's certificate comes to us in PEM format as a string. + # Write that string to a file for Racoon to use. + peer_cert_file = "%s/ovs-%s.pem" % (self.cert_dir, host) + f = open(peer_cert_file, "w") + f.write(vals["peer_cert"]) + f.close() + + vals["peer_cert_file"] = peer_cert_file + + self.cert_hosts[host] = vals + self.commit() + + def _del_cert(self, host): + peer_cert_file = self.cert_hosts[host]["peer_cert_file"] + del self.cert_hosts[host] + self.commit() + try: + os.remove(peer_cert_file) + except OSError: + pass + + def add_entry(self, host, vals): + if vals["peer_cert"]: + self._add_cert(host, vals) + elif vals["psk"]: + self._add_psk(host, vals) + + def del_entry(self, host): + if host in self.cert_hosts: + self._del_cert(host) + elif host in self.psk_hosts: del self.psk_hosts[host] - self.commit_psk() + self.commit() # Class to configure IPsec on a system using racoon for IKE and setkey @@ -132,6 +261,7 @@ class IPsec: self.sad_flush() self.spd_flush() self.racoon = Racoon() + self.entries = [] def call_setkey(self, cmds): try: @@ -190,38 +320,35 @@ class IPsec: self.call_setkey("spdflush;") def spd_add(self, local_ip, remote_ip): - cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;" % + cmds = ("spdadd %s %s gre -P out ipsec esp/transport//require;\n" % (local_ip, remote_ip)) - cmds += "\n" - cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" % + cmds += ("spdadd %s %s gre -P in ipsec esp/transport//require;" % (remote_ip, local_ip)) self.call_setkey(cmds) def spd_del(self, local_ip, remote_ip): - cmds = "spddelete %s %s gre -P out;" % (local_ip, remote_ip) - cmds += "\n" + cmds = "spddelete %s %s gre -P out;\n" % (local_ip, remote_ip) cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip) self.call_setkey(cmds) - def ipsec_cert_del(self, local_ip, remote_ip): - # Need to support cert...right now only PSK supported - self.racoon.del_psk(remote_ip) - self.spd_del(local_ip, remote_ip) - self.sad_del(local_ip, remote_ip) + def add_entry(self, local_ip, remote_ip, vals): + if remote_ip in self.entries: + raise error.Error("host %s already configured for ipsec" + % remote_ip) - def ipsec_cert_update(self, local_ip, remote_ip, cert): - # Need to support cert...right now only PSK supported - self.racoon.add_psk(remote_ip, "abc12345") + self.racoon.add_entry(remote_ip, vals) self.spd_add(local_ip, remote_ip) - def ipsec_psk_del(self, local_ip, remote_ip): - self.racoon.del_psk(remote_ip) - self.spd_del(local_ip, remote_ip) - self.sad_del(local_ip, remote_ip) + self.entries.append(remote_ip) - def ipsec_psk_update(self, local_ip, remote_ip, psk): - self.racoon.add_psk(remote_ip, psk) - self.spd_add(local_ip, remote_ip) + + def del_entry(self, local_ip, remote_ip): + if remote_ip in self.entries: + self.racoon.del_entry(remote_ip) + self.spd_del(local_ip, remote_ip) + self.sad_del(local_ip, remote_ip) + + self.entries.remove(remote_ip) def keep_table_columns(schema, table_name, column_types): @@ -247,6 +374,8 @@ def keep_table_columns(schema, table_name, column_types): def monitor_uuid_schema_cb(schema): string_type = types.Type(types.BaseType(types.StringType)) + optional_ssl_type = types.Type(types.BaseType(types.UuidType, + ref_table='SSL'), None, 0, 1) string_map_type = types.Type(types.BaseType(types.StringType), types.BaseType(types.StringType), 0, sys.maxint) @@ -255,8 +384,12 @@ def monitor_uuid_schema_cb(schema): new_tables["Interface"] = keep_table_columns( schema, "Interface", {"name": string_type, "type": string_type, - "options": string_map_type, - "other_config": string_map_type}) + "options": string_map_type}) + new_tables["Open_vSwitch"] = keep_table_columns( + schema, "Open_vSwitch", {"ssl": optional_ssl_type}) + new_tables["SSL"] = keep_table_columns( + schema, "SSL", {"certificate": string_type, + "private_key": string_type}) schema.tables = new_tables def usage(): @@ -267,6 +400,35 @@ def usage(): print " -h, --help display this help message" sys.exit(0) +def update_ipsec(ipsec, interfaces, new_interfaces): + for name, vals in interfaces.iteritems(): + if name not in new_interfaces: + ipsec.del_entry(vals["local_ip"], vals["remote_ip"]) + + for name, vals in new_interfaces.iteritems(): + orig_vals = interfaces.get(name) + if orig_vals: + # Configuration for this host already exists. Check if it's + # changed. + if vals == orig_vals: + continue + else: + ipsec.del_entry(vals["local_ip"], vals["remote_ip"]) + + try: + ipsec.add_entry(vals["local_ip"], vals["remote_ip"], vals) + except error.Error, msg: + s_log.warning("skipping ipsec config for %s: %s" % (name, msg)) + +def get_ssl_cert(data): + for ovs_rec in data["Open_vSwitch"].itervalues(): + if ovs_rec.ssl.as_list(): + ssl_rec = data["SSL"][ovs_rec.ssl.as_scalar()] + return (ssl_rec.certificate.as_scalar(), + ssl_rec.private_key.as_scalar()) + + return None + def main(argv): try: options, args = getopt.gnu_getopt( @@ -304,43 +466,45 @@ def main(argv): idl.wait(poller) poller.block() continue + + ssl_cert = get_ssl_cert(idl.data) new_interfaces = {} for rec in idl.data["Interface"].itervalues(): - name = rec.name.as_scalar() - ipsec_cert = rec.other_config.get("ipsec_cert") - ipsec_psk = rec.other_config.get("ipsec_psk") - is_ipsec = ipsec_cert or ipsec_psk - - if rec.type.as_scalar() == "gre" and is_ipsec: - new_interfaces[name] = { - "remote_ip": rec.options.get("remote_ip"), - "local_ip": rec.options.get("local_ip", "0.0.0.0/0"), - "ipsec_cert": ipsec_cert, - "ipsec_psk": ipsec_psk } - - if interfaces != new_interfaces: - for name, vals in interfaces.items(): - if name not in new_interfaces.keys(): - ipsec.ipsec_cert_del(vals["local_ip"], vals["remote_ip"]) - for name, vals in new_interfaces.items(): - if vals == interfaces.get(name): - s_log.warning( - "configuration changed for %s, need to delete " - "interface first" % name) + if rec.type.as_scalar() == "ipsec_gre": + name = rec.name.as_scalar() + entry = { + "remote_ip": rec.options.get("remote_ip"), + "local_ip": rec.options.get("local_ip", "0.0.0.0/0"), + "certificate": rec.options.get("certificate"), + "private_key": rec.options.get("private_key"), + "use_ssl_cert": rec.options.get("use_ssl_cert"), + "peer_cert": rec.options.get("peer_cert"), + "psk": rec.options.get("psk") } + + if entry["peer_cert"] and entry["psk"]: + s_log.warning("both 'peer_cert' and 'psk' defined for %s" + % name) continue - - if vals["ipsec_cert"]: - ipsec.ipsec_cert_update(vals["local_ip"], - vals["remote_ip"], vals["ipsec_cert"]) - elif vals["ipsec_psk"]: - ipsec.ipsec_psk_update(vals["local_ip"], - vals["remote_ip"], vals["ipsec_psk"]) - else: - s_log.warning( - "no ipsec_cert or ipsec_psk defined for %s" % name) + elif not entry["peer_cert"] and not entry["psk"]: + s_log.warning("no 'peer_cert' or 'psk' defined for %s" + % name) continue + # The "use_ssl_cert" option is deprecated and will + # likely go away in the near future. + if entry["use_ssl_cert"] == "true": + if not ssl_cert: + s_log.warning("no valid SSL entry for %s" % name) + continue + + entry["certificate"] = ssl_cert[0] + entry["private_key"] = ssl_cert[1] + + new_interfaces[name] = entry + + if interfaces != new_interfaces: + update_ipsec(ipsec, interfaces, new_interfaces) interfaces = new_interfaces if __name__ == '__main__':