diff --git a/ovn/utilities/ovn-nbctl.8.xml b/ovn/utilities/ovn-nbctl.8.xml
index ba3cc82..1555b02 100644
--- a/ovn/utilities/ovn-nbctl.8.xml
+++ b/ovn/utilities/ovn-nbctl.8.xml
@@ -66,6 +66,34 @@
       </dd>
     </dl>
 
+    <h1>ACL Commands</h1>
+    <dl>
+      <dt><code>acl-add</code> <var>lswitch</var> <var>direction</var> <var>priority</var> <var>match</var> <var>action</var> [<code>log</code>]</dt>
+      <dd>
+        Adds the specified ACL to <var>lswitch</var>.
+        <var>direction</var> must be either <code>from-lport</code> or
+        <code>to-lport</code>.  <var>priority</var> must be between
+        <code>1</code> and <code>65534</code>, inclusive.  If
+        <code>log</code> is supplied, packet logging is enabled for the
+        ACL.  A full description of the fields are in <code>ovn-nb</code>(5).
+      </dd>
+
+      <dt><code>acl-del</code> <var>lswitch</var> [<var>direction</var> [<var>priority</var> <var>match</var>]]</dt>
+      <dd>
+        Deletes ACLs from <var>lswitch</var>.  If only
+        <var>lswitch</var> is supplied, all the ACLs from the logical
+        switch are deleted.  If <var>direction</var> is also specified,
+        then all the flows in that direction will be deleted from the
+        logical switch.  If all the fields are given, then a single flow
+        that matches all the fields will be deleted.
+      </dd>
+
+      <dt><code>acl-list</code> <var>lswitch</var></dt>
+      <dd>
+        Lists the ACLs on <var>lswitch</var>.
+      </dd>
+    </dl>
+
     <h1>Logical Port Commands</h1>
     <dl>
       <dt><code>lport-add</code> <var>lswitch</var> <var>lport</var></dt>
diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c
index d095df1..0b19521 100644
--- a/ovn/utilities/ovn-nbctl.c
+++ b/ovn/utilities/ovn-nbctl.c
@@ -63,6 +63,13 @@ Logical switch commands:\n\
   lswitch-get-external-id LSWITCH [KEY]\n\
                             list one or all external-ids on LSWITCH\n\
 \n\
+ACL commands:\n\
+  acl-add LSWITCH DIRECTION PRIORITY MATCH ACTION [log]\n\
+                            add an ACL to LSWITCH\n\
+  acl-del LSWITCH [DIRECTION [PRIORITY MATCH]]\n\
+                            remove ACLs from LSWITCH\n\
+  acl-list LSWITCH          print ACLs for LSWITCH\n\
+\n\
 Logical port commands:\n\
   lport-add LSWITCH LPORT   add logical port LPORT on LSWITCH\n\
   lport-add LSWITCH LPORT PARENT TAG\n\
@@ -747,6 +754,220 @@ do_lport_get_options(struct ovs_cmdl_context *ctx)
         printf("%s=%s\n", node->key, node->value);
     }
 }
