, ,

Network Policy and Microsegmentation, NSX-T DFW to OVN-Kubernetes (TKGI to OpenShift Series, Part 16)

Your NSX-T distributed firewall holds far more rules than kubectl will ever show you. Here is the DFW to OVN-Kubernetes mapping, a namespace baseline that does not break DNS, and where AdminNetworkPolicy actually belongs.

TKGI to OpenShift Series · Part 16 of 26
Key takeaways:
1. Exporting your Kubernetes NetworkPolicy objects captures less than half of what your NSX-T distributed firewall was actually enforcing. Inventory both sides or you will migrate a hole.
2. Do not open with a cluster wide AdminNetworkPolicy deny. Namespace scoped baselines plus a BaselineAdminNetworkPolicy guardrail leave tenants able to fix their own breakage.
3. NSX-T Reject has no equivalent. OVN-Kubernetes drops silently, so applications that failed instantly on TKGI now hang until their client timeout fires.
4. Every default deny you write needs a DNS exception before it needs anything else, because cluster DNS is egress traffic.
5. Headline command: oc annotate namespace wave1-web k8s.ovn.org/acl-logging='{ "deny": "alert", "allow": "notice" }'

A freshly installed OpenShift cluster enforces nothing between workloads. Every pod reaches every other pod in every namespace, on every port, and the cluster considers that correct behaviour. That is the flat network your NSX-T distributed firewall spent three years carving up on the TKGI side, and on day one of the target platform none of that segmentation exists.

Distributed firewall, usually shortened to DFW, is the NSX-T feature that enforces rules at each virtual machine network interface rather than at a perimeter appliance. On TKGI it did two jobs at once. It translated Kubernetes NetworkPolicy objects into firewall rules through the NSX Container Plugin (NCP), and it carried a second body of rules that your network team wrote by hand in NSX Manager against NSGroups. Only the first body of rules is visible to kubectl. Miss the second and the migrated estate is quieter than it should be, in a way nobody notices until an audit.

Part 15 gave people a way to log in and a place to pull images from. This part decides what those workloads are allowed to talk to once they arrive. It has to land before wave one moves, because retrofitting segmentation onto running production is how you end up with a change freeze.

Who this is for: You run an OpenShift Container Platform (OCP) 4 cluster on vSphere with OVN-Kubernetes as the network plugin, Routes and load balancing working from Part 13, and Projects, quotas and identity in place from Parts 14 and 15. Your TKGI clusters still run behind NSX-T with NCP, and you have read access to NSX Manager or a network engineer who does. Nothing has migrated yet.

Preflight and DFW Rule Inventory

Three things have to be true before you write a single policy. Your cluster network type must actually be OVN-Kubernetes, the AdminNetworkPolicy custom resource definitions must be present, and you need a rule count from the NSX side that you can defend in a meeting. That last one is the step teams skip, and it is the one that decides whether this migration reproduces your security posture or quietly relaxes it.

# Tested against: OCP 4.18.9, oc 4.18.9, TKGI 1.18.2, NSX-T 4.1.2 with NCP 4.1.2, # vSphere 8.0 U3, cluster installed IPI per Part 12. $ oc version Client Version: 4.18.9 Server Version: 4.18.9 Kubernetes Version: v1.31.6 $ oc get network.config cluster -o jsonpath='{.spec.networkType}' OVNKubernetes # ANP and BANP arrive as CRDs with the cluster network operator. No Operator install needed. $ oc get crd | grep policy.networking.k8s.io adminnetworkpolicies.policy.networking.k8s.io 2026-08-02T09:14:31Z baselineadminnetworkpolicies.policy.networking.k8s.io 2026-08-02T09:14:31Z # Side one of the inventory: what TKGI holds as Kubernetes objects. $ kubectl –context tkgi-prod get networkpolicies –all-namespaces –no-headers | wc -l 168 # Side two: what NSX Manager holds in the default domain. Credentials from the # environment, never on the command line, never in a manifest. $ export NSX_USER=svc-nsx-ro $ export NSX_PW=$(cat /run/secrets/nsx-ro-pw) $ curl -sk -u "$NSX_USER:$NSX_PW" https://nsxmgr.corp.example.com/policy/api/v1/infra/domains/default/security-policies | jq '[.results[].rule_count] | add' 365

Three hundred and sixty five enforced rules against one hundred and sixty eight Kubernetes objects. That gap is the whole point of this section. Forty seven of the remainder belong to infrastructure and the default section, which leaves one hundred and fifty tenant rules that exist only in NSX Manager, written by people who were never going to file a pull request against an application repository. Every migration guide that says export your NetworkPolicy YAML and reapply it is describing forty six percent of your firewall.

