PRACTICAL GUIDE / Selenium Grid event bus network segmentation

Why Grid nodes disappear behind a healthy router

Learn to isolate Selenium Grid's Event Bus, identify the blocked traffic direction, and roll out least-privilege rules without losing Nodes.

By The Testing AcademyUpdated August 4, 202623 min read
All field guides
In this guide6 sections
  1. Why a healthy Router can still have no usable Nodes
  2. Draw the real traffic map before writing policy
  3. Reproduce the blocked path without guessing
  4. Example 1: check the two registration legs from their real sources
  5. Example 2: prove an allow rule and a deny rule in Kubernetes
  6. Example 3: require a real browser session after socket checks
  7. Tell Event Bus isolation from similar Grid failures
  8. Roll out least privilege without losing the Grid
  9. Know what the isolation costs, and when to skip it

What you will learn

  • Why a healthy Router can still have no usable Nodes
  • Draw the real traffic map before writing policy
  • Reproduce the blocked path without guessing
  • Tell Event Bus isolation from similar Grid failures

The Grid endpoint answers on port 4444, yet a freshly started Chrome Node never appears in the console. A session request waits until it fails, while the Router's health check stays green. That combination usually sends teams toward browser images and capacity tuning, even though the first broken operation may be a Node heartbeat crossing a network boundary.

A distributed Grid has more than one kind of internal traffic. The Event Bus carries asynchronous messages, while HTTP handles operations that need a direct response. Network isolation has to preserve both paths. Treating the Grid as a single service with one open port produces a deployment that is reachable from the test runner but unable to assemble itself.

Why a healthy Router can still have no usable Nodes

Port 4444 is the client-facing entrance, not proof that the rest of the Grid can communicate. The Router accepts new-session requests and routes commands. A fully distributed deployment also has a New Session Queue, Distributor, Session Map, Event Bus, and one or more Nodes. Each component can be running and answering its own health endpoint while the system as a whole has no usable browser slot.

Node registration exposes this difference clearly. On startup, a Node sends a registration message and later heartbeat events through the Event Bus. The Distributor listens for those events. After it learns about a Node, it performs an HTTP GET against that Node's status endpoint and uses the response to update its Grid model. Registration therefore has two independently controlled legs:

  1. The Node must establish Event Bus connections to the publish and subscribe endpoints.
  2. The Distributor must resolve and reach the HTTP address advertised by the Node.

A policy can allow the first leg and block the second. It can also allow the Distributor to call every existing Node while preventing a new Node from announcing itself. Both cases result in an empty or stale Grid model, but their evidence differs. With a blocked event path, the Distributor never has a Node address to check. With blocked HTTP, the Distributor sees the announcement and then fails when it tries to verify the Node.

The default ports make the topology look simpler than it is. Selenium documents TCP 4442 as the Event Bus publishing connection and TCP 4443 as the subscribing connection. In fully distributed mode, the Event Bus also starts an HTTP server on port 5557. A successful request to 5557 proves that the component's HTTP listener is alive. It says nothing about the two ZeroMQ sockets that carry events. Likewise, a 200 response from the Router says nothing about Node registration.

Hub and Node mode compresses several components into the Hub process, but remote Nodes still need the Hub's Event Bus ports. Standalone mode compresses everything, including browser slots, into one process. Network rules useful for a fully distributed Grid may add no value to a Standalone instance because no cross-pod Event Bus path exists there.

This is also why a generic Kubernetes readiness probe can lie by omission. A TCP probe against 4444 answers, so the Router pod becomes ready. The service sends test traffic to it. Session requests then enter the queue, but no Distributor-visible slot can consume them. A better deployment-level readiness signal checks the Grid status for the expected Node population or creates a short canary session. That signal is more expensive, but it measures the capability users need.

The official Grid architecture describes the split between asynchronous Event Bus messages and synchronous HTTP calls. The component guide also records the registration sequence. Keep those two contracts in view whenever a firewall ticket reduces the requirement to "open Selenium."

Draw the real traffic map before writing policy

Start with processes and directions, not port numbers. A rule that says "allow 4442-4444 within the namespace" loses the source, destination, and reason for each connection. It also misses several HTTP services that a distributed Grid needs.

