diff --git a/lib/packets.h b/lib/packets.h
index bf12937..be6b6b3 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -986,6 +986,22 @@ in6_addr_solicited_node(struct in6_addr *addr, const struct in6_addr *ip6)
     memcpy(&addr->s6_addr[13], &ip6->s6_addr[13], 3);
 }
 
+/*
+ * Generates ipv6 link local address from the given eth addr
+ * with prefix 'fe80::/64' and stores it in 'lla'
+ */
+static inline void
+in6_generate_lla(struct eth_addr ea, struct in6_addr *lla)
+{
+    union ovs_16aligned_in6_addr *taddr = (void *) lla;
+    memset(taddr->be16, 0, sizeof(taddr->be16));
+    taddr->be16[0] = htons(0xfe80);
+    taddr->be16[4] = htons(((ea.ea[0] ^ 0x02) << 8) | ea.ea[1]);
+    taddr->be16[5] = htons(ea.ea[2] << 8 | 0x00ff);
+    taddr->be16[6] = htons(0xfe << 8 | ea.ea[3]);
+    taddr->be16[7] = ea.be16[2];
+}
+
 static inline void
 ipv6_multicast_to_ethernet(struct eth_addr *eth, const struct in6_addr *ip6)
 {
diff --git a/ovn/northd/ovn-northd.8.xml b/ovn/northd/ovn-northd.8.xml
index cacd760..53c0aff 100644
--- a/ovn/northd/ovn-northd.8.xml
+++ b/ovn/northd/ovn-northd.8.xml
@@ -111,7 +111,7 @@
 
     <h2>Logical Switch Datapaths</h2>
 
-    <h3>Ingress Table 0: Admission Control and Ingress Port Security</h3>
+    <h3>Ingress Table 0: Admission Control and Ingress Port Security - L2</h3>
 
     <p>
       Ingress table 0 contains these logical flows:
@@ -139,17 +139,101 @@
       be dropped.
     </p>
 
-    <h3>Ingress Table 1: <code>from-lport</code> Pre-ACLs</h3>
+    <h3>Ingress Table 1: Ingress Port Security - IP</h3>
 
     <p>
-      Ingress table 1 prepares flows for possible stateful ACL processing
-      in table 2.  It contains a priority-0 flow that simply moves
-      traffic to table 2.  If stateful ACLs are used in the logical
+      Ingress table 1 contains these logical flows:
+    </p>
+
+    <ul>
+      <li>
+        <p>
+          For each element in the port security set having one or more IPv4 or
+          IPv6 addresses (or both),
+        </p>
+
+        <ul>
+          <li>
+            Priority 90 flow to allow IPv4 traffic if it has IPv4 addresses
+            which match the <code>inport</code>, valid <code>eth.src</code>
+            and valid <code>ip4.src</code> address(es).
+          </li>
+
+          <li>
+            Priority 90 flow to allow IPv6 traffic if it has IPv6 addresses
+            which match the <code>inport</code>, valid <code>eth.src</code> and
+            valid <code>ip6.src</code> address(es).
+          </li>
+
+          <li>
+            Priority 80 flow to drop IP (both IPv4 and IPv6) traffic which
+            match the <code>inport</code> and valid <code>eth.src</code>.
+          </li>
+        </ul>
+      </li>
+
+      <li>
+        One priority-0 fallback flow that matches all packets and advances to
+        table 2.
+      </li>
+    </ul>
+
+    <h3>Ingress Table 2: Ingress Port Security - Neighbor discovery</h3>
+
+    <p>
+      Ingress table 2 contains these logical flows:
+    </p>
+
+    <ul>
+      <li>
+        <p>
+          For each element in the port security set,
+        </p>
+
+        <ul>
+          <li>
+            Priority 90 flow to allow ARP traffic which match the
+            <code>inport</code> and valid <code>eth.src</code> and
+            <code>arp.sha</code>. If the element has one or more
+            IPv4 addresses, then it also matches the valid
+            <code>arp.spa</code>.
+          </li>
+
+          <li>
+            Priority 90 flow to allow IPv6 Neighbor Solicitation and
+            Advertisement traffic which match the <code>inport</code>,
+            valid <code>eth.src</code> and
+            <code>nd.sll</code>/<code>nd.tll</code>.
+            If the element has one or more IPv6 addresses, then it also
+            matches the valid <code>nd.target</code> address(es) for Neighbor
+            Advertisement traffic.
+          </li>
+
+          <li>
+            Priority 80 flow to drop ARP and IPv6 Neighbor Solicitation and
+            Advertisement traffic which match the <code>inport</code> and
+            valid <code>eth.src</code>.
+          </li>
+        </ul>
+      </li>
+
+      <li>
+        One priority-0 fallback flow that matches all packets and advances to
+        table 3.
+      </li>
+    </ul>
+
+    <h3>Ingress Table 3: <code>from-lport</code> Pre-ACLs</h3>
+
+    <p>
+      Ingress table 3 prepares flows for possible stateful ACL processing
+      in table 4.  It contains a priority-0 flow that simply moves
+      traffic to table 4.  If stateful ACLs are used in the logical
       datapath, a priority-100 flow is added that sends IP packets to
-      the connection tracker before advancing to table 2.
+      the connection tracker before advancing to table 4.
     </p>
 
-    <h3>Ingress table 2: <code>from-lport</code> ACLs</h3>
+    <h3>Ingress table 4: <code>from-lport</code> ACLs</h3>
 
     <p>
       Logical flows in this table closely reproduce those in the
@@ -163,7 +247,7 @@
     </p>
 
     <p>
-      Ingress table 2 also contains a priority 0 flow with action
+      Ingress table 4 also contains a priority 0 flow with action
       <code>next;</code>, so that ACLs allow packets by default.  If the
       logical datapath has a statetful ACL, the following flows will
       also be added:
@@ -195,7 +279,7 @@
       </li>
     </ul>
 
-    <h3>Ingress Table 3: ARP responder</h3>
+    <h3>Ingress Table 5: ARP responder</h3>
 
     <p>
       This table implements ARP responder for known IPs.  It contains these
@@ -205,7 +289,7 @@
     <ul>
       <li>
         Priority-100 flows to skip ARP responder if inport is of type
-        <code>localnet</code>, and advances directly to table 3.
+        <code>localnet</code>, and advances directly to table 6.
       </li>
 
       <li>
@@ -236,11 +320,11 @@ output;
 
       <li>
         One priority-0 fallback flow that matches all packets and advances to
-        table 4.
+        table 6.
       </li>
     </ul>
 
-    <h3>Ingress Table 4: Destination Lookup</h3>
+    <h3>Ingress Table 6: Destination Lookup</h3>
 
     <p>
       This table implements switching behavior.  It contains these logical
@@ -274,17 +358,26 @@ output;
     <h3>Egress Table 0: <code>to-lport</code> Pre-ACLs</h3>
 
     <p>
-      This is similar to ingress table 1 except for <code>to-lport</code>
+      This is similar to ingress table 3 except for <code>to-lport</code>
       traffic.
     </p>
 
     <h3>Egress Table 1: <code>to-lport</code> ACLs</h3>
 
     <p>
-      This is similar to ingress table 2 except for <code>to-lport</code> ACLs.
+      This is similar to ingress table 4 except for <code>to-lport</code> ACLs.
+    </p>
+
+    <h3>Egress Table 2: Egress Port Security - IP</h3>
+
+    <p>
+      This is similar to the ingress port security logic in table 1 except
+      that <code>outport</code>, <code>eth.dst</code>, <code>ip4.dst</code>
+      and <code>ip6.dst</code> are checked instead of <code>inport</code>,
+      <code>eth.src</code>, <code>ip4.src</code> and <code>ip6.src</code>
     </p>
 
-    <h3>Egress Table 2: Egress Port Security</h3>
+    <h3>Egress Table 3: Egress Port Security - L2</h3>
 
     <p>
       This is similar to the ingress port security logic in ingress table 0,
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 35ec267..59bb725 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -80,21 +80,24 @@ enum ovn_datapath_type {
  * An "enum ovn_stage" indicates whether the stage is part of a logical switch
  * or router, whether the stage is part of the ingress or egress pipeline, and
  * the table within that pipeline.  The first three components are combined to
- * form the stage's full name, e.g. S_SWITCH_IN_PORT_SEC,
+ * form the stage's full name, e.g. S_SWITCH_IN_PORT_SEC_L2,
  * S_ROUTER_OUT_DELIVERY. */
 enum ovn_stage {
 #define PIPELINE_STAGES                                               \
     /* Logical switch ingress stages. */                              \
-    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC,    0, "ls_in_port_sec")     \
-    PIPELINE_STAGE(SWITCH, IN,  PRE_ACL,     1, "ls_in_pre_acl")      \
-    PIPELINE_STAGE(SWITCH, IN,  ACL,         2, "ls_in_acl")          \
-    PIPELINE_STAGE(SWITCH, IN,  ARP_RSP,     3, "ls_in_arp_rsp")      \
-    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,     4, "ls_in_l2_lkup")      \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_L2,    0, "ls_in_port_sec_l2")     \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_IP,    1, "ls_in_port_sec_ip")     \
+    PIPELINE_STAGE(SWITCH, IN,  PORT_SEC_ND,    2, "ls_in_port_sec_nd")     \
+    PIPELINE_STAGE(SWITCH, IN,  PRE_ACL,        3, "ls_in_pre_acl")      \
+    PIPELINE_STAGE(SWITCH, IN,  ACL,            4, "ls_in_acl")          \
+    PIPELINE_STAGE(SWITCH, IN,  ARP_RSP,        5, "ls_in_arp_rsp")      \
+    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,        6, "ls_in_l2_lkup")      \
                                                                       \
     /* Logical switch egress stages. */                               \
     PIPELINE_STAGE(SWITCH, OUT, PRE_ACL,     0, "ls_out_pre_acl")     \
     PIPELINE_STAGE(SWITCH, OUT, ACL,         1, "ls_out_acl")         \
-    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC,    2, "ls_out_port_sec")    \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP, 2, "ls_out_port_sec_ip")    \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2, 3, "ls_out_port_sec_l2")    \
                                                                       \
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,   0, "lr_in_admission")    \
@@ -1023,9 +1026,9 @@ extract_lport_addresses(char *address, struct lport_addresses *laddrs,
  * 'n_port_security' elements, is the collection of port_security constraints
  * from an OVN_NB Logical_Port row. */
 static void
-build_port_security(const char *eth_addr_field,
-                    char **port_security, size_t n_port_security,
-                    struct ds *match)
+build_port_security_l2(const char *eth_addr_field,
+                       char **port_security, size_t n_port_security,
+                       struct ds *match)
 {
     size_t base_len = match->length;
     ds_put_format(match, " && %s == {", eth_addr_field);
@@ -1048,6 +1051,227 @@ build_port_security(const char *eth_addr_field,
     }
 }
 
+static void
+build_port_security_ipv6_nd_flow(
+    struct ds *match, struct eth_addr ea, struct ipv6_netaddr *ipv6_addrs,
+    int n_ipv6_addrs)
+{
+    ds_put_format(match, " && ip6 && nd && ((nd.sll == "ETH_ADDR_FMT" || "
+                  "nd.sll == "ETH_ADDR_FMT") || ((nd.tll == "ETH_ADDR_FMT" || "
+                  "nd.tll == "ETH_ADDR_FMT")", ETH_ADDR_ARGS(eth_addr_zero),
+                  ETH_ADDR_ARGS(ea), ETH_ADDR_ARGS(eth_addr_zero),
+                  ETH_ADDR_ARGS(ea));
+    if (!n_ipv6_addrs) {
+        ds_put_cstr(match, "))");
+        return;
+    }
+
+    char ip6_str[INET6_ADDRSTRLEN + 1];
+    struct in6_addr lla;
+    in6_generate_lla(ea, &lla);
+    memset(ip6_str, 0, sizeof(ip6_str));
+    ipv6_string_mapped(ip6_str, &lla);
+    ds_put_format(match, " && (nd.target == %s", ip6_str);
+
+    for(int i = 0; i < n_ipv6_addrs; i++) {
+        memset(ip6_str, 0, sizeof(ip6_str));
+        ipv6_string_mapped(ip6_str, &ipv6_addrs[i].addr);
+        ds_put_format(match, " || nd.target == %s", ip6_str);
+    }
+
+    ds_put_format(match, ")))");
+}
+
+static void
+build_port_security_ipv6_flow(
+    enum ovn_pipeline pipeline, struct ds *match, struct eth_addr ea,
+    struct ipv6_netaddr *ipv6_addrs, int n_ipv6_addrs)
+{
+    char ip6_str[INET6_ADDRSTRLEN + 1];
+
+    ds_put_format(match, " && %s == {",
+                  pipeline == P_IN ? "ip6.src" : "ip6.dst");
+
+    /* Allow link-local address. */
+    struct in6_addr lla;
+    in6_generate_lla(ea, &lla);
+    ipv6_string_mapped(ip6_str, &lla);
+    ds_put_format(match, "%s, ", ip6_str);
+
+    /* Allow ip6.src=:: and ip6.dst=ff00::/8 for ND packets */
+    ds_put_cstr(match, pipeline == P_IN ? "::" : "ff00::/8");
+    for(int i = 0; i < n_ipv6_addrs; i++) {
+        ipv6_string_mapped(ip6_str, &ipv6_addrs[i].addr);
+        ds_put_format(match, ", %s", ip6_str);
+    }
+    ds_put_cstr(match, "}");
+}
+
+/**
+ * Build port security constraints on ARP and IPv6 ND fields
+ * and add logical flows to S_SWITCH_IN_PORT_SEC_ND stage.
+ *
+ * For each port security of the logical port, following
+ * logical flows are added
+ *   - If the port security has no IP (both IPv4 and IPv6) or
+ *     if it has IPv4 address(es)
+ *      - Priority 90 flow to allow ARP packets for known MAC addresses
+ *        in the eth.src and arp.spa fields. If the port security
+ *        has IPv4 addresses, allow known IPv4 addresses in the arp.tpa field.
+ *
+ *   - If the port security has no IP (both IPv4 and IPv6) or
+ *     if it has IPv6 address(es)
+ *     - Priority 90 flow to allow IPv6 ND packets for known MAC addresses
+ *       in the eth.src and nd.sll/nd.tll fields. If the port security
+ *       has IPv6 addresses, allow known IPv6 addresses in the nd.target field
+ *       for IPv6 Neighbor Advertisement packet.
+ *
+ *   - Priority 80 flow to drop ARP and IPv6 ND packets.
+ */
+static void
+build_port_security_nd(struct ovn_port *op, struct hmap *lflows)
+{
+    for (size_t i = 0; i < op->nbs->n_port_security; i++) {
+        struct lport_addresses ps;
+        if (!extract_lport_addresses(op->nbs->port_security[i], &ps, true)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_INFO_RL(&rl, "invalid syntax '%s' in port security. No MAC"
+                         " address found", op->nbs->port_security[i]);
+            continue;
+        }
+
+        bool no_ip = !(ps.n_ipv4_addrs || ps.n_ipv6_addrs);
+        struct ds match = DS_EMPTY_INITIALIZER;
+
+        if (ps.n_ipv4_addrs || no_ip) {
+            ds_put_format(
+                &match, "inport == %s && eth.src == "ETH_ADDR_FMT" && arp.sha == "
+                ETH_ADDR_FMT, op->json_key, ETH_ADDR_ARGS(ps.ea),
+                ETH_ADDR_ARGS(ps.ea));
+
+            if (ps.n_ipv4_addrs) {
+                ds_put_cstr(&match, " && (");
+                for (size_t i = 0; i < ps.n_ipv4_addrs; i++) {
+                    ds_put_format(&match, "arp.spa == "IP_FMT" || ",
+                                  IP_ARGS(ps.ipv4_addrs[i].addr));
+                }
+                ds_chomp(&match, ' ');
+                ds_chomp(&match, '|');
+                ds_chomp(&match, '|');
+                ds_put_cstr(&match, ")");
+            }
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND, 90,
+                          ds_cstr(&match), "next;");
+            ds_destroy(&match);
+        }
+
+        if (ps.n_ipv6_addrs || no_ip) {
+            ds_init(&match);
+            ds_put_format(&match, "inport == %s && eth.src == "ETH_ADDR_FMT,
+                          op->json_key, ETH_ADDR_ARGS(ps.ea));
+            build_port_security_ipv6_nd_flow(&match, ps.ea, ps.ipv6_addrs,
+                                             ps.n_ipv6_addrs);
+            ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND, 90,
+                          ds_cstr(&match), "next;");
+            ds_destroy(&match);
+        }
+        free(ps.ipv4_addrs);
+        free(ps.ipv6_addrs);
+    }
+
+    char *match = xasprintf("inport == %s && (arp || nd)", op->json_key);
+    ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_ND, 80,
+                  match, "drop;");
+    free(match);
+}
+
+/**
+ * Build port security constraints on IPv4 and IPv6 src and dst fields
+ * and add logical flows to S_SWITCH_(IN/OUT)_PORT_SEC_IP stage.
+ *
+ * For each port security of the logical port, following
+ * logical flows are added
+ *   - If the port security has IPv4 addresses,
+ *     - Priority 90 flow to allow IPv4 packets for known IPv4 addresses
+ *
+ *   - If the port security has IPv6 addresses,
+ *     - Priority 90 flow to allow IPv6 packets for known IPv6 addresses
+ *
+ *   - If the port security has IPv4 addresses or IPv6 addresses or both
+ *     - Priority 80 flow to drop all IPv4 and IPv6 traffic
+ */
+static void
+build_port_security_ip(enum ovn_pipeline pipeline, struct ovn_port *op,
+                       struct hmap *lflows)
+{
+    char *port_direction;
+    enum ovn_stage stage;
+    if (pipeline == P_IN) {
+        port_direction = "inport";
+        stage = S_SWITCH_IN_PORT_SEC_IP;
+    } else {
+        port_direction = "outport";
+        stage = S_SWITCH_OUT_PORT_SEC_IP;
+    }
+
+    for (size_t i = 0; i < op->nbs->n_port_security; i++) {
+        struct lport_addresses ps;
+        if (!extract_lport_addresses(op->nbs->port_security[i], &ps, true)) {
+            continue;
+        }
+
+        if (!(ps.n_ipv4_addrs || ps.n_ipv6_addrs)) {
+            continue;
+        }
+
+        if (ps.n_ipv4_addrs) {
+            struct ds match = DS_EMPTY_INITIALIZER;
+            if (pipeline == P_IN) {
+                ds_put_format(&match, "inport == %s && eth.src == "ETH_ADDR_FMT
+                              " && ip4.src == {0.0.0.0, ", op->json_key,
+                              ETH_ADDR_ARGS(ps.ea));
+            } else {
+                ds_put_format(&match, "outport == %s && eth.dst == "ETH_ADDR_FMT
+                              " && ip4.dst == {255.255.255.255, 224.0.0.0/4, ",
+                              op->json_key, ETH_ADDR_ARGS(ps.ea));
+            }
+
+            for(int i = 0; i < ps.n_ipv4_addrs; i++) {
+                ds_put_format(&match, IP_FMT", ", IP_ARGS(ps.ipv4_addrs[i].addr));
+            }
+
+            /* Replace ", " by "}". */
+            ds_chomp(&match, ' ');
+            ds_chomp(&match, ',');
+            ds_put_cstr(&match, "}");
+            ovn_lflow_add(lflows, op->od, stage, 90, ds_cstr(&match), "next;");
+            ds_destroy(&match);
+            free(ps.ipv4_addrs);
+        }
+
+        if (ps.n_ipv6_addrs) {
+            struct ds match = DS_EMPTY_INITIALIZER;
+            ds_put_format(&match, "%s == %s && %s == "ETH_ADDR_FMT"",
+                          port_direction, op->json_key,
+                          pipeline == P_IN ? "eth.src" : "eth.dst",
+                          ETH_ADDR_ARGS(ps.ea));
+            build_port_security_ipv6_flow(pipeline, &match, ps.ea,
+                                          ps.ipv6_addrs, ps.n_ipv6_addrs);
+            ovn_lflow_add(lflows, op->od, stage, 90,
+                          ds_cstr(&match), "next;");
+            ds_destroy(&match);
+            free(ps.ipv6_addrs);
+        }
+
+        char *match = xasprintf(
+            "%s == %s && %s == "ETH_ADDR_FMT" && ip", port_direction,
+            op->json_key, pipeline == P_IN ? "eth.src" : "eth.dst",
+            ETH_ADDR_ARGS(ps.ea));
+        ovn_lflow_add(lflows, op->od, stage, 80, match, "drop;");
+        free(match);
+    }
+}
+
 static bool
 lport_is_enabled(const struct nbrec_logical_port *lport)
 {
@@ -1229,7 +1453,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
      * update ovn-northd.8.xml if you change anything. */
 
     /* Build pre-ACL and ACL tables for both ingress and egress.
-     * Ingress tables 1 and 2.  Egress tables 0 and 1. */
+     * Ingress tables 3 and 4.  Egress tables 0 and 1. */
     struct ovn_datapath *od;
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
@@ -1247,18 +1471,22 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
 
         /* Logical VLANs not supported. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC, 100, "vlan.present",
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_L2, 100, "vlan.present",
                       "drop;");
 
         /* Broadcast/multicast source address is invalid. */
-        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC, 100, "eth.src[40]",
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_L2, 100, "eth.src[40]",
                       "drop;");
 
         /* Port security flows have priority 50 (see below) and will continue
          * to the next table if packet source is acceptable. */
     }
 
