diff --git a/debian/openvswitch-switch.install b/debian/openvswitch-switch.install
index bfb391fe8..6a6e9a543 100644
--- a/debian/openvswitch-switch.install
+++ b/debian/openvswitch-switch.install
@@ -12,5 +12,6 @@ usr/sbin/ovs-vswitchd
 usr/sbin/ovsdb-server
 usr/share/openvswitch/scripts/ovs-check-dead-ifs
 usr/share/openvswitch/scripts/ovs-ctl
+usr/share/openvswitch/scripts/ovs-kmod-ctl
 usr/share/openvswitch/scripts/ovs-save
 usr/share/openvswitch/vswitch.ovsschema
diff --git a/debian/openvswitch-switch.manpages b/debian/openvswitch-switch.manpages
index c85cbfd30..1161cfda7 100644
--- a/debian/openvswitch-switch.manpages
+++ b/debian/openvswitch-switch.manpages
@@ -3,6 +3,7 @@ ovsdb/ovsdb-server.5
 utilities/ovs-ctl.8
 utilities/ovs-dpctl-top.8
 utilities/ovs-dpctl.8
+utilities/ovs-kmod-ctl.8
 utilities/ovs-pcap.1
 utilities/ovs-tcpdump.8
 utilities/ovs-tcpundump.1
diff --git a/rhel/openvswitch-fedora.spec.in b/rhel/openvswitch-fedora.spec.in
index 64a87a793..151a1aa85 100644
--- a/rhel/openvswitch-fedora.spec.in
+++ b/rhel/openvswitch-fedora.spec.in
@@ -547,6 +547,7 @@ fi
 %{_datadir}/openvswitch/scripts/ovs-save
 %{_datadir}/openvswitch/scripts/ovs-vtep
 %{_datadir}/openvswitch/scripts/ovs-ctl
+%{_datadir}/openvswitch/scripts/ovs-kmod-ctl
 %{_datadir}/openvswitch/scripts/ovs-systemd-reload
 %config %{_datadir}/openvswitch/vswitch.ovsschema
 %config %{_datadir}/openvswitch/vtep.ovsschema
@@ -580,6 +581,7 @@ fi
 %{_mandir}/man8/ovs-ctl.8*
 %{_mandir}/man8/ovs-dpctl.8*
 %{_mandir}/man8/ovs-dpctl-top.8*
+%{_mandir}/man8/ovs-kmod-ctl.8*
 %{_mandir}/man8/ovs-ofctl.8*
 %{_mandir}/man8/ovs-pki.8*
 %{_mandir}/man8/ovs-vsctl.8*
diff --git a/rhel/openvswitch.spec.in b/rhel/openvswitch.spec.in
index 2c5f0409a..883d25607 100644
--- a/rhel/openvswitch.spec.in
+++ b/rhel/openvswitch.spec.in
@@ -237,6 +237,7 @@ exit 0
 /usr/share/man/man8/ovs-ctl.8.gz
 /usr/share/man/man8/ovs-dpctl.8.gz
 /usr/share/man/man8/ovs-dpctl-top.8.gz
+/usr/share/man/man8/ovs-kmod-ctl.8.gz
 /usr/share/man/man8/ovs-ofctl.8.gz
 /usr/share/man/man8/ovs-parse-backtrace.8.gz
 /usr/share/man/man8/ovs-pki.8.gz
@@ -250,6 +251,7 @@ exit 0
 /usr/share/openvswitch/scripts/ovs-bugtool-*
 /usr/share/openvswitch/scripts/ovs-check-dead-ifs
 /usr/share/openvswitch/scripts/ovs-ctl
+/usr/share/openvswitch/scripts/ovs-kmod-ctl
 /usr/share/openvswitch/scripts/ovs-lib
 /usr/share/openvswitch/scripts/ovs-save
 /usr/share/openvswitch/scripts/ovs-vtep
