From 09d7c5ac00543fb9a2976a946b467dfcf499b50d Mon Sep 17 00:00:00 2001 From: Ilya Maximets Date: Wed, 11 Dec 2024 00:07:03 +0100 Subject: [PATCH] ipsec: Add support for using non-root ipsec.conf. Typical configuration file hierarchy for Libreswan in distributions looks like this: /etc /ipsec.conf /ipsec.d /*.conf /crypto-policies/back-ends/libreswan.config The root ipsec.conf contains the 'setup' section with the base configuration of the IKE daemon, includes system-wide crypto-policies and all the sub-config files in ipsec.d folder describing connections. ovs-monitor-ipsec today is not able to leverage this structure, because it requires the complete ownership of the ipsec.conf. If someone attempts to pass a sub-config file to the daemon in order to make it not overwrite the root ipsec.conf, this may cause a lot of trouble: 1. New tunnel is created in OVS. 2. ovs-monitor-ipsec writes it into sub-config file. 3. ovs-monitor-ipsec calls ipsec --start conn --config sub-config 4. Libreswan starts connection using configuration from only the sub-config and not taking into account any other file. 5. Re-start Libreswan. 6. Libreswan now reads all the files and configures connections using information from all the configuration files, including system-wide crypto policies and other potential 'conn %default' sections from all the files. 7. Now the connection is configured differently and potentially in an incompatible way with the other side. Worst of all is the behavior is unpredictable, taking into account the re-start can happen due to a crash or other random event. Another point is that 'setup' and 'conn %default' sections defined in our sub-config file will also bleed out configuration to connections defined in other files. And it's hard to say in which order configuration will be applied, because it's not clear in which order the files are included and parsed. So, this kind of file structure cannot be safely used. Let's add a minimal support for running with a sub-config. A new option '--root-ipsec-conf' is introduced to specify the location of the root ipsec.conf file, so ovs-monitor-ipsec can provide it while calling ipec commands instead. This will make Libreswan (pluto) to parse the whole tree of includes and apply the same configuration every time, regardless of restarts and other issues. When this new option is set, ovs-monitor-ipsec will also not define the 'setup' section to avoid overriding global configuration and will not define 'conn %default' section for the same reason. Instead, important connection options will be defined for every connection, so they are still applied without polluting defaults. The 'setup' section is just omitted in this case. We only define 'uniqeids', but it's true by default and we may assume users know what are they doing if they are changing this config in the main ipsec.conf. The Libreswan documentation also discourages from turning this option off and mentions that it may be removed in the future. Only implementing for Libreswan, because we do not even support non-default location of ipsec.conf with StrongSwan today. Acked-by: Mike Pattrick Signed-off-by: Ilya Maximets --- NEWS | 4 +++ ipsec/ovs-monitor-ipsec.in | 67 +++++++++++++++++++++++++++----------- tests/system-ipsec.at | 58 ++++++++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 24 deletions(-) diff --git a/NEWS b/NEWS index ab664ef7eca..9c074c6d47a 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,10 @@ Post-v3.4.0 - Tunnels: * LISP and STT tunnel port types are deprecated and will be removed in the next release. + - IPsec: + * New option '--root-ipsec-conf' for ovs-monitor-ipsec with Libreswan + to allow cases where '--ipsec-conf' is not the main ipsec.conf, but + included from it. The value should be the path to the main ipsec.conf. v3.4.0 - 15 Aug 2024 diff --git a/ipsec/ovs-monitor-ipsec.in b/ipsec/ovs-monitor-ipsec.in index 6c60c07e3f9..41ed2392055 100755 --- a/ipsec/ovs-monitor-ipsec.in +++ b/ipsec/ovs-monitor-ipsec.in @@ -425,19 +425,24 @@ conn prevent_unencrypted_vxlan class LibreSwanHelper(object): """This class does LibreSwan specific configurations.""" - CONF_HEADER = """%s + CONF_DEFAULT_HEADER = """\ config setup uniqueids=yes -conn %%default - keyingtries=%%forever +conn %default +""" + + CONN_CONF_BASE = """\ + keyingtries=%forever type=transport auto=route +""" + + CONN_CONF_CRYPTO = """\ ike=aes_gcm256-sha2_256 esp=aes_gcm256 ikev2=insist - -""" % (FILE_HEADER) +""" SHUNT_POLICY = """conn prevent_unencrypted_gre type=drop @@ -524,6 +529,9 @@ conn prevent_unencrypted_vxlan else "/run/pluto/pluto.ctl") self.IPSEC_CONF = libreswan_root_prefix + ipsec_conf + self.ROOT_IPSEC_CONF = self.IPSEC_CONF + if args.root_ipsec_conf: + self.ROOT_IPSEC_CONF = args.root_ipsec_conf self.IPSEC_SECRETS = libreswan_root_prefix + ipsec_secrets self.IPSEC_D = "sql:" + libreswan_root_prefix + ipsec_d self.IPSEC_CTL = libreswan_root_prefix + ipsec_ctl @@ -531,8 +539,10 @@ conn prevent_unencrypted_vxlan self.conns_not_active = set() self.last_refresh = time.time() self.secrets_file = None + self.use_default_conn = self.IPSEC_CONF == self.ROOT_IPSEC_CONF vlog.dbg("Using: " + self.IPSEC) vlog.dbg("Configuration file: " + self.IPSEC_CONF) + vlog.dbg("Root configuration file: " + self.ROOT_IPSEC_CONF) vlog.dbg("Secrets file: " + self.IPSEC_SECRETS) vlog.dbg("ipsec.d: " + self.IPSEC_D) vlog.dbg("Pluto socket: " + self.IPSEC_CTL) @@ -543,7 +553,12 @@ conn prevent_unencrypted_vxlan self._nss_clear_database() f = open(self.IPSEC_CONF, "w") - f.write(self.CONF_HEADER) + f.write(FILE_HEADER) + if self.use_default_conn: + f.write(self.CONF_DEFAULT_HEADER) + f.write(self.CONN_CONF_BASE) + f.write(self.CONN_CONF_CRYPTO) + f.write("\n") f.close() f = open(self.IPSEC_SECRETS, "w") @@ -556,7 +571,13 @@ conn prevent_unencrypted_vxlan def config_init(self): self.conf_file = open(self.IPSEC_CONF, "w") self.secrets_file = open(self.IPSEC_SECRETS, "w") - self.conf_file.write(self.CONF_HEADER) + self.conf_file.write(FILE_HEADER) + if self.use_default_conn: + self.conf_file.write(self.CONF_DEFAULT_HEADER) + self.conf_file.write(self.CONN_CONF_BASE) + self.conf_file.write(self.CONN_CONF_CRYPTO) + self.conf_file.write("\n") + self.secrets_file.write(FILE_HEADER) def config_global(self, monitor): @@ -614,6 +635,10 @@ conn prevent_unencrypted_vxlan if tunnel.conf["address_family"] == "IPv6": auth_section = self.IPV6_CONN + auth_section + if not self.use_default_conn: + auth_section = self.CONN_CONF_BASE + auth_section + auth_section = self.CONN_CONF_CRYPTO + auth_section + if "custom_options" in tunnel.conf: for key, value in tunnel.conf["custom_options"].items(): auth_section += "\n " + key + "=" + value @@ -638,7 +663,7 @@ conn prevent_unencrypted_vxlan def refresh(self, monitor): vlog.info("Refreshing LibreSwan configuration") run_command(self.IPSEC_AUTO + ["--ctlsocket", self.IPSEC_CTL, - "--config", self.IPSEC_CONF, + "--config", self.ROOT_IPSEC_CONF, "--rereadsecrets"], "re-read secrets") @@ -708,43 +733,43 @@ conn prevent_unencrypted_vxlan if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]: if monitor.conf["skb_mark"]: run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--add", "--asynchronous", "prevent_unencrypted_gre"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--add", "--asynchronous", "prevent_unencrypted_geneve"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--add", "--asynchronous", "prevent_unencrypted_stt"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--add", "--asynchronous", "prevent_unencrypted_vxlan"]) else: run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--delete", "--asynchronous", "prevent_unencrypted_gre"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--delete", "--asynchronous", "prevent_unencrypted_geneve"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--delete", "--asynchronous", "prevent_unencrypted_stt"]) run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--delete", "--asynchronous", "prevent_unencrypted_vxlan"]) @@ -838,13 +863,13 @@ conn prevent_unencrypted_vxlan self.conns_not_active.discard(conn) run_command(self.IPSEC_AUTO + ["--ctlsocket", self.IPSEC_CTL, - "--config", self.IPSEC_CONF, + "--config", self.ROOT_IPSEC_CONF, "--delete", conn], "delete %s" % conn) def _start_ipsec_connection(self, conn, action): asynchronous = [] if action == "add" else ["--asynchronous"] ret, pout, perr = run_command(self.IPSEC_AUTO + - ["--config", self.IPSEC_CONF, + ["--config", self.ROOT_IPSEC_CONF, "--ctlsocket", self.IPSEC_CTL, "--" + action, *asynchronous, conn], @@ -1388,7 +1413,11 @@ def main(): help="Don't restart the IKE daemon on startup.") parser.add_argument("--ipsec-conf", metavar="IPSEC-CONF", help="Use DIR/IPSEC-CONF as location for " - " ipsec.conf (libreswan only).") + " ipsec.conf to overwrite (libreswan only).") + parser.add_argument("--root-ipsec-conf", metavar="ROOT-IPSEC-CONF", + help="The read-only root configuration file that" + " 'include's the one provided in --ipsec-conf. Will" + " be used to call ipsec commands (libreswan only).") parser.add_argument("--ipsec-d", metavar="IPSEC-D", help="Use DIR/IPSEC-D as location for " " ipsec.d (libreswan only).") diff --git a/tests/system-ipsec.at b/tests/system-ipsec.at index 4ab384d89c5..9bae7279942 100644 --- a/tests/system-ipsec.at +++ b/tests/system-ipsec.at @@ -20,7 +20,8 @@ m4_define([START_PLUTO], [ --rundir $ovs_base/$1], [0], [], [stderr]) ]) -dnl IPSEC_ADD_NODE([namespace], [device], [address], [peer address])) +dnl IPSEC_ADD_NODE([namespace], [device], [address], [peer address], +dnl [custom-ipsec-conf]) dnl dnl Creates a dummy host that acts as an IPsec endpoint. Creates host in dnl 'namespace' and attaches a veth 'device' to 'namespace' to act as the host @@ -28,7 +29,10 @@ dnl NIC. Assigns 'address' to 'device' and adds the other end of veth 'device' t dnl 'br0' which is an OVS bridge in the default namespace acting as an underlay dnl switch. Sets the default gateway of 'namespace' to 'peer address'. dnl -dnl Starts all daemons in 'namespace' that are required for IPsec +dnl Starts all daemons in 'namespace' that are required for IPsec. +dnl +dnl If 'custom-ipsec-conf' is provided, then it will be used as --ipsec-conf +dnl and the ipsec.conf will be used as --root-ipsec-conf. m4_define([IPSEC_ADD_NODE], [ADD_NAMESPACES($1) dnl Disable DAD. We know we wont get duplicates on this underlay network. @@ -56,6 +60,12 @@ m4_define([IPSEC_ADD_NODE], [0], [], [stderr]) on_exit "kill_ovs_vswitchd `cat $ovs_base/$1/vswitchd.pid`" + m4_if([$5], [], [], [ + AT_CHECK([echo "## A read-only root config. ##" > $ovs_base/$1/ipsec.conf]) + AT_CHECK([echo "include $ovs_base/$1/$5" >> $ovs_base/$1/ipsec.conf]) + ]) + AT_CHECK + dnl Start pluto START_PLUTO([$1]) on_exit 'kill $(cat $ovs_base/$1/pluto.pid)' @@ -63,7 +73,9 @@ m4_define([IPSEC_ADD_NODE], dnl Start ovs-monitor-ipsec NS_CHECK_EXEC([$1], [ovs-monitor-ipsec unix:${OVS_RUNDIR}/$1/db.sock\ --pidfile=${OVS_RUNDIR}/$1/ovs-monitor-ipsec.pid --ike-daemon=libreswan\ - --ipsec-conf=$ovs_base/$1/ipsec.conf --ipsec-d=$ovs_base/$1/ipsec.d \ + --ipsec-conf=$ovs_base/$1/m4_if([$5], [], [ipsec.conf], [$5]) \ + m4_if([$5], [], [], [--root-ipsec-conf=$ovs_base/$1/ipsec.conf]) \ + --ipsec-d=$ovs_base/$1/ipsec.d \ --ipsec-secrets=$ovs_base/$1/secrets \ --log-file=$ovs_base/$1/ovs-monitor-ipsec.log \ --ipsec-ctl=$ovs_base/$1/pluto.ctl \ @@ -75,8 +87,10 @@ m4_define([IPSEC_ADD_NODE], [ovs-vsctl --db unix:$ovs_base/$1/db.sock add-br br-ipsec \ -- set-controller br-ipsec punix:$ovs_base/br-ipsec.$1.mgmt])] ) -m4_define([IPSEC_ADD_NODE_LEFT], [IPSEC_ADD_NODE(left, p0, $1, $2)]) -m4_define([IPSEC_ADD_NODE_RIGHT], [IPSEC_ADD_NODE(right, p1, $1, $2)]) +m4_define([IPSEC_ADD_NODE_LEFT], + [IPSEC_ADD_NODE(left, p0, $1, $2, [$3])]) +m4_define([IPSEC_ADD_NODE_RIGHT], + [IPSEC_ADD_NODE(right, p1, $1, $2, [$3])]) dnl OVS_VSCTL([namespace], [sub-command]) dnl @@ -411,6 +425,40 @@ CHECK_ESP_TRAFFIC OVS_TRAFFIC_VSWITCHD_STOP() AT_CLEANUP +AT_SETUP([IPsec -- Libreswan (ipv4, geneve, custom conf)]) +AT_KEYWORDS([ipsec libreswan ipv4 geneve psk custom conf]) +dnl Note: Geneve test may not work on older kernels due to CVE-2020-25645 +dnl https://bugzilla.redhat.com/show_bug.cgi?id=1883988 + +CHECK_LIBRESWAN() +OVS_TRAFFIC_VSWITCHD_START() +IPSEC_SETUP_UNDERLAY() + +dnl Set up hosts. +IPSEC_ADD_NODE_LEFT(10.1.1.1, 10.1.1.2, [custom.conf]) +IPSEC_ADD_NODE_RIGHT(10.1.1.2, 10.1.1.1, [custom.conf]) + +dnl Set up IPsec tunnel on 'left' host. +IPSEC_ADD_TUNNEL_LEFT([geneve], + [options:remote_ip=10.1.1.2 options:psk=swordfish]) + +dnl Set up IPsec tunnel on 'right' host. +IPSEC_ADD_TUNNEL_RIGHT([geneve], + [options:remote_ip=10.1.1.1 options:psk=swordfish]) +CHECK_ESP_TRAFFIC + +dnl Check that custom.conf doesn't include default section, but has +dnl ike and esp configuration per connection. +AT_CHECK([grep -q "conn %default" $ovs_base/left/custom.conf], [1]) +AT_CHECK([grep -c -E "(ike|ikev2|esp)=" $ovs_base/left/custom.conf], [0], [6 +]) +AT_CHECK([grep -q "conn %default" $ovs_base/right/custom.conf], [1]) +AT_CHECK([grep -c -E "(ike|ikev2|esp)=" $ovs_base/right/custom.conf], [0], [6 +]) + +OVS_TRAFFIC_VSWITCHD_STOP() +AT_CLEANUP + AT_SETUP([IPsec -- Libreswan NxN geneve tunnels + reconciliation]) AT_KEYWORDS([ipsec libreswan scale reconciliation]) dnl Note: Geneve test may not work on older kernels due to CVE-2020-25645