diff --git a/lib/stream-ssl.c b/lib/stream-ssl.c
index a5c32a1..2443005 100644
--- a/lib/stream-ssl.c
+++ b/lib/stream-ssl.c
@@ -778,13 +778,14 @@ pssl_pstream_cast(struct pstream *pstream)
 }
 
 static int
-pssl_open(const char *name OVS_UNUSED, char *suffix, struct pstream **pstreamp,
+pssl_open(const char *name, char *suffix, struct pstream **pstreamp,
           uint8_t dscp)
 {
     char bound_name[SS_NTOP_BUFSIZE + 16];
     char addrbuf[SS_NTOP_BUFSIZE];
     struct sockaddr_storage ss;
     struct pssl_pstream *pssl;
+    const char *access = "";
     uint16_t port;
     int retval;
     int fd;
@@ -799,9 +800,13 @@ pssl_open(const char *name OVS_UNUSED, char *suffix, struct pstream **pstreamp,
         return -fd;
     }
 
+    if (!strncmp(name, "pssl:ro:", 8)) {
+        access = "ro:";
+    }
+
     port = ss_get_port(&ss);
-    snprintf(bound_name, sizeof bound_name, "pssl:%"PRIu16":%s",
-             port, ss_format_address(&ss, addrbuf, sizeof addrbuf));
+    snprintf(bound_name, sizeof bound_name, "pssl:%s%"PRIu16":%s",
+             access, port, ss_format_address(&ss, addrbuf, sizeof addrbuf));
 
     pssl = xmalloc(sizeof *pssl);
     pstream_init(&pssl->pstream, &pssl_pstream_class, bound_name);
diff --git a/lib/stream-tcp.c b/lib/stream-tcp.c
index 1749fad..e0aaa68 100644
--- a/lib/stream-tcp.c
+++ b/lib/stream-tcp.c
@@ -84,13 +84,13 @@ static int
 new_pstream(char *suffix, const char *name, struct pstream **pstreamp,
             int dscp, char *unlink_path, bool kernel_print_port)
 {
-    char bound_name[SS_NTOP_BUFSIZE + 16];
+    char bound_name[SS_NTOP_BUFSIZE + 20];
     char addrbuf[SS_NTOP_BUFSIZE];
     struct sockaddr_storage ss;
+    const char *access = "";
     int error;
     uint16_t port;
     int fd;
-    char *conn_name = CONST_CAST(char *, name);
 
     fd = inet_open_passive(SOCK_STREAM, suffix, -1, &ss, dscp,
                            kernel_print_port);
@@ -98,14 +98,15 @@ new_pstream(char *suffix, const char *name, struct pstream **pstreamp,
         return -fd;
     }
 
-    port = ss_get_port(&ss);
-    if (!conn_name) {
-        snprintf(bound_name, sizeof bound_name, "ptcp:%"PRIu16":%s",
-                 port, ss_format_address(&ss, addrbuf, sizeof addrbuf));
-        conn_name = bound_name;
+    if (!strncmp(name, "ptcp:ro:", 8)) {
+        access = "ro:";
     }
 
-    error = new_fd_pstream(conn_name, fd, ptcp_accept, unlink_path, pstreamp);
+    port = ss_get_port(&ss);
+    snprintf(bound_name, sizeof bound_name, "ptcp:%s%"PRIu16":%s",
+             access, port, ss_format_address(&ss, addrbuf, sizeof addrbuf));
+
+    error = new_fd_pstream(bound_name, fd, ptcp_accept, unlink_path, pstreamp);
     if (!error) {
         pstream_set_bound_port(*pstreamp, htons(port));
     }
@@ -113,10 +114,10 @@ new_pstream(char *suffix, const char *name, struct pstream **pstreamp,
 }
 
 static int
-ptcp_open(const char *name OVS_UNUSED, char *suffix, struct pstream **pstreamp,
+ptcp_open(const char *name, char *suffix, struct pstream **pstreamp,
           uint8_t dscp)
 {
-    return new_pstream(suffix, NULL, pstreamp, dscp, NULL, true);
+    return new_pstream(suffix, name, pstreamp, dscp, NULL, true);
 }
 
 static int
diff --git a/lib/stream.c b/lib/stream.c
index f6ea849..47cd0fb 100644
--- a/lib/stream.c
+++ b/lib/stream.c
@@ -116,7 +116,7 @@ check_stream_classes(void)
  * connection methods supported by the stream. */
 void
 stream_usage(const char *name, bool active, bool passive,
-             bool bootstrap OVS_UNUSED)
+             bool bootstrap OVS_UNUSED, bool access)
 {
     /* Really this should be implemented via callbacks into the stream
      * providers, but that seems too heavy-weight to bother with at the
@@ -135,6 +135,17 @@ stream_usage(const char *name, bool active, bool passive,
                "Unix domain socket named FILE\n");
     }
 
+    if (active && access) {
+        printf("  tcp:ro:IP:PORT          "
+               "PORT at remote IP (read-only access)\n");
+#ifdef HAVE_OPENSSL
+        printf("  ssl:ro:IP:PORT          "
+               "SSL PORT at remote IP (read-only access)\n");
+#endif
+        printf("  unix:FILE               "
+               "Unix domain socket named FILE (read-only access)\n");
+    }
+
     if (passive) {
         printf("Passive %s connection methods:\n", name);
         printf("  ptcp:PORT[:IP]          "
@@ -147,6 +158,17 @@ stream_usage(const char *name, bool active, bool passive,
                "listen on Unix domain socket FILE\n");
     }
 
+    if (passive && access) {
+        printf("  ptcp:ro:PORT[:IP]       "
+               "listen to TCP PORT on IP (read-only access)\n");
+#ifdef HAVE_OPENSSL
+        printf("  pssl:ro:PORT[:IP]       "
+               "listen for SSL on PORT on IP (read-only access)\n");
+#endif
+        printf("  punix:ro:FILE           "
+               "listen on Unix domain socket FILE (read-only access)\n");
+    }
+
 #ifdef HAVE_OPENSSL
     printf("PKI configuration (required to use SSL):\n"
            "  -p, --private-key=FILE  file with private key\n"
@@ -209,6 +231,7 @@ stream_open(const char *name, struct stream **streamp, uint8_t dscp)
     const struct stream_class *class;
     struct stream *stream;
     char *suffix_copy;
+    const char *next;
     int error;
 
     COVERAGE_INC(stream_open);
@@ -219,8 +242,14 @@ stream_open(const char *name, struct stream **streamp, uint8_t dscp)
         goto error;
     }
 
+    /* Check for read-only access specifier */
+    next = strchr(name, ':') + 1;
+    if (!strncmp(next, "ro:", 3)) {
+        next += 3;
+    }
+
     /* Call class's "open" function. */
-    suffix_copy = xstrdup(strchr(name, ':') + 1);
+    suffix_copy = xstrdup(next);
     error = class->open(name, suffix_copy, &stream, dscp);
     free(suffix_copy);
     if (error) {
@@ -486,6 +515,30 @@ stream_or_pstream_needs_probes(const char *name)
     }
 }
 
+/* Returns 1 if the stream or pstream specified by 'name' includes a read-
+ * only access specification. */
+int
+stream_or_pstream_is_read_only(const char *name)
+{
+    const struct pstream_class *pclass;
+    const struct stream_class *class;
+    const char *next;
+
+    if (!stream_lookup_class(name, &class)) {
+        next = strchr(name, ':') + 1;
+        if (!strncmp(next, "ro:", 3)) {
+            return true;
+        }
+    } else if (!pstream_lookup_class(name, &pclass)) {
+        next = strchr(name, ':') + 1;
+        if (!strncmp(next, "ro:", 3)) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 /* Attempts to start listening for remote stream connections.  'name' is a
  * connection name in the form "TYPE:ARGS", where TYPE is an passive stream
  * class's name and ARGS are stream class-specific.
@@ -499,6 +552,7 @@ pstream_open(const char *name, struct pstream **pstreamp, uint8_t dscp)
     const struct pstream_class *class;
     struct pstream *pstream;
     char *suffix_copy;
+    const char *next;
     int error;
 
     COVERAGE_INC(pstream_open);
@@ -509,8 +563,14 @@ pstream_open(const char *name, struct pstream **pstreamp, uint8_t dscp)
         goto error;
     }
 
+    /* Check for read-only access specifier */
+    next = strchr(name, ':') + 1;
+    if (!strncmp(next, "ro:", 3)) {
+        next += 3;
+    }
+
     /* Call class's "open" function. */
-    suffix_copy = xstrdup(strchr(name, ':') + 1);
+    suffix_copy = xstrdup(next);
     error = class->listen(name, suffix_copy, &pstream, dscp);
     free(suffix_copy);
     if (error) {
@@ -674,7 +734,19 @@ stream_open_with_default_port(const char *name_,
     char *name;
     int error;
 
-    if ((!strncmp(name_, "tcp:", 4) || !strncmp(name_, "ssl:", 4))
+    if ((!strncmp(name_, "tcp:ro:", 7) || !strncmp(name_, "ssl:ro:", 7))
+        && count_fields(name_) < 4) {
+        if (default_port == OFP_PORT) {
+            VLOG_WARN_ONCE("The default OpenFlow port number has changed "
+                           "from %d to %d",
+                           OFP_OLD_PORT, OFP_PORT);
+        } else if (default_port == OVSDB_PORT) {
+            VLOG_WARN_ONCE("The default OVSDB port number has changed "
+                           "from %d to %d",
+                           OVSDB_OLD_PORT, OVSDB_PORT);
+        }
+        name = xasprintf("%s:%d", name_, default_port);
+    } else if ((!strncmp(name_, "tcp:", 4) || !strncmp(name_, "ssl:", 4))
         && count_fields(name_) < 3) {
         if (default_port == OFP_PORT) {
             VLOG_WARN_ONCE("The default OpenFlow port number has changed "
@@ -706,7 +778,10 @@ pstream_open_with_default_port(const char *name_,
     char *name;
     int error;
 
-    if ((!strncmp(name_, "ptcp:", 5) || !strncmp(name_, "pssl:", 5))
+    if ((!strncmp(name_, "ptcp:ro:", 8) || !strncmp(name_, "pssl:ro:", 8))
+        && count_fields(name_) < 3) {
+        name = xasprintf("%s%d", name_, default_port);
+    } else if ((!strncmp(name_, "ptcp:", 5) || !strncmp(name_, "pssl:", 5))
         && count_fields(name_) < 2) {
         name = xasprintf("%s%d", name_, default_port);
     } else {
@@ -731,8 +806,13 @@ stream_parse_target_with_default_port(const char *target,
                                       uint16_t default_port,
                                       struct sockaddr_storage *ss)
 {
-    return ((!strncmp(target, "tcp:", 4) || !strncmp(target, "ssl:", 4))
-            && inet_parse_active(target + 4, default_port, ss));
+    if (!strncmp(target, "tcp:ro:", 7) || !strncmp(target, "ssl:ro:", 7)) {
+            return inet_parse_active(target + 7, default_port, ss);
+    } else if (!strncmp(target, "tcp:", 4) || !strncmp(target, "ssl:", 4)) {
+            return inet_parse_active(target + 4, default_port, ss);
+    } else {
+        return false;
+    }
 }
 
 /* Attempts to guess the content type of a stream whose first few bytes were
diff --git a/lib/stream.h b/lib/stream.h
index f8e1891..bded34d 100644
--- a/lib/stream.h
+++ b/lib/stream.h
@@ -29,7 +29,8 @@ struct pstream;
 struct stream;
 struct vlog_module;
 
-void stream_usage(const char *name, bool active, bool passive, bool bootstrap);
+void stream_usage(const char *name, bool active, bool passive,
+                  bool bootstrap, bool access);
 
 /* Bidirectional byte streams. */
 int stream_verify_name(const char *name);
@@ -79,6 +80,7 @@ bool stream_parse_target_with_default_port(const char *target,
                                            uint16_t default_port,
                                            struct sockaddr_storage *ss);
 int stream_or_pstream_needs_probes(const char *name);
+int stream_or_pstream_is_read_only(const char *name);
 
 /* Error reporting. */
 
diff --git a/manpages.mk b/manpages.mk
index fa9e59b..4fa58df 100644
--- a/manpages.mk
+++ b/manpages.mk
@@ -63,7 +63,9 @@ ovsdb/ovsdb-server.1: \
 	lib/vlog-syn.man \
 	lib/vlog-unixctl.man \
 	lib/vlog.man \
+	ovsdb/remote-active-ro.man \
 	ovsdb/remote-active.man \
+	ovsdb/remote-passive-ro.man \
 	ovsdb/remote-passive.man \
 	ovsdb/replication-syn.man \
 	ovsdb/replication.man
@@ -87,7 +89,9 @@ lib/unixctl.man:
 lib/vlog-syn.man:
 lib/vlog-unixctl.man:
 lib/vlog.man:
+ovsdb/remote-active-ro.man:
 ovsdb/remote-active.man:
+ovsdb/remote-passive-ro.man:
 ovsdb/remote-passive.man:
 ovsdb/replication-syn.man:
 ovsdb/replication.man:
diff --git a/ovn/controller-vtep/ovn-controller-vtep.c b/ovn/controller-vtep/ovn-controller-vtep.c
index baee789..9c2ed75 100644
--- a/ovn/controller-vtep/ovn-controller-vtep.c
+++ b/ovn/controller-vtep/ovn-controller-vtep.c
@@ -256,7 +256,7 @@ Options:\n\
   -o, --options             list available options\n\
   -V, --version             display version information\n\
 ", program_name, program_name, default_db(), default_db());
-    stream_usage("database", true, false, false);
+    stream_usage("database", true, false, false, false);
     daemon_usage();
     vlog_usage();
     exit(EXIT_SUCCESS);
diff --git a/ovn/controller/ovn-controller.c b/ovn/controller/ovn-controller.c
index 4ac1425..2c7d743 100644
--- a/ovn/controller/ovn-controller.c
+++ b/ovn/controller/ovn-controller.c
@@ -749,7 +749,7 @@ usage(void)
            "usage %s [OPTIONS] [OVS-DATABASE]\n"
            "where OVS-DATABASE is a socket on which the OVS OVSDB server is listening.\n",
                program_name, program_name);
-    stream_usage("OVS-DATABASE", true, false, false);
+    stream_usage("OVS-DATABASE", true, false, false, false);
     daemon_usage();
     vlog_usage();
     printf("\nOther options:\n"
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 9d74ec6..a77e5c4 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -223,7 +223,7 @@ Options:\n\
 ", program_name, program_name, default_nb_db(), default_sb_db());
     daemon_usage();
     vlog_usage();
-    stream_usage("database", true, true, false);
+    stream_usage("database", true, true, false, false);
 }
 
 struct tnlid_node {
diff --git a/ovn/utilities/ovn-sbctl.c b/ovn/utilities/ovn-sbctl.c
index b894b8b..e064330 100644
--- a/ovn/utilities/ovn-sbctl.c
+++ b/ovn/utilities/ovn-sbctl.c
@@ -326,7 +326,7 @@ Options:\n\
 Other options:\n\
   -h, --help                  display this help message\n\
   -V, --version               display version information\n");
-    stream_usage("database", true, true, false);
+    stream_usage("database", true, true, false, false);
     exit(EXIT_SUCCESS);
 }
 
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index 7863f70..7b0eb2a 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -255,7 +255,7 @@ Other options:\n\
   --unixctl=SOCKET            set control socket name\n\
   -h, --help                  display this help message\n\
   -V, --version               display version information\n");
-    stream_usage("database", true, true, false);
+    stream_usage("database", true, true, false, false);
     exit(EXIT_SUCCESS);
 }
 
diff --git a/ovsdb/automake.mk b/ovsdb/automake.mk
index 099ed3c..112cc9b 100644
--- a/ovsdb/automake.mk
+++ b/ovsdb/automake.mk
@@ -44,7 +44,9 @@ pkgconfig_DATA += \
 
 MAN_FRAGMENTS += \
 	ovsdb/remote-active.man \
+	ovsdb/remote-active-ro.man \
 	ovsdb/remote-passive.man \
+	ovsdb/remote-passive-ro.man \
 	ovsdb/replication.man \
 	ovsdb/replication-syn.man
 
diff --git a/ovsdb/jsonrpc-server.c b/ovsdb/jsonrpc-server.c
index 87fc240..9252a20 100644
--- a/ovsdb/jsonrpc-server.c
+++ b/ovsdb/jsonrpc-server.c
@@ -272,7 +272,8 @@ ovsdb_jsonrpc_server_add_remote(struct ovsdb_jsonrpc_server *svr,
 
     if (!listener) {
         ovsdb_jsonrpc_session_create(remote, jsonrpc_session_open(name, true),
-                                      svr->read_only);
+                                      svr->read_only ||
+                                      stream_or_pstream_is_read_only(name));
     }
     return remote;
 }
@@ -364,9 +365,15 @@ ovsdb_jsonrpc_server_run(struct ovsdb_jsonrpc_server *svr)
             error = pstream_accept(remote->listener, &stream);
             if (!error) {
                 struct jsonrpc_session *js;
+                const char *stream_name;
+                bool read_only;
+
                 js = jsonrpc_session_open_unreliably(jsonrpc_open(stream),
                                                      remote->dscp);
-                ovsdb_jsonrpc_session_create(remote, js, svr->read_only);
+                stream_name = pstream_get_name(remote->listener);
+                read_only = svr->read_only ||
+                            stream_or_pstream_is_read_only(stream_name);
+                ovsdb_jsonrpc_session_create(remote, js, read_only);
             } else if (error != EAGAIN) {
                 VLOG_WARN_RL(&rl, "%s: accept failed: %s",
                              pstream_get_name(remote->listener),
diff --git a/ovsdb/ovsdb-client.c b/ovsdb/ovsdb-client.c
index 5f569e8..3bc4898 100644
--- a/ovsdb/ovsdb-client.c
+++ b/ovsdb/ovsdb-client.c
@@ -278,7 +278,7 @@ usage(void)
            "\nThe default SERVER is unix:%s/db.sock.\n"
            "The default DATABASE is Open_vSwitch.\n",
            program_name, program_name, ovs_rundir());
-    stream_usage("SERVER", true, true, true);
+    stream_usage("SERVER", true, true, true, false);
     printf("\nOutput formatting options:\n"
            "  -f, --format=FORMAT         set output formatting to FORMAT\n"
            "                              (\"table\", \"html\", \"csv\", "
diff --git a/ovsdb/ovsdb-server.1.in b/ovsdb/ovsdb-server.1.in
index e2e96ae..7ed9ea3 100644
--- a/ovsdb/ovsdb-server.1.in
+++ b/ovsdb/ovsdb-server.1.in
@@ -61,7 +61,9 @@ Adds \fIremote\fR as a connection method used by \fBovsdb\-server\fR.
 .
 .RS
 .so ovsdb/remote-passive.man
+.so ovsdb/remote-passive-ro.man
 .so ovsdb/remote-active.man
+.so ovsdb/remote-active-ro.man
 .
 .IP "\fBdb:\fIdb\fB,\fItable\fB,\fIcolumn\fR"
 Reads additional connection methods from \fIcolumn\fR in all of the
diff --git a/ovsdb/ovsdb-server.c b/ovsdb/ovsdb-server.c
index 0e3f9ac..ab284b8 100644
--- a/ovsdb/ovsdb-server.c
+++ b/ovsdb/ovsdb-server.c
@@ -1623,7 +1623,7 @@ usage(void)
            program_name, program_name, ovs_dbdir());
     printf("\nJSON-RPC options (may be specified any number of times):\n"
            "  --remote=REMOTE         connect or listen to REMOTE\n");
-    stream_usage("JSON-RPC", true, true, true);
+    stream_usage("JSON-RPC", true, true, true, true);
     daemon_usage();
     vlog_usage();
     replication_usage();
diff --git a/ovsdb/remote-active-ro.man b/ovsdb/remote-active-ro.man
new file mode 100644
index 0000000..ff6b089
--- /dev/null
+++ b/ovsdb/remote-active-ro.man
@@ -0,0 +1,28 @@
+.IP "\fBssl:ro:\fIip\fB:\fIport\fR"
+Connect to the specified SSL \fIport\fR on the host at the given \fIip\fR, which
+must be expressed as an IP address (not a DNS name) in IPv4 or IPv6 address
+format.  If \fIip\fR is an IPv6 address, then wrap \fIip\fR with square
+brackets, e.g.: \fBssl:[::1]:6640\fR.
+The \fB\-\-private\-key\fR, \fB\-\-certificate\fR, and \fB\-\-ca\-cert\fR
+options are mandatory when this form is used.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
+.
+.IP "\fBtcp:ro:\fIip\fB:\fIport\fR"
+Connect to the given TCP \fIport\fR on \fIip\fR, where \fIip\fR can be IPv4
+or IPv6 address. If \fIip\fR is an IPv6 address, then wrap \fIip\fR with
+square brackets, e.g.: \fBtcp:[::1]:6640\fR.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
+.
+.IP "\fBunix:ro:\fIfile\fR"
+On POSIX, connect to the Unix domain server socket named \fIfile\fR.
+.IP
+On Windows, connect to a local named pipe that is represented by a file
+created in the path \fIfile\fR to mimic the behavior of a Unix domain
+socket.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
diff --git a/ovsdb/remote-passive-ro.man b/ovsdb/remote-passive-ro.man
new file mode 100644
index 0000000..4e47a97
--- /dev/null
+++ b/ovsdb/remote-passive-ro.man
@@ -0,0 +1,35 @@
+.IP "\fBpssl:ro:\fIport\fR[\fB:\fIip\fR]"
+Listen on the given SSL \fIport\fR for a connection.  By default,
+connections are not bound to a particular local IP address and
+it listens only on IPv4 (but not IPv6) addresses, but
+specifying \fIip\fR limits connections to those from the given
+\fIip\fR, either IPv4 or IPv6 address.  If \fIip\fR is
+an IPv6 address, then wrap \fIip\fR with square brackets, e.g.:
+\fBpssl:6640:[::1]\fR.  The \fB\-\-private\-key\fR,
+\fB\-\-certificate\fR, and \fB\-\-ca\-cert\fR options are mandatory
+when this form is used.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
+.
+.IP "\fBptcp:ro:\fIport\fR[\fB:\fIip\fR]"
+Listen on the given TCP \fIport\fR for a connection.  By default,
+connections are not bound to a particular local IP address and
+it listens only on IPv4 (but not IPv6) addresses, but
+\fIip\fR may be specified to listen only for connections to the given
+\fIip\fR, either IPv4 or IPv6 address.  If \fIip\fR is
+an IPv6 address, then wrap \fIip\fR with square brackets, e.g.:
+\fBptcp:6640:[::1]\fR.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
+.
+.IP "\fBpunix:ro:\fIfile\fR"
+On POSIX, listen on the Unix domain server socket named \fIfile\fR for a
+connection.
+.IP
+On Windows, listen on a local named pipe.  A file is created in the
+path \fIfile\fR to mimic the behavior of a Unix domain socket.
+.IP
+The \fBro:\fR element indicates that database modifications are not allowed
+on this connection.
diff --git a/tests/ovsdb-server.at b/tests/ovsdb-server.at
index d04941b..6cd600c 100644
--- a/tests/ovsdb-server.at
+++ b/tests/ovsdb-server.at
@@ -1367,3 +1367,227 @@ AT_CHECK([diff dump1 dump2])
 dnl OVSDB_SERVER_SHUTDOWN
 dnl OVSDB_SERVER_SHUTDOWN2
 AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only ptcp connection])
+AT_KEYWORDS([ovsdb server read-only])
+ordinal_schema > schema
+AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
+AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=ptcp:ro:0:127.0.0.1 db], [0], [ignore], [ignore])
+PARSE_LISTENING_PORT([ovsdb-server.log], [TCP_PORT])
+AT_CHECK([ovsdb-client get-schema-version tcp:127.0.0.1:$TCP_PORT ordinals], [0], [5.1.3
+])
+
+AT_CHECK([ovsdb-client transact tcp:127.0.0.1:$TCP_PORT \
+        ['["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only punix connection])
+AT_KEYWORDS([ovsdb server read-only])
+ordinal_schema > schema
+AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
+AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=punix:ro:test-socket db], [0], [ignore], [ignore])
+AT_CHECK([ovsdb-client get-schema-version unix:test-socket ordinals], [0], [5.1.3
+])
+
+AT_CHECK([ovsdb-client transact unix:test-socket \
+        ['["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only pssl connection])
+AT_KEYWORDS([ovsdb server read-only])
+AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
+PKIDIR="$(cd $abs_top_builddir/tests && pwd)"
+AT_SKIP_IF([expr "$PKIDIR" : ".*[ 	'\"
+\\]"])
+AT_DATA([schema],
+  [[{"name": "mydb",
+     "tables": {
+       "SSL": {
+         "columns": {
+           "private_key": {"type": "string"},
+           "certificate": {"type": "string"},
+           "ca_cert": {"type": "string"}}},
+       "ordinals": {
+         "columns": {
+           "number": {"type": "integer"},
+           "name": {"type": "string"}},
+         "indexes": [["number"]]}
+    },
+     "version": "5.1.3",
+     "cksum": "12345678 9"}
+]])
+AT_CHECK([ovsdb-tool create db schema], [0], [stdout], [ignore])
+AT_CHECK(
+  [[ovsdb-tool transact db \
+     '["mydb",
+       {"op": "insert",
+        "table": "SSL",
+        "row": {"private_key": "'"$PKIDIR/testpki-privkey2.pem"'",
+                "certificate": "'"$PKIDIR/testpki-cert2.pem"'",
+                "ca_cert": "'"$PKIDIR/testpki-cacert.pem"'"}}]']],
+  [0], [ignore], [ignore])
+AT_CHECK(
+  [ovsdb-server --log-file --detach --no-chdir --pidfile \
+        --private-key=db:mydb,SSL,private_key \
+        --certificate=db:mydb,SSL,certificate \
+        --ca-cert=db:mydb,SSL,ca_cert \
+        --remote=pssl:ro:0:127.0.0.1 db],
+  [0], [ignore], [ignore])
+PARSE_LISTENING_PORT([ovsdb-server.log], [SSL_PORT])
+AT_CHECK([ovsdb-client \
+        --private-key=$PKIDIR/testpki-privkey.pem \
+        --certificate=$PKIDIR/testpki-cert.pem \
+        --ca-cert=$PKIDIR/testpki-cacert.pem \
+        get-schema-version ssl:127.0.0.1:$SSL_PORT mydb], \
+        [0], [5.1.3
+])
+AT_CHECK([ovsdb-client \
+        --private-key=$PKIDIR/testpki-privkey.pem \
+        --certificate=$PKIDIR/testpki-cert.pem \
+        --ca-cert=$PKIDIR/testpki-cacert.pem \
+        transact ssl:127.0.0.1:$SSL_PORT \
+        ['["mydb",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only tcp connection])
+AT_KEYWORDS([ovsdb server read-only])
+ordinal_schema > schema
+AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
+
+# find a free TCP port
+AT_CHECK([ovsdb-server --log-file '-vPATTERN:console:ovsdb-server|%c|%m' --detach --no-chdir --pidfile --remote=ptcp:0:127.0.0.1 db], [0], [ignore], [ignore])
+on_exit 'kill `cat ovsdb-server.pid`'
+PARSE_LISTENING_PORT([ovsdb-server.log], [TCP_PORT])
+OVSDB_SERVER_SHUTDOWN
+
+AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=tcp:ro:127.0.0.1:$TCP_PORT db], [0], [ignore], [ignore])
+AT_CHECK([ovsdb-client -vwarn get-schema-version ptcp:$TCP_PORT:127.0.0.1 ordinals], [0], [5.1.3
+])
+
+AT_CHECK([ovsdb-client transact ptcp:$TCP_PORT:127.0.0.1 \
+        ['["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only unix connection])
+AT_KEYWORDS([ovsdb server read-only])
+ordinal_schema > schema
+AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
+AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile --remote=unix:ro:test-socket db], [0], [ignore], [ignore])
+ovsdb-client -v get-schema-version punix:test-socket ordinals
+AT_CHECK([ovsdb-client -vwarn get-schema-version punix:test-socket ordinals], [0], [5.1.3
+])
+
+AT_CHECK([ovsdb-client transact punix:test-socket \
+        ['["ordinals",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
+AT_SETUP([ovsdb-server/read-only ssl connection])
+AT_KEYWORDS([ovsdb server read-only])
+AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
+PKIDIR="$(cd $abs_top_builddir/tests && pwd)"
+AT_SKIP_IF([expr "$PKIDIR" : ".*[ 	'\"
+\\]"])
+AT_DATA([schema],
+  [[{"name": "mydb",
+     "tables": {
+       "SSL": {
+         "columns": {
+           "private_key": {"type": "string"},
+           "certificate": {"type": "string"},
+           "ca_cert": {"type": "string"}}},
+       "ordinals": {
+         "columns": {
+           "number": {"type": "integer"},
+           "name": {"type": "string"}},
+         "indexes": [["number"]]}
+    },
+     "version": "5.1.3",
+     "cksum": "12345678 9"}
+]])
+AT_CHECK([ovsdb-tool create db schema], [0], [stdout], [ignore])
+AT_CHECK(
+  [[ovsdb-tool transact db \
+     '["mydb",
+       {"op": "insert",
+        "table": "SSL",
+        "row": {"private_key": "'"$PKIDIR/testpki-privkey2.pem"'",
+                "certificate": "'"$PKIDIR/testpki-cert2.pem"'",
+                "ca_cert": "'"$PKIDIR/testpki-cacert.pem"'"}}]']],
+  [0], [ignore], [ignore])
+
+# find a free TCP port
+AT_CHECK([ovsdb-server --log-file '-vPATTERN:console:ovsdb-server|%c|%m' --detach --no-chdir --pidfile --remote=ptcp:0:127.0.0.1 db], [0], [ignore], [ignore])
+on_exit 'kill `cat ovsdb-server.pid`'
+PARSE_LISTENING_PORT([ovsdb-server.log], [TCP_PORT])
+OVSDB_SERVER_SHUTDOWN
+
+AT_CHECK(
+  [ovsdb-server --log-file --detach --no-chdir --pidfile \
+        --private-key=db:mydb,SSL,private_key \
+        --certificate=db:mydb,SSL,certificate \
+        --ca-cert=db:mydb,SSL,ca_cert \
+        --remote=ssl:ro:127.0.0.1:$TCP_PORT db],
+  [0], [ignore], [ignore])
+AT_CHECK([ovsdb-client -vwarn \
+        --private-key=$PKIDIR/testpki-privkey.pem \
+        --certificate=$PKIDIR/testpki-cert.pem \
+        --ca-cert=$PKIDIR/testpki-cacert.pem \
+        get-schema-version pssl:$TCP_PORT:127.0.0.1 mydb], \
+        [0], [5.1.3
+])
+AT_CHECK([ovsdb-client \
+        --private-key=$PKIDIR/testpki-privkey.pem \
+        --certificate=$PKIDIR/testpki-cert.pem \
+        --ca-cert=$PKIDIR/testpki-cacert.pem \
+        transact pssl:$TCP_PORT:127.0.0.1 \
+        ['["mydb",
+         {"op": "insert",
+          "table": "ordinals",
+          "row": {"name": "two", "number": '2'}}
+         ]']], [0], [stdout], [ignore])
+cat stdout >> output
+AT_CHECK([${PERL} $srcdir/uuidfilt.pl output], [0], [[[{"details":"insert operation not allowed when database server is in read only mode","error":"not allowed"}]]
+], [ignore])
+OVSDB_SERVER_SHUTDOWN
+AT_CLEANUP
+
diff --git a/tests/test-jsonrpc.c b/tests/test-jsonrpc.c
index 684601a..7ee0681 100644
--- a/tests/test-jsonrpc.c
+++ b/tests/test-jsonrpc.c
@@ -108,7 +108,7 @@ usage(void)
            "  request REMOTE METHOD PARAMS   send request, print reply\n"
            "  notify REMOTE METHOD PARAMS  send notification and exit\n",
            program_name, program_name);
-    stream_usage("JSON-RPC", true, true, true);
+    stream_usage("JSON-RPC", true, true, true, false);
     daemon_usage();
     vlog_usage();
     printf("\nOther options:\n"
diff --git a/utilities/ovs-vsctl.c b/utilities/ovs-vsctl.c
index e710095..c61f7df 100644
--- a/utilities/ovs-vsctl.c
+++ b/utilities/ovs-vsctl.c
@@ -424,7 +424,7 @@ Options:\n\
     vlog_usage();
     printf("\
   --no-syslog             equivalent to --verbose=vsctl:syslog:warn\n");
-    stream_usage("database", true, true, false);
+    stream_usage("database", true, true, false, false);
     printf("\n\
 Other options:\n\
   -h, --help                  display this help message\n\
diff --git a/vswitchd/ovs-vswitchd.c b/vswitchd/ovs-vswitchd.c
index 72448bb..ebc8d12 100644
--- a/vswitchd/ovs-vswitchd.c
+++ b/vswitchd/ovs-vswitchd.c
@@ -250,7 +250,7 @@ usage(void)
            "where DATABASE is a socket on which ovsdb-server is listening\n"
            "      (default: \"unix:%s/db.sock\").\n",
            program_name, program_name, ovs_rundir());
-    stream_usage("DATABASE", true, false, true);
+    stream_usage("DATABASE", true, false, true, false);
     daemon_usage();
     vlog_usage();
     printf("\nDPDK options:\n"
diff --git a/vtep/vtep-ctl.c b/vtep/vtep-ctl.c
index 245ba0d..01586ac 100644
--- a/vtep/vtep-ctl.c
+++ b/vtep/vtep-ctl.c
@@ -370,7 +370,7 @@ Options:\n\
     vlog_usage();
     printf("\
   --no-syslog                 equivalent to --verbose=vtep_ctl:syslog:warn\n");
-    stream_usage("database", true, true, false);
+    stream_usage("database", true, true, false, false);
     printf("\n\
 Other options:\n\
   -h, --help                  display this help message\n\