diff --git a/utilities/.gitignore b/utilities/.gitignore
index aca5f1a34..10185c5d8 100644
--- a/utilities/.gitignore
+++ b/utilities/.gitignore
@@ -13,6 +13,7 @@
 /ovs-dpctl.8
 /ovs-dpctl-top
 /ovs-dpctl-top.8
+/ovs-kmod-ctl
 /ovs-l3ping
 /ovs-l3ping.8
 /ovs-lib
diff --git a/utilities/automake.mk b/utilities/automake.mk
index eb4fd6faa..4d4ab09ce 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -20,6 +20,7 @@ endif
 scripts_SCRIPTS += \
 	utilities/ovs-check-dead-ifs \
 	utilities/ovs-ctl \
+	utilities/ovs-kmod-ctl \
 	utilities/ovs-save
 scripts_DATA += utilities/ovs-lib
 
@@ -44,6 +45,7 @@ EXTRA_DIST += \
 	utilities/ovs-dev.py \
 	utilities/ovs-docker \
 	utilities/ovs-dpctl-top.in \
+	utilities/ovs-kmod-ctl.in \
 	utilities/ovs-l3ping.in \
 	utilities/ovs-lib.in \
 	utilities/ovs-parse-backtrace.in \
@@ -63,6 +65,7 @@ MAN_ROOTS += \
 	utilities/ovs-ctl.8 \
 	utilities/ovs-dpctl.8.in \
 	utilities/ovs-dpctl-top.8.in \
+	utilities/ovs-kmod-ctl.8 \
 	utilities/ovs-l3ping.8.in \
 	utilities/ovs-ofctl.8.in \
 	utilities/ovs-parse-backtrace.8 \
@@ -81,6 +84,7 @@ CLEANFILES += \
 	utilities/ovs-dpctl.8 \
 	utilities/ovs-dpctl-top \
 	utilities/ovs-dpctl-top.8 \
+	utilities/ovs-kmod-ctl \
 	utilities/ovs-l3ping \
 	utilities/ovs-l3ping.8 \
 	utilities/ovs-lib \
@@ -106,6 +110,7 @@ man_MANS += \
 	utilities/ovs-testcontroller.8 \
 	utilities/ovs-dpctl.8 \
 	utilities/ovs-dpctl-top.8 \
+	utilities/ovs-kmod-ctl.8 \
 	utilities/ovs-l3ping.8 \
 	utilities/ovs-ofctl.8 \
 	utilities/ovs-parse-backtrace.8 \
diff --git a/utilities/ovs-ctl.in b/utilities/ovs-ctl.in
index ef06dd967..8fdf8909a 100755
--- a/utilities/ovs-ctl.in
+++ b/utilities/ovs-ctl.in
@@ -30,37 +30,9 @@ done
 ## start ##
 ## ----- ##
 