For a conventional fully distributed deployment, the important paths include the following:

SourceDestinationDefault portWhy it exists
NodeEvent BusTCP 4442 and 4443Registration and heartbeat-related event exchange
DistributorEvent BusTCP 4442 and 4443Receives Node events and participates in asynchronous coordination
Session MapEvent BusTCP 4442 and 4443Participates in Grid event exchange
New Session QueueEvent BusTCP 4442 and 4443Participates in queue-related event exchange
DistributorNodeNode HTTP port, commonly 5555Reads Node status and creates sessions
RouterNodeNode HTTP portForwards commands for an existing session
RouterDistributorTCP 5553Uses the Distributor's HTTP service
RouterSession MapTCP 5556Finds the Node that owns a session
RouterNew Session QueueTCP 5559Submits and follows new-session work
DistributorSession Map and New Session QueueTCP 5556 and 5559Stores session ownership and consumes queued requests
Test clientRouterTCP 4444Sends WebDriver commands

These defaults are a starting point. Flags, service ports, sidecars, host networking, and container images can change the effective destination. Ask the running Selenium server for configuration help when versions differ. The command below reports the options implemented by the jar you are actually deploying:

Shell
java -jar selenium-server.jar info config

That command is more trustworthy than a copied chart value from six months ago. The official CLI option reference explicitly warns that generated documentation can lag an implementation.

Record the address each process advertises, not only the service name operators intend it to use. Once a heartbeat arrives, the Distributor calls the Node URI contained in the Node status information. A Node that advertises localhost, a pod IP unreachable across clusters, or a hostname absent from the Distributor's DNS view will fail the HTTP leg even when both event sockets are open. This is a routing problem, not an Event Bus ACL problem.

Kubernetes policies are normally stateful at the connection level. If a Node is allowed to initiate a connection to the Event Bus, return packets for that established connection do not require an unrelated rule that lets the Event Bus initiate a fresh connection to the Node. Avoid translating "publish and subscribe" into two guessed source directions. In Selenium's default topology, Grid components connect to the Event Bus endpoints; the port names describe the event role, not two opposite firewall directions.

Namespace placement matters too. A NetworkPolicy peer with only a pod selector matches pods in the policy's namespace. It does not automatically admit identically labelled Nodes from a separate Windows, GPU, or browser namespace. Conversely, an empty namespace selector can admit matching traffic from every namespace, depending on how the peer is written. Read the combined selector as an identity boundary, not decorative YAML.

Do not forget traffic outside the control plane. A browser still needs DNS, the application under test, certificate services, download hosts, and perhaps a proxy. Selecting Node pods with a default-deny egress policy can preserve Grid registration and still make every navigation time out. Sidecars need telemetry destinations. Dynamic browser containers may use another network namespace or inherit the Node pod's policy behavior, depending on the runtime. Capture actual flows during a representative test before denying them.

Port 5557 deserves a separate decision. Operators may expose it to a monitoring namespace for component health, but Nodes do not substitute it for 4442 or 4443. Letting every workload reach 5557 because it is "part of the Event Bus" expands diagnostic access without fixing event delivery. Document the monitoring source separately from Grid component identities.

Reproduce the blocked path without guessing

One browser test is a poor first probe. It crosses the Router, queue, Distributor, Node, driver, browser, DNS, and application. A timeout anywhere in that chain looks like "Grid is broken." Use progressively richer checks so each result narrows the fault.

Example 1: check the two registration legs from their real sources

The following Bash script is intended to run inside the Node pod or the same network namespace. It checks the Router for context, then tests both Event Bus sockets. Set NODE_URL only when the same environment should be able to reach the Node's own HTTP endpoint. The script requires curl, jq, and an nc implementation with the common -z and -w options.

Shell
#!/usr/bin/env bash
set -euo pipefail

ROUTER_URL=http://selenium-router:4444
EVENT_BUS_HOST=selenium-event-bus
NODE_URL=http://127.0.0.1:5555

router_code=$(
  curl --silent --show-error \
    --output /tmp/grid-status.json \
    --write-out "%{http_code}" \
    --max-time 4 \
    "$ROUTER_URL/status" || true
)
printf 'router_http=%s\n' "$router_code"