Getting those hand written rules out of NSX Manager is tedious rather than hard. Walk the security policies endpoint, expand each rule, and pull the source groups, destination groups, services and action into a spreadsheet. What you are building is not a translation, it is a reconciliation list, and every row ends in one of exactly two states. Either it becomes a Kubernetes object, or somebody signs off in writing that the rule is retired. A third category, meaning rules you meant to look at later, is how estates arrive in production with a segmentation story nobody can defend. Budget half a day per cluster for this and do it before you write any policy, because the inventory changes which objects you reach for.

Mapping NSX-T DFW Constructs to OVN-Kubernetes Objects

Enforced DFW rules by origin, three TKGI clusters Reference estate, NSX-T default domain, policy API rule_count, August 2026 NCP translated NetworkPolicy 168 Hand written in NSX Manager 150 Infrastructure and default section 47 075150 Only the red bar survives a kubectl get networkpolicies export.
One hundred and fifty tenant rules had no Kubernetes representation at all. Those are the ones that get lost.

Once you have both inventories, you need a translation. Some DFW concepts map cleanly, some map with a caveat, and a few have no target at all. Keep the table below beside you while you write policy, because arguing about it in the middle of a cutover window is expensive.

NSX-T DFW constructOVN-Kubernetes equivalentCaveat
Security policy section, admin ownedAdminNetworkPolicy (ANP), cluster scopedMaximum of 100 ANPs per cluster
Rule sequence number inside a sectionspec.priority, integer 0 to 99Lower value wins. Anything outside 0 to 99 is rejected
Applied To fieldspec.subject in ANP, spec.podSelector in NetworkPolicyAn empty subject selector also selects OpenShift system namespaces
Allow actionNetworkPolicy rule, or ANP action AllowANP Allow cannot be overridden by a tenant
Drop actionANP or BANP action Deny, or implicit deny once a NetworkPolicy selects a podNetworkPolicy has no explicit deny verb
Reject actionNo equivalentTraffic is dropped silently. Clients block instead of failing fast
NSGroup with dynamic membership criteriapodSelector and namespaceSelector matchLabelsLabel your namespaces during migration, not after
NSGroup built from an IP setnetworks CIDR peer in ANP or BANPEgress rules only. There is no ingress networks peer
Rule scoped to hypervisor or transport nodenodes peer in ANP egressEgress only, matched by node label
Bottom of section catch all ruleBaselineAdminNetworkPolicy (BANP)Singleton. It must be named default
Per rule logging togglek8s.ovn.org/acl-logging namespace annotationPer namespace, not per rule
Layer 7 context profile, service objectsNo equivalentPort and protocol only. Layer 7 needs a service mesh
Time based rule schedulingNo equivalentHandle it outside the cluster or drop the requirement

Two rows deserve more than a table cell. Reject is the one that generates support tickets, and I cover it in the field note below. Layer 7 context profiles are the one that generates escalations, because somebody in security signed off on a rule that inspects HTTP method and there is no way to reproduce that with a NetworkPolicy. If your DFW ruleset leans on layer 7, that requirement moves to Red Hat OpenShift Service Mesh or it goes away, and that decision belongs in the design review, not in the cutover.

One difference sits underneath the whole table and it is a mental model rather than a mapping. NSX-T DFW evaluates rules in sequence and the first match wins, so a Drop at position 1010 genuinely stops everything below it from being considered. Kubernetes NetworkPolicy does not work that way at all. Policies are additive. Once any policy selects a pod, that pod is deny by default for the listed directions, and every other policy selecting the same pod contributes its allows to a union. There is no ordering, no first match, and critically no way for one NetworkPolicy to subtract permission that another one granted. Engineers who spent years thinking in rule sequence write a policy expecting it to override an earlier one, and instead they widen the opening. AdminNetworkPolicy reintroduces ordering through priority values, which is the reason it exists, and it is also the reason it deserves the restraint I argue for in the next section.

Step by Step, Namespace Baseline and Tenant Policies

Work namespace by namespace. Four objects per namespace give you a posture equivalent to a well built DFW section, and they go in this order every time.

Step 1. Label the namespace so cluster scope policy can find it later. Step 2. Apply a default deny for both directions. Step 3. Immediately add the DNS egress exception, in the same oc apply, not as a follow up. Step 4. Add the ingress exceptions for the router and for monitoring, then the application specific rules translated from your DFW inventory.