-insert_mods () {
-    # Try loading openvswitch again.
-    action "Inserting openvswitch module" modprobe openvswitch
-}
-
 insert_mod_if_required () {
-    # If this kernel has no module support, expect we're done.
-    if test ! -e /proc/modules
-    then
-        log_success_msg "Kernel has no loadable module support. Skipping modprobe"
-        return 0
-    fi
-
-    # If openvswitch is already loaded then we're done.
-    test -e /sys/module/openvswitch && return 0
-
-    # Load openvswitch.  If that's successful then we're done.
-    insert_mods && return 0
-
-    # If the bridge module is loaded, then that might be blocking
-    # openvswitch.  Try to unload it, if there are no bridges.
-    test -e /sys/module/bridge || return 1
-    bridges=`echo /sys/class/net/*/bridge | sed 's,/sys/class/net/,,g;s,/bridge,,g'`
-    if test "$bridges" != "*"; then
-        log_warning_msg "not removing bridge module because bridges exist ($bridges)"
-        return 1
-    fi
-    action "removing bridge module" rmmod bridge || return 1
-
-    # Try loading openvswitch again.
-    insert_mods
+    ## This takes care of inserting any required kernel modules
+    ovs_kmod_ctl insert
 }
 
 set_hostname () {
diff --git a/utilities/ovs-kmod-ctl.8 b/utilities/ovs-kmod-ctl.8
new file mode 100644
index 000000000..c36638e79
--- /dev/null
+++ b/utilities/ovs-kmod-ctl.8
@@ -0,0 +1,109 @@
+.\" -*- nroff -*-
+.de IQ
+.  br
+.  ns
+.  IP "\\$1"
+..
+.de ST
+.  PP
+.  RS -0.15in
+.  I "\\$1"
+.  RE
+..
+.TH ovs\-ctl 8 "February 2018" "Open vSwitch" "Open vSwitch Manual"
+.ds PN ovs\-ctl
+.
+.SH NAME
+ovs\-kmod\-ctl \- OVS startup helper script for loading kernel modules
+.
+.SH SYNOPSIS
+\fBovs\-kmod\-ctl\fR \fBinsert
+.br
+\fBovs\-kmod\-ctl \fBremove
+.br
+\fBovs\-kmod\-ctl help \fR| \fB\-h \fR| \fB\-\-help
+.br
+\fBovs\-kmod\-ctl \-\-version
+.br
+\fBovs\-kmod\-ctl version
+.
+.SH DESCRIPTION
+.
+.PP
+The \fBovs\-kmod\-ctl\fR program is responsible for inserting and
+removing Open vSwitch kernel modules.  It is not meant to be invoked
+directly by system administrators but to be called internally by
+system startup scripts.  The script is used as part of an SELinux
+transition domain.
+.
+.PP
+Each of \fBovs\-kmod\-ctl\fR's commands is described separately below.
+.
+.SH "The ``insert'' command"
+.
+.PP
+The \fBinsert\fR command loads the Open vSwitch kernel modules, if
+needed.  If this fails, and the Linux bridge module is loaded but no
+bridges exist, it tries to unload the bridge module and tries loading
+the Open vSwitch kernel module again.
+.
+.SH "The ``remove'' command"
+.
+.PP
+The \fBremove\fR command unloads the Open vSwitch kernel module (including
+the bridge compatibility module, if loaded) and any associated vport
+modules.
+.
+.SH "EXIT STATUS"
+.
+\fBovs\-kmod\-ctl\fR exits with status 0 on success and nonzero on
+failure.  The \fBinsert\fR command is considered to succeed if kernel
+modules are already loaded; the \fBremove\fR command is considered to
+succeed if none of the kernel modules are loaded.
+.
+.SH "ENVIRONMENT"
+.
+The following environment variables affect \fBovs\-kmod\-ctl\fR:
+.
+.IP "\fBPATH\fR"
+\fBovs\-kmod\-ctl\fR does not hardcode the location of any of the programs
+that it runs.  \fBovs\-kmod\-ctl\fR will add the \fIsbindir\fR and
+\fIbindir\fR that were specified at \fBconfigure\fR time to
+\fBPATH\fR, if they are not already present.
+.
+.IP "\fBOVS_LOGDIR\fR"
+.IQ "\fBOVS_RUNDIR\fR"
+.IQ "\fBOVS_DBDIR\fR"
+.IQ "\fBOVS_SYSCONFDIR\fR"
+.IQ "\fBOVS_PKGDATADIR\fR"
+.IQ "\fBOVS_BINDIR\fR"
+.IQ "\fBOVS_SBINDIR\fR"
+Setting one of these variables in the environment overrides the
+respective \fBconfigure\fR option, both for \fBovs\-kmod\-ctl\fR itself
+and for the other Open vSwitch programs that it runs.
+.
+.SH "FILES"
+.
+\fBovs\-kmod\-ctl\fR uses the following files:
+.
+.IP "\fBovs\-lib"
+Shell function library used internally by \fBovs\-kmod\-ctl\fR.  It must
+be installed in the same directory as \fBovs\-kmod\-ctl\fR.
+.
+.SH "EXAMPLE"
+.
+.PP
+\fBovs\-kmod\-ctl\fR isn't intended to be manually executed.  However, the
+following examples demonstrate loading the kernel modules.
+.
+.TP
+\fBovs\-kmod\-ctl\fR insert
+Attempts to insert the Open vSwitch kernel modules.
+.
+.TP
+\fBovs\-kmod\-ctl\fR remove
+Attempts to remove the Open vSwitch kernel modules.
+.
+.SH "SEE ALSO"
+.
+\fBREADME.rst\fR, \fBovs\-ctl\fR(8)
diff --git a/utilities/ovs-kmod-ctl.in b/utilities/ovs-kmod-ctl.in
new file mode 100644
index 000000000..7c50d6422
--- /dev/null
+++ b/utilities/ovs-kmod-ctl.in
@@ -0,0 +1,183 @@
+#! /bin/sh
+# SPDX-License-Identifier: Apache-2.0
+# Copyright (C) 2018 Red Hat, 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.
+
+. "@pkgdatadir@/scripts/ovs-lib" || exit 1
+
+for dir in "$sbindir" "$bindir" /sbin /bin /usr/sbin /usr/bin; do
+    case :$PATH: in
+        *:$dir:*) ;;
+        *) PATH=$PATH:$dir ;;
+    esac
+done
+
+insert_mods () {
+    # Try loading openvswitch kernel module.
+    action "Inserting openvswitch module" modprobe openvswitch
+}
+
+insert_kmods_if_required() {
+    # If this kernel has no module support, expect we're done.
+    if test ! -e /proc/modules
+    then
+        log_success_msg "Kernel has no loadable module support. Skipping modprobe"
+        return 0
+    fi
+
+    # If openvswitch is already loaded then we're done.
+    test -e /sys/module/openvswitch && return 0
+
+    # Load openvswitch.  If that's successful then we're done.
+    insert_mods && return 0
+
+    # If the bridge module is loaded, then that might be blocking
+    # openvswitch.  Try to unload it, if there are no bridges.
+    test -e /sys/module/bridge || return 1
+    bridges=`echo /sys/class/net/*/bridge | sed 's,/sys/class/net/,,g;s,/bridge,,g'`
+    if test "$bridges" != "*"; then
+        log_warning_msg "not removing bridge module because bridges exist ($bridges)"
+        return 1
+    fi
+    action "removing bridge module" rmmod bridge || return 1
+
+    # Try loading openvswitch again.
+    insert_mods
+}
+
+remove_kmods() {
+    for vport in `awk '/^vport_/ { print $1 }' /proc/modules`; do
+        action "Removing $vport module" rmmod $vport
+    done
+
+    if test -e /sys/module/ip_gre; then
+        action "Forcing removal of ip_gre module" rmmod ip_gre
+    fi
+
+    if test -e /sys/module/gre; then
+        action "Forcing removal of gre module" rmmod gre
+    fi
+
+    if test -e /sys/module/openvswitch; then
+        action "Removing openvswitch module" rmmod openvswitch
+    fi
+}
+
+usage () {
+    cat <<EOF
+$0: controls Open vSwitch kernel modules
+usage: $0 [OPTIONS] COMMAND
+
+This program is intended to be invoked internally by Open vSwitch startup
+scripts.  System administrators should not normally invoke it directly.
+
+Commands:
+  insert                  insert the Open vSwitch kernel modules
+  remove                  remove the Open vSwitch kernel modules
+
+Options:
+  -h, --help              display this help message
+  -V, --version           display version information
+
+Default directories with "configure" option and environment variable override:
+  logs: @LOGDIR@ (--with-logdir, OVS_LOGDIR)
+  pidfiles and sockets: @RUNDIR@ (--with-rundir, OVS_RUNDIR)
+  conf.db: @DBDIR@ (--with-dbdir, OVS_DBDIR)
+  system configuration: @sysconfdir@ (--sysconfdir, OVS_SYSCONFDIR)
+  data files: @pkgdatadir@ (--pkgdatadir, OVS_PKGDATADIR)
+  user binaries: @bindir@ (--bindir, OVS_BINDIR)
+  system binaries: @sbindir@ (--sbindir, OVS_SBINDIR)
+
+Please report bugs to bugs@openvswitch.org (see REPORTING-BUGS for details).
+EOF
+
+    exit 0
+}
+
+set_option () {
+    var=`echo "$option" | tr abcdefghijklmnopqrstuvwxyz- ABCDEFGHIJKLMNOPQRSTUVWXYZ_`
+    eval set=\${$var+yes}
+    eval old_value=\$$var
+    if test X$set = X || \
+        (test $type = bool && \
+        test X"$old_value" != Xno && test X"$old_value" != Xyes); then
+        echo >&2 "$0: unknown option \"$arg\" (use --help for help)"
+        return
+    fi
+    eval $var=\$value
+}
+
+extra_ids=
+command=
+for arg
+do
+    case $arg in
+        -h | --help)
+            usage
+            ;;
+        -V | --version)
+            echo "$0 (Open vSwitch) $VERSION"
+            exit 0
+            ;;
+        --[a-z]*=*)
+            option=`expr X"$arg" : 'X--\([^=]*\)'`
+            value=`expr X"$arg" : 'X[^=]*=\(.*\)'`
+            type=string
+            set_option
+            ;;
+        --no-[a-z]*)
+            option=`expr X"$arg" : 'X--no-\(.*\)'`
+            value=no
+            type=bool
+            set_option
+            ;;
+        --[a-z]*)
+            option=`expr X"$arg" : 'X--\(.*\)'`
+            value=yes
+            type=bool
+            set_option
+            ;;
+        -*)
+            echo >&2 "$0: unknown option \"$arg\" (use --help for help)"
+            exit 1
+            ;;
+        *)
+            if test X"$command" = X; then
+                command=$arg
+            else
+                echo >&2 "$0: exactly one non-option argument required (use --help for help)"
+                exit 1
+            fi
+            ;;
+    esac
+done
+case $command in
+    remove)
+        remove_kmods
+        ;;
+    insert)
+        insert_kmods_if_required
+        ;;
+    help)
+        usage
+        ;;
+    '')
+        echo >&2 "$0: missing command name (use --help for help)"
+        exit 1
+        ;;
+    *)
+        echo >&2 "$0: unknown command \"$command\" (use --help for help)"
+        exit 1
+        ;;
+esac
diff --git a/utilities/ovs-lib.in b/utilities/ovs-lib.in
index 5ba4f8b08..6a958cbdf 100644
--- a/utilities/ovs-lib.in
+++ b/utilities/ovs-lib.in
@@ -503,6 +503,10 @@ ovs_vsctl () {
 ## force-reload-kmod ##
 ## ----------------- ##
 
+ovs_kmod_ctl () {
+    "$dir0/ovs-kmod-ctl" "$@"
+}
+
 internal_interfaces () {
     # Outputs a list of internal interfaces:
     #
@@ -618,21 +622,7 @@ force_reload_kmod () {
     done
     action "ovs-appctl dpctl/flush-conntrack"
 
-    for vport in `awk '/^vport_/ { print $1 }' /proc/modules`; do
-        action "Removing $vport module" rmmod $vport
-    done
-
-    if test -e /sys/module/ip_gre; then
-        action "Forcing removal of ip_gre module" rmmod ip_gre
-    fi
-
-    if test -e /sys/module/gre; then
-        action "Forcing removal of gre module" rmmod gre
-    fi
-
-    if test -e /sys/module/openvswitch; then
-        action "Removing openvswitch module" rmmod openvswitch
-    fi
+    ovs_kmod_ctl remove
 
     # Start vswitchd by asking it to wait till flow restore is finished.
     flow_restore_wait