+
+enum {
+    DIR_FROM_LPORT,
+    DIR_TO_LPORT
+};
+
+static int
+dir_encode(const char *dir)
+{
+    if (!strcmp(dir, "from-lport")) {
+        return DIR_FROM_LPORT;
+    } else if (!strcmp(dir, "to-lport")) {
+        return DIR_TO_LPORT;
+    }
+
+    OVS_NOT_REACHED();
+}
+
+static int
+acl_cmp(const void *acl1_, const void *acl2_)
+{
+    const struct nbrec_acl *acl1, *acl2;
+
+    acl1 = *((struct nbrec_acl **) acl1_);
+    acl2 = *((struct nbrec_acl **) acl2_);
+
+    int dir1 = dir_encode(acl1->direction);
+    int dir2 = dir_encode(acl2->direction);
+
+#define CMP(expr) \
+    do { \
+        int res; \
+        res = (expr); \
+        if (res) { \
+            return res; \
+        } \
+    } while (0)
+
+    CMP(dir1 - dir2);
+    CMP(acl1->priority > acl2->priority ? -1 :
+            (acl1->priority < acl2->priority ? 1 : 0));
+    CMP(strcmp(acl1->match, acl2->match));
+
+#undef CMP
+
+    return 0;
+}
+
+static void
+do_acl_list(struct ovs_cmdl_context *ctx)
+{
+    const struct nbrec_logical_switch *lswitch;
+    struct nbctl_context *nb_ctx = ctx->pvt;
+    const struct nbrec_acl **acls;
+    size_t i;
+
+    lswitch = lswitch_by_name_or_uuid(nb_ctx, ctx->argv[1]);
+    if (!lswitch) {
+        return;
+    }
+
+    acls = xmalloc(sizeof *acls * lswitch->n_acls);
+    for (i = 0; i < lswitch->n_acls; i++) {
+        acls[i] = lswitch->acls[i];
+    }
+
+    qsort(acls, lswitch->n_acls, sizeof *acls, acl_cmp);
+
+    for (i = 0; i < lswitch->n_acls; i++) {
+        const struct nbrec_acl *acl = acls[i];
+        printf("%10s %5ld (%s) %s%s\n", acl->direction, acl->priority,
+                acl->match, acl->action, acl->log ? " log" : "");
+    }
+
+    free(acls);
+}
+
+static void
+do_acl_add(struct ovs_cmdl_context *ctx)
+{
+    const struct nbrec_logical_switch *lswitch;
+    struct nbctl_context *nb_ctx = ctx->pvt;
+    const char *action = ctx->argv[5];
+    const char *direction;
+    int64_t priority;
+
+    lswitch = lswitch_by_name_or_uuid(nb_ctx, ctx->argv[1]);
+    if (!lswitch) {
+        return;
+    }
+
+    /* Validate direction.  Only require the first letter. */
+    if (ctx->argv[2][0] == 't') {
+        direction = "to-lport";
+    } else if (ctx->argv[2][0] == 'f') {
+        direction = "from-lport";
+    } else {
+        VLOG_WARN("Invalid direction '%s'", ctx->argv[2]);
+        return;
+    }
+
+    /* Validate priority. */
+    if (!ovs_scan(ctx->argv[3], "%"SCNd64, &priority) || priority < 1
+        || priority > 65535) {
+        VLOG_WARN("Invalid priority '%s'", ctx->argv[3]);
+        return;
+    }
+
+    /* Validate action. */
+    if (strcmp(action, "allow") && strcmp(action, "allow-related")
+        && strcmp(action, "drop") && strcmp(action, "reject")) {
+        VLOG_WARN("Invalid action '%s'", action);
+        return;
+    }
+
+    /* Create the acl. */
+    struct nbrec_acl *acl = nbrec_acl_insert(nb_ctx->txn);
+    nbrec_acl_set_priority(acl, priority);
+    nbrec_acl_set_direction(acl, direction);
+    nbrec_acl_set_match(acl, ctx->argv[4]);
+    nbrec_acl_set_action(acl, action);
+    if (ctx->argc == 7 && ctx->argv[6][0] == 'l') {
+        nbrec_acl_set_log(acl, true);
+    }
+
+    /* Insert the acl into the logical switch. */
+    nbrec_logical_switch_verify_acls(lswitch);
+    struct nbrec_acl **new_acls = xmalloc(sizeof *new_acls *
+                                          (lswitch->n_acls + 1));
+    memcpy(new_acls, lswitch->acls, sizeof *new_acls * lswitch->n_acls);
+    new_acls[lswitch->n_acls] = acl;
+    nbrec_logical_switch_set_acls(lswitch, new_acls, lswitch->n_acls + 1);
+    free(new_acls);
+}
+
+static void
+do_acl_del(struct ovs_cmdl_context *ctx)
+{
+    const struct nbrec_logical_switch *lswitch;
+    struct nbctl_context *nb_ctx = ctx->pvt;
+    const char *direction;
+    int64_t priority = 0;
+
+    lswitch = lswitch_by_name_or_uuid(nb_ctx, ctx->argv[1]);
+    if (!lswitch) {
+        return;
+    }
+
+    if (ctx->argc != 2 && ctx->argc != 3 && ctx->argc != 5) {
+        VLOG_WARN("Invalid number of arguments");
+        return;
+    }
+
+    if (ctx->argc == 2) {
+        /* If direction, priority, and match are not specified, delete
+         * all ACLs. */
+        nbrec_logical_switch_verify_acls(lswitch);
+        nbrec_logical_switch_set_acls(lswitch, NULL, 0);
+        return;
+    }
+
+    /* Validate direction.  Only require first letter. */
+    if (ctx->argv[2][0] == 't') {
+        direction = "to-lport";
+    } else if (ctx->argv[2][0] == 'f') {
+        direction = "from-lport";
+    } else {
+        VLOG_WARN("Invalid direction '%s'", ctx->argv[2]);
+        return;
+    }
+
+    /* If priority and match are not specified, delete all ACLs with the
+     * specified direction. */
+    if (ctx->argc == 3) {
+        struct nbrec_acl **new_acls
+            = xmalloc(sizeof *new_acls * lswitch->n_acls);
+
+        int n_acls = 0;
+        for (size_t i = 0; i < lswitch->n_acls; i++) {
+            if (strcmp(direction, lswitch->acls[i]->direction)) {
+                new_acls[n_acls++] = lswitch->acls[i];
+            }
+        }
+
+        nbrec_logical_switch_verify_acls(lswitch);
+        nbrec_logical_switch_set_acls(lswitch, new_acls, n_acls);
+        free(new_acls);
+        return;
+    }
+
+    /* Validate priority. */
+    if (!ovs_scan(ctx->argv[3], "%"SCNd64, &priority) || priority < 1
+        || priority > 65535) {
+        VLOG_WARN("Invalid priority '%s'", ctx->argv[3]);
+        return;
+    }
+
+    /* Remove the matching rule. */
+    for (size_t i = 0; i < lswitch->n_acls; i++) {
+        struct nbrec_acl *acl = lswitch->acls[i];
+
+        if (priority == acl->priority && !strcmp(ctx->argv[4], acl->match) &&
+             !strcmp(direction, acl->direction)) {
+            struct nbrec_acl **new_acls
+                = xmemdup(lswitch->acls, sizeof *new_acls * lswitch->n_acls);
+            new_acls[i] = lswitch->acls[lswitch->n_acls - 1];
+            nbrec_logical_switch_verify_acls(lswitch);
+            nbrec_logical_switch_set_acls(lswitch, new_acls,
+                                          lswitch->n_acls - 1);
+            free(new_acls);
+            return;
+        }
+    }
+}
 
 static void
 parse_options(int argc, char *argv[])