$ oc label namespace wave1-web tier=web security=internal namespace/wave1-web labeled $ cat baseline.yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: wave1-web spec: podSelector: {} policyTypes: # Both types must be listed explicitly. Omit Egress – Ingress # and egress traffic stays wide open. – Egress — apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-egress-dns namespace: wave1-web spec: podSelector: {} policyTypes: – Egress egress: – to: – namespaceSelector: matchLabels: kubernetes.io/metadata.name: openshift-dns podSelector: matchLabels: dns.operator.openshift.io/daemonset-dns: default ports: – protocol: UDP port: 5353 – protocol: TCP port: 5353 — apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-router-and-monitoring namespace: wave1-web spec: podSelector: {} policyTypes: – Ingress ingress: – from: – namespaceSelector: matchLabels: policy-group.network.openshift.io/ingress: "" – from: – namespaceSelector: matchLabels: kubernetes.io/metadata.name: openshift-monitoring $ oc apply -f baseline.yaml networkpolicy.networking.k8s.io/default-deny-all created networkpolicy.networking.k8s.io/allow-egress-dns created networkpolicy.networking.k8s.io/allow-ingress-router-and-monitoring created

Apply the deny without the DNS rule, even for ninety seconds, and this is what your application logs produce. I include it because it is the single most common failure in this whole part, and because the error names a timeout rather than a firewall, which sends people looking in the wrong place.

$ oc logs -n wave1-web deploy/storefront –tail=3 2026-08-11T14:12:07Z ERROR db: dial tcp: lookup pgsql.wave1-data.svc.cluster.local on 172.30.0.10:53: read udp 10.128.4.31:41022->172.30.0.10:53: i/o timeout 2026-08-11T14:12:07Z ERROR handler: connection pool exhausted, 0 of 20 available # Pod stays Running and Ready the entire time, because the readiness probe is an # HTTP GET on / that never touches the database. $ oc get pods -n wave1-web NAME READY STATUS RESTARTS AGE storefront-77c9f4b8d5-4kzq2 1/1 Running 0 19m
Ordering rule: Never apply a default deny as a standalone change. Put the deny and its DNS exception in one manifest and one oc apply, so there is no window where name resolution is broken. Ninety seconds is long enough to exhaust a connection pool and trigger a page.

Cluster Scope Guardrails with AdminNetworkPolicy

AdminNetworkPolicy is the object that looks most like a DFW section, and that resemblance is a trap. It is cluster scoped, priority ordered and owned by the platform team, exactly like the section your network engineers used to edit. So the instinct is to lift the whole DFW section into one big ANP and be done. Resist it. An ANP Deny is non overridable by design, which means when it blocks something legitimate the namespace owner cannot unblock it, cannot see why, and files a ticket that lands on you at 22:00.

Use ANP for the handful of rules that genuinely must never be overridden, and use a BaselineAdminNetworkPolicy as the catch all underneath. BANP is a singleton, it must be named default, and its rules are overridable by tenant NetworkPolicy objects. That combination gives you a deny by default cluster where a team can still ship without a platform ticket.

Pass is the action with no DFW ancestor and it is worth understanding properly, because it is what makes the three tier model usable. Allow and Deny both settle the question immediately and skip everything below. Pass explicitly declines to settle it and hands evaluation down to tier 2, where namespace NetworkPolicy objects get their say, and then to tier 3 if nothing selected the pod. In practice that gives you a way to say something a DFW section could never express, which is that platform engineering has an opinion about this traffic class but delegates the final call to the team that owns the workload. Monitoring scrape traffic is the obvious candidate. Some teams must be scraped, some must never be, and most should choose. Pass at tier 1 with a Deny in the BANP underneath gets all three outcomes from one policy pair.

flowchart TD
  P[Packet on OVN logical switch] --> T1[Tier 1, AdminNetworkPolicy]
  T1 -->|Allow| A[Delivered]
  T1 -->|Deny| D[Dropped, no ICMP]
  T1 -->|Pass| T2[Tier 2, NetworkPolicy]
  T1 -->|No ANP matches| T2
  T2 -->|A rule allows it| A
  T2 -->|Pod selected, no rule matches| D
  T2 -->|No policy selects the pod| T3[Tier 3, BaselineAdminNetworkPolicy]
  T3 -->|Allow| A
  T3 -->|Deny| D
  T3 -->|No BANP present| A
OVN-Kubernetes evaluates three tiers of access control lists. A Pass action in tier 1 hands the decision down instead of settling it.

