From 3c52fa7b69609ca8fcfc8de7426f7ebbcba493eb Mon Sep 17 00:00:00 2001 From: Justin Pettit Date: Wed, 15 Dec 2010 23:44:41 -0800 Subject: [PATCH] vswitch: Add support for IPsec certificate authentication. Previously, it was possible to fake configuring the use of certificate authentication for IPsec, but it really just used a static pre-shared key behind the scenes. This commit publicly mentions certificate authentication and finally does the real work behind the scenes. --- debian/ovs-monitor-ipsec | 314 +++++++++++++++++++++++++++------------ lib/netdev-vport.c | 30 +++- vswitchd/vswitch.xml | 46 ++++-- 3 files changed, 280 insertions(+), 110 deletions(-) diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec index 27c15e8c..00fcd3c4 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,199 @@ 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() + # 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) - # Clear out any pre-shared keys - self.commit_psk() - - 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 "certificate" not in vals: + raise error.Error("'certificate' not defined for %s" % host) + elif "private_key" not in vals: + # 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 +258,7 @@ class IPsec: self.sad_flush() self.spd_flush() self.racoon = Racoon() + self.entries = [] def call_setkey(self, cmds): try: @@ -190,38 +317,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//default;\n" % (local_ip, remote_ip)) - cmds += "\n" cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" % (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): @@ -266,6 +390,26 @@ 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 main(argv): try: options, args = getopt.gnu_getopt( @@ -306,44 +450,30 @@ def main(argv): new_interfaces = {} for rec in idl.data["Interface"].itervalues(): - name = rec.name.as_scalar() - ipsec_cert = rec.options.get("ipsec_cert") - ipsec_psk = rec.options.get("ipsec_psk") - is_ipsec = ipsec_cert or ipsec_psk - if rec.type.as_scalar() == "ipsec_gre": - if ipsec_cert or ipsec_psk: - 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 } - else: - s_log.warning( - "no ipsec_cert or ipsec_psk defined for %s" % name) - - 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(): - orig_vals = interfaces.get(name): - if orig_vals: - # Configuration for this host already exists. If - # it has changed, this is an error. - if vals != orig_vals: - s_log.warning( - "configuration changed for %s, need to delete " - "interface first" % name) - continue + name = rec.name.as_scalar() + peer_cert = rec.options.get("peer_cert") + psk = rec.options.get("psk") - if vals["ipsec_cert"]: - ipsec.ipsec_cert_update(vals["local_ip"], - vals["remote_ip"], vals["ipsec_cert"]) - else: - ipsec.ipsec_psk_update(vals["local_ip"], - vals["remote_ip"], vals["ipsec_psk"]) + if peer_cert and psk: + s_log.warning("both 'peer_cert' and 'psk' defined for %s" + % name) + continue + elif not peer_cert and not psk: + s_log.warning("no 'peer_cert' or 'psk' defined for %s" + % name) + continue + new_interfaces[name] = { + "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"), + "peer_cert": peer_cert, + "psk": psk } + + if interfaces != new_interfaces: + update_ipsec(ipsec, interfaces, new_interfaces) interfaces = new_interfaces if __name__ == '__main__': diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c index 681bc696..9ae21d12 100644 --- a/lib/netdev-vport.c +++ b/lib/netdev-vport.c @@ -514,19 +514,37 @@ parse_tunnel_config(const struct netdev_dev *dev, const struct shash *args, if (!strcmp(node->data, "false")) { config.flags &= ~TNL_F_HDR_CACHE; } - } else if ((!strcmp(node->name, "ipsec_cert") - || !strcmp(node->name, "ipsec_psk")) && is_ipsec) { + } else if (!strcmp(node->name, "peer_cert") && is_ipsec) { + if (shash_find(args, "certificate")) { + ipsec_mech_set = true; + } else { + VLOG_WARN("%s: 'peer_cert' requires 'certificate' argument", + name); + return EINVAL; + } + } else if (!strcmp(node->name, "psk") && is_ipsec) { ipsec_mech_set = true; + } else if (is_ipsec + && (!strcmp(node->name, "certificate") + || !strcmp(node->name, "private_key"))) { + /* Ignore options not used by the netdev. */ } else { VLOG_WARN("%s: unknown %s argument '%s'", name, type, node->name); } } - if (is_ipsec && !ipsec_mech_set) { - VLOG_WARN("%s: IPsec requires an 'ipsec_cert' or ipsec_psk' argument", - name); - return EINVAL; + if (is_ipsec) { + if (shash_find(args, "peer_cert") && shash_find(args, "psk")) { + VLOG_WARN("%s: cannot define both 'peer_cert' and 'psk'", name); + return EINVAL; + } + + if (!ipsec_mech_set) { + VLOG_WARN("%s: IPsec requires an 'peer_cert' or psk' argument", + name); + return EINVAL; + } } if (!config.daddr) { diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml index 4cc29da0..4aa46494 100644 --- a/vswitchd/vswitch.xml +++ b/vswitchd/vswitch.xml @@ -759,15 +759,16 @@
ipsec_gre
-
An Ethernet over RFC 2890 Generic Routing Encapsulation over - IPv4 IPsec tunnel. Each tunnel (including those of type - gre) must be uniquely identified by the - combination of remote_ip and - local_ip. Note that if two ports are defined - that are the same except one has an optional identifier and - the other does not, the more specific one is matched first. - The following options may be specified in the - column: +
An Ethernet over RFC 2890 Generic Routing Encapsulation + over IPv4 IPsec tunnel. Each tunnel (including those of type + gre) must be uniquely identified by the + combination of remote_ip and + local_ip. Note that if two ports are defined + that are the same except one has an optional identifier and + the other does not, the more specific one is matched first. + An authentication method of peer_cert or + psk must be defined. The following options may + be specified in the column:
remote_ip
Required. The tunnel endpoint.
@@ -778,9 +779,30 @@ match. Default is to match all addresses.
-
ipsec_psk
-
Required. Specifies a pre-shared key for authentication - that must be identical on both sides of the tunnel.
+
peer_cert
+
Required for certificate authentication. A string + containing the peer's certificate in PEM format. + Additionally the host's certificate must be specified + with the certificate option.
+
+
+
certificate
+
Required for certificate authentication. The name of a + PEM file containing a certificate that will be presented + to the peer during authentication.
+
+
+
private_key
+
Optional for certificate authentication. The name of + a PEM file containing the private key associated with + certificate. If certificate + contains the private key, this option may be omitted.
+
+
+
psk
+
Required for pre-shared key authentication. Specifies a + pre-shared key for authentication that must be identical on + both sides of the tunnel.
in_key
-- 2.30.2