if [[ -s /tmp/grid-status.json ]]; then
  jq '{ready: .value.ready, message: .value.message}' /tmp/grid-status.json
fi

for port in 4442 4443; do
  if nc -z -w 3 "$EVENT_BUS_HOST" "$port"; then
    printf 'bus_port_%s=reachable\n' "$port"
  else
    printf 'bus_port_%s=blocked_or_unbound\n' "$port"
  fi
done

if curl --fail --silent --show-error --max-time 4 "$NODE_URL/status" |
    jq '.' >/tmp/node-status.json; then
  printf 'node_http=reachable\n'
else
  printf 'node_http=unreachable\n'
fi

A segmented run might print:

Example
router_http=200
{
  "ready": false,
  "message": "Selenium Grid not ready."
}
bus_port_4442=reachable
bus_port_4443=blocked_or_unbound
node_http=reachable

The important line is not the Router's 200. One of the two event sockets cannot be opened from the Node's network identity. That is enough to investigate the policy, service endpoint, listener, or route before launching Chrome.

Now run the HTTP check from the Distributor pod, changing NODE_URL to the exact URI shown in the Node's startup configuration or logs:

Shell
kubectl -n selenium-grid exec deploy/selenium-distributor -- \
  curl --fail --silent --show-error --max-time 4 \
  http://selenium-node-chrome:5555/status

If both Event Bus ports pass from the Node but this request fails from the Distributor, opening more access to the Event Bus is the wrong fix. Check the policy selecting the Node, its Service or headless Service, the advertised hostname, and DNS from the Distributor.

This diagnostic script checks both registration legs from their actual workloads and returns a distinct line for each failed boundary:

Shell
#!/usr/bin/env bash
set -u