Here is the pair I run in the reference estate. One ANP holds the three rules nobody may break, and the BANP closes everything else. Note the priority values, and note the failure underneath, which is the mistake every NSX engineer makes on their first try because DFW sequence numbers start at 1000 and climb.

apiVersion: policy.networking.k8s.io/v1alpha1 kind: AdminNetworkPolicy metadata: name: platform-guardrails spec: priority: 20 # 0 is highest precedence, 99 is lowest subject: namespaces: matchLabels: security: internal ingress: – name: allow-from-monitoring action: Allow from: – namespaces: matchLabels: kubernetes.io/metadata.name: openshift-monitoring egress: – name: allow-to-dns action: Allow to: – namespaces: matchLabels: kubernetes.io/metadata.name: openshift-dns ports: – portNumber: protocol: UDP port: 5353 – name: allow-to-kube-api action: Allow ports: – portNumber: protocol: TCP port: 6443 to: – nodes: matchExpressions: – key: node-role.kubernetes.io/control-plane operator: Exists — apiVersion: policy.networking.k8s.io/v1alpha1 kind: BaselineAdminNetworkPolicy metadata: name: default # Singleton. Any other name is invalid. spec: subject: namespaces: matchLabels: security: internal ingress: – name: deny-all-other-ingress action: Deny from: – namespaces: {}

Now the failure. Set priority: 600 because that is what the equivalent DFW rule was numbered, and the object is accepted by the API server but never programmed. Nothing warns you at apply time. You only find out when you check status, which is why the verification section below is not optional.

$ oc apply -f guardrails.yaml adminnetworkpolicy.policy.networking.k8s.io/platform-guardrails configured $ oc describe anp platform-guardrails | tail -12 Status: Conditions: Last Transition Time: 2026-08-11T15:02:44Z Message: error attempting to add ANP platform-guardrails with priority 600 because, OVNK only supports priority ranges 0-99 Reason: SetupFailed Status: False Type: Ready-In-Zone-ocp-worker-01.corp.example.com # Fix and reapply. $ oc patch anp platform-guardrails –type=merge -p '{"spec":{"priority":20}}' adminnetworkpolicy.policy.networking.k8s.io/platform-guardrails patched

Verification, Audit Logging and Rollback

Four checks prove the chain. Each fails loudly rather than quietly, which is why I picked them over a connectivity smoke test that only tells you the happy path works.

# 1. Every ANP and BANP reports SetupSucceeded in every zone. $ oc describe anp platform-guardrails | grep -c SetupSucceeded 6 # 2. Confirm the policies reached the OVN northbound database as tier 1 ACLs. $ oc rsh -c nbdb -n openshift-ovn-kubernetes ovnkube-node-524dt ovn-nbctl –columns=name,action,tier find ACL 'external_ids{>=}{"k8s.ovn.org/name"=platform-guardrails}' name : "ANP:platform-guardrails:Egress:0" action : allow-related tier : 1 # 3. Turn on audit logging for the namespace, then prove a drop is recorded. $ oc annotate namespace wave1-web k8s.ovn.org/acl-logging='{ "deny": "alert", "allow": "notice" }' namespace/wave1-web annotated $ oc rsh -n openshift-ovn-kubernetes ovnkube-node-524dt tail -1 /var/log/ovn/acl-audit-log.log 2026-08-11T15:22:41.802Z|00019|acl_log(ovn_pinctrl0)|INFO| name="NP:wave1-web:Ingress", verdict=drop, severity=alert, direction=to-lport # 4. Negative test. This SHOULD fail, and a hang rather than a refusal is correct. $ oc run probe -n wave1-web –rm -it –image=registry.redhat.io/ubi9/ubi-minimal –restart=Never — curl -sS –connect-timeout 5 http://pgsql.wave1-data:5432 curl: (28) Connection timed out after 5001 milliseconds pod "probe" deleted

Rollback is one command per namespace and it is fast, which is the good news about doing segmentation with Kubernetes objects rather than a firewall appliance. Delete the policies and the namespace returns to open within a second or two, because OVN-Kubernetes removes the access control lists as soon as the object goes.

# Back out one namespace, leaving cluster guardrails in place. $ oc delete networkpolicy –all -n wave1-web networkpolicy.networking.k8s.io "default-deny-all" deleted networkpolicy.networking.k8s.io "allow-egress-dns" deleted networkpolicy.networking.k8s.io "allow-ingress-router-and-monitoring" deleted # Back out cluster scope. Remove the BANP first, or the ANP Pass rules # fall through to a deny that is still in place. $ oc delete banp default $ oc delete anp platform-guardrails # Turn logging back off when you are done, it is not free. $ oc annotate namespace wave1-web k8s.ovn.org/acl-logging-