@@ -849,6 +1070,27 @@ static const struct ovs_cmdl_command all_commands[] = {
         .handler = do_lswitch_get_external_id,
     },
     {
+        .name = "acl-add",
+        .usage = "LSWITCH DIRECTION PRIORITY MATCH ACTION [log]",
+        .min_args = 5,
+        .max_args = 6,
+        .handler = do_acl_add,
+    },
+    {
+        .name = "acl-del",
+        .usage = "LSWITCH [DIRECTION [PRIORITY MATCH]]",
+        .min_args = 1,
+        .max_args = 4,
+        .handler = do_acl_del,
+    },
+    {
+        .name = "acl-list",
+        .usage = "LSWITCH",
+        .min_args = 1,
+        .max_args = 1,
+        .handler = do_acl_list,
+    },
+    {
         .name = "lport-add",
         .usage = "LSWITCH LPORT [PARENT] [TAG]",
         .min_args = 2,
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 8729ec0..fd9fce8 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -117,3 +117,52 @@ AT_CHECK([ovn-nbctl lport-get-port-security lp0], [0], [dnl
 
 OVN_NBCTL_TEST_STOP
 AT_CLEANUP
+
+dnl ---------------------------------------------------------------------
+
+AT_SETUP([ovn-nbctl - ACLs])
+OVN_NBCTL_TEST_START
+
+AT_CHECK([ovn-nbctl lswitch-add ls0])
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 600 udp drop log])
+AT_CHECK([ovn-nbctl acl-add ls0 to-lport 500 udp drop log])
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 400 tcp drop])
+AT_CHECK([ovn-nbctl acl-add ls0 to-lport 300 tcp drop])
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 200 ip drop])
+AT_CHECK([ovn-nbctl acl-add ls0 to-lport 100 ip drop])
+
+AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
+from-lport   600 (udp) drop log
+from-lport   400 (tcp) drop
+from-lport   200 (ip) drop
+  to-lport   500 (udp) drop log
+  to-lport   300 (tcp) drop
+  to-lport   100 (ip) drop
+])
+
+dnl Delete in one direction.
+AT_CHECK([ovn-nbctl acl-del ls0 to-lport])
+AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
+from-lport   600 (udp) drop log
+from-lport   400 (tcp) drop
+from-lport   200 (ip) drop
+])
+
+dnl Delete all ACLs.
+AT_CHECK([ovn-nbctl acl-del ls0])
+AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
+])
+
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 600 udp drop])
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 400 tcp drop])
+AT_CHECK([ovn-nbctl acl-add ls0 from-lport 200 ip drop])
+
+dnl Delete a single flow.
+AT_CHECK([ovn-nbctl acl-del ls0 from-lport 400 tcp])
+AT_CHECK([ovn-nbctl acl-list ls0], [0], [dnl
+from-lport   600 (udp) drop
+from-lport   200 (ip) drop
+])
+
+OVN_NBCTL_TEST_STOP
+AT_CLEANUP