grid_namespace=${GRID_NAMESPACE:-selenium-grid}
node_workload=${NODE_WORKLOAD:-deployment/selenium-node-chrome}
distributor_workload=${DISTRIBUTOR_WORKLOAD:-deployment/selenium-distributor}
event_bus_host=${EVENT_BUS_HOST:-selenium-event-bus}
node_url=${NODE_URL:-http://selenium-node-chrome:5555/status}
failures=0

for port in 4442 4443; do
  if kubectl -n "$grid_namespace" exec "$node_workload" -- \
      nc -z -w 3 "$event_bus_host" "$port"; then
    printf 'node_to_event_bus_%s=passed\n' "$port"
  else
    printf 'node_to_event_bus_%s=failed\n' "$port" >&2
    failures=$((failures + 1))
  fi
done

if kubectl -n "$grid_namespace" exec "$distributor_workload" -- \
    curl --fail --silent --show-error --max-time 4 \
    --output /dev/null "$node_url"; then
  printf 'distributor_to_node_http=passed\n'
else
  printf 'distributor_to_node_http=failed\n' >&2
  failures=$((failures + 1))
fi

exit "$failures"

A TCP connect is deliberately a shallow assertion. It cannot prove that the endpoint speaks the expected protocol, that subscriptions are established, or that the Distributor processed an event. Its value is speed and location. Use it to reject a basic L3/L4 block, then move to Grid state.

Example 2: prove an allow rule and a deny rule in Kubernetes

An ingress-only first step limits who may connect to the Event Bus without accidentally denying the Node's access to the application under test. This manifest assumes the Event Bus pod has the name label shown, approved Grid components carry the part-of label, and health checking is local to the pod. If a kubelet or monitoring pod reaches port 5557 over the network, give that real source a separate, narrow rule before applying this example.

YAML
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: event-bus-accept-grid-components
  namespace: selenium-grid
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: selenium-event-bus
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/part-of: selenium-grid
      ports:
        - protocol: TCP
          port: 4442
        - protocol: TCP
          port: 4443

The pod selector in the peer limits allowed sources to the same namespace. For Nodes in another namespace, add an intentional namespace selector and a pod selector in the same from entry. Do not add an empty namespace selector merely to make the test pass.

For example, this policy admits only Node pods from a namespace named selenium-nodes to the two Event Bus data ports:

YAML
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: event-bus-from-node-namespace
  namespace: selenium-grid
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: selenium-event-bus
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: selenium-nodes
          podSelector:
            matchLabels:
              app.kubernetes.io/component: node
      ports:
        - protocol: TCP
          port: 4442
        - protocol: TCP
          port: 4443

Apply the policy to a canary namespace first, then launch an approved probe:

Shell
kubectl -n selenium-grid run allowed-bus-probe \
  --rm -i \
  --restart=Never \
  --image=nicolaka/netshoot:v0.13 \
  --labels=app.kubernetes.io/part-of=selenium-grid \
  --command -- sh -euxc '
    nc -z -w 3 selenium-event-bus 4442
    nc -z -w 3 selenium-event-bus 4443
  '

The pod should exit with status 0. A probe without the approved label should be rejected:

Shell
kubectl -n selenium-grid run denied-bus-probe \
  --rm -i \
  --restart=Never \
  --image=nicolaka/netshoot:v0.13 \
  --labels=app.kubernetes.io/part-of=untrusted-probe \
  --command -- sh -c '
    if nc -z -w 3 selenium-event-bus 4442; then
      echo "unexpected access to port 4442"
      exit 1
    fi
    if nc -z -w 3 selenium-event-bus 4443; then
      echo "unexpected access to port 4443"
      exit 1
    fi
    echo "both Event Bus ports are blocked as expected"
  '

Expected output from the second command is:

Example
both Event Bus ports are blocked as expected
pod "denied-bus-probe" deleted

This pair matters. A positive probe alone shows availability but does not prove isolation. A negative probe alone can pass when the Service has no endpoints or the Event Bus is down. Run both against the same deployment revision, and inspect the endpoint set if both are blocked:

Shell
kubectl -n selenium-grid get endpoints selenium-event-bus -o wide
kubectl -n selenium-grid get pods -l app.kubernetes.io/name=selenium-event-bus \
  --show-labels

A policy engine needs time to converge after a change. Do not interpret the first packet after apply as a durable result. Repeat the checks from fresh pods and retain the policy YAML, pod labels, endpoint addresses, and command output with the rollout record.

Example 3: require a real browser session after socket checks

A successful connect to 4442 and 4443 still leaves protocol, registration, Grid model, capacity, and browser startup untested. This Python canary creates a real Remote WebDriver session, navigates to an in-memory page, checks its title, and always quits the session. It uses only public Selenium APIs.

Python
import os
import time

from selenium import webdriver
from selenium.common.exceptions import WebDriverException

grid_url = os.environ.get("GRID_URL", "http://localhost:4444")
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")

driver = None
started = time.monotonic()

try:
    driver = webdriver.Remote(
        command_executor=grid_url,
        options=options,
    )
    elapsed = time.monotonic() - started
    print(
        f"session_created id={driver.session_id} "
        f"elapsed_seconds={elapsed:.2f}"
    )

    driver.get("data:text/html,<title>grid-canary</title>")
    actual_title = driver.title
    assert actual_title == "grid-canary", actual_title
    print("navigation_assertion=passed")
except WebDriverException as exc:
    elapsed = time.monotonic() - started
    print(
        f"session_creation_failed elapsed_seconds={elapsed:.2f} "
        f"type={type(exc).__name__}"
    )
    print(str(exc))
    raise
finally:
    if driver is not None:
        driver.quit()

A passing run provides evidence at a higher level:

Example
session_created id=8db5cbb22f47d3f4a68b52e16a0c4c67 elapsed_seconds=1.84
navigation_assertion=passed

On a failed run, preserve the first line printed by the exception path and the server-side records from the same time window. Exception wording varies by Selenium version and by which component rejects the request. The exception class alone cannot distinguish a blocked Event Bus from no matching Chrome capability or a browser process that crashes during startup.

Pair this canary with the Grid status endpoint documented in Grid endpoints:

Shell
curl --silent --show-error http://selenium-router:4444/status |
  jq '.value.nodes[] | {
    id: .id,
    uri: .uri,
    availability: .availability,
    slots: (.slots | length)
  }'

Zero rows after the positive TCP probes point toward registration processing or the Distributor-to-Node HTTP check. Existing Nodes with the wrong stereotypes point toward capability matching. Nodes with occupied slots point toward capacity. That one JSON response is usually more useful than rerunning the entire suite.

Tell Event Bus isolation from similar Grid failures

The closest near-miss is a blocked Node status path. Its user-visible symptom is nearly identical: no usable Node and session requests that wait. The separating evidence is whether the Distributor learned enough about the Node to attempt HTTP verification.

Collect four views for the same startup window:

  • From the Node network identity, resolve the Event Bus host and connect to both 4442 and 4443.
  • From the Event Bus pod, confirm listeners exist on both ports.
  • From the Distributor network identity, resolve the Node's advertised host and request its status endpoint.
  • From the Router, inspect registered Nodes and queued requests.

A listener check inside the Event Bus pod removes the policy from the question:

Shell
kubectl -n selenium-grid exec deploy/selenium-event-bus -- \
  sh -c 'ss -lnt | grep -E ":(4442|4443|5557)[[:space:]]"'

If ss is not present in the image, use the container runtime or an approved ephemeral debug container rather than installing packages into production. A successful local listener plus a failed remote connection supports a network path problem. A missing local listener supports a startup or configuration problem. Those require different owners.

Structured server logs add correlation. Selenium supports structured logs, tracing, and FINE-level event output. Enable them on a canary component with documented flags, then revert the verbosity after capture because FINE logs increase volume:

Shell
java -jar selenium-server.jar node \
  --publish-events tcp://selenium-event-bus:4442 \
  --subscribe-events tcp://selenium-event-bus:4443 \
  --structured-logs true \
  --log-level FINE

Do not search only for the word "error." Look for the last completed boundary. Did the Node finish startup and begin registration attempts? Did the Distributor record a Node event and then attempt status? Does the status response contain the expected slots? The observability guide explains that trace records include trace IDs, span IDs, event names, and attributes. Preserve those identifiers when an HTTP request crosses components.

Several other failures imitate segmentation:

A registration secret mismatch. Both event ports are reachable, but the Node and Distributor do not agree on the configured registration secret. Server logs show registration being rejected rather than a socket timeout. Compare the effective configuration on both components. Do not remove the secret to make a canary green.

An advertised address that only the Node can resolve. The Node connects outward to the bus successfully, but the Distributor cannot resolve or route back to the URI the Node reports. Run getent hosts and curl from the Distributor pod using that exact host, not a convenient replacement Service. A successful curl to a different hostname proves little.

A capability mismatch. The Node is visible and up, but its slots do not match the request. Query status for stereotypes and compare them with the canary's requested browser. A NetworkPolicy change cannot create a Chrome slot on a Firefox-only Node.

A saturated pool. Registered Nodes and their slots appear normally, but every matching slot already owns a session. Queue size rises while active session count equals capacity. Event Bus access may be perfectly healthy. Inspect live sessions and the session timeout policy before scaling.

A dead Service endpoint. DNS resolves and the policy looks correct, but the Service selector points to no ready Event Bus pod. Both allowed and denied probes fail, which is why the positive and negative test pair is essential. Kubernetes endpoint output settles this quickly.

A browser egress block. Session creation succeeds, producing a session ID, but navigation to the application times out or returns a network error page. That places the fault after Grid allocation. Check Node or browser-container egress to the application and DNS rather than changing control-plane rules.

A transient registration window. The Node retries initial registration on a schedule. A short policy interruption may disappear before an engineer inspects it, while the Node later joins successfully. Record pod start time, policy generation, first successful registration, and restart count. The CLI reference documents registration cycle and period settings, but changing them to hide a slow network usually makes diagnosis harder.

GraphQL can provide a compact model of Nodes and queue size when it is enabled through the Router:

Shell
curl --silent --show-error \
  --header 'Content-Type: application/json' \
  --data '{"query":"{ grid { sessionQueueSize } nodesInfo { nodes { id uri status slotCount sessionCount } } }"}' \
  http://selenium-router:4444/graphql |
  jq '.data'

Interpret it with the socket and HTTP probes. An empty nodes array plus failed event connections supports segmentation. An empty array plus successful event connections and failed Node status calls supports the return HTTP path. Populated Nodes with a growing queue move the investigation toward matching or capacity.

Roll out least privilege without losing the Grid

Begin with an inventory captured from a passing deployment. Save component labels, namespaces, Service selectors, endpoints, effective Selenium commands, advertised Node URIs, and a short packet-flow sample if your platform permits it. Include at least one real test that reaches the application. This baseline gives reviewers something concrete to compare after policy changes.

Limit the first change to Event Bus ingress. The example policy above lets approved Grid components connect while leaving their egress behavior untouched. That reduces one exposure without simultaneously blocking DNS, the application, artifact storage, or telemetry. Verify one persistent Node and one newly created Node. Existing TCP connections can survive a policy mistake long enough to mislead you, so a fresh Node is mandatory.

Use a canary pool with the same CNI, namespace policy, labels, and service discovery as production. A separate cluster with different defaults is a weak rehearsal. Apply the rule, start a new Node, wait for it to appear in Grid status, create a session, navigate, quit, and confirm the slot returns to available. Then run the denied probe. Store each result by deployment revision.

Roll out by Node group rather than all browsers at once. Chrome, Firefox, Windows, mobile relay, and dynamic Docker Nodes may advertise different addresses or live in different network domains. A label that works for Linux pods can exclude a Windows Node namespace. Keeping one old pool available also gives the team a clean comparison when a canary fails.

Egress restrictions require a separate change set. Enumerate Event Bus, DNS, application, proxy, package-independent certificate services, telemetry, and any dynamic browser endpoints. Add the Distributor-to-Node and Router-to-Node HTTP flows on the receiving side. Test downloads, redirects, WebSockets, and cross-origin dependencies if the suite uses them. A homepage canary does not exercise an artifact download host.

Set monitoring around outcomes, not only pods:

  • Expected registered Node count by browser stereotype
  • Time from Node process start to appearance in Grid status
  • New-session queue size and oldest request age
  • Session creation success and latency from the canary
  • Node restarts during its initial registration period
  • Denied connections to 4442, 4443, and Node HTTP ports by source identity

A policy denial without Grid impact may be the desired rejection test. A falling Node count with no denial metric may point to DNS, listener, configuration, or an observability gap. Alert wording should preserve that uncertainty.

Keep rollback narrow. Revert the single policy revision or remove its selector from the canary pods according to your platform's approved procedure. Do not keep a permanent "allow all namespace traffic" manifest as an emergency button. That broad rule can union with restrictive policies and silently defeat the isolation after the incident ends.

When the Node option for shutting down after unsuccessful initial registration is enabled, a policy mistake can create a restart loop. The behavior is useful for disposable infrastructure because an unusable Node does not linger forever. Its cost is lost diagnostic time and noisier orchestration. Capture logs across restarts and prove the network path before enabling that behavior across every pool.

Finally, turn the three worked checks into a release gate. The TCP pair runs from the Node identity, the status check runs from the Distributor identity, and the browser canary runs through the Router. Each has a different owner and a different failure message. Together they prevent a green Router probe from certifying an empty Grid.

If the Python canary above is saved as ci/grid_canary.py, these GitHub Actions steps wire the positive socket check, the required denial, and the real session into one gate:

YAML
- name: Prove approved Event Bus access
  run: |
    kubectl -n selenium-grid run ci-bus-allow-${{ github.run_id }} \
      --rm -i --restart=Never \
      --image=nicolaka/netshoot:v0.13 \
      --labels=app.kubernetes.io/part-of=selenium-grid \
      --command -- sh -ec '
        nc -z -w 3 selenium-event-bus 4442
        nc -z -w 3 selenium-event-bus 4443
      '

- name: Prove untrusted Event Bus access is denied
  run: |
    kubectl -n selenium-grid run ci-bus-deny-${{ github.run_id }} \
      --rm -i --restart=Never \
      --image=nicolaka/netshoot:v0.13 \
      --labels=app.kubernetes.io/part-of=untrusted-probe \
      --command -- sh -ec '
        if nc -z -w 3 selenium-event-bus 4442 ||
           nc -z -w 3 selenium-event-bus 4443; then
          printf "unexpected Event Bus access\n" >&2
          exit 1
        fi
      '