Policy Failures and Remediation

What you seeCauseFix
read udp … 172.30.0.10:53: i/o timeoutDefault deny closed egress to cluster DNSAdd the allow-egress-dns policy to openshift-dns on UDP and TCP 5353
503 Application is not availableIngress deny blocks the router podsAllow ingress from namespaces labelled policy-group.network.openshift.io/ingress
SetupFailed, OVNK only supports priority ranges 0-99ANP priority copied from a DFW sequence numberRenumber into 0 to 99 and reapply, then recheck status
Prometheus targets show as down for one namespace onlyScrape traffic from openshift-monitoring blocked on ingressAllow ingress from openshift-monitoring, or add an ANP Allow at low priority
Policy applied, egress traffic still flows freelypolicyTypes omitted, so only Ingress was defaultedList Ingress and Egress explicitly in policyTypes
Application threads pile up, no connection errors in logsDFW Reject replaced by a silent dropSet an explicit connect timeout in the client, five seconds or less
An ANP Deny blocks traffic a tenant policy should permitANP Deny is non overridable by NetworkPolicyChange the ANP rule action to Pass and let tier 2 decide
System namespaces lose connectivity after a cluster wide ruleEmpty namespace selector also matches OpenShift namespacesScope subject by label, never with an empty selector

Field Note from a Deny That Broke Name Resolution

Pilot window, a Tuesday afternoon. I applied the default deny to wave1-web and wave1-data at 14:10, planning to add exceptions rule by rule and watch what broke. Textbook approach, and it is wrong. Two minutes later the storefront started returning 500s while every pod stayed Running and Ready, because the readiness probe was an HTTP GET on the root path that never opened a database connection. It took 26 minutes to find, and 24 of those went into checking the Route, the Service endpoints and the ingress controller, because a lookup timeout reads like a DNS outage and not like a firewall.

Cluster DNS is egress traffic. That sentence is obvious in hindsight and invisible at 14:12. On TKGI it never came up, because the NCP generated section carried an infrastructure allow that predated any of us.

Audit logging would have answered it in ninety seconds, and I had not turned it on, because I planned to enable it once the policies were stable. Wrong order. Annotate the namespace before the first deny lands, watch acl-audit-log.log while you apply, and the verdict lines name the policy that dropped the packet. Logging costs disk and some throughput on busy nodes, which is a fair reason to turn it off later, and no reason at all to leave it off during a migration wave.

Second surprise the same afternoon, and this one cost more. Our NSX-T tenant boundary used Reject, so a blocked connection came back as a refusal in single digit milliseconds and the application failed fast and retried. OVN-Kubernetes drops. Same policy intent, and now the client sat on an open socket for its full 30 second default until the pool of 20 connections was gone and the pod was effectively dead while reporting itself healthy. I reversed my plan after that. Segmentation went in with the DNS and router exceptions preloaded, audit logging on before the deny rather than after, and a pull request against three application repositories setting an explicit five second connect timeout. That last change had nothing to do with OpenShift and everything to do with the fact that Reject had been papering over sloppy client configuration for years.

Verdict: Namespace scoped NetworkPolicy plus one BANP is the pattern to build on. AdminNetworkPolicy Deny is the one to avoid until you have a rule that genuinely must survive a tenant trying to remove it, and in the reference estate that came to three rules out of one hundred and fifty.

Namespace Baselines First, Cluster Guardrails Second

A clean result looks like this. Every migrated namespace carries a default deny with DNS, router and monitoring exceptions applied in a single manifest. One BANP named default closes the gaps underneath. One ANP holds three rules at priority 20, all reporting SetupSucceeded in every zone. Your NSX rule inventory reconciles to a Kubernetes object or to a written decision that the rule is retired, with no third category. Audit logging is on for the namespaces still in a migration wave and off everywhere else.

Monday morning, run the two inventory commands from the preflight section against one TKGI cluster and subtract. Whatever number falls out is the part of your security posture that no export will carry across, and it is better to know it now than during the pilot. If you are still weighing whether OpenShift is the right landing place at all, the TKGI to VKS guide covers the VMware side of that fork. Otherwise the next part starts moving actual workloads, with Velero on TKGI and OADP on OpenShift, and it assumes the segmentation in this part is already standing. Worth rereading Part 8 on the NSX-T and NCP assessment alongside it, because the egress identity decisions you made there constrain what you can write here.

TKGI to OpenShift Series · Part 16 of 26
« Previous: Part 15  |  Guide  |  Next: Part 17 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading