debian: Add support for GRE-over-IPsec
authorJustin Pettit <jpettit@nicira.com>
Fri, 17 Sep 2010 02:19:11 +0000 (19:19 -0700)
committerJustin Pettit <jpettit@nicira.com>
Thu, 23 Sep 2010 05:23:23 +0000 (22:23 -0700)
The ovs-monitor-ipsec daemon monitors the Interface table for GRE
entries.  If an entry specifies other-config parameters "ipsec-local-ip"
and ("ipsec-psk" or "ipsec-cert"), it will create the appropriate
security associations so that all GRE traffic to the remote host will be
encrypted.  In order for the two GRE tunnels to communicate, both sides
need to be configured for IPsec with appropriate authentication.

Currently, ovs-monitor-ipsec does not support certificate authentication
or ensure that an interface is actually attached to a bridge.  Both of
these issues will be addressed in a forthcoming patch.

NB: While GRE-over-IPsec should work on any system with a relatively
recent racoon and setkey, it has only been tested on Debian.  As such,
only Debian packaging has been provided.

debian/.gitignore
debian/automake.mk
debian/control
debian/openvswitch-ipsec.dirs [new file with mode: 0644]
debian/openvswitch-ipsec.init [new file with mode: 0755]
debian/openvswitch-ipsec.install [new file with mode: 0644]
debian/ovs-monitor-ipsec [new file with mode: 0755]
vswitchd/vswitch.ovsschema
vswitchd/vswitch.xml

index 7f43aa6ed9898b863838689770eaa7b537e8185c..24e62d94b966f1dd6025c79d75da7094b82783d5 100644 (file)
@@ -12,6 +12,7 @@
 /openvswitch-controller
 /openvswitch-datapath-source
 /openvswitch-dbg
+/openvswitch-ipsec
 /openvswitch-pki
 /openvswitch-pki-server
 /openvswitch-switch
index c768d56b56bbeb853316e9c576966764a77ec03b..20432062a2c9cf38dd301885901ba0cf870b90bb 100644 (file)
@@ -24,6 +24,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-server.apache2 \
        debian/openvswitch-pki-server.dirs \
        debian/openvswitch-pki-server.install \
@@ -39,6 +42,7 @@ EXTRA_DIST += \
        debian/openvswitch-switch.postrm \
        debian/openvswitch-switch.template \
        debian/ovs-bugtool \
+       debian/ovs-monitor-ipsec \
        debian/python-openvswitch.dirs \
        debian/python-openvswitch.install \
        debian/rules \
index 53e5b98d66789b3399bb4a968fc605947c4ba4c5..622daeb3bf8d2cf9452b25b2254f7d514dcdeabe 100644 (file)
@@ -41,6 +41,19 @@ Description: Open vSwitch switch implementations
  .
  Open vSwitch is a full-featured software-based Ethernet switch.
 
+Package: openvswitch-ipsec
+Architecture: any
+Depends:
+ ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, ipsec-tools, racoon,
+ openvswitch-common (= ${binary:Version}),
+ openvswitch-switch (= ${binary:Version}),
+ python-openvswitch (= ${binary:Version})
+Description: Open vSwitch GRE-over-IPsec support
+ The ovs-monitor-ipsec script provides support for encrypting GRE
+ tunnels with IPsec.
+ .
+ Open vSwitch is a full-featured software-based Ethernet switch.
+
 Package: openvswitch-pki
 Architecture: all
 Depends:
@@ -90,13 +103,14 @@ Depends:
  ${shlibs:Depends}, ${misc:Depends},
  openvswitch-common (= ${binary:Version}),
  openvswitch-controller (= ${binary:Version}),
+ openvswitch-ipsec (= ${binary:Version}),
  openvswitch-switch (= ${binary:Version})
 Description: Debug symbols for Open vSwitch packages
  This package contains the debug symbols for all the other openvswitch-*
  packages.  Install it to debug one of them or to examine a core dump
  produced by one of them.
 
-Package: python-openvswitch 
+Package: python-openvswitch
 Architecture: all
 Section: python
 Depends: ${python:Depends}, openvswitch-switch (= ${binary:Version})
diff --git a/debian/openvswitch-ipsec.dirs b/debian/openvswitch-ipsec.dirs
new file mode 100644 (file)
index 0000000..02130d0
--- /dev/null
@@ -0,0 +1 @@
+usr/share/openvswitch/scripts
diff --git a/debian/openvswitch-ipsec.init b/debian/openvswitch-ipsec.init
new file mode 100755 (executable)
index 0000000..f3c9a13
--- /dev/null
@@ -0,0 +1,184 @@
+#!/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
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Open vSwitch GRE-over-IPsec daemon
+### 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() {
+    PYTHONPATH=/usr/share/openvswitch/python \
+           /usr/share/openvswitch/scripts/ovs-monitor-ipsec \
+           --pidfile-name=$PIDFILE --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 "$DIETIME"s
+        if running ; then
+            kill -9 $pid
+            sleep "$DIETIME"s
+            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 "$DIETIME" ] && sleep $DIETIME
+        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
diff --git a/debian/openvswitch-ipsec.install b/debian/openvswitch-ipsec.install
new file mode 100644 (file)
index 0000000..72cacfa
--- /dev/null
@@ -0,0 +1 @@
+debian/ovs-monitor-ipsec usr/share/openvswitch/scripts
diff --git a/debian/ovs-monitor-ipsec b/debian/ovs-monitor-ipsec
new file mode 100755 (executable)
index 0000000..1caece3
--- /dev/null
@@ -0,0 +1,349 @@
+#!/usr/bin/python
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# 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.
+
+
+# A daemon to monitor attempts to create GRE-over-IPsec tunnels.
+# Uses racoon and setkey to support the configuration.  Assumes that
+# OVS has complete control over IPsec configuration for the box.
+
+# xxx To-do:
+#  - Doesn't actually check that Interface is connected to bridge
+#  - Doesn't support cert authentication
+
+
+import getopt
+import logging, logging.handlers
+import os
+import stat
+import subprocess
+import sys
+
+from ovs.db import error
+from ovs.db import types
+import ovs.util
+import ovs.daemon
+import ovs.db.idl
+
+
+# By default log messages as DAEMON into syslog
+s_log = logging.getLogger("ovs-monitor-ipsec")
+l_handler = logging.handlers.SysLogHandler(
+        "/dev/log",
+        facility=logging.handlers.SysLogHandler.LOG_DAEMON)
+l_formatter = logging.Formatter('%(filename)s: %(levelname)s: %(message)s')
+l_handler.setFormatter(l_formatter)
+s_log.addHandler(l_handler)
+
+
+setkey = "/usr/sbin/setkey"
+
+# Class to configure the racoon daemon, which handles IKE negotiation
+class Racoon:
+    # Default locations for files
+    conf_file = "/etc/racoon/racoon.conf"
+    cert_file = "/etc/racoon/certs"
+    psk_file = "/etc/racoon/psk.txt"
+
+    # Default racoon configuration file we use for IKE
+    conf_template = """# Configuration file generated by Open vSwitch
+#
+# Do not modify by hand!
+
+path pre_shared_key "/etc/racoon/psk.txt";
+path certificate "/etc/racoon/certs";
+
+remote anonymous {
+        exchange_mode main;
+        proposal {
+                encryption_algorithm aes;
+                hash_algorithm sha1;
+                authentication_method pre_shared_key;
+                dh_group 2;
+        }
+}
+
+sainfo anonymous {
+        pfs_group 2;
+        lifetime time 1 hour;
+        encryption_algorithm aes;
+        authentication_algorithm hmac_sha1, hmac_md5;
+        compression_algorithm deflate;
+}
+"""
+
+    def __init__(self):
+        self.psk_hosts = {}
+        self.cert_hosts = {}
+
+        # Replace racoon's conf file with our template
+        f = open(Racoon.conf_file, "w")
+        f.write(Racoon.conf_template)
+        f.close()
+
+        # Clear out any pre-shared keys
+        self.commit_psk()
+
+        self.reload()
+
+    def reload(self):
+        exitcode = subprocess.call(["/etc/init.d/racoon", "reload"])
+        if exitcode != 0:
+            s_log.warning("couldn't reload racoon")
+
+    def commit_psk(self):
+        f = open(Racoon.psk_file, 'w')
+        # The file must only be accessible by root
+        os.chmod(Racoon.psk_file, stat.S_IRUSR | stat.S_IWUSR)
+
+        f.write("# Generated by Open vSwitch...do not modify by hand!\n\n")
+        for host, psk in self.psk_hosts.iteritems():
+            f.write("%s   %s\n" % (host, psk))
+        f.close()
+
+    def add_psk(self, host, psk):
+        self.psk_hosts[host] = psk
+        self.commit_psk()
+
+    def del_psk(self, host):
+        if host in self.psk_hosts:
+            del self.psk_hosts[host]
+            self.commit_psk()
+
+
+# Class to configure IPsec on a system using racoon for IKE and setkey
+# for maintaining the Security Association Database (SAD) and Security
+# Policy Database (SPD).  Only policies for GRE are supported.
+class IPsec:
+    def __init__(self):
+        self.sad_flush()
+        self.spd_flush()
+        self.racoon = Racoon()
+
+    def call_setkey(self, cmds):
+        try:
+            p = subprocess.Popen([setkey, "-c"], stdin=subprocess.PIPE, 
+                    stdout=subprocess.PIPE)
+        except:
+            s_log.error("could not call setkey")
+            sys.exit(1)
+
+        # xxx It is safer to pass the string into the communicate()
+        # xxx method, but it didn't work for slightly longer commands.
+        # xxx An alternative may need to be found.
+        p.stdin.write(cmds)
+        return p.communicate()[0]
+
+    def get_spi(self, local_ip, remote_ip, proto="esp"):
+        # Run the setkey dump command to retrieve the SAD.  Then, parse
+        # the output looking for SPI buried in the output.  Note that
+        # multiple SAD entries can exist for the same "flow", since an
+        # older entry could be in a "dying" state.
+        spi_list = []
+        host_line = "%s %s" % (local_ip, remote_ip)
+        results = self.call_setkey("dump ;").split("\n")
+        for i in range(len(results)):
+            if results[i].strip() == host_line:
+                # The SPI is in the line following the host pair
+                spi_line = results[i+1]
+                if (spi_line[1:4] == proto):
+                    spi = spi_line.split()[2]
+                    spi_list.append(spi.split('(')[1].rstrip(')'))
+        return spi_list
+
+    def sad_flush(self):
+        self.call_setkey("flush;")
+
+    def sad_del(self, local_ip, remote_ip):
+        # To delete all SAD entries, we should be able to use setkey's
+        # "deleteall" command.  Unfortunately, it's fundamentally broken
+        # on Linux and not documented as such.
+        cmds = ""
+
+        # Delete local_ip->remote_ip SAD entries
+        spi_list = self.get_spi(local_ip, remote_ip)
+        for spi in spi_list:
+            cmds += "delete %s %s esp %s;\n" % (local_ip, remote_ip, spi)
+
+        # Delete remote_ip->local_ip SAD entries
+        spi_list = self.get_spi(remote_ip, local_ip)
+        for spi in spi_list:
+            cmds += "delete %s %s esp %s;\n" % (remote_ip, local_ip, spi)
+
+        if cmds:
+            self.call_setkey(cmds)
+
+    def spd_flush(self):
+        self.call_setkey("spdflush;")
+
+    def spd_add(self, local_ip, remote_ip):
+        cmds = ("spdadd %s %s gre -P out ipsec esp/transport//default;" %
+                    (local_ip, remote_ip))
+        cmds += "\n"
+        cmds += ("spdadd %s %s gre -P in ipsec esp/transport//default;" %
+                    (remote_ip, local_ip))
+        self.call_setkey(cmds)
+
+    def spd_del(self, local_ip, remote_ip):
+        cmds = "spddelete %s %s gre -P out;" % (local_ip, remote_ip)
+        cmds += "\n"
+        cmds += "spddelete %s %s gre -P in;" % (remote_ip, local_ip)
+        self.call_setkey(cmds)
+
+    def ipsec_cert_del(self, local_ip, remote_ip):
+        # Need to support cert...right now only PSK supported
+        self.racoon.del_psk(remote_ip)
+        self.spd_del(local_ip, remote_ip)
+        self.sad_del(local_ip, remote_ip)
+
+    def ipsec_cert_update(self, local_ip, remote_ip, cert):
+        # Need to support cert...right now only PSK supported
+        self.racoon.add_psk(remote_ip, "abc12345")
+        self.spd_add(local_ip, remote_ip)
+
+    def ipsec_psk_del(self, local_ip, remote_ip):
+        self.racoon.del_psk(remote_ip)
+        self.spd_del(local_ip, remote_ip)
+        self.sad_del(local_ip, remote_ip)
+
+    def ipsec_psk_update(self, local_ip, remote_ip, psk):
+        self.racoon.add_psk(remote_ip, psk)
+        self.spd_add(local_ip, remote_ip)
+
+
+def keep_table_columns(schema, table_name, column_types):
+    table = schema.tables.get(table_name)
+    if not table:
+        raise error.Error("schema has no %s table" % table_name)
+
+    new_columns = {}
+    for column_name, column_type in column_types.iteritems():
+        column = table.columns.get(column_name)
+        if not column:
+            raise error.Error("%s table schema lacks %s column"
+                              % (table_name, column_name))
+        if column.type != column_type:
+            raise error.Error("%s column in %s table has type \"%s\", "
+                              "expected type \"%s\""
+                              % (column_name, table_name,
+                                 column.type.toEnglish(),
+                                 column_type.toEnglish()))
+        new_columns[column_name] = column
+    table.columns = new_columns
+    return table
+def monitor_uuid_schema_cb(schema):
+    string_type = types.Type(types.BaseType(types.StringType))
+    string_map_type = types.Type(types.BaseType(types.StringType),
+                                 types.BaseType(types.StringType),
+                                 0, sys.maxint)
+    new_tables = {}
+    new_tables["Interface"] = keep_table_columns(
+        schema, "Interface", {"name": string_type,
+                              "type": string_type,
+                              "options": string_map_type,
+                              "other_config": string_map_type})
+    schema.tables = new_tables
+
+def usage():
+    print "usage: %s [OPTIONS] DATABASE" % sys.argv[0]
+    print "where DATABASE is a socket on which ovsdb-server is listening."
+    ovs.daemon.usage()
+    print "Other options:"
+    print "  -h, --help               display this help message"
+    sys.exit(0)
+def main(argv):
+    try:
+        options, args = getopt.gnu_getopt(
+            argv[1:], 'h', ['help'] + ovs.daemon.LONG_OPTIONS)
+    except getopt.GetoptError, geo:
+        sys.stderr.write("%s: %s\n" % (ovs.util.PROGRAM_NAME, geo.msg))
+        sys.exit(1)
+    for key, value in options:
+        if key in ['-h', '--help']:
+            usage()
+        elif not ovs.daemon.parse_opt(key, value):
+            sys.stderr.write("%s: unhandled option %s\n"
+                             % (ovs.util.PROGRAM_NAME, key))
+            sys.exit(1)
+    if len(args) != 1:
+        sys.stderr.write("%s: exactly one nonoption argument is required "
+                         "(use --help for help)\n" % ovs.util.PROGRAM_NAME)
+        sys.exit(1)
+
+    ovs.daemon.die_if_already_running()
+    remote = args[0]
+    idl = ovs.db.idl.Idl(remote, "Open_vSwitch", monitor_uuid_schema_cb)
+
+    ovs.daemon.daemonize()
+
+    ipsec = IPsec()
+
+    interfaces = {}
+    while True:
+        if not idl.run():
+            poller = ovs.poller.Poller()
+            idl.wait(poller)
+            poller.block()
+            continue
+        new_interfaces = {}
+        for rec in idl.data["Interface"].itervalues():
+            name = rec.name.as_scalar()
+            local_ip = rec.other_config.get("ipsec_local_ip")
+            if rec.type.as_scalar() == "gre" and local_ip:
+                new_interfaces[name] = {
+                        "remote_ip": rec.options.get("remote_ip"),
+                        "local_ip": local_ip,
+                        "ipsec_cert": rec.other_config.get("ipsec_cert"),
+                        "ipsec_psk": rec.other_config.get("ipsec_psk") }
+        if interfaces != new_interfaces:
+            for name, vals in interfaces.items():
+                if name not in new_interfaces.keys():
+                    ipsec.ipsec_cert_del(vals["local_ip"], vals["remote_ip"])
+            for name, vals in new_interfaces.items():
+                if vals == interfaces.get(name):
+                    s_log.warning(
+                        "configuration changed for %s, need to delete "
+                        "interface first" % name)
+                    continue
+
+                if vals["ipsec_cert"]:
+                    ipsec.ipsec_cert_update(vals["local_ip"],
+                            vals["remote_ip"], vals["ipsec_cert"])
+                elif vals["ipsec_psk"]:
+                    ipsec.ipsec_psk_update(vals["local_ip"], 
+                            vals["remote_ip"], vals["ipsec_psk"])
+                else:
+                    s_log.warning(
+                        "no ipsec_cert or ipsec_psk defined for %s" % name)
+                    continue
+
+            interfaces = new_interfaces
+if __name__ == '__main__':
+    try:
+        main(sys.argv)
+    except SystemExit:
+        # Let system.exit() calls complete normally
+        raise
+    except:
+        s_log.exception("traceback")
index a7d25703609e4e6f4e67a0503b019c8b7c832b3b..07dd79fbc3a2df2bf131c3a9b526501f466b83f3 100644 (file)
        "ofport": {
          "type": {"key": "integer", "min": 0, "max": 1},
          "ephemeral": true},
+       "other_config": {
+         "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}},
        "statistics": {
          "type": {"key": "string", "value": "integer", "min": 0, "max": "unlimited"},
          "ephemeral": true},
index 86fd3f9be8100ff12f9881d8c6bcf10bc7caafe8..5b5655ddd36fd7ad4c87f0988d695cba39339a8e 100644 (file)
         </dl>
       </column>
 
+      <column name="other_config">
+        Key-value pairs for rarely used interface features.  Currently,
+        the only keys are for configuring GRE-over-IPsec, which is only
+        available through the <code>openvswitch-ipsec</code> package for
+        Debian.  The currently defined key-value pairs are:
+        <dl>
+          <dt><code>ipsec-local-ip</code></dt>
+          <dd>Required key for GRE-over-IPsec interfaces.  Additionally,
+            the <ref column="type"/> must be <code>gre</code> and the
+            <code>ipsec-psk</code> <ref column="other_config"/> key must
+            be set.  The <code>in_key</code>, <code>out_key</code>, and
+            <code>key</code> <ref column="options"/> must not be
+            set.</dd>
+          <dt><code>ipsec-psk</code></dt>
+          <dd>Required key for GRE-over-IPsec interfaces.  Specifies a
+            pre-shared key for authentication that must be identical on 
+            both sides of the tunnel.  Additionally, the
+            <code>ipsec-local-ip</code> key must also be set.</dd>
+        </dl>
+      </column>
+
       <column name="statistics">
         <p>
           Key-value pairs that report interface statistics.  The current