- name: Create a browser session through the Router
  env:
    GRID_URL: http://selenium-router.selenium-grid.svc:4444
  run: python ci/grid_canary.py

Know what the isolation costs, and when to skip it

Least privilege adds configuration proportional to the number of real trust boundaries. A single namespace with fixed components may need only a small ingress rule. Separate browser namespaces, multi-cluster Nodes, dynamic pods, service meshes, and several application environments create more selectors, DNS paths, and exceptions. Every exception needs an owner or the policy gradually becomes an undocumented allow list.

The first cost is startup sensitivity. Event connections and Node registration happen early. A slow policy controller, unavailable DNS service, or delayed endpoint can push registration beyond the useful startup window. Aggressive restarts then amplify a brief dependency failure. Measure registration time before shortening timeouts or increasing restart pressure.

The second cost is diagnostic complexity. NetworkPolicy tools usually report IPs, ports, and labels, while Selenium reports component names, Node IDs, session IDs, and trace IDs. Joining those records takes deliberate timestamps and metadata. Higher log levels help during rollout but increase storage and can bury the first useful event in repeated retries.

The third cost is test coverage risk. Node egress restrictions apply to the browser's work as well as Grid control traffic in many container designs. A narrow allow list can exclude third-party identity providers, CDNs, analytics endpoints that affect the page, or a test download service. Whether those calls should be reachable is a product and security decision, but blocking them changes what the test covers. Document that change instead of calling every resulting timeout flakiness.

Port-level isolation is not message-level authorization. Allowing a pod to reach 4442 and 4443 allows it to establish connections to those listeners. A NetworkPolicy cannot decide which Selenium event types that pod may publish. Registration secrets protect a different boundary and should not be presented as encryption for Event Bus traffic. Keep internal Grid services off public networks and use infrastructure controls appropriate to the environment.

Skip strict Event Bus segmentation when all components run inside one Standalone process. There is no cross-host event path to isolate, and opening remote Event Bus ports would create exposure rather than reduce it. A simple Hub and a small trusted Node subnet may also gain more from a precise perimeter rule than from dozens of pod-level egress exceptions.

Delay enforcement while the topology is still unknown. Applying default deny to "discover dependencies" during a production run turns customers into packet probes. Observe a representative passing flow first, then test the proposed identity rules in a disposable environment.

Avoid this approach when the actual requirement is tenant isolation inside one shared Grid. Network rules separate endpoints and workloads, not individual test sessions or messages carried over an allowed connection. Strong tenant boundaries may require separate Grid deployments, namespaces, credentials, and application access policies.

Multi-region Nodes need special caution. Ordinary Kubernetes NetworkPolicy does not make a private route exist across clusters, preserve source identity through a gateway, or guarantee that the Distributor can call the Node's advertised URI. Solve routing and naming first. Apply segmentation only after both registration legs work reliably across the real path.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why is Selenium Grid reachable when no Nodes appear?

The Router can answer HTTP requests even when Node registration is broken. A Node first announces itself through the Event Bus, and the Distributor then calls the Node's HTTP status endpoint, so either leg can fail independently.

Which Selenium Grid Event Bus ports must a Node reach?

By default, a remote Node connects to the Event Bus on TCP ports 4442 and 4443. Port 5557 is the Event Bus component's HTTP server in a distributed deployment, but reaching it does not prove that either event socket works.

Does opening ports 4442 and 4443 guarantee Node registration?

No. Successful TCP probes only establish that the Node can reach both Event Bus listeners. The Distributor must also resolve the Node's advertised address and reach its status endpoint, normally on the Node's HTTP port.

How can I test a Grid NetworkPolicy without risking the main pool?

Run one allowed probe and one deliberately untrusted probe in a disposable namespace or canary pool. The allowed identity should reach both event ports and create a browser session, while the untrusted identity should fail at the TCP boundary.

Should every Grid component receive Event Bus access?

Grant access from components that actually participate in event exchange, based on the deployment mode and the traffic you observed. The Router has different synchronous dependencies, so copying one broad rule to every Grid pod hides mistakes and expands the blast radius.