diff mbox series

[ovs-dev,v2,2/4] ipsec: reintroduce IPsec support for tunneling

Message ID 20180718212302.3080-3-qiuyu.xiao.qyx@gmail.com
State Superseded
Headers show
Series IPsec support for tunneling | expand

Commit Message

Qiuyu Xiao July 18, 2018, 9:23 p.m. UTC
This patch reintroduces ovs-monitor-ipsec daemon that
was previously removed by commit 2b02d770 ("openvswitch:
Allow external IPsec tunnel management.")

After this patch, there are no IPsec flavored tunnels anymore.
IPsec is enabled by setting up the right values in:
1. OVSDB:Interface:options column;
2. OVSDB:Open_vSwitch:other_config column;
3. OpenFlow pipeline.

GRE, VXLAN, GENEVE, and STT IPsec tunnels are supported. LibreSwan and
StrongSwan IKE daemons are supported. User can choose pre-shared key,
self-signed peer certificate, or CA-signed certificate as authentication
method.

Signed-off-by: Ansis Atteka <aatteka@ovn.org>
Signed-off-by: Qiuyu Xiao <qiuyu.xiao.qyx@gmail.com>
---
 Documentation/automake.mk        |    1 +
 Documentation/howto/index.rst    |    1 +
 Documentation/howto/ipsec.rst    |  256 +++++++
 Makefile.am                      |    1 +
 debian/automake.mk               |    3 +
 debian/control                   |   21 +
 debian/openvswitch-ipsec.dirs    |    1 +
 debian/openvswitch-ipsec.init    |  189 +++++
 debian/openvswitch-ipsec.install |    1 +
 ipsec/automake.mk                |   10 +
 ipsec/ovs-monitor-ipsec          | 1152 ++++++++++++++++++++++++++++++
 vswitchd/vswitch.xml             |  124 +++-
 12 files changed, 1751 insertions(+), 9 deletions(-)
 create mode 100644 Documentation/howto/ipsec.rst
 create mode 100644 debian/openvswitch-ipsec.dirs
 create mode 100644 debian/openvswitch-ipsec.init
 create mode 100644 debian/openvswitch-ipsec.install
 create mode 100644 ipsec/automake.mk
 create mode 100755 ipsec/ovs-monitor-ipsec

Comments

0-day Robot July 18, 2018, 9:58 p.m. UTC | #1
Bleep bloop.  Greetings Qiuyu Xiao, I am a robot and I have tried out your patch.
Thanks for your contribution.

I encountered some error that I wasn't expecting.  See the details below.


checkpatch:
ERROR: Too many signoffs; are you missing Co-authored-by lines?
WARNING: Line is 80 characters long (recommended limit is 79)
#118 FILE: Documentation/howto/ipsec.rst:45:
ovs-monitor-ipsec configures IKE daemom accordingly based on the tunnel options.

WARNING: Line is 80 characters long (recommended limit is 79)
#137 FILE: Documentation/howto/ipsec.rst:64:
   self-signed.  Refer to the ovs-pki(8) man page for more information regarding

WARNING: Line is 81 characters long (recommended limit is 79)
#138 FILE: Documentation/howto/ipsec.rst:65:
   certificate and key generation. Then, copy the remote certificate to the local

WARNING: Line is 81 characters long (recommended limit is 79)
#148 FILE: Documentation/howto/ipsec.rst:75:
                                     options:remote_cert=/path/to/remote_cert.pem

WARNING: Line is 85 characters long (recommended limit is 79)
#425 FILE: debian/openvswitch-ipsec.init:27:
# Description:       The ovs-monitor-ipsec script provides support for encrypting GRE

WARNING: Line is 80 characters long (recommended limit is 79)
#577 FILE: debian/openvswitch-ipsec.init:179:
        log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon"

WARNING: Line is 80 characters long (recommended limit is 79)
#582 FILE: debian/openvswitch-ipsec.init:184:
        echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2

WARNING: Line is 81 characters long (recommended limit is 79)
#771 FILE: ipsec/ovs-monitor-ipsec:154:
            if len(a) >= 4 and a[0] == "sel" and a[1] == "src" and a[3] == "dst":

WARNING: Line is 80 characters long (recommended limit is 79)
#828 FILE: ipsec/ovs-monitor-ipsec:211:
        self.CHARON_CONF = strongswan_root_prefix + "/etc/strongswan.d/ovs.conf"

WARNING: Line is 83 characters long (recommended limit is 79)
#908 FILE: ipsec/ovs-monitor-ipsec:291:
                                (tunnel.conf["local_ip"], tunnel.conf["remote_ip"],

WARNING: Line is 83 characters long (recommended limit is 79)
#913 FILE: ipsec/ovs-monitor-ipsec:296:
                                (tunnel.conf["local_ip"], tunnel.conf["remote_ip"],

WARNING: Line is 83 characters long (recommended limit is 79)
#916 FILE: ipsec/ovs-monitor-ipsec:299:
                auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)

WARNING: Line has trailing whitespace
#1068 FILE: ipsec/ovs-monitor-ipsec:451:
            self.secrets_file.write('%s %s : PSK "%s"\n' % 

WARNING: Line is 80 characters long (recommended limit is 79)
WARNING: Line has trailing whitespace
#1069 FILE: ipsec/ovs-monitor-ipsec:452:
                            (tunnel.conf["local_ip"], tunnel.conf["remote_ip"], 

WARNING: Line is 81 characters long (recommended limit is 79)
#1131 FILE: ipsec/ovs-monitor-ipsec:514:
            # In a corner case, LibreSwan daemon restarts for some reason and the

WARNING: Line is 81 characters long (recommended limit is 79)
#1484 FILE: ipsec/ovs-monitor-ipsec:867:
                                the supported daemon (StrongSwan or LibreSwan).")

WARNING: Line is 82 characters long (recommended limit is 79)
#1582 FILE: ipsec/ovs-monitor-ipsec:965:
        """This function reads all configuration from OVSDB that ovs-monitor-ipsec

WARNING: Line is 82 characters long (recommended limit is 79)
#1739 FILE: ipsec/ovs-monitor-ipsec:1122:
    seqno = idl.change_seqno  # Sequence number when OVSDB was processed last time

Lines checked: 1945, Warnings: 19, Errors: 1


Please check this out.  If you feel there has been an error, please email aconole@bytheb.org

Thanks,
0-day Robot
diff mbox series

Patch

diff --git a/Documentation/automake.mk b/Documentation/automake.mk
index 2202df45b..3a505924a 100644
--- a/Documentation/automake.mk
+++ b/Documentation/automake.mk
@@ -59,6 +59,7 @@  DOC_SOURCE = \
 	Documentation/howto/docker.rst \
 	Documentation/howto/dpdk.rst \
 	Documentation/howto/firewalld.rst \
+	Documentation/howto/ipsec.rst \
 	Documentation/howto/kvm.rst \
 	Documentation/howto/libvirt.rst \
 	Documentation/howto/selinux.rst \
diff --git a/Documentation/howto/index.rst b/Documentation/howto/index.rst
index 201d6936b..9a3487be3 100644
--- a/Documentation/howto/index.rst
+++ b/Documentation/howto/index.rst
@@ -37,6 +37,7 @@  OVS
    :maxdepth: 2
 
    kvm
+   ipsec
    selinux
    libvirt
    ssl
diff --git a/Documentation/howto/ipsec.rst b/Documentation/howto/ipsec.rst
new file mode 100644
index 000000000..67f0f4a00
--- /dev/null
+++ b/Documentation/howto/ipsec.rst
@@ -0,0 +1,256 @@ 
+..
+      Licensed under the Apache License, Version 2.0 (the "License"); you may
+      not use this file except in compliance with the License. You may obtain
+      a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+      Unless required by applicable law or agreed to in writing, software
+      distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+      WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+      License for the specific language governing permissions and limitations
+      under the License.
+
+      Convention for heading levels in Open vSwitch documentation:
+
+      =======  Heading 0 (reserved for the title in a document)
+      -------  Heading 1
+      ~~~~~~~  Heading 2
+      +++++++  Heading 3
+      '''''''  Heading 4
+
+      Avoid deeper levels because they do not render well.
+
+=======================================
+Encrypt Open vSwitch Tunnels with IPsec
+=======================================
+
+This document describes how to use Open vSwitch to provide IPsec security for
+STT, GENEVE, GRE and VXLAN tunnels.  This document assumes that you have
+already installed Open vSwitch.
+
+Setup
+-----
+
+Install strongSwan and openvswitch-ipsec debian packages::
+
+      $ apt-get install strongswan
+      $ dpkg -i openvswitch-ipsec_<version>_amd64.deb
+
+
+Configuration
+-------------
+
+The IPsec configuration is done by setting options of the tunnel interface.
+ovs-monitor-ipsec configures IKE daemom 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:local_ip=1.1.1.1 \
+                                     options:remote_ip=2.2.2.2 \
+                                     options:psk=swordfish])
+
+2) You can use the certificate of remote host 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 remote 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:local_ip=1.1.1.1 \
+                                     options:remote_ip=2.2.2.2 \
+                                     options:remote_cert=/path/to/remote_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. `remote_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. 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:local_ip=1.1.1.1 \
+                                     options:remote_ip=2.2.2.2 \
+                                     options:remote_name=remote_cn
+
+   strongSwan extracts identity from the certificate's `subjectAltName` field,
+   so you have to use x509 v3 certificate. `remote_cn` is the hostname from the
+   `subjectAltName` field of the remote host's certificate.
+
+Forwarding Modes
+~~~~~~~~~~~~~~~~
+There is delay between the user setting up IPsec tunnel and the IPsec tunnel
+actually taking affect to encrypt packets. To offset the risk of unencrypted
+packets leaking out during this period, you can choose a more secure forwarding
+mode.  There are three different forwarding modes:
+
+1) ovs-monitor-ipsec assumes that packets from all OVS tunnels by default
+   should be allowed to exit unencrypted unless particular tunnel is
+   explicitly configured with IPsec parameters::
+
+      % ovs-vsctl add-port br0 ipsec_gre0 -- \
+                  set interface ipsec_gre0 type=gre \
+                                     options:remote_ip=1.2.3.4 \
+                                     options:psk=swordfish])
+
+
+   The problem with this mode is that there is inherent race condition
+   between ovs-monitor-ipsec and ovs-vswitchd daemons where ovs-vswitchd
+   could enable forwarding before ovs-monitor-ipsec actually had a chance
+   to configure IPsec policies.
+   This mode should be used only and only if tunnel configuration is static
+   and/or if there is firewall that can drop the plain packets that
+   occasionally leak the tunnel unencrypted on OVSDB (re)configuration
+   events.
+
+2) ovs-monitor-ipsec assumes that packets marked with a given SKB mark
+   must be encrypted and hence should not be allowed to leave the system
+   unencrypted::
+
+     % ovs-vsctl set Open_vSwitch . other_config:ipsec_skb_mark=1/1
+     % ovs-vsctl add-port br0 ipsec_gre0 -- \
+                 set interface ipsec_gre0 type=gre \
+                                   options:remote_ip=1.2.3.4 \
+                                   options:psk=swordfish])
+
+   With this mode ovs-monitor-ipsec makes IKE daemon to install IPsec shunt
+   policies that serve as safety net and prevent unencrypted tunnel packets
+   to leave the host in case ovs-vswitchd configured datapath before
+   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 packet was able to leave
+   the OVS tunnel.
+
+3) ovs-monitor-ipsec assumes that packets coming from all OVS tunnels
+   by default need to be protected with IPsec unless the tunnel is explicitly
+   allowed to leave unencrypted.  This is inverse behavior of the second
+   mode::
+
+     % ovs-vsctl set Open_vSwitch . other_config:ipsec_skb_mark=0/1
+     % ovs-vsctl add-port br0 ipsec_gre0 -- \
+                 set interface ipsec_gre0 type=gre \
+                                   options:remote_ip=1.2.3.4 \
+                                   options:psk=swordfish])
+
+   With this solution ovs-monitor-ipsec tells IKE daemon
+   to install IPsec shunt policies to match on skb mark 0
+   which should be the default skb mark value for all tunnel packets
+   going through NetFilter/XFRM hooks.  As a result IPsec assumes
+   that all packets coming from tunnels should be encrypted unless
+   OpenFlow controller explicitly set skb mark to a non-zero value.
+   This is the most secure mode, but at the same time the most intrusive
+   one, because the OpenFlow pipeline needs to be overhauled just because
+   now IPsec is enabled.
+
+   Note that instead of using least significant bit of SKB mark, you could
+   just as well have used any other bit.
+
+
+Troubleshooting
+---------------
+
+Use following ovs-apptcl command to get ovs-monitor-ipsec internal
+representation of tunnel configuration::
+
+    % ovs-appctl -t ovs-monitor-ipsec tunnels/show
+
+If there is misconfiguration then ovs-appctl should indicate why.
+For example::
+
+   Interface name: gre0 v5 (CONFIGURED) <--- Should be set to CONFIGURED.
+                                             Otherwise, error message will
+                                             be provided
+   Tunnel Type:    gre
+   Local IP:       1.1.1.1
+   Remote IP:      2.2.2.2
+   SKB mark:       None
+   Local cert:     None
+   Local name:     None
+   Local key:      None
+   Remote cert:    None
+   Remote name:    None
+   CA cert:        None
+   PSK:            swordfish
+   Ofport:         1          <--- Whether ovs-vswitchd has assigned Ofport
+                                   number to this Tunnel Port
+   CFM state:      Up         <--- Whether CFM declared this tunnel healthy
+   Kernel policies installed:
+   ...                          <--- IPsec policies for this OVS tunnel in
+                                     Linux Kernel installed by strongSwan
+   Kernel security associations installed:
+   ...                          <--- IPsec security associations for this OVS
+                                     tunnel in Linux Kernel installed by
+                                     strongswan
+   Strongswan connections that are active:
+   ...                          <--- strongSwan "connections" for this OVS
+                                     tunnel
+
+You can also check the logs of the ovs-ipsec-monitor daemon and the IKE daemon
+to locate issues.
+
+Limitations
+-----------
+
+There are several limitations:
+
+1) Some older Open vSwitch datapath kernel modules (in Linux Kernel tree)
+   do not support route lookups with transport L4 ports properly.  In
+   this case Ethernet over L4 tunneling protocols (e.g. STT, GENEVE)
+   would not work.  However, GRE would still work because it does not
+   have concept of L4 ports.
+
+2) Some strongSwan versions might not support certain features.  For
+   example:
+
+   a) AES GCM ciphers that improve performance.
+   b) xfrm_acq_expires setting in strongSwan configuration file.
+      This setting tells strongSwan how aggressively to retry
+      establishing tunnel, if peer did not respond to previous keying
+      request.
+   c) set_proto_port_transport_sa in charon configuration file that tells
+      Linux Kernel to filter out unexpected packets that came over IPsec
+      tunnel.
+   d) IPsec transport mode for NAT deployment.
+
+3) Since IPsec decryption is done from the software when packet has already
+   gotten past NIC hardware, then benefits of the most tunneling offloads,
+   like GENEVE or TSO for STT can't be leveraged unless NIC has IPsec offloads
+   as well and the NIC knows how to leverage them.
+
+Bug Reporting
+-------------
+
+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 :doc:`/internals/security`.
+
+If bug does not have security implications, then report it according to
+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.
diff --git a/Makefile.am b/Makefile.am
index e02799a90..c0fef11fd 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -480,6 +480,7 @@  include tests/automake.mk
 include include/automake.mk
 include third-party/automake.mk
 include debian/automake.mk
+include ipsec/automake.mk
 include vswitchd/automake.mk
 include ovsdb/automake.mk
 include rhel/automake.mk
diff --git a/debian/automake.mk b/debian/automake.mk
index 4d8e204bb..8a8d43c9f 100644
--- a/debian/automake.mk
+++ b/debian/automake.mk
@@ -20,6 +20,9 @@  EXTRA_DIST += \
 	debian/openvswitch-datapath-source.copyright \
 	debian/openvswitch-datapath-source.dirs \
 	debian/openvswitch-datapath-source.install \
+	debian/openvswitch-ipsec.dirs \
+	debian/openvswitch-ipsec.init \
+	debian/openvswitch-ipsec.install \
 	debian/openvswitch-pki.dirs \
 	debian/openvswitch-pki.postinst \
 	debian/openvswitch-pki.postrm \
diff --git a/debian/control b/debian/control
index a4c031d85..9443e91c9 100644
--- a/debian/control
+++ b/debian/control
@@ -320,3 +320,24 @@  Description: Open vSwitch development package
  1000V.
  .
  This package provides openvswitch headers and libopenvswitch for developers.
+
+Package: openvswitch-ipsec
+Architecture: linux-any
+Depends: iproute2,
+         openvswitch-common (= ${binary:Version}),
+         openvswitch-switch (= ${binary:Version}),
+         python,
+         python-openvswitch (= ${source:Version}),
+         strongswan,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: Open vSwitch IPsec tunneling support
+ Open vSwitch is a production quality, multilayer, software-based,
+ Ethernet virtual switch. It is designed to enable massive network
+ automation through programmatic extension, while still supporting
+ standard management interfaces and protocols (e.g. NetFlow, IPFIX,
+ sFlow, SPAN, RSPAN, CLI, LACP, 802.1ag). In addition, it is designed
+ to support distribution across multiple physical servers similar to
+ VMware's vNetwork distributed vswitch or Cisco's Nexus 1000V.
+ .
+ This package provides IPsec tunneling support for OVS tunnels.
diff --git a/debian/openvswitch-ipsec.dirs b/debian/openvswitch-ipsec.dirs
new file mode 100644
index 000000000..fca44aa7b
--- /dev/null
+++ b/debian/openvswitch-ipsec.dirs
@@ -0,0 +1 @@ 
+usr/share/openvswitch/scripts
\ No newline at end of file
diff --git a/debian/openvswitch-ipsec.init b/debian/openvswitch-ipsec.init
new file mode 100644
index 000000000..1a9683785
--- /dev/null
+++ b/debian/openvswitch-ipsec.init
@@ -0,0 +1,189 @@ 
+#!/bin/sh
+#
+# Copyright (c) 2007, 2009 Javier Fernandez-Sanguino <jfs@debian.org>
+#
+# This is free software; you may redistribute it and/or modify
+# it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2,
+# or (at your option) any later version.
+#
+# This is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License with
+# the Debian operating system, in /usr/share/common-licenses/GPL;  if
+# not, write to the Free Software Foundation, Inc., 59 Temple Place,
+# Suite 330, Boston, MA 02111-1307 USA
+#
+### BEGIN INIT INFO
+# Provides:          openvswitch-ipsec
+# Required-Start:    $network $local_fs $remote_fs openvswitch-switch
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Open vSwitch GRE-over-IPsec daemon
+# Description:       The ovs-monitor-ipsec script provides support for encrypting GRE
+#                    tunnels with IPsec.
+### END INIT INFO
+
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+
+DAEMON=/usr/share/openvswitch/scripts/ovs-monitor-ipsec # Daemon's location
+NAME=ovs-monitor-ipsec          # Introduce the short server's name here
+LOGDIR=/var/log/openvswitch     # Log directory to use
+
+PIDFILE=/var/run/openvswitch/$NAME.pid
+
+test -x $DAEMON || exit 0
+
+. /lib/lsb/init-functions
+
+DODTIME=10              # Time to wait for the server to die, in seconds
+                        # If this value is set too low you might not
+                        # let some servers to die gracefully and
+                        # 'restart' will not work
+
+set -e
+
+running_pid() {
+# Check if a given process pid's cmdline matches a given name
+    pid=$1
+    name=$2
+    [ -z "$pid" ] && return 1
+    [ ! -d /proc/$pid ] &&  return 1
+    cmd=`cat /proc/$pid/cmdline | tr "\000" " "|cut -d " " -f 2`
+    # Is this the expected server
+    [ "$cmd" != "$name" ] &&  return 1
+    return 0
+}
+
+running() {
+# Check if the process is running looking at /proc
+# (works for all users)
+
+    # No pidfile, probably no daemon present
+    [ ! -f "$PIDFILE" ] && return 1
+    pid=`cat $PIDFILE`
+    running_pid $pid $DAEMON || return 1
+    return 0
+}
+
+start_server() {
+    if [ ! -d /var/run/openvswitch ]; then
+        install -d -m 755 -o root -g root /var/run/openvswitch
+    fi
+
+    /usr/share/openvswitch/scripts/ovs-monitor-ipsec \
+           --pidfile=$PIDFILE --log-file --detach --monitor \
+           unix:/var/run/openvswitch/db.sock
+
+    return 0
+}
+
+stop_server() {
+    if [ -e $PIDFILE ]; then
+        kill `cat $PIDFILE`
+    fi
+
+    return 0
+}
+
+force_stop() {
+# Force the process to die killing it manually
+    [ ! -e "$PIDFILE" ] && return
+    if running ; then
+        kill -15 $pid
+        # Is it really dead?
+        sleep "$DODTIME"
+        if running ; then
+            kill -9 $pid
+            sleep "$DODTIME"
+            if running ; then
+                echo "Cannot kill $NAME (pid=$pid)!"
+                exit 1
+            fi
+        fi
+    fi
+    rm -f $PIDFILE
+}
+
+
+case "$1" in
+  start)
+        log_daemon_msg "Starting $NAME"
+        # Check if it's running first
+        if running ;  then
+            log_progress_msg "apparently already running"
+            log_end_msg 0
+            exit 0
+        fi
+        if start_server && running ;  then
+            # It's ok, the server started and is running
+            log_end_msg 0
+        else
+            # Either we could not start it or it is not running
+            # after we did
+            # NOTE: Some servers might die some time after they start,
+            # this code does not try to detect this and might give
+            # a false positive (use 'status' for that)
+            log_end_msg 1
+        fi
+        ;;
+  stop)
+        log_daemon_msg "Stopping $NAME"
+        if running ; then
+            # Only stop the server if we see it running
+            stop_server
+            log_end_msg $?
+        else
+            # If it's not running don't do anything
+            log_progress_msg "apparently not running"
+            log_end_msg 0
+            exit 0
+        fi
+        ;;
+  force-stop)
+        # First try to stop gracefully the program
+        $0 stop
+        if running; then
+            # If it's still running try to kill it more forcefully
+            log_daemon_msg "Stopping (force) $NAME"
+            force_stop
+            log_end_msg $?
+        fi
+        ;;
+  restart|force-reload)
+        log_daemon_msg "Restarting $NAME"
+        stop_server
+        # Wait some sensible amount, some server need this
+        [ -n "$DODTIME" ] && sleep $DODTIME
+        start_server
+        running
+        log_end_msg $?
+        ;;
+  status)
+        log_daemon_msg "Checking status of $NAME"
+        if running ;  then
+            log_progress_msg "running"
+            log_end_msg 0
+        else
+            log_progress_msg "apparently not running"
+            log_end_msg 1
+            exit 1
+        fi
+        ;;
+  # Use this if the daemon cannot reload
+  reload)
+        log_warning_msg "Reloading $NAME daemon: not implemented, as the daemon"
+        log_warning_msg "cannot re-read the config file (use restart)."
+        ;;
+  *)
+        N=/etc/init.d/openvswitch-ipsec
+        echo "Usage: $N {start|stop|force-stop|restart|force-reload|status}" >&2
+        exit 1
+        ;;
+esac
+
+exit 0
\ No newline at end of file
diff --git a/debian/openvswitch-ipsec.install b/debian/openvswitch-ipsec.install
new file mode 100644
index 000000000..8fe665cb3
--- /dev/null
+++ b/debian/openvswitch-ipsec.install
@@ -0,0 +1 @@ 
+ipsec/ovs-monitor-ipsec usr/share/openvswitch/scripts
diff --git a/ipsec/automake.mk b/ipsec/automake.mk
new file mode 100644
index 000000000..1e530cb42
--- /dev/null
+++ b/ipsec/automake.mk
@@ -0,0 +1,10 @@ 
+# Copyright (C) 2017 Nicira, Inc.
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.  This file is offered as-is,
+# without warranty of any kind.
+
+EXTRA_DIST += \
+        ipsec/ovs-monitor-ipsec
+FLAKE8_PYFILES += ipsec/ovs-monitor-ipsec
diff --git a/ipsec/ovs-monitor-ipsec b/ipsec/ovs-monitor-ipsec
new file mode 100755
index 000000000..8cb34ab9f
--- /dev/null
+++ b/ipsec/ovs-monitor-ipsec
@@ -0,0 +1,1152 @@ 
+#!/usr/bin/env python
+# Copyright (c) 2017 Nicira, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import glob
+import os
+import re
+import subprocess
+import sys
+import copy
+from string import Template
+
+from ovs.db import error
+from ovs.db import types
+import ovs.daemon
+import ovs.db.idl
+import ovs.dirs
+import ovs.unixctl
+import ovs.unixctl.server
+import ovs.util
+import ovs.vlog
+
+
+FILE_HEADER = "# Generated by ovs-monitor-ipsec...do not modify by hand!\n\n"
+SHUNT_POLICY = """conn prevent_unencrypted_gre
+    type=drop
+    leftprotoport=gre
+    mark={0}
+
+conn prevent_unencrypted_geneve
+    type=drop
+    leftprotoport=udp/6081
+    mark={0}
+
+conn prevent_unencrypted_stt
+    type=drop
+    leftprotoport=tcp/7471
+    mark={0}
+
+conn prevent_unencrypted_vxlan
+    type=drop
+    leftprotoport=udp/4789
+    mark={0}
+
+"""
+transp_tmpl = {"gre" : Template("""\
+conn $ifname-$version
+$auth_section
+    leftprotoport=gre
+    rightprotoport=gre
+
+"""), "gre64" : Template("""\
+conn $ifname-$version
+$auth_section
+    leftprotoport=gre
+    rightprotoport=gre
+
+"""), "geneve" : Template("""\
+conn $ifname-in-$version
+$auth_section
+    leftprotoport=udp/6081
+    rightprotoport=udp
+
+conn $ifname-out-$version
+$auth_section
+    leftprotoport=udp
+    rightprotoport=udp/6081
+
+"""), "stt" : Template("""\
+conn $ifname-in-$version
+$auth_section
+    leftprotoport=tcp/7471
+    rightprotoport=tcp
+
+conn $ifname-out-$version
+$auth_section
+    leftprotoport=tcp
+    rightprotoport=tcp/7471
+
+"""), "vxlan" : Template("""\
+conn $ifname-in-$version
+$auth_section
+    leftprotoport=udp/4789
+    rightprotoport=udp
+
+conn $ifname-out-$version
+$auth_section
+    leftprotoport=udp
+    rightprotoport=udp/4789
+
+""")}
+vlog = ovs.vlog.Vlog("ovs-monitor-ipsec")
+exiting = False
+monitor = None
+xfrm = None
+
+class XFRM(object):
+    """This class is a simple wrapper around ip-xfrm (8) command line
+    utility.  We are using this class only for informational purposes
+    so that ovs-monitor-ipsec could verify that IKE keying daemon has
+    installed IPsec policies and security associations into kernel as
+    expected."""
+
+    def __init__(self, ip_root_prefix):
+        self.IP = ip_root_prefix + "/sbin/ip"
+
+    def get_policies(self):
+        """This function returns IPsec policies (from kernel) in a dictionary
+        where <key> is destination IPv4 address and <value> is SELECTOR of
+        the IPsec policy."""
+        policies = {}
+        proc = subprocess.Popen([self.IP, 'xfrm', 'policy'],
+                                stdout=subprocess.PIPE)
+        while True:
+            line = proc.stdout.readline().strip()
+            if line == '':
+                break
+            a = line.split(" ")
+            if len(a) >= 4 and a[0] == "src" and a[2] == "dst":
+                dst = (a[3].split("/"))[0]
+                if not dst in policies:
+                    policies[dst] = []
+                policies[dst].append(line)
+                src = (a[3].split("/"))[0]
+                if not src in policies:
+                    policies[src] = []
+                policies[src].append(line)
+        return policies
+
+    def get_securities(self):
+        """This function returns IPsec security associations (from kernel)
+        in a dictionary where <key> is destination IPv4 address and <value>
+        is SELECTOR."""
+        securities = {}
+        proc = subprocess.Popen([self.IP, 'xfrm', 'state'],
+                                stdout=subprocess.PIPE)
+        while True:
+            line = proc.stdout.readline().strip()
+            if line == '':
+                break
+            a = line.split(" ")
+            if len(a) >= 4 and a[0] == "sel" and a[1] == "src" and a[3] == "dst":
+                remote_ip = a[4].rstrip().split("/")[0]
+                local_ip = a[2].rstrip().split("/")[0]
+                if not remote_ip in securities:
+                    securities[remote_ip] = []
+                securities[remote_ip].append(line)
+                if not local_ip in securities:
+                    securities[local_ip] = []
+                securities[local_ip].append(line)
+        return securities
+
+class StrongSwanHelper(object):
+    """This class does StrongSwan specific configurations."""
+
+    STRONGSWAN_CONF = """%s
+charon.plugins.kernel-netlink.set_proto_port_transport_sa = yes
+charon.plugins.kernel-netlink.xfrm_ack_expires = 10
+""" % (FILE_HEADER)
+
+    CONF_HEADER = """%s
+config setup
+    uniqueids=yes
+
+conn %%default
+    keyingtries=%%forever
+    type=transport
+    keyexchange=ikev2
+    auto=route
+    ike=aes256gcm16-sha256-modp2048
+    esp=aes256gcm16-modp2048
+
+""" % (FILE_HEADER)
+
+    CA_SECTION = """ca ca_auth
+    cacert=%s
+
+"""
+
+    auth_tmpl = {"psk" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    authby=psk"""),
+                 "pki_remote" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    leftid=$local_name
+    rightid=$remote_name
+    leftcert=$certificate
+    rightcert=$remote_cert"""),
+                 "pki_ca" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    leftid=$local_name
+    rightid=$remote_name
+    leftcert=$certificate""")}
+
+    def __init__(self, strongswan_root_prefix):
+        self.CHARON_CONF = strongswan_root_prefix + "/etc/strongswan.d/ovs.conf"
+        self.IPSEC = strongswan_root_prefix + "/usr/sbin/ipsec"
+        self.IPSEC_CONF = strongswan_root_prefix + "/etc/ipsec.conf"
+        self.IPSEC_SECRETS = strongswan_root_prefix + "/etc/ipsec.secrets"
+        self.conf_file = None
+        self.secrets_file = None
+
+    def start_ike_daemon(self):
+        """This function starts StrongSwan."""
+        f = open(self.CHARON_CONF, "w")
+        f.write(self.STRONGSWAN_CONF)
+        f.close()
+
+        f = open(self.IPSEC_CONF, "w")
+        f.write(self.CONF_HEADER)
+        f.close()
+
+        f = open(self.IPSEC_SECRETS, "w")
+        f.write(FILE_HEADER)
+        f.close()
+
+        vlog.info("Starting StrongSwan")
+        subprocess.call([self.IPSEC, "start"])
+
+    def get_active_conns(self):
+        """This function parses output from 'ipsec status' command.
+        It returns dictionary where <key> is interface name (as in OVSDB)
+        and <value> is another dictionary.  This another dictionary
+        uses strongSwan connection name as <key> and more detailed
+        sample line from the parsed outpus as <value>. """
+
+        conns = {}
+        proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE)
+
+        while True:
+            line = proc.stdout.readline().strip()
+            if line == '':
+                break
+            tunnel_name = line.split(":")
+            if len(tunnel_name) < 2:
+                continue
+            m = re.match(r"(.*)(-in-\d+|-out-\d+).*", tunnel_name[0])
+            if not m:
+                continue
+            ifname = m.group(1)
+            if not ifname in conns:
+                conns[ifname] = {}
+            (conns[ifname])[tunnel_name[0]] = line
+
+        return conns
+
+    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.secrets_file.write(FILE_HEADER)
+
+    def config_global(self, monitor):
+        """Configure the global state of IPsec tunnels."""
+        needs_refresh = False
+
+        if monitor.conf_in_use != monitor.conf:
+            monitor.conf_in_use = copy.deepcopy(monitor.conf)
+            needs_refresh = True
+
+        # Configure the shunt policy
+        if monitor.conf_in_use["skb_mark"]:
+            skb_mark = monitor.conf_in_use["skb_mark"]
+            self.conf_file.write(SHUNT_POLICY.format(skb_mark))
+
+        # Configure the CA cert
+        if monitor.conf_in_use["pki"]["ca_cert"]:
+            cacert = monitor.conf_in_use["pki"]["ca_cert"]
+            self.conf_file.write(self.CA_SECTION % cacert)
+
+        return needs_refresh
+
+    def config_tunnel(self, tunnel):
+        if tunnel.conf["psk"]:
+            self.secrets_file.write('%s %s : PSK "%s"\n' %
+                                (tunnel.conf["local_ip"], tunnel.conf["remote_ip"],
+                                 tunnel.conf["psk"]))
+            auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
+        else:
+            self.secrets_file.write("%s %s : RSA %s\n" %
+                                (tunnel.conf["local_ip"], tunnel.conf["remote_ip"],
+                                 tunnel.conf["private_key"]))
+            if tunnel.conf["remote_cert"]:
+                auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)
+            else:
+                auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf)
+
+        vals = tunnel.conf.copy()
+        vals["auth_section"] = auth_section
+        vals["version"] = tunnel.version
+        conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals)
+        self.conf_file.write(conf_text)
+
+    def config_fini(self):
+        self.secrets_file.close()
+        self.conf_file.close()
+        self.secrets_file = None
+        self.conf_file = None
+
+    def refresh(self, monitor):
+        """This functions refreshes strongSwan configuration.  Behind the
+        scenes this function calls:
+        1. once "ipsec update" command that tells strongSwan to load
+           all new tunnels from "ipsec.conf"; and
+        2. once "ipsec rereadsecrets" command that tells strongswan to load
+           secrets from "ipsec.conf" file
+        3. for every removed tunnel "ipsec stroke down-nb <tunnel>" command
+           that removes old tunnels.
+        Once strongSwan vici bindings will be distributed with major
+        Linux distributions this function could be simplified."""
+        vlog.info("Refreshing StrongSwan configuration")
+        subprocess.call([self.IPSEC, "update"])
+        subprocess.call([self.IPSEC, "rereadsecrets"])
+        # "ipsec update" command does not remove those tunnels that were
+        # updated or that disappeared from the ipsec.conf file.  So, we have
+        # to manually remove them by calling "ipsec stroke down-nb <tunnel>"
+        # command.  We use <version> number to tell apart tunnels that
+        # were just updated.
+        # "ipsec down-nb" command is designed to be non-blocking (opposed
+        # to "ipsec down" command).  This means that we should not be concerned
+        # about possibility of ovs-monitor-ipsec to block for each tunnel
+        # while strongSwan sends IKE messages over Internet.
+        conns_dict = self.get_active_conns()
+        for ifname, conns in conns_dict.iteritems():
+            tunnel = monitor.tunnels.get(ifname)
+            for conn in conns:
+                # IPsec "connection" names that we choose in strongswan
+                # must start with Interface name
+                if not conn.startswith(ifname):
+                    vlog.err("%s does not start with %s" % (conn, ifname))
+                    continue
+
+                # version number should be the first integer after
+                # interface name in IPsec "connection"
+                try:
+                    ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
+                except ValueError, IndexError:
+                    vlog.err("%s does not contain version number")
+                    continue
+
+                if not tunnel or tunnel.version != ver:
+                    vlog.info("%s is outdated %u" % (conn, ver))
+                    subprocess.call([self.IPSEC, "stroke", "down-nb", conn])
+
+class LibreSwanHelper(object):
+    """This class does LibreSwan specific configurations."""
+    CONF_HEADER = """%s
+config setup
+    uniqueids=yes
+
+conn %%default
+    keyingtries=%%forever
+    type=transport
+    auto=route
+    ike=aes_gcm256-sha2_256
+    esp=aes_gcm256
+    ikev2=insist
+
+""" % (FILE_HEADER)
+
+    auth_tmpl = {"psk" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    authby=secret"""),
+                 "pki_remote" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    leftid=@$local_name
+    rightid=@$remote_name
+    leftcert="$local_name"
+    rightcert="$remote_name"
+    leftrsasigkey=%cert"""),
+                 "pki_ca" : Template("""\
+    left=$local_ip
+    right=$remote_ip
+    leftid=@$local_name
+    rightid=@$remote_name
+    leftcert="$local_name"
+    leftrsasigkey=%cert
+    rightca=%same""")}
+
+    def __init__(self, libreswan_root_prefix):
+        self.IPSEC = libreswan_root_prefix + "/usr/sbin/ipsec"
+        self.IPSEC_CONF = libreswan_root_prefix + "/etc/ipsec.conf"
+        self.IPSEC_SECRETS = libreswan_root_prefix + "/etc/ipsec.secrets"
+        self.conf_file = None
+        self.secrets_file = None
+
+    def start_ike_daemon(self):
+        """This function starts LibreSwan."""
+        f = open(self.IPSEC_CONF, "w")
+        f.write(self.CONF_HEADER)
+        f.close()
+
+        f = open(self.IPSEC_SECRETS, "w")
+        f.write(FILE_HEADER)
+        f.close()
+
+        vlog.info("Starting LibreSwan")
+        subprocess.call([self.IPSEC, "start"])
+
+    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.secrets_file.write(FILE_HEADER)
+
+    def config_global(self, monitor):
+        """Configure the global state of IPsec tunnels."""
+        needs_refresh = False
+
+        if monitor.conf_in_use["pki"] != monitor.conf["pki"]:
+            # Clear old state
+            if monitor.conf_in_use["pki"]["certificate"]:
+                self._delete_local_certs_and_key(monitor.conf_in_use["pki"])
+
+            # Load new state
+            if monitor.conf["pki"]["certificate"]:
+                self._import_local_certs_and_key(monitor.conf["pki"])
+
+            monitor.conf_in_use["pki"] = copy.deepcopy(monitor.conf["pki"])
+            needs_refresh = True
+
+        # Configure the shunt policy
+        if monitor.conf["skb_mark"]:
+            self.conf_file.write(SHUNT_POLICY.format(monitor.conf["skb_mark"]))
+
+        if monitor.conf_in_use["skb_mark"] != monitor.conf["skb_mark"]:
+            monitor.conf_in_use["skb_mark"] = monitor.conf["skb_mark"]
+            needs_refresh = True
+
+        return needs_refresh
+
+    def config_tunnel(self, tunnel):
+        if tunnel.conf["psk"]:
+            self.secrets_file.write('%s %s : PSK "%s"\n' % 
+                            (tunnel.conf["local_ip"], tunnel.conf["remote_ip"], 
+                             tunnel.conf["psk"]))
+            auth_section = self.auth_tmpl["psk"].substitute(tunnel.conf)
+        elif tunnel.conf["remote_cert"]:
+            auth_section = self.auth_tmpl["pki_remote"].substitute(tunnel.conf)
+            self._import_remote_cert(tunnel.conf["remote_cert"],
+                                     tunnel.conf["remote_name"])
+        else:
+            auth_section = self.auth_tmpl["pki_ca"].substitute(tunnel.conf)
+
+        vals = tunnel.conf.copy()
+        vals["auth_section"] = auth_section
+        vals["version"] = tunnel.version
+        conf_text = transp_tmpl[tunnel.conf["tunnel_type"]].substitute(vals)
+        self.conf_file.write(conf_text)
+
+    def config_fini(self):
+        self.secrets_file.close()
+        self.conf_file.close()
+        self.secrets_file = None
+        self.conf_file = None
+
+    def clear_tunnel_state(self, tunnel):
+        if tunnel.conf["remote_cert"]:
+            self._delete_remote_cert(tunnel.conf["remote_name"])
+
+    def refresh(self, monitor):
+        vlog.info("Refreshing LibreSwan configuration")
+        subprocess.call([self.IPSEC, "auto", "--rereadsecrets"])
+        tunnels = set(monitor.tunnels.keys())
+
+        # Delete old connections
+        conns_dict = self.get_active_conns()
+        for ifname, conns in conns_dict.iteritems():
+            tunnel = monitor.tunnels.get(ifname)
+
+            for conn in conns:
+                # IPsec "connection" names must start with Interface name
+                if not conn.startswith(ifname):
+                    vlog.err("%s does not start with %s" % (conn, ifname))
+                    continue
+
+                # version number should be the first integer after
+                # interface name in IPsec "connection"
+                try:
+                    ver = int(re.findall(r'\d+', conn[len(ifname):])[0])
+                except ValueError, IndexError:
+                    vlog.err("%s does not contain version number")
+                    continue
+
+                if not tunnel or tunnel.version != ver:
+                    vlog.info("%s is outdated %u" % (conn, ver))
+                    subprocess.call([self.IPSEC, "auto", "--delete", conn])
+                elif ifname in tunnels:
+                    tunnels.remove(ifname)
+
+        # Activate new connections
+        for name in tunnels:
+            ver = monitor.tunnels[name].version
+            conn_in = "%s-in-%s" % (name, ver)
+            conn_out = "%s-out-%s" % (name, ver)
+
+            # In a corner case, LibreSwan daemon restarts for some reason and the
+            # "ipsec auto --start" command is lost. Just retry to make sure the
+            # command is received by LibreSwan.
+            while True:
+                proc = subprocess.Popen([self.IPSEC, "auto", "--start",
+                                        "--asynchronous", conn_in],
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.PIPE)
+                perr = str(proc.stderr.read())
+                pout = str(proc.stdout.read())
+                if not re.match(r".*Connection refused.*", perr) and \
+                        not re.match(r".*need --listen.*", pout):
+                    break
+
+            while True:
+                proc = subprocess.Popen([self.IPSEC, "auto", "--start",
+                                        "--asynchronous", conn_out],
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.PIPE)
+                perr = str(proc.stderr.read())
+                pout = str(proc.stdout.read())
+                if not re.match(r".*Connection refused.*", perr) and \
+                        not re.match(r".*need --listen.*", pout):
+                    break
+
+        # Activate shunt policy if configured
+        if monitor.conf["skb_mark"]:
+            subprocess.call([self.IPSEC, "auto", "--start",
+                            "--asynchronous", "prevent_unencrypted_gre"])
+            subprocess.call([self.IPSEC, "auto", "--start",
+                            "--asynchronous", "prevent_unencrypted_geneve"])
+            subprocess.call([self.IPSEC, "auto", "--start",
+                            "--asynchronous", "prevent_unencrypted_stt"])
+            subprocess.call([self.IPSEC, "auto", "--start",
+                            "--asynchronous", "prevent_unencrypted_vxlan"])
+
+    def get_active_conns(self):
+        """This function parses output from 'ipsec status' command.
+        It returns dictionary where <key> is interface name (as in OVSDB)
+        and <value> is another dictionary.  This another dictionary
+        uses LibreSwan connection name as <key> and more detailed
+        sample line from the parsed outpus as <value>. """
+
+        conns = {}
+        proc = subprocess.Popen([self.IPSEC, 'status'], stdout=subprocess.PIPE)
+
+        while True:
+            line = proc.stdout.readline().strip()
+            if line == '':
+                break
+
+            m = re.search(r"#\d+: \"(.*)\".*", line)
+            if not m:
+                continue
+
+            conn = m.group(1)
+            m = re.match(r"(.*)(-in-\d+|-out-\d+)", conn)
+            if not m:
+                continue
+
+            ifname = m.group(1)
+            if not ifname in conns:
+                conns[ifname] = {}
+            (conns[ifname])[conn] = line
+
+        return conns
+
+    def _delete_remote_cert(self, remote_name):
+        """Delete remote certiticate from the NSS database."""
+        try:
+            proc = subprocess.Popen(['certutil', '-D', '-d',
+                                    'sql:/etc/ipsec.d/', '-n', remote_name],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+        except Exception as e:
+            vlog.err("Delete remote certificate failed.\n" + str(e))
+
+    def _import_remote_cert(self, cert, remote_name):
+        """Import remote certificate to the NSS database."""
+        try:
+            proc = subprocess.Popen(['certutil', '-A', '-a', '-i', cert,
+                                    '-d', 'sql:/etc/ipsec.d/', '-n',
+                                    remote_name, '-t', 'P,P,P'],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+        except Exception as e:
+            vlog.err("Import remote certificate failed.\n" + str(e))
+
+    def _delete_local_certs_and_key(self, pki):
+        """Delete certs and key from the NSS database."""
+        name = pki["local_name"]
+        cacert = pki["ca_cert"]
+
+        try:
+            # Delete certificate and private key
+            proc = subprocess.Popen(['certutil', '-F', '-d',
+                                    'sql:/etc/ipsec.d/', '-n', name],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+
+            if cacert:
+                proc = subprocess.Popen(['certutil', '-D', '-d',
+                                        'sql:/etc/ipsec.d/', '-n', 'cacert'],
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.PIPE)
+                proc.wait()
+                if proc.returncode:
+                    raise Exception(proc.stderr.read())
+        except Exception as e:
+            vlog.err("Delete certs and key failed.\n" + str(e))
+
+    def _import_local_certs_and_key(self, pki):
+        """Import certs and key to the NSS database."""
+        cert = pki["certificate"]
+        key = pki["private_key"]
+        name = pki["local_name"]
+        cacert = pki["ca_cert"]
+
+        try:
+            # Create p12 file from pem files
+            proc = subprocess.Popen(['openssl', 'pkcs12', '-export',
+                                    '-in', cert, '-inkey', key, '-out',
+                                    '/tmp/%s.p12' % name, '-name', name,
+                                    '-passout', 'pass:'],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+
+            # Load p12 file to the database
+            proc = subprocess.Popen(['pk12util', '-i', '/tmp/%s.p12' % name,
+                                    '-d', 'sql:/etc/ipsec.d/', '-W', ''],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+
+            if cacert:
+                proc= subprocess.Popen(['certutil', '-A', '-a', '-i',
+                                       cacert, '-d', 'sql:/etc/ipsec.d/',
+                                       '-n', 'cacert', '-t', 'CT,,'],
+                                       stdout=subprocess.PIPE,
+                                       stderr=subprocess.PIPE)
+                proc.wait()
+                if proc.returncode:
+                    raise Exception(proc.stderr.read())
+        except Exception as e:
+            vlog.err("Import cert and key failed.\n" + str(e))
+        subprocess.call(['rm', '-f', '/tmp/%s.p12' % name])
+
+class IPsecTunnel(object):
+    """This is the base class for IPsec tunnel."""
+
+    unixctl_config_tmpl = Template("""\
+  Tunnel Type:    $tunnel_type
+  Local IP:       $local_ip
+  Remote IP:      $remote_ip
+  SKB mark:       $skb_mark
+  Local cert:     $certificate
+  Local name:     $local_name
+  Local key:      $private_key
+  Remote cert:    $remote_cert
+  Remote name:    $remote_name
+  CA cert:        $ca_cert
+  PSK:            $psk
+""")
+
+    unixctl_status_tmpl = Template("""\
+  Ofport:         $ofport
+  CFM state:      $cfm_state
+""")
+
+    def __init__(self, name, row):
+        self.name = name  # 'name' will not change because it is key in OVSDB
+        self.version = 0  # 'version' is increased on configuration changes
+        self.last_refreshed_version = -1
+        self.state = "INIT"
+        self.conf = {}
+        self.status = {}
+        self.update_conf(row)
+
+    def update_conf(self, row):
+        """This function updates IPsec tunnel configuration by using 'row'
+        from OVSDB interface table.  If configuration was actually changed
+        in OVSDB then this function returns True.  Otherwise, it returns
+        False."""
+        ret = False
+        options = row.options
+        remote_cert = options.get("remote_cert")
+        remote_name = options.get("remote_name")
+        if remote_cert:
+            remote_name = monitor._get_cn_from_cert(remote_cert)
+
+        new_conf = {
+            "ifname" : self.name,
+            "tunnel_type" : row.type,
+            "remote_ip" : options.get("remote_ip"),
+            "local_ip" : options.get("local_ip"),
+            "skb_mark" : monitor.conf["skb_mark"],
+            "certificate" : monitor.conf["pki"]["certificate"],
+            "private_key" : monitor.conf["pki"]["private_key"],
+            "ca_cert" : monitor.conf["pki"]["ca_cert"],
+            "remote_cert" : remote_cert,
+            "remote_name" : remote_name,
+            "local_name" : monitor.conf["pki"]["local_name"],
+            "psk" : options.get("psk")}
+
+        if self.conf != new_conf:
+            # Configuration was updated in OVSDB.  Validate it and figure
+            # out what to do next with this IPsec tunnel.  Also, increment
+            # version number of this IPsec tunnel so that we could tell
+            # apart old and new tunnels in "ipsec status" output.
+            self.version += 1
+            ret = True
+            self.conf = new_conf
+
+            if self._is_valid_tunnel_conf():
+                self.state = "CONFIGURED"
+            else:
+                vlog.warn("%s contains invalid configuration%s" %
+                          (self.name, self.invalid_reason))
+                self.state = "INVALID"
+
+        new_status = {
+            "cfm_state" : "Up" if row.cfm_fault == [False] else
+                          "Down" if row.cfm_fault == [True] else
+                          "Disabled",
+            "ofport" : "Not assigned" if (row.ofport in [[], [-1]]) else
+                       row.ofport[0]}
+
+        if self.status != new_status:
+            # Tunnel has become unhealthy or ofport changed.  Simply log this.
+            vlog.dbg("%s changed status from %s to %s" %
+                     (self.name, str(self.status), str(new_status)))
+            self.status = new_status
+        return ret
+
+    def mark_for_removal(self):
+        """This function marks tunnel for removal."""
+        self.version += 1
+        self.state = "REMOVED"
+
+    def show(self, policies, securities, conns):
+        state = self.state
+        if self.state == "INVALID":
+            state += self.invalid_reason
+        header = "Interface name: %s v%u (%s)\n" % (self.name, self.version,
+                                                    state)
+        conf = self.unixctl_config_tmpl.substitute(self.conf)
+        status = self.unixctl_status_tmpl.substitute(self.status)
+        spds = "Kernel policies installed:\n"
+        remote_ip = self.conf["remote_ip"]
+        if remote_ip in policies:
+            for line in policies[remote_ip]:
+                spds += "  " + line + "\n"
+        sas = "Kernel security associations installed:\n"
+        if remote_ip in securities:
+            for line in securities[remote_ip]:
+                sas += "  " + line + "\n"
+        cons = "IPsec connections that are active:\n"
+        if self.name in conns:
+            for tname in conns[self.name]:
+                cons += "  " + conns[self.name][tname] + "\n"
+
+        return header + conf + status + spds + sas + cons + "\n"
+
+    def _is_valid_tunnel_conf(self):
+        """This function verifies if IPsec tunnel has valid configuration
+        set in 'conf'.  If it is valid, then it returns True.  Otherwise,
+        it returns False and sets the reason why configuration was considered
+        as invalid.
+
+        This function could be improved in future to also verify validness
+        of certificates themselves so that ovs-monitor-ipsec would not
+        pass malformed configuration to IKE daemon."""
+
+        self.invalid_reason = None
+
+        if not self.conf["remote_ip"]:
+            self.invalid_reason = ": 'remote_ip' is not set"
+            return False
+
+        if not self.conf["local_ip"]:
+            self.invalid_reason = ": 'local_ip' is not set"
+            return False
+
+        if self.conf["psk"]:
+            if self.conf["certificate"] or self.conf["private_key"] \
+                    or self.conf["ca_cert"] or self.conf["remote_cert"]
+                    or self.conf["remote_name"]:
+                self.invalid_reason = ": 'certificate', 'private_key', "\
+                                      "'ca_cert', 'remote_cert', and "\
+                                      "'remote_name' must be unset with PSK"
+                return False
+        elif self.conf["remote_name"]:
+            if not self.conf["certificate"]:
+                self.invalid_reason = ": must set 'certificate' with PKI"
+                return False
+            elif not self.conf["private_key"]:
+                self.invalid_reason = ": must set 'private_key' with PKI"
+                return False
+            if not self.conf["remote_cert"] and not self.conf["ca_cert"]:
+                self.invalid_reason = ": must set 'remote_cert' or 'ca_cert'"
+                return False
+        else:
+            self.invalid_reason = ": must choose a authentication method"
+            return False
+
+        return True
+
+class IPsecMonitor(object):
+    """This class monitors and configures IPsec tunnels"""
+
+    def __init__(self, root_prefix):
+        self.IPSEC = root_prefix + "/usr/sbin/ipsec"
+        self.tunnels = {}
+
+        # Global configuration shared by all tunnels
+        self.conf = {
+            "pki" : {
+                "private_key" : None,
+                "certificate" : None,
+                "ca_cert" : None,
+                "local_name" : None
+            },
+            "skb_mark" : None
+        }
+        self.conf_in_use = copy.deepcopy(self.conf)
+
+        # Choose to either use StrongSwan or LibreSwan as IKE daemon
+        try:
+            proc = subprocess.Popen([self.IPSEC, "--version"],
+                                    stdout=subprocess.PIPE)
+            line = proc.stdout.readline().strip().split(" ")
+
+            if len(line) >= 2 and line[1] in ["strongSwan", "Libreswan"]:
+                if line[1] == "strongSwan":
+                    self.ike_helper = StrongSwanHelper(root_prefix)
+                else:
+                    self.ike_helper = LibreSwanHelper(root_prefix)
+            else:
+                raise Exception("IKE daemon is not installed. Please install
+                                the supported daemon (StrongSwan or LibreSwan).")
+        except Exception as e:
+            vlog.err(str(e))
+            sys.exit(1)
+
+        self.ike_helper.start_ike_daemon()
+
+    def is_tunneling_type_supported(self, tunnel_type):
+        """Returns True if we know how to configure IPsec for these
+        types of tunnels.  Otherwise, returns False."""
+        return tunnel_type in ["gre", "geneve", "vxlan", "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 \
+                "remote_name" in options_column or \
+                "remote_cert" in options_column
+
+    def add_tunnel(self, name, row):
+        """Adds a new tunnel that monitor will provision with 'name'."""
+        vlog.info("Tunnel %s appeared in OVSDB" % (name))
+        self.tunnels[name] = IPsecTunnel(name, row)
+
+    def update_tunnel(self, name, row):
+        """Updates configuration of already existing tunnel with 'name'."""
+        tunnel = self.tunnels[name]
+        if tunnel.update_conf(row):
+            vlog.info("Tunnel's '%s' configuration changed in OVSDB to %u" %
+                      (tunnel.name, tunnel.version))
+
+    def del_tunnel(self, name):
+        """Deletes tunnel by 'name'."""
+        vlog.info("Tunnel %s disappeared from OVSDB" % (name))
+        self.tunnels[name].mark_for_removal()
+
+    def update_conf(self, pki, skb_mark):
+        """Update the global configuration for IPsec tunnels"""
+        # Update PKI certs and key
+        self.conf["pki"]["certificate"] = pki[0]
+        self.conf["pki"]["private_key"] = pki[1]
+        self.conf["pki"]["ca_cert"] = pki[2]
+        self.conf["pki"]["local_name"] = pki[3]
+
+        # Update skb_mark used in IPsec policies.
+        self.conf["skb_mark"] = skb_mark
+
+    def read_ovsdb_open_vswitch_table(self, data):
+        """This functions reads IPsec relevant configuration from Open_vSwitch
+        table."""
+        pki = [None, None, None, None]
+        skb_mark = None
+        is_valid = False
+
+        for row in data["Open_vSwitch"].rows.itervalues():
+            pki[0] = row.other_config.get("certificate")
+            pki[1] = row.other_config.get("private_key")
+            pki[2] = row.other_config.get("ca_cert")
+            skb_mark = row.other_config.get("ipsec_skb_mark")
+
+        # Test whether it's a valid configration
+        if pki[0] and pki[1]:
+            pki[3] = self._get_cn_from_cert(pki[0])
+            if pki[3]:
+                is_valid = True
+        elif not pki[0] and not pki[1] and not pki[2]:
+            is_valid = True
+
+        if not is_valid:
+            vlog.warn("The cert and key configuration is not valid.
+                The valid configuations are 1): certificate, private_key
+                and ca_cert are not set; or 2): certificate and
+                private_key are all set.")
+        else:
+            self.update_conf(pki, skb_mark)
+
+    def read_ovsdb_interface_table(self, data):
+        """This function reads the IPsec relevant configuration from Interface
+        table."""
+        ifaces = set()
+
+        for row in data["Interface"].rows.itervalues():
+            if not self.is_tunneling_type_supported(row.type):
+                continue
+            if not self.is_ipsec_required(row.options):
+                continue
+            if row.name in self.tunnels:
+                self.update_tunnel(row.name, row)
+            else:
+                self.add_tunnel(row.name, row)
+            ifaces.add(row.name)
+
+        # Mark for removal those tunnels that just disappeared from OVSDB
+        for tunnel in self.tunnels.keys():
+            if not tunnel in ifaces:
+                self.del_tunnel(tunnel)
+
+    def read_ovsdb(self, data):
+        """This function reads all configuration from OVSDB that ovs-monitor-ipsec
+        is interested in."""
+        self.read_ovsdb_open_vswitch_table(data)
+        self.read_ovsdb_interface_table(data)
+
+    def show(self, unix_conn, policies, securities):
+        """This function prints all tunnel state in 'unix_conn'.
+        It uses 'policies' and securities' received from Linux Kernel
+        to show if tunnels were actually configured by the IKE deamon."""
+        if not self.tunnels:
+            unix_conn.reply("No tunnels configured with IPsec")
+            return
+        s = ""
+        conns = self.ike_helper.get_active_conns()
+        for name, tunnel in self.tunnels.iteritems():
+            s += tunnel.show(policies, securities, conns)
+        unix_conn.reply(s)
+
+    def run(self):
+        """This function runs state machine that represents whole
+        IPsec configuration (i.e. merged together from individual
+        tunnel state machines).  It creates configuration files and
+        tells IKE daemon to update configuration."""
+        needs_refresh = False
+        removed_tunnels = []
+
+        self.ike_helper.config_init()
+
+        if self.ike_helper.config_global(self):
+            needs_refresh = True
+
+        for name, tunnel in self.tunnels.iteritems():
+            if tunnel.last_refreshed_version != tunnel.version:
+                tunnel.last_refreshed_version = tunnel.version
+                needs_refresh = True
+
+            if tunnel.state == "REMOVED" or tunnel.state == "INVALID":
+                removed_tunnels.append(name)
+            elif tunnel.state == "CONFIGURED":
+                self.ike_helper.config_tunnel(self.tunnels[name])
+
+        self.ike_helper.config_fini()
+
+        for name in removed_tunnels:
+            # LibreSwan needs to clear state from database
+            if hasattr(self.ike_helper, "clear_tunnel_state"):
+                self.ike_helper.clear_tunnel_state(self.tunnels[name])
+            del self.tunnels[name]
+
+        if needs_refresh:
+            self.ike_helper.refresh(self)
+
+    def _get_cn_from_cert(self, cert):
+        try:
+            proc = subprocess.Popen(['openssl', 'x509', '-noout', '-subject',
+                                    '-nameopt', 'RFC2253', '-in', cert],
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE)
+            proc.wait()
+            if proc.returncode:
+                raise Exception(proc.stderr.read())
+            m = re.search(r"CN=(.+?),", proc.stdout.readline())
+            if not m:
+                raise Exception("No CN in the certificate subject.")
+        except Exception as e:
+            vlog.warn(str(e))
+            return None
+
+        return m.group(1)
+
+def unixctl_xfrm_policies(conn, unused_argv, unused_aux):
+    global xfrm
+    policies = xfrm.get_policies()
+    conn.reply(str(policies))
+
+def unixctl_xfrm_state(conn, unused_argv, unused_aux):
+    global xfrm
+    securities = xfrm.get_securities()
+    conn.reply(str(securities))
+
+def unixctl_ipsec_status(conn, unused_argv, unused_aux):
+    global monitor
+    conns = monitor.ike_helper.get_active_conns()
+    conn.reply(str(conns))
+
+def unixctl_show(conn, unused_argv, unused_aux):
+    global monitor
+    global xfrm
+    policies = xfrm.get_policies()
+    securities = xfrm.get_securities()
+    monitor.show(conn, policies, securities)
+
+def unixctl_refresh(conn, unused_argv, unused_aux):
+    global monitor
+    monitor.ike_helper.refresh(monitor)
+    conn.reply(None)
+
+def unixctl_exit(conn, unused_argv, unused_aux):
+    global monitor
+    global exiting
+    exiting = True
+
+    # Make sure persistent global states are cleared
+    monitor.update_conf([None, None, None, None], None)
+    # Make sure persistent tunnel states are cleared
+    for tunnel in monitor.tunnels.keys():
+        monitor.del_tunnel(tunnel)
+    monitor.run()
+
+    conn.reply(None)
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("database", metavar="DATABASE",
+                        help="A socket on which ovsdb-server is listening.")
+    parser.add_argument("--root-prefix", metavar="DIR",
+                        help="Use DIR as alternate root directory"
+                        " (for testing).")
+
+    ovs.vlog.add_args(parser)
+    ovs.daemon.add_args(parser)
+    args = parser.parse_args()
+    ovs.vlog.handle_args(args)
+    ovs.daemon.handle_args(args)
+
+    global monitor
+    global xfrm
+
+    root_prefix = args.root_prefix if args.root_prefix else ""
+    xfrm = XFRM(root_prefix)
+    monitor = IPsecMonitor(root_prefix)
+
+    remote = args.database
+    schema_helper = ovs.db.idl.SchemaHelper()
+    schema_helper.register_columns("Interface",
+                                   ["name", "type", "options", "cfm_fault",
+                                    "ofport"])
+    schema_helper.register_columns("Open_vSwitch", ["other_config"])
+    idl = ovs.db.idl.Idl(remote, schema_helper)
+
+    ovs.daemon.daemonize()
+
+    ovs.unixctl.command_register("xfrm/policies", "", 0, 0,
+                                 unixctl_xfrm_policies, None)
+    ovs.unixctl.command_register("xfrm/state", "", 0, 0,
+                                 unixctl_xfrm_state, None)
+    ovs.unixctl.command_register("ipsec/status", "", 0, 0,
+                                 unixctl_ipsec_status, None)
+    ovs.unixctl.command_register("tunnels/show", "", 0, 0,
+                                 unixctl_show, None)
+    ovs.unixctl.command_register("refresh", "", 0, 0, unixctl_refresh, None)
+    ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
+
+    error, unixctl_server = ovs.unixctl.server.UnixctlServer.create(None)
+    if error:
+        ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
+
+    seqno = idl.change_seqno  # Sequence number when OVSDB was processed last time
+
+    while True:
+        unixctl_server.run()
+        if exiting:
+            break
+
+        idl.run()
+        if seqno != idl.change_seqno:
+            monitor.read_ovsdb(idl.tables)
+            seqno = idl.change_seqno
+
+        monitor.run()
+
+        poller = ovs.poller.Poller()
+        unixctl_server.wait(poller)
+        idl.wait(poller)
+        poller.block()
+
+    unixctl_server.close()
+    idl.close()
+
+if __name__ == '__main__':
+    try:
+        main()
+    except SystemExit:
+        # Let system.exit() calls complete normally
+        raise
+    except:
+        vlog.exception("traceback")
+        sys.exit(ovs.daemon.RESTART_EXIT_CODE)
diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml
index b93da69bd..d2ac31cdb 100644
--- a/vswitchd/vswitch.xml
+++ b/vswitchd/vswitch.xml
@@ -473,10 +473,8 @@ 
           OpenFlow specification mandates the timeout to be at least one
           second. The default is 10 seconds.
         </p>
-    </column>
-
+      </column>
     </group>
-
     <group title="Status">
       <column name="next_cfg">
         Sequence number for client to increment.  When a client modifies
@@ -740,6 +738,87 @@ 
       </column>
     </group>
 
+    <group title="IPsec">
+      <p>
+        The global configuration of IPsec tunnels is set in the
+        <code>other_config</code> column of the <code>Open_vSwitch</code>
+        table. The individual IPsec tunnel configuration is set in the
+        <code>options</code> column of the <code>Interface</code> table.
+      </p>
+      <p>
+        <code>ipsec_skb_mark</code> is set to choose forwarding mode which
+        prevents unencrypted packets being sent out since there is delay
+        between the user setting up IPsec tunnel and the IPsec tunnel actually
+        taking affect to encrypt packets.
+      </p>
+      <p>
+        There are three authentication modes:
+      </p>
+      <ol>
+        <li>
+          To authenticate remote switch with preshared key,
+          <code>private_key</code>, <code>certificate</code>, and
+          <code>ca_cert</code> must not be set.  <code>psk</code> in the
+          <code>options</code> column of the <code>Interface</code> table
+          should be set as the preshared key.
+        </li>
+        <li>
+          To authenticate remote switch with certificate,
+          <code>private_key</code> and <code>certificate</code> must be set. In
+          the CA based authentication mode, <code>ca_cert</code> must be set.
+          <code>remote_name</code> in the <code>options</code> column of the
+          <code>Interface</code> table must be set as the common name (CN) of
+          the remote certificate. The remote certificate must be signed by the
+          CA.
+        </li>
+        <li>
+          To authenticate remote switch without CA certificate,
+          <code>remote_cert</code> in the <code>options</code> column of the
+          <code>Interface</code> table must be set. The remote certificate can
+          be self-signed.
+        </li>
+      </ol>
+      <column name="other_config" key="ipsec_skb_mark"
+              type='{"type": "string"}'>
+          <p>
+            Setting ipsec_skb_mark as 1/1 blocks unecrypted packets with skb
+            mark set as 1. Besides, the OpenFlow rule with set SKB mark action
+            specified in OVSDB needs to be installed in Open_vSwitch table
+            before the first packet was able to leave the OVS tunnel.
+          </p>
+          <p>
+            Setting ipsec_skb_mark as 0/1 blocks unecrypted packets without skb
+            mark. As a result, IPsec assumes that all packets coming from
+            tunnels should be encrypted unless OpenFlow actions explicitly
+            set skb mark to a non-zero value.
+          </p>
+      </column>
+      <column name="other_config" key="private_key"
+              type='{"type": "string"}'>
+          <p>
+            Name of a PEM file containing the private key used as the switch's
+            identity for IPsec tunnels.
+          </p>
+      </column>
+      <column name="other_config" key="certificate"
+              type='{"type": "string"}'>
+          <p>
+            Name of a PEM file containing a certificate that certifies the
+            switch's private key, and identifies a trustworthy switch for IPsec
+            tunnels. The certificate must be x.509 version 3 and with the
+            string in common name (CN) also set in the subject alternative name
+            (SAN).
+          </p>
+      </column>
+      <column name="other_config" key="ca_cert"
+              type='{"type": "string"}'>
+          <p>
+            Name of a PEM file containing the CA certificate used to verify
+            that a remote switch of the IPsec tunnel is trustworthy.
+          </p>
+      </column>
+    </group>
+
     <group title="Common Columns">
       The overall purpose of these columns is described under <code>Common
       Columns</code> at the beginning of this document.
@@ -1379,7 +1458,7 @@ 
       <column name="external_ids"/>
     </group>
   </table>
-  
+
   <table name="Port" table="Port or bond configuration.">
     <p>A port within a <ref table="Bridge"/>.</p>
     <p>Most commonly, a port has exactly one ``interface,'' pointed to by its
@@ -2368,9 +2447,9 @@ 
 
       <column name="options" key="local_ip">
         <p>
-          Optional.  The tunnel destination IP that received packets must
-          match.  Default is to match all addresses.  If specified, may be one
-          of:
+          Optional (except for IPsec tunnels).  The tunnel destination IP that
+          received packets must match.  Default is to match all addresses.  If
+          specified, may be one of:
         </p>
 
         <ul>
@@ -2625,7 +2704,7 @@ 
 
         <column name="options" key="csum" type='{"type": "boolean"}'>
           <p>
-            Optional.  Compute encapsulation header (either GRE or UDP) 
+            Optional.  Compute encapsulation header (either GRE or UDP)
             checksums on outgoing packets.  Default is disabled, set to
             <code>true</code> to enable.  Checksums present on incoming
             packets will be validated regardless of this setting.
@@ -2643,8 +2722,35 @@ 
 
         </column>
       </group>
-    </group>
 
+      <group title="Tunnel Options: IPsec">
+        <p>
+          <code>gre</code>, <code>geneve</code>, <code>vxlan</code>, and
+          <code>stt</code>interfaces support these options.
+        </p>
+        <column name="options" key="psk" type='{"type": "string"}'>
+          <p>
+            The preshared secret to negotiate tunnel in PSK mode. This value
+            must match on both tunnel ends and must be unset when
+            <code>remote_cert</code> or <code>remote_name</code>is set.
+          </p>
+        </column>
+        <column name="options" key="remote_cert" type='{"type": "string"}'>
+          <p>
+            Name of a PEM file containing a certificate of the remote switch.
+            The certificate must be x.509 version 3 and with the string in
+            common name (CN) also set in the subject alternative name (SAN). It
+            must be unset when <code>psk</code> is set.
+          </p>
+        </column>
+        <column name="options" key="remote_name" type='{"type": "string"}'>
+          <p>
+            Common name (CN) of the remote certificate. It must be unset when
+            <code>psk</code> is set.
+          </p>
+        </column>
+      </group>
+    </group>
     <group title="Tunnel Options: erspan only">
       <p>
         Only <code>erspan</code> interfaces support these options.