From patchwork Wed Jun 27 17:58:43 2018 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Qiuyu Xiao X-Patchwork-Id: 935655 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=openvswitch.org (client-ip=140.211.169.12; helo=mail.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=) Authentication-Results: ozlabs.org; dmarc=fail (p=none dis=none) header.from=gmail.com Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="NFsOBLn8"; dkim-atps=neutral Received: from mail.linuxfoundation.org (mail.linuxfoundation.org [140.211.169.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 41G9gr3yvQz9s2g for ; Thu, 28 Jun 2018 04:00:56 +1000 (AEST) Received: from mail.linux-foundation.org (localhost [127.0.0.1]) by mail.linuxfoundation.org (Postfix) with ESMTP id 5C6B1D65; Wed, 27 Jun 2018 17:59:34 +0000 (UTC) X-Original-To: ovs-dev@openvswitch.org Delivered-To: ovs-dev@mail.linuxfoundation.org Received: from smtp1.linuxfoundation.org (smtp1.linux-foundation.org [172.17.192.35]) by mail.linuxfoundation.org (Postfix) with ESMTPS id 0A691D5E for ; Wed, 27 Jun 2018 17:59:32 +0000 (UTC) X-Greylist: whitelisted by SQLgrey-1.7.6 Received: from mail-pl0-f54.google.com (mail-pl0-f54.google.com [209.85.160.54]) by smtp1.linuxfoundation.org (Postfix) with ESMTPS id 79A0E17E for ; Wed, 27 Jun 2018 17:59:29 +0000 (UTC) Received: by mail-pl0-f54.google.com with SMTP id k1-v6so1413080plt.2 for ; Wed, 27 Jun 2018 10:59:29 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id:in-reply-to:references; bh=HLgt+x57dc8VSzWVlwtOs+nXwMnrhFqlLChiACsMNic=; b=NFsOBLn8TUxg/gxvY3BgOvPZ8a9TH/9v2YtRpE1VkACXeu+1L++DadlCGCEVWIqER9 uD+tuH0w13TdSa+nO5wITvz4w6BIb6TkpvOIBL/NGk/kjmS1hKNAej9Pq9GxJR/gRtLi FK0Moe5oGK6zzEscDLjbRpoeL/8DzkRu4Td8U9wTSk0S/FCdrkTsMb8HkKiqoUTG59HK /6lkwMnACQcbOqfWHH5cae8HeF5q+d0NYX5W8j5n2GByDle/9PnE8lIA1BWwHzT4mgIP aNi0p6leUnkVHAbc9vifOVN4VJ/FHPUbwBT166TfWBA85cZkoZdzX1XzX1vWsy6JQwRf rzhA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references; bh=HLgt+x57dc8VSzWVlwtOs+nXwMnrhFqlLChiACsMNic=; b=Dn/GMfwVQOG/YVg7ESEkFetxj/7PUS6tFVGmg8MB3Js8dE8a7up5V0Y3qaNQswNmXe YWE4/0XVb/pc08IGkkzK9V6dBV4x1xNj5TZYONrsa1hAbzhAZjDT+PwlqJLoBXeps+TY 08A+xlkvPeWqvEAP8/vBtczb0Z3gyW6yP5usZ3hryigY6OjJyidYhzRbnmwiDxnTbsVS LsOqaliKAlAgMhzzFgHYrXIYzg9vIq8eZ136w3OcCh2xTPlLGtFKe5sZKy3BzQ2GA1II zp5rp2B5HcAhv1soIRtfsr162fA07ahCv/Nc/UNZSab8GpzPRTQ99dH2tIiu8wSrXuMv MDow== X-Gm-Message-State: APt69E3XcwfhMj6qjvOa1KPbU6ED1E3+qZ+LDzH9aZ0GItNpmxHtxOf/ ZP1lsy5CRBKIxszQgxLSxA9vs2U0 X-Google-Smtp-Source: AAOMgpcn4gEuz8BwoGLpmJkE3TarbrVkjxvOWe0xIv5LjEWJWGSkbbXdvnAPGhhJ+lCf9AIs6shFuA== X-Received: by 2002:a17:902:7e43:: with SMTP id a3-v6mr6506570pln.151.1530122368752; Wed, 27 Jun 2018 10:59:28 -0700 (PDT) Received: from vm2.eng.vmware.com ([66.170.99.2]) by smtp.gmail.com with ESMTPSA id b62-v6sm7698187pfj.123.2018.06.27.10.59.27 (version=TLS1_2 cipher=ECDHE-RSA-AES128-SHA bits=128/128); Wed, 27 Jun 2018 10:59:28 -0700 (PDT) From: Qiuyu Xiao To: ovs-dev@openvswitch.org Date: Wed, 27 Jun 2018 10:58:43 -0700 Message-Id: <20180627175844.2809-3-qiuyu.xiao.qyx@gmail.com> X-Mailer: git-send-email 2.17.1 In-Reply-To: <20180627175844.2809-1-qiuyu.xiao.qyx@gmail.com> References: <20180627175844.2809-1-qiuyu.xiao.qyx@gmail.com> X-Spam-Status: No, score=-2.0 required=5.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, FREEMAIL_FROM, RCVD_IN_DNSWL_NONE autolearn=ham version=3.3.1 X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on smtp1.linux-foundation.org Cc: aatteka@ovn.org Subject: [ovs-dev] [PATCH 2/3] ipsec: add CA-cert based authentication X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.12 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Sender: ovs-dev-bounces@openvswitch.org Errors-To: ovs-dev-bounces@openvswitch.org This patch adds CA-cert based authentication to the ovs-monitor-ipsec daemon. With CA-cert based authentication enabled, OVS approves IPsec tunnel if the peer has a cert signed by a trusted CA and the identity of the peer cert is as expected. Belows are the major changes and the reasons: 1) Added CA-cert based authentication. Compared with peer-cert based authentication, this one doesn't need to import peer cert to the local host to do configuration. This is especially beneficial if host has mutiple peers and peers frequently update their certs. This feature is required for the upcoming OVN IPsec support. 2) Changed the host cert and private key configuration interface. Previously, the host's cert and private key can be configured in either Open_vSwitch's SSL column or the SSL table. Now, the host certificate and private key can be only configured in the Open_vSwitch table's other_config column. Since it is not SSL cert and key, we'd better not to confuse users by saying so. 3) Changed the peer cert configuration interface. Previously, the peer cert is configured by setting the interface's options column as the content of the peer cert. It's changed to setting the column as the path of the peer cert. This is easier to be configured by the command line tool, and is consistent with other cert and key configuration interface which is better from a usability point of view. Signed-off-by: Qiuyu Xiao --- Documentation/howto/ipsec.rst | 78 ++++++++++++++++--- ipsec/ovs-monitor-ipsec | 138 +++++++++++++++++++++------------- 2 files changed, 156 insertions(+), 60 deletions(-) diff --git a/Documentation/howto/ipsec.rst b/Documentation/howto/ipsec.rst index 4e4f4d211..b42312da5 100644 --- a/Documentation/howto/ipsec.rst +++ b/Documentation/howto/ipsec.rst @@ -21,9 +21,9 @@ Avoid deeper levels because they do not render well. -============================================== -How to Encrypt Open vSwitch Tunnels with IPsec -============================================== +======================================= +Encrypt Open vSwitch Tunnels with IPsec +======================================= This document describes how to use Open vSwitch integration with strongSwan 5.1 or later to provide IPsec security for STT, GENEVE, GRE and VXLAN tunnels. @@ -77,6 +77,67 @@ Install strongSwan and openvswitch-ipsec debian packages:: Configuration ------------- +The IPsec configuration is done by setting options of the tunnel interface. +ovs-monitor-ipsec configures strongSwan accordingly based on the tunnel options. + +Authentication Methods +~~~~~~~~~~~~~~~~~~~~~~ + +Hosts of the IPsec tunnel need to authenticate each other to build a secure +channel. There are three authentication methods: + +1) You can set a pre-shared key in both hosts to do authentication. This + method is easier to use but less secure:: + + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:psk=swordfish]) + +2) You can use peer certificates to do authentication. First, generate + certificate and private key in each host. The certificate could be + self-signed. Refer to the ovs-pki(8) man page for more information + regarding certificate and key generation. Then, copy the peer certificate + to the local host and type:: + + % ovs-vsctl set Open_vSwitch . \ + other_config:certificate=/path/to/local_cert.pem \ + other_config:private_key=/path/to/priv_key.pem + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:pki=peer_auth \ + options:peer_cert=/path/to/peer_cert.pem + + `local_cert.pem` is the certificate of the local host. `priv_key.pem` + is the private key of the local host. `priv_key.pem` needs to be stored in + a secure location. `peer_cert.pem` is the certificate of the remote host. + +3) You can also use CA certificate to do authentication. First, you need to + establish your public key infrastructure. The certificate of each host + needs to be signed by the CA certificate. Refer to the ovs-pki(8) man page + for more information regarding PKI establishment. Then, copy the CA + certificate to the local host and type:: + + % ovs-vsctl set Open_vSwitch . \ + other_config:certificate=/path/to/local_cert.pem \ + other_config:private_key=/path/to/priv_key.pem \ + other_config:ca_cert=/path/to/ca_cert.pem + % ovs-vsctl add-port br0 ipsec_gre0 -- \ + set interface ipsec_gre0 type=gre \ + options:remote_ip=1.2.3.4 \ + options:pki=ca_auth \ + options:local_name=local_cn \ + options:remote_name=remote_cn + + strongSwan extracts identity from the certificate's `subjectAltName` field, + so you have to use x509 v3 certificate. `local_cn` is the hostname from the + `subjectAltName` field of the local host certificate. `remote_cn` is the + hostname from the `subjectAltName` field of the peer host certificate. + +Forwarding Modes +~~~~~~~~~~~~~~~~ + IPsec with ovs-monitor-ipsec daemon can be configured in three different forwarding policy modes: @@ -116,7 +177,7 @@ different forwarding policy modes: ovs-monitor-ipsec installed IPsec policies. However, assumption here is that OpenFlow controller was careful and installed OpenFlow rule with set SKB mark action specified in - OVSDB Open_vSwitch table before the first packets were able to leave + OVSDB Open_vSwitch table before the first packet was able to leave the OVS tunnel. 3) ovs-monitor-ipsec assumes that packets coming from all OVS tunnels @@ -126,7 +187,7 @@ different forwarding policy modes: % ovs-vsctl set Open_vSwitch . other_config:default_ipsec_skb_mark=0/1 % ovs-vsctl add-port br0 ipsec_gre0 -- \ - set interface gre0 type=gre \ + set interface ipsec_gre0 type=gre \ options:remote_ip=1.2.3.4 \ options:psk=swordfish]) @@ -189,11 +250,10 @@ If you think you may have found a bug with security implications, like 1) IPsec protected tunnel accepted packets that came unencrypted; OR 2) IPsec protected tunnel allowed packets to leave unencrypted; -Then report such bugs according to `security guidelines -`__. +Then report such bugs according to :doc:`/internals/security`. If bug does not have security implications, then report it according to -instructions in ``__. +instructions in :doc:`/internals/bugs`. There is also a possibility that there is a bug in strongSwan. In that -case report it to strongSwan mailing list. \ No newline at end of file +case report it to strongSwan mailing list. diff --git a/ipsec/ovs-monitor-ipsec b/ipsec/ovs-monitor-ipsec index 322a7045a..65f360a80 100755 --- a/ipsec/ovs-monitor-ipsec +++ b/ipsec/ovs-monitor-ipsec @@ -179,21 +179,30 @@ $auth_section left=$local_ip right=$remote_ip authby=psk"""), - "rsa" : Template("""\ + "pki_peer" : Template("""\ left=$local_ip right=$remote_ip - rightcert=ovs-$remote_ip.pem - leftcert=$certificate""")} + leftcert=$certificate + rightcert=$peer_cert"""), + "pki_ca" : Template("""\ + left=$local_ip + right=$remote_ip + leftcert=$certificate + leftid=$local_name + rightid=$remote_name""")} unixctl_config_tmpl = Template("""\ - Remote IP: $remote_ip Tunnel Type: $tunnel_type + Remote IP: $remote_ip Local IP: $local_ip SKB mark: $skb_mark - Use SSL cert: $use_ssl_cert - My cert: $certificate - My key: $private_key - His cert: $peer_cert + Local cert: $certificate + Local key: $private_key + CA cert: $ca_cert + Remote cert: $peer_cert + Remote name: $remote_name + Local name: $local_name + PKI: $pki PSK: $psk """) @@ -209,7 +218,6 @@ $auth_section self.state = "INIT" self.conf = {} self.status = {} - self.cert_file = None self.update_conf(row) def update_conf(self, row): @@ -219,11 +227,7 @@ $auth_section False.""" ret = False options = row.options - use_ssl_cert = options.get("use_ssl_cert") == "true" - cert = options.get("certificate") - key = options.get("private_key") - if use_ssl_cert: # Override with SSL certs if told so - (cert, key) = (keyer.public_cert, keyer.private_key) + (cert, key, ca_cert) = (keyer.public_cert, keyer.private_key, keyer.ca_cert) new_conf = { "ifname" : self.name, @@ -233,8 +237,11 @@ $auth_section "skb_mark" : keyer.wanted_skb_mark, "certificate" : cert, "private_key" : key, - "use_ssl_cert" : use_ssl_cert, + "ca_cert" : ca_cert, "peer_cert" : options.get("peer_cert"), + "remote_name" : options.get("remote_name"), + "local_name" : options.get("local_name"), + "pki" : options.get("pki"), "psk" : options.get("psk")} if self.conf != new_conf: # Configuration was updated in OVSDB. Validate it and figure @@ -303,7 +310,10 @@ $auth_section else: secrets.write("%s : RSA %s\n" % (self.conf["remote_ip"], self.conf["private_key"])) - auth_section = self.auth_tmpl["rsa"].substitute(self.conf) + if self.conf["pki"] == "ca_auth": + auth_section = self.auth_tmpl["pki_ca"].substitute(self.conf) + else: + auth_section = self.auth_tmpl["pki_peer"].substitute(self.conf) vals = self.conf.copy() vals["auth_section"] = auth_section vals["version"] = self.version @@ -344,45 +354,54 @@ $auth_section pass malformed configuration to strongSwan.""" self.invalid_reason = None + if not self.conf["remote_ip"]: self.invalid_reason = ": 'remote_ip' is not set" - elif self.conf["peer_cert"]: + + if self.conf["pki"]: if self.conf["psk"]: self.invalid_reason = ": 'psk' must be unset with PKI" elif not self.conf["certificate"]: self.invalid_reason = ": must set 'certificate' with PKI" elif not self.conf["private_key"]: self.invalid_reason = ": must set 'private_key' with PKI" + + if self.conf["pki"] == "ca_auth": + if not self.conf["ca_cert"]: + self.invalid_reason = ": must set 'ca_cert' "\ + "for ca_cert-based authentication" + elif not self.conf["local_name"]: + self.invalid_reason = ": must set 'local_name' "\ + "for ca_cert-based authentication" + elif not self.conf["remote_name"]: + self.invalid_reason = ": must set 'remote_name' "\ + "for ca_cert-based authentication" + elif self.conf["pki"] == "peer_auth": + if not self.conf["peer_cert"]: + self.invalid_reason = ": must set 'peer_cert' "\ + "for peer_cert-based authentication" + else: + self.invalid_reason = ": must set 'pki' "\ + "as 'ca_auth' or 'peer_auth'." elif self.conf["psk"]: - if self.conf["certificate"] or self.conf["private_key"]: - self.invalid_reason = ": 'certificate', 'private_key' and "\ - "'use_ssl_cert' must be unset with PSK" + if self.conf["certificate"] or self.conf["private_key"] \ + or self.conf["ca_cert"] or self.conf["peer_cert"]: + self.invalid_reason = ": 'certificate', 'private_key', "\ + "'ca_cert' and 'peer_cert' must be unset with PSK" + else: + self.invalid_reason = ": must use 'pki' or 'psk' option "\ + "for authentication" + if self.invalid_reason: return False + return True def _cleanup_old_state(self): - if self.cert_file: - try: - os.remove(self.cert_file) - except OSError: - vlog.warn("could not remove '%s'" % (self.cert_file)) - self.cert_file = None + vlog.info("Clean old state.") def _push_new_state(self): - if self.conf["peer_cert"]: - fn = keyer.CERT_DIR + "/ovs-%s.pem" % (self.name) - try: - cert = open(fn, "w") - try: - cert.write(self.conf["peer_cert"]) - finally: - cert.close() - vlog.info("created peer certificate %s" % (fn)) - self.cert_file = fn - except (OSError, IOError) as e: - vlog.err("could not create certificate '%s'" % (fn)) - + vlog.info("Push new state.") class StrongSwanKeyer(object): """This class is a wrapper around strongSwan.""" @@ -407,18 +426,33 @@ conn %%default SHUNT_POLICY = """conn prevent_unencrypted_gre type=drop leftprotoport=gre - mark=%s + mark={0} + +conn prevent_unencrypted_geneve + type=drop + leftprotoport=udp/6081 + mark={0} conn prevent_unencrypted_stt type=drop leftprotoport=tcp/7471 - mark=%s + mark={0} + +conn prevent_unencrypted_vxlan + type=drop + leftprotoport=udp/4789 + mark={0} + +""" + CA_SECTION = """ca ca_auth + cacert=%s """ def __init__(self, strongswan_root_prefix): self.private_key = None self.public_cert = None + self.ca_cert = None self.wanted_skb_mark = None self.in_use_skb_mark = None self.tunnels = {} @@ -436,7 +470,7 @@ conn prevent_unencrypted_stt def is_ipsec_required(self, options_column): """Return True if tunnel needs to be encrypted. Otherwise, returns False.""" - return "psk" in options_column or "peer_cert" in options_column + return "psk" in options_column or "pki" in options_column def restart(self): """This function restarts strongSwan.""" @@ -465,6 +499,7 @@ conn prevent_unencrypted_stt """Updates already existing """ self.public_cert = credentials[0] self.private_key = credentials[1] + self.ca_cert = credentials[2] def update_skb_mark(self, skb_mark): """Update skb_mark used in IPsec policies.""" @@ -499,8 +534,12 @@ conn prevent_unencrypted_stt needs_refresh = True if self.in_use_skb_mark: - ipsec_conf.write(" mark_out=%s\n" % self.in_use_skb_mark) - ipsec_conf.write(self.SHUNT_POLICY % (self.in_use_skb_mark, self.in_use_skb_mark)) + ipsec_conf.write(self.SHUNT_POLICY.format(self.in_use_skb_mark)) + + # No need to set needs_refresh here if ca_cert is changed. The tunnel + # iteration will set needs_refresh. + if self.ca_cert: + ipsec_conf.write(self.CA_SECTION % self.ca_cert) for name, tunnel in self.tunnels.iteritems(): if tunnel.run(): @@ -565,7 +604,6 @@ conn prevent_unencrypted_stt vlog.info("%s is outdated %u" % (conn, ver)) subprocess.call([self.IPSEC, "stroke", "down-nb", conn]) - def _get_strongswan_conns(self): """This function parses output from 'ipsec status' command. It returns dictionary where is interface name (as in OVSDB) @@ -612,11 +650,12 @@ conn prevent_unencrypted_stt def read_ovsdb_open_vswitch_table(data): """This functions reads IPsec relevant configuration from Open_vSwitch table.""" - ssl = (None, None) + ssl = (None, None, None) global_ipsec_skb_mark = None for row in data["Open_vSwitch"].rows.itervalues(): - if row.ssl: - ssl = (row.ssl[0].certificate, row.ssl[0].private_key) + ssl = (row.other_config.get("certificate"), \ + row.other_config.get("private_key"), \ + row.other_config.get("ca_cert")) global_ipsec_skb_mark = row.other_config.get("default_ipsec_skb_mark") keyer.update_ssl_credentials(ssl) keyer.update_skb_mark(global_ipsec_skb_mark) @@ -646,7 +685,6 @@ def read_ovsdb(data): read_ovsdb_open_vswitch_table(data) read_ovsdb_interface_table(data) - def main(): parser = argparse.ArgumentParser() parser.add_argument("database", metavar="DATABASE", @@ -674,7 +712,6 @@ def main(): ["name", "type", "options", "cfm_fault", "ofport"]) schema_helper.register_columns("Open_vSwitch", ["ssl", "other_config"]) - schema_helper.register_columns("SSL", ["certificate", "private_key"]) idl = ovs.db.idl.Idl(remote, schema_helper) ovs.daemon.daemonize() @@ -715,7 +752,6 @@ def main(): unixctl_server.close() idl.close() - if __name__ == '__main__': try: main()