-    /* Logical switch ingress table 0: Ingress port security (priority 50). */
+    /* Logical switch ingress table 0: Ingress port security - L2
+     *  (priority 50).
+     *  Ingress table 1: Ingress port security - IP (priority 90 and 80)
+     *  Ingress table 2: Ingress port security - ND (priority 90 and 80)
+     */
     struct ovn_port *op;
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbs) {
@@ -1273,12 +1501,28 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
 
         struct ds match = DS_EMPTY_INITIALIZER;
         ds_put_format(&match, "inport == %s", op->json_key);
-        build_port_security("eth.src",
-                            op->nbs->port_security, op->nbs->n_port_security,
-                            &match);
-        ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC, 50,
+        build_port_security_l2(
+            "eth.src", op->nbs->port_security, op->nbs->n_port_security,
+            &match);
+        ovn_lflow_add(lflows, op->od, S_SWITCH_IN_PORT_SEC_L2, 50,
                       ds_cstr(&match), "next;");
         ds_destroy(&match);
+
+        if (op->nbs->n_port_security) {
+            build_port_security_ip(P_IN, op, lflows);
+            build_port_security_nd(op, lflows);
+        }
+    }
+
+    /* Ingress table 1 and 2: Port security - IP and ND, by default goto next.
+     * (priority 0)*/
+    HMAP_FOR_EACH (od, key_node, datapaths) {
+        if (!od->nbs) {
+            continue;
+        }
+
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_ND, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_IN_PORT_SEC_IP, 0, "1", "next;");
     }
 
     /* Ingress table 3: ARP responder, skip requests coming from localnet ports.
@@ -1296,7 +1540,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
     }
 
-    /* Ingress table 3: ARP responder, reply for known IPs.
+    /* Ingress table 5: ARP responder, reply for known IPs.
      * (priority 50). */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbs) {
@@ -1346,7 +1590,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
     }
 
-    /* Ingress table 3: ARP responder, by default goto next.
+    /* Ingress table 5: ARP responder, by default goto next.
      * (priority 0)*/
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
@@ -1356,7 +1600,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ARP_RSP, 0, "1", "next;");
     }
 
-    /* Ingress table 4: Destination lookup, broadcast and multicast handling
+    /* Ingress table 6: Destination lookup, broadcast and multicast handling
      * (priority 100). */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbs) {
@@ -1376,7 +1620,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
                       "outport = \""MC_FLOOD"\"; output;");
     }
 
-    /* Ingress table 4: Destination lookup, unicast handling (priority 50), */
+    /* Ingress table 6: Destination lookup, unicast handling (priority 50), */
     HMAP_FOR_EACH (op, key_node, ports) {
         if (!op->nbs) {
             continue;
@@ -1413,7 +1657,7 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
     }
 
-    /* Ingress table 4: Destination lookup for unknown MACs (priority 0). */
+    /* Ingress table 6: Destination lookup for unknown MACs (priority 0). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
             continue;
@@ -1425,18 +1669,23 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         }
     }
 
-    /* Egress table 2: Egress port security multicast/broadcast (priority
+    /* Egress table 2: Egress port security - IP (priority 0)
+     * port security L2 - multicast/broadcast (priority
      * 100). */
     HMAP_FOR_EACH (od, key_node, datapaths) {
         if (!od->nbs) {
             continue;
         }
 
-        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PORT_SEC, 100, "eth.mcast",
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PORT_SEC_IP, 0, "1", "next;");
+        ovn_lflow_add(lflows, od, S_SWITCH_OUT_PORT_SEC_L2, 100, "eth.mcast",
                       "output;");
     }
 
-    /* Egress table 2: Egress port security (priorities 50 and 150).
+    /* Egress table 2: Egress port security - IP (priorities 90 and 80)
+     * if port security enabled.
+     *
+     * Egress table 3: Egress port security - L2 (priorities 50 and 150).
      *
      * Priority 50 rules implement port security for enabled logical port.
      *
@@ -1450,16 +1699,20 @@ build_lswitch_flows(struct hmap *datapaths, struct hmap *ports,
         struct ds match = DS_EMPTY_INITIALIZER;
         ds_put_format(&match, "outport == %s", op->json_key);
         if (lport_is_enabled(op->nbs)) {
-            build_port_security("eth.dst", op->nbs->port_security,
-                                op->nbs->n_port_security, &match);
-            ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_PORT_SEC, 50,
+            build_port_security_l2("eth.dst", op->nbs->port_security,
+                                   op->nbs->n_port_security, &match);
+            ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2, 50,
                           ds_cstr(&match), "output;");
         } else {
-            ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_PORT_SEC, 150,
+            ovn_lflow_add(lflows, op->od, S_SWITCH_OUT_PORT_SEC_L2, 150,
                           ds_cstr(&match), "drop;");
         }
 
         ds_destroy(&match);
+
+        if (op->nbs->n_port_security) {
+            build_port_security_ip(P_OUT, op, lflows);
+        }
     }
 }
 
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index 5c8e942..e65bc3a 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -362,21 +362,125 @@
 
       <column name="port_security">
         <p>
-          A set of L2 (Ethernet) addresses from which the logical port is
-          allowed to send packets and to which it is allowed to receive
-          packets.  If this column is empty, all addresses are permitted.
-          Logical ports are always allowed to receive packets addressed to
-          multicast and broadcast addresses.
+          This column controls the addresses from which the host attached to the
+          logical port (``the host'') is allowed to send packets and to which it
+          is allowed to receive packets.  If this column is empty, all addresses
+          are permitted.
         </p>
 
         <p>
-          Each member of the set is an Ethernet address in the form
-          <var>xx</var>:<var>xx</var>:<var>xx</var>:<var>xx</var>:<var>xx</var>:<var>xx</var>.
+          Each element in the set must begin with one Ethernet address.
+          This would restrict the host to sending packets from and receiving
+          packets to the ethernet addresses defined in the logical port's
+          <ref column="port_security"/> column. It also restricts the inner
+          source MAC addresses that the host may send in ARP and IPv6
+          Neighbor Discovery packets. The host is always allowed to receive packets
+          to multicast and broadcast Ethernet addresses.
         </p>
 
         <p>
-          This specification will be extended to support L3 port security.
+          Each element in the set may additionally contain one or more IPv4 or
+          IPv6 addresses (or both), with optional masks.  If a mask is given, it
+          must be a CIDR mask.  In addition to the restrictions described for
+          Ethernet addresses above, such an element restricts the IPv4 or IPv6
+          addresses from which the host may send and to which it may receive
+          packets to the specified addresses.  A masked address, if the host part
+          is zero, indicates that the host is allowed to use any address in the
+          subnet; if the host part is nonzero, the mask simply indicates the size
+          of the subnet. In addition:
         </p>
+
+        <ul>
+          <li>
+            <p>
+              If any IPv4 address is given, the host is also allowed to receive
+              packets to the IPv4 local broadcast address 255.255.255.255 and to
+              IPv4 multicast addresses (224.0.0.0/4).  If an IPv4 address with a
+              mask is given, the host is also allowed to receive packets to the
+              broadcast address in that specified subnet.
+            </p>
+
+            <p>
+              If any IPv4 address is given, the host is additionally restricted
+              to sending ARP packets with the specified source IPv4 address.
+              (RARP is not restricted.)
+            </p>
+          </li>
+
+          <li>
+            <p>
+              If any IPv6 address is given, the host is also allowed to receive
+              packets to IPv6 multicast addresses (ff00::/8).
+            </p>
+
+            <p>
+              If any IPv6 address is given, the host is additionally restricted
+              to sending IPv6 Neighbor Discovery Solicitation or Advertisement
+              packets with the specified source address or, for solicitations,
+              the unspecified address.
+            </p>
+          </li>
+        </ul>
+
+        <p>
+          If an element includes an IPv4 address, but no IPv6 addresses, then
+          IPv6 traffic is not allowed.  If an element includes an IPv6 address,
+          but no IPv4 address, then IPv4 and ARP traffic is not allowed.
+        </p>
+
+        <p>
+          This column uses the same lexical syntax as the <ref column="match"
+          table="Pipeline" db="OVN_Southbound"/> column in the OVN Southbound
+          database's <ref table="Pipeline" db="OVN_Southbound"/> table.  Multiple
+          addresses within an element may be space or comma separated.
+        </p>
+
+        <p>
+          This column is provided as a convenience to cloud management systems,
+          but all of the features that it implements can be implemented as ACLs
+          using the <ref table="ACL"/> table.
+        </p>
+
+        <p>
+          Examples:
+        </p>
+
+        <dl>
+          <dt><code>80:fa:5b:06:72:b7</code></dt>
+          <dd>
+            The host may send traffic from and receive traffic to the specified
+            MAC address, and to receive traffic to Ethernet multicast and
+            broadcast addresses, but not otherwise.  The host may not send ARP or
+            IPv6 Neighbor Discovery packets with inner source Ethernet addresses
+            other than the one specified.
+          </dd>
+
+          <dt><code>80:fa:5b:06:72:b7 192.168.1.10/24</code></dt>
+          <dd>
+            This adds further restrictions to the first example.  The host may
+            send IPv4 packets from or receive IPv4 packets to only 192.168.1.10,
+            except that it may also receive IPv4 packets to 192.168.1.255 (based
+            on the subnet mask), 255.255.255.255, and any address in 224.0.0.0/4.
+            The host may not send ARPs with a source Ethernet address other than
+            80:fa:5b:06:72:b7 or source IPv4 address other than 192.168.1.10.
+            The host may not send or receive any IPv6 (including IPv6 Neighbor
+            Discovery) traffic.
+          </dd>
+
+          <dt><code>"80:fa:5b:12:42:ba", "80:fa:5b:06:72:b7 192.168.1.10/24"</code></dt>
+          <dd>
+            The host may send traffic from and receive traffic to the
+            specified MAC addresses, and
+            to receive traffic to Ethernet multicast and broadcast addresses,
+            but not otherwise.   With MAC 80:fa:5b:12:42:ba, the host may
+            send traffic from and receive traffic to any L3 address.
+            With MAC 80:fa:5b:06:72:b7, the host may send IPv4 packets from or
+            receive IPv4 packets to only 192.168.1.10, except that it may also
+            receive IPv4 packets to 192.168.1.255 (based on the subnet mask),
+            255.255.255.255, and any address in 224.0.0.0/4.  The host may not
+            send or receive any IPv6 (including IPv6 Neighbor Discovery) traffic.
+          </dd>
+        </dl>
       </column>
     </group>
 
diff --git a/tests/ovn.at b/tests/ovn.at
index 5cb7d8b..9d69526 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1376,3 +1376,321 @@ for daemon in ovn-controller ovn-northd ovsdb-server; do
     ovs-appctl -t $daemon exit
 done
 AT_CLEANUP
+
+# 3 hypervisors, one logical switch, 3 logical ports per hypervisor
+AT_SETUP([ovn -- portsecurity : 3 HVs, 1 LS, 3 lports/HV])
+AT_KEYWORDS([portsecurity])
+AT_SKIP_IF([test $HAVE_PYTHON = no])
+ovn_start
+
+# Create hypervisors hv[123].
+# Add vif1[123] to hv1, vif2[123] to hv2, vif3[123] to hv3.
+# Add all of the vifs to a single logical switch lsw0.
+# Turn off port security on vifs vif[123]1
+# Turn on l2 port security on vifs vif[123]2
+# Turn of l2 and l3 port security on vifs vif[123]3
+# Make vif13, vif2[23], vif3[123] destinations for unknown MACs.
+ovn-nbctl lswitch-add lsw0
+net_add n1
+for i in 1 2 3; do
+    sim_add hv$i
+    as hv$i
+    ovs-vsctl add-br br-phys
+    ovn_attach n1 br-phys 192.168.0.$i
+
+    for j in 1 2 3; do
+        ovs-vsctl add-port br-int vif$i$j -- set Interface vif$i$j external-ids:iface-id=lp$i$j options:tx_pcap=hv$i/vif$i$j-tx.pcap options:rxq_pcap=hv$i/vif$i$j-rx.pcap ofport-request=$i$j
+        ovn-nbctl lport-add lsw0 lp$i$j
+        if test $j = 1; then
+            ovn-nbctl lport-set-addresses lp$i$j "f0:00:00:00:00:$i$j 192.168.0.$i$j" unknown
+        elif test $j = 2; then
+            ovn-nbctl lport-set-addresses lp$i$j "f0:00:00:00:00:$i$j 192.168.0.$i$j"
+            ovn-nbctl lport-set-port-security lp$i$j f0:00:00:00:00:$i$j
+        else
+            extra_addr="f0:00:00:00:0$i:$i$j fe80::ea2a:eaff:fe28:$i$j"
+            ovn-nbctl lport-set-addresses lp$i$j "f0:00:00:00:00:$i$j 192.168.0.$i$j" "$extra_addr"
+            ovn-nbctl lport-set-port-security lp$i$j "f0:00:00:00:00:$i$j 192.168.0.$i$j" "$extra_addr"
+        fi
+    done
+done
+
+ovn-nbctl show
+
+# Pre-populate the hypervisors' ARP tables so that we don't lose any
+# packets for ARP resolution (native tunneling doesn't queue packets
+# for ARP resolution).
+ovn_populate_arp
+
+# Allow some time for ovn-northd and ovn-controller to catch up.
+# XXX This should be more systematic.
+sleep 1
+ovn-sbctl dump-flows -- list multicast_group
+
+echo "------ hv1 dump ------"
+as hv1 ovs-vsctl show
+as hv1 ovs-ofctl -O OpenFlow13 dump-flows br-int
+
+echo "------ hv2 dump ------"
+as hv2 ovs-vsctl show
+as hv2 ovs-ofctl -O OpenFlow13 dump-flows br-int
+
+echo "------ hv3 dump ------"
+as hv3 ovs-vsctl show
+as hv3 ovs-ofctl -O OpenFlow13 dump-flows br-int
+
+# Given the name of a logical port, prints the name of the hypervisor
+# on which it is located.
+vif_to_hv() {
+    echo hv${1%?}
+}
+
+
+trim_zeros() {
+    sed 's/\(00\)\{1,\}$//'
+}
+for i in 1 2 3; do
+    for j in 1 2 3; do
+        : > $i$j.expected
+    done
+done
+
+# test_ip INPORT SRC_MAC DST_MAC SRC_IP DST_IP OUTPORT...
+#
+# This shell function causes an ip packet to be received on INPORT.
+# The packet's content has Ethernet destination DST and source SRC
+# (each exactly 12 hex digits) and Ethernet type ETHTYPE (4 hex digits).
+# The OUTPORTs (zero or more) list the VIFs on which the packet should
+# be received.  INPORT and the OUTPORTs are specified as lport numbers,
+# e.g. 11 for vif11.
+test_ip() {
+    # This packet has bad checksums but logical L3 routing doesn't check.
+    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5
+    local packet=${dst_mac}${src_mac}08004500001c0000000040110000${src_ip}${dst_ip}003511110008
+    shift; shift; shift; shift; shift
+    hv=`vif_to_hv $inport`
+    as $hv ovs-appctl netdev-dummy/receive vif$inport $packet
+    #as $hv ovs-appctl ofproto/trace br-int in_port=$inport $packet
+    for outport; do
+        echo $packet | trim_zeros >> $outport.expected
+    done
+}
+
+# test_arp INPORT SHA SPA TPA DROP [REPLY_HA]
+#
+# Causes a packet to be received on INPORT.  The packet is an ARP
+# request with SHA, SPA, and TPA as specified.  If REPLY_HA is provided, then
+# it should be the hardware address of the target to expect to receive in an
+# ARP reply; otherwise no reply is expected.
+#
+# INPORT is an lport number, e.g. 11 for vif11.
+# SHA and REPLY_HA are each 12 hex digits.
+# SPA and TPA are each 8 hex digits.
+test_arp() {
+    local inport=$1 smac=$2 sha=$3 spa=$4 tpa=$5 drop=$6 reply_ha=$7
+    local request=ffffffffffff${smac}08060001080006040001${sha}${spa}ffffffffffff${tpa}
+    hv=`vif_to_hv $inport`
+    as $hv ovs-appctl netdev-dummy/receive vif$inport $request
+    #as $hv ovs-appctl ofproto/trace br-int in_port=$inport $request
+    if test $drop != 1; then
+        if test X$reply_ha == X; then
+            # Expect to receive the broadcast ARP on the other logical switch ports
+            # if no reply is expected.
+            local i j
+            for i in 1 2 3; do
+                for j in 1 2 3; do
+                    if test $i$j != $inport; then
+                        echo $request >> $i$j.expected
+                    fi
+                done
+            done
+        else
+            # Expect to receive the reply, if any.
+            local reply=${smac}${reply_ha}08060001080006040002${reply_ha}${tpa}${sha}${spa}
+            echo $reply >> $inport.expected
+        fi
+    fi
+}
+
+# test_ipv6 INPORT SRC_MAC DST_MAC SRC_IP DST_IP OUTPORT...
+# This function is similar to test_ip() except that it sends
+# ipv6 packet
+test_ipv6() {
+    local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5
+    local packet=${dst_mac}${src_mac}86dd6000000000083aff${src_ip}${dst_ip}0000000000000000
+    shift; shift; shift; shift; shift
+    hv=`vif_to_hv $inport`
+    as $hv ovs-appctl netdev-dummy/receive vif$inport $packet
+    #as $hv ovs-appctl ofproto/trace br-int in_port=$inport $packet
+    for outport; do
+        echo $packet | trim_zeros >> $outport.expected
+    done
+}
+
+ip_to_hex() {
+    printf "%02x%02x%02x%02x" "$@"
+}
+
+# no port security
+sip=`ip_to_hex 192 168 0 12`
+tip=`ip_to_hex 192 168 0 13`
+# the arp packet should be allowed even if lp[123]1 is
+# not configured with mac f00000000023 and ip 192.168.0.12
+for i in 1 2 3; do
+    test_arp ${i}1 f00000000023 f00000000023 $sip $tip 0 f00000000013
+    for j in 1 2 3; do
+        if test $i != $j; then
+            test_ip ${i}1 f000000000${i}1 f000000000${j}1 $sip $tip ${j}1
+        fi
+    done
+done
+
+# l2 port security
+sip=`ip_to_hex 192 168 0 12`
+tip=`ip_to_hex 192 168 0 13`
+
+# arp packet should be allowed since lp22 is configured with
+# mac f00000000022
+test_arp 22 f00000000022 f00000000022 $sip $tip 0 f00000000013
+
+# arp packet should not be allowed since lp32 is not configured with
+# mac f00000000021
+test_arp 32 f00000000021 f00000000021 $sip $tip 1
+
+# arp packet with sha set to f00000000021 should not be allowed
+# for lp12
+test_arp 12 f00000000012 f00000000021 $sip $tip 1
+
+# ip packets should be allowed and received since lp[123]2 do not
+# have l3 port security
+sip=`ip_to_hex 192 168 0 55`
+tip=`ip_to_hex 192 168 0 66`
+for i in 1 2 3; do
+    for j in 1 2 3; do
+        if test $i != $j; then
+            test_ip ${i}2 f000000000${i}2 f000000000${j}2 $sip $tip ${j}2
+        fi
+    done
+done
+
+# ipv6 packets should be received by lp[123]2
+# lp[123]1 can send ipv6 traffic as there is no port security
+sip=fe800000000000000000000000000000
+tip=ff020000000000000000000000000000
+
+for i in 1 2 3; do
+    test_ipv6 ${i}1 f000000000${i}1 f000000000${i}2 $sip $tip ${i}2
+done
+
+
+# l2 and l3 port security
+sip=`ip_to_hex 192 168 0 13`
+tip=`ip_to_hex 192 168 0 22`
+# arp packet should be allowed since lp13 is configured with
+# f00000000013 and 192.168.0.13
+test_arp 13 f00000000013 f00000000013 $sip $tip 0 f00000000022
+
+# the arp packet should be dropped because lp23 is not configured
+# with mac f00000000022
+sip=`ip_to_hex 192 168 0 13`
+tip=`ip_to_hex 192 168 0 22`
+test_arp 23 f00000000022 f00000000022 $sip $tip 1
+
+# the arp packet should be dropped because lp33 is not configured
+# with ip 192.168.0.55
+spa=`ip_to_hex 192 168 0 55`
+tpa=`ip_to_hex 192 168 0 22`
+test_arp 33 f00000000031 f00000000031 $spa $tpa 1
+
+# ip packets should not be received by lp[123]3 since
+# l3 port security is enabled
+sip=`ip_to_hex 192 168 0 55`
+tip=`ip_to_hex 192 168 0 66`
+for i in 1 2 3; do
+    for j in 1 2 3; do
+        test_ip ${i}2 f000000000${i}2 f000000000${j}3 $sip $tip
+    done
+done
+
+# ipv6 packets should be dropped for lp[123]3 since
+# it is configured with only ipv4 address
+sip=fe800000000000000000000000000000
+tip=ff020000000000000000000000000000
+
+for i in 1 2 3; do
+    test_ipv6 ${i}3 f000000000${i}3 f00000000022 $sip $tip
+done
+
+# ipv6 packets should not be received by lp[123]3 with mac f000000000$[123]3
+# lp[123]1 can send ipv6 traffic as there is no port security
+for i in 1 2 3; do
+    test_ipv6 ${i}1 f000000000${i}1 f000000000${i}3 $sip $tip
+done
+
+# lp13 has extra port security with mac f0000000113 and ipv6 addr
+# fe80::ea2a:eaff:fe28:0012
+
+# ipv4 packet should be dropped for lp13 with mac f0000000113
+sip=`ip_to_hex 192 168 0 13`
+tip=`ip_to_hex 192 168 0 23`
+test_ip 13 f00000000113 f00000000023 $sip $tip
+
+# ipv6 packet should be received by lp[123]3 with mac f0000000{i}{i}3
+# and ip6.dst as fe80::ea2a:eaff:fe28:0{i}{i}3.
+# lp11 can send ipv6 traffic as there is no port security
+sip=ee800000000000000000000000000000
+for i in 1 2 3; do
+    tip=fe80000000000000ea2aeafffe2800{i}3
+    test_ipv6 11 f00000000011 f000000000{i}${i}3 $sip $tip {i}3
+done
+
+
+# ipv6 packet should not be received by lp33 with mac f0000000333
+# and ip6.dst as fe80::ea2a:eaff:fe28:0023 as it is
+# configured with fe80::ea2a:eaff:fe28:0033
+# lp11 can send ipv6 traffic as there is no port security
+
+sip=ee800000000000000000000000000000
+tip=fe80000000000000ea2aeafffe280023
+test_ipv6 11 f00000000011 f00000000333 $sip $tip
+
+# ipv6 packet should be allowed for lp[123]3 with mac f0000000{i}{i}3
+# and ip6.src fe80::ea2a:eaff:fe28:0{i}{i}3 and ip6.src ::.
+# and should be dropped for any other ip6.src
+# lp21 can receive ipv6 traffic as there is no port security
+
+tip=ee800000000000000000000000000000
+for i in 1 2 3; do
+    sip=fe80000000000000ea2aeafffe2800${i}3
+    test_ipv6 ${i}3 f00000000${i}${i}3 f00000000021 $sip $tip 21
+
+    sip=00000000000000000000000000000000
+    test_ipv6 ${i}3 f00000000${i}${i}3 f00000000021 $sip $tip 21
+
+    # should be dropped
+    sip=ae80000000000000ea2aeafffe2800aa
+    test_ipv6 ${i}3 f00000000${i}${i}3 f00000000021 $sip $tip
+done
+
+
+# Allow some time for packet forwarding.
+
+# XXX This can be improved.
+sleep 1
+
+# Now check the packets actually received against the ones expected.
+for i in 1 2 3; do
+    for j in 1 2 3; do
+        file=hv$i/vif$i$j-tx.pcap
+        echo $file
+        $PYTHON "$top_srcdir/utilities/ovs-pcap.in" $file | trim_zeros > $i$j.packets
+        sort $i$j.expected > expout
+        AT_CHECK([sort $i$j.packets], [0], [expout])
+        echo
+    done
+done
+
+# Gracefully terminate daemons
+for daemon in ovn-controller ovn-northd ovsdb-server; do
+    ovs-appctl -t $daemon exit
+done
+AT_CLEANUP
