30

Service Traffic Baseline: Why Add a Mesh?

Freeze the working Kubernetes traffic contract before changing how traffic is secured.

Run it in the public monorepo

This course is built around the public Zheta Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.

Source: scripts/probe-network-policy.sh

set +e
kubectl -n "$namespace" exec pod/"$DENIED_SOURCE_POD" -- env TARGET_IP="$target_ip" python -c 'import os,socket,sys
try:
 socket.create_connection((os.environ["TARGET_IP"],8080),timeout=3)
except TimeoutError as error:
 print(error,file=sys.stderr); raise SystemExit(42)
except Exception as error:
 print(error,file=sys.stderr); raise SystemExit(43)
raise SystemExit(0)' 2> "$NETWORK_POLICY_EVIDENCE_DIR/negative-stderr.txt"
negative_status=$?
set -e
[ "$negative_status" = 42 ] || { echo "NetworkPolicy probe inconclusive: expected enforced timeout status 42, got $negative_status" >&2; exit 1; }

Code to reality

Declared intent
Establish a live pre-mesh baseline by requiring the reviewed evidence Pod to time out against the exact generator Pod IP.
Interpreter
The shell and kubectl execute a bounded socket attempt; the pinned kindnet NetworkPolicy controller enforces the denied path.
Software effect
Only exact timeout status 42 is accepted; success, refusal, DNS failure, or another exception makes the probe inconclusive.
Hardware effect
The source Pod, destination node network stack, and node-local kindnet agent spend CPU and packets on the denied connection attempt.
Observable evidence
Reviewed Pod UIDs, kindnet status, exact policies, two positive controls before/after, and captured timeout stderr prove the baseline.

Start with the people and the result they need

The source tables below remain the detailed contract. Begin with these customer paths:

  • D30-UC-01
    • Person: Enterprise builder
    • Job: Publish through the Forge control plane while its internal services remain reachable by stable names
    • Observable result: The request enters control-plane:8080 and the required internal Service endpoints are ready
  • D30-UC-02
    • Person: Platform operator
    • Job: Record the traffic and identity contract before adding a mesh
    • Observable result: Source revision, five Services, five ServiceAccounts, ready workloads, EndpointSlices, and four NetworkPolicies agree

Turn each customer job into a testable story

Now turn each customer job into a story with a result that an engineer can check:

  • D30-US-01
    • Story: As an enterprise builder, I want publishing to use stable internal service names, so that Pod replacement does not change my workflow
    • Observable acceptance: The same request succeeds through control-plane:8080; Service selectors have ready endpoints; the denied probe still fails
  • D30-US-02
    • Story: As a platform operator, I want an exact pre-mesh traffic map, so that I can detect migration regressions instead of guessing
    • Observable acceptance: Evidence names Git revision, namespace, Service, port, selected Pods, ServiceAccount, NetworkPolicy, expected result, observed result, context,…

Add real state and observable proof

Finally trace each story through the system that owns its state and the evidence that proves the outcome:

  • D30-FLOW-01
    • Trigger: Builder sends an authenticated publish request to the control-plane Service
    • Responsible systems: Kubernetes Service routing; control-plane, generator, runtime, evidence, and broker workloads; enforcing pod network
    • Authoritative state: Git manifests plus Kubernetes API objects and EndpointSlices
    • Owned record: ServiceTrafficContract
    • Observable evidence: Request result, Service and EndpointSlice names, Pod UIDs, ServiceAccounts, policies, context, source revision, time, and trace or run ID
    • Failure signal: Failed publish, empty endpoint set, or an approved TCP path denied
  • D30-FLOW-02
    • Trigger: Operator probes an undeclared direct path to an internal service
    • Responsible systems: Kustomize, Kubernetes API server, Deployment controllers, Service proxy, and NetworkPolicy implementation
    • Authoritative state: Git is desired configuration; Kubernetes API is observed object state; evidence record stores the comparison
    • Owned record: WorkloadIdentityContract
    • Observable evidence: Denied source and destination, port, policy, observed failure, unaffected positive control, context, source revision, time, and run ID
    • Failure signal: Selector drift, unexpected allowed path, missing policy enforcement, or stale evidence

The enterprise problem and today’s slice

Enterprise problem: A service mesh can appear to improve security while silently breaking a customer publish request, because the team never recorded which service names, ports, workload identities, and NetworkPolicies already made that request work. Whole-course context: The incoming Forge application is rendered from Git into a local Kubernetes cluster; today captures its pre-mesh traffic contract before any Istio component or namespace label is added. Today’s slice: We inspect the five Services, five Deployments, five ServiceAccounts, and four effective NetworkPolicies, then run the checked-in kindnet positive/negative packet probe before explaining the remaining identity, encryption, and application-aware policy gap. End-of-day evidence: Only a successful probe may seal a source-revision-bound baseline containing reviewed Pod names and UIDs, kindnet status, exact policies, two allowed controls before and after, and one exact denied timeout. Still unsolved: Installing Istio, enrolling zheta-forge, mutual TLS, waypoints, mesh authorization, telemetry, failure recovery, and rollback remain deferred.

Customer use cases

Without a baseline, the application owner cannot tell whether a later failure came from the application, Kubernetes networking, or the mesh. A baseline is a before-state with both an allowed request and a denied request.

Use case IDActorCustomer jobSuccess outcomeDenial or recovery evidence
D30-UC-01Enterprise builderPublish through the Forge control plane while its internal services remain reachable by stable namesThe request enters control-plane:8080 and the required internal Service endpoints are readyA disallowed pod path is denied while the approved publish path remains healthy
D30-UC-02Platform operatorRecord the traffic and identity contract before adding a meshSource revision, five Services, five ServiceAccounts, ready workloads, EndpointSlices, and four NetworkPolicies agreeMissing endpoint, wrong selector, unexpected caller, or policy mismatch blocks mesh enrollment

Actor-centred user stories

A green Pod alone does not prove that a customer request can cross every required boundary. These stories join the customer action to network and identity evidence.

Story IDUse case IDsUser storyObservable acceptance conditions
D30-US-01D30-UC-01As an enterprise builder, I want publishing to use stable internal service names, so that Pod replacement does not change my workflowThe same request succeeds through control-plane:8080; Service selectors have ready endpoints; the denied probe still fails
D30-US-02D30-UC-02As a platform operator, I want an exact pre-mesh traffic map, so that I can detect migration regressions instead of guessingEvidence names Git revision, namespace, Service, port, selected Pods, ServiceAccount, NetworkPolicy, expected result, observed result, context, time, and run ID

End-to-end product flows

A customer sees one publish action, but Kubernetes must resolve several replaceable Pods behind stable Services. A Kubernetes Service supplies a long-lived address for changing backends, while EndpointSlices record the selected backends (Kubernetes networking).

Flow IDUse case IDsPathTriggerNumbered stepsTerminal evidence
D30-FLOW-01D30-UC-01HappyBuilder sends an authenticated publish request to the control-plane Service1. Resolve control-plane. 2. Route TCP port 8080 to a ready control-plane Pod. 3. Resolve required generator, runtime, evidence, or broker Service names. 4. NetworkPolicy admits only declared Layer 3/4 paths. 5. Return the publish result.Request result, Service and EndpointSlice names, Pod UIDs, ServiceAccounts, policies, context, source revision, time, and trace or run ID
D30-FLOW-02D30-UC-01, D30-UC-02DeniedOperator probes an undeclared direct path to an internal service1. Record the approved path as a positive control. 2. Send the bounded disallowed probe. 3. Observe timeout or rejection. 4. Re-run the approved path.Denied source and destination, port, policy, observed failure, unaffected positive control, context, source revision, time, and run ID

The smallest complete path is the builder’s action and its response. The diagram starts there because mesh work is safe only when the same customer evidence can be compared before and after the change.

System design derived from the flows

The baseline can control IP- and port-level reachability, but it cannot by itself prove workload identity or encrypt application traffic. Kubernetes also warns that a NetworkPolicy object has no effect when the cluster’s network implementation does not enforce it (Kubernetes networking).

Use case IDEntry pointResponsible servicesAuthoritative storeFailure evidence
D30-UC-01control-plane:8080Kubernetes Service routing; control-plane, generator, runtime, evidence, and broker workloads; enforcing pod networkGit manifests plus Kubernetes API objects and EndpointSlicesFailed publish, empty endpoint set, or an approved TCP path denied
D30-UC-02Git revision and read-only kubectl inspectionKustomize, Kubernetes API server, Deployment controllers, Service proxy, and NetworkPolicy implementationGit is desired configuration; Kubernetes API is observed object state; evidence record stores the comparisonSelector drift, unexpected allowed path, missing policy enforcement, or stale evidence

The existing action and result keep their meaning. The added components show responsibility: Service selection finds Pods, workload processes consume node CPU and memory, and the network implementation admits or drops packets according to the policies.

Data model and ownership

Traffic evidence becomes unreliable when desired configuration, observed endpoints, and test results are mixed into one mutable note. Keep each owner explicit and correlate them by source revision and run ID.

Generated-application database: Not created in this slice — Kubernetes objects and a durable baseline evidence record are sufficient to compare transport behavior; customer domain data remains owned by the existing application stores.

Record or entityStore and ownerPrimary keyForeign key or opaque referenceTenant keyMaterial constraintLifecycle and deletionUse case IDs
ServiceTrafficContractGit services.yaml and config.yaml, owned by Forge platform teamnamespace plus Service nameSelector and named target-port reference to workload labelszheta-forge namespace — this slice has one bounded workload tenantFive names select their matching workload label and expose port 8080 to named target port httpReview in Git, promote by revision, replace through review, retain historyD30-UC-01, D30-UC-02
WorkloadIdentityContractGit workloads.yaml, owned by Forge platform teamnamespace plus ServiceAccount or Deployment nameDeployment serviceAccountName and Pod-label referencezheta-forge namespace — this slice has one bounded workload tenantEach of five Deployments uses its same-named ServiceAccount and restrictive container settingsCreate by reconciliation, update by revision, remove with workload after evidence retentionD30-UC-01, D30-UC-02
NetworkPolicyContractGit network-policy.yaml, owned by Forge security teamnamespace plus NetworkPolicy namePod-selector and peer-selector referenceszheta-forge namespace — policies do not grant cross-tenant application authorityDefault deny is complemented only by named control-plane, internal-service, and broker pathsReview, reconcile, test allowed and denied paths, retain Git history, delete with namespaceD30-UC-01, D30-UC-02
TrafficBaselineRunEvidence artifact store, owned by platform operationsrun_idOpaque Git revision, context, object UID, request, and probe referencesenvironment plus zheta-forge namespaceBoth approved and denied paths must be fresh and from one contextCreate before migration, seal for comparison, retain through rollback window, expire by evidence policyD30-UC-01, D30-UC-02

The data layer adds four separately owned records without changing the request path. A later mesh result can now be compared with a sealed run instead of with memory.

Read the source contract exactly

A mesh migration can fail before any mesh component is involved if a Service selector, port, or identity is already wrong. These excerpts are exact slices of gitops/apps/forge/base/services.yaml, workloads.yaml, and network-policy.yaml in the implementation repository.

apiVersion: v1
kind: Service
metadata: { name: control-plane, namespace: zheta-forge }
spec: { selector: { app.kubernetes.io/name: control-plane }, ports: [{ name: http, port: 8080, targetPort: http }] }
Effect fieldWhat happens
Declared intentGive control-plane Pods a stable in-cluster name and port
InterpreterKubernetes API server, EndpointSlice controller, and the cluster’s Service proxy implementation
Software effectThe Service selector tracks ready backends with label app.kubernetes.io/name: control-plane
Hardware effectNode networking moves packets to the selected Pod; the process uses Pod CPU and memory
Evidencekubectl get service,endpointslice shows the Service and ready backend addresses
apiVersion: v1
kind: ServiceAccount
metadata: { name: control-plane, namespace: zheta-forge }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: control-plane, namespace: zheta-forge }
spec:
  replicas: 0
  selector: { matchLabels: { app.kubernetes.io/name: control-plane } }
  template:
    metadata: { labels: { app.kubernetes.io/name: control-plane, app.kubernetes.io/part-of: zheta-forge } }
    spec:
      serviceAccountName: control-plane
Effect fieldWhat happens
Declared intentBind control-plane Pods to the control-plane Kubernetes ServiceAccount; the base deliberately runs zero replicas
InterpreterKustomize, Kubernetes API server, Deployment controller, scheduler, kubelet, and container runtime
Software effectThe local overlay replaces every Deployment replica count with one; the base alone starts no process
Hardware effectEach local Pod consumes requested node CPU and memory and opens container port 8080
Evidencekubectl get deployment,pod,serviceaccount shows desired replicas, Pod UIDs, readiness, and the bound identity name
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: zheta-forge }
spec: { podSelector: {}, policyTypes: [Ingress, Egress] }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: broker-publish-subscribe, namespace: zheta-forge }
spec:
  podSelector: { matchLabels: { app.kubernetes.io/name: broker } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchExpressions: [{ key: app.kubernetes.io/name, operator: In, values: [control-plane, evidence] }] } }]
      ports: [{ protocol: TCP, port: 8080 }]
Effect fieldWhat happens
Declared intentIsolate every Pod by default, then admit broker ingress from control-plane and evidence Pods on TCP 8080
InterpreterKubernetes API server and a NetworkPolicy-capable cluster network implementation
Software effectSelected packet paths become allowed or denied at Layer 3/4; no TLS certificate or HTTP rule is created
Hardware effectNode packet processing admits or drops traffic before the destination process handles it
EvidenceAn allowed broker probe succeeds and a probe from an undeclared Pod fails, while policy objects alone are not accepted as runtime proof

Capture a reproducible before-state

Reading YAML cannot prove runtime behavior, and changing policy before collecting evidence destroys the comparison. Render and deploy the local overlay, review the exact control-plane, evidence, broker, and generator Pods, then run the repository’s checked-in positive/negative probe before any Ambient label exists.

git rev-parse HEAD
kubectl kustomize gitops/apps/forge/overlays/local >/tmp/forge-baseline.yaml
kubectl apply -f /tmp/forge-baseline.yaml
kubectl -n zheta-forge rollout status deployment --all --timeout=180s
ALLOWED_SOURCE_POD="$(kubectl -n zheta-forge get pods -l app.kubernetes.io/name=control-plane -o jsonpath='{.items[0].metadata.name}')"
ALLOWED_SOURCE_POD_UID="$(kubectl -n zheta-forge get pod "$ALLOWED_SOURCE_POD" -o jsonpath='{.metadata.uid}')"
DENIED_SOURCE_POD="$(kubectl -n zheta-forge get pods -l app.kubernetes.io/name=evidence -o jsonpath='{.items[0].metadata.name}')"
DENIED_SOURCE_POD_UID="$(kubectl -n zheta-forge get pod "$DENIED_SOURCE_POD" -o jsonpath='{.metadata.uid}')"
DENIED_SOURCE_CONTROL_POD="$(kubectl -n zheta-forge get pods -l app.kubernetes.io/name=broker -o jsonpath='{.items[0].metadata.name}')"
DENIED_SOURCE_CONTROL_POD_UID="$(kubectl -n zheta-forge get pod "$DENIED_SOURCE_CONTROL_POD" -o jsonpath='{.metadata.uid}')"
TARGET_POD="$(kubectl -n zheta-forge get pods -l app.kubernetes.io/name=generator -o jsonpath='{.items[0].metadata.name}')"
TARGET_POD_UID="$(kubectl -n zheta-forge get pod "$TARGET_POD" -o jsonpath='{.metadata.uid}')"
NETWORK_POLICY_EVIDENCE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/forge-network-policy.XXXXXX")"
export ALLOWED_SOURCE_POD ALLOWED_SOURCE_POD_UID DENIED_SOURCE_POD DENIED_SOURCE_POD_UID
export DENIED_SOURCE_CONTROL_POD DENIED_SOURCE_CONTROL_POD_UID TARGET_POD TARGET_POD_UID NETWORK_POLICY_EVIDENCE_DIR
printf 'review allowed=%s/%s denied=%s/%s control=%s/%s target=%s/%s\n' "$ALLOWED_SOURCE_POD" "$ALLOWED_SOURCE_POD_UID" "$DENIED_SOURCE_POD" "$DENIED_SOURCE_POD_UID" "$DENIED_SOURCE_CONTROL_POD" "$DENIED_SOURCE_CONTROL_POD_UID" "$TARGET_POD" "$TARGET_POD_UID"
NETWORK_POLICY_PROBE_APPROVED=1 make cni-probe

Approval is valid only after reviewing the printed names and immutable UIDs. make cni-probe invokes scripts/probe-network-policy.sh, which first runs scripts/verify-kind-network-policy.sh. That verifier requires the pinned Kind version, exact kind-$CLUSTER_NAME context, expected kindnet image, one ready kindnet agent per node, and ready nodes. The probe then vetoes non-local or already Ambient-labelled namespaces and refuses ambiguous, unready, wrongly owned, or changed Pods.

The experiment resolves the exact generator Pod IP. It proves control-plane-to-generator and evidence-to-broker reachability, attempts evidence-to-generator directly, and accepts only the script’s timeout status 42 as the denied result. Connection success, refusal, DNS failure, or any other status is inconclusive and fails the run. Both allowed controls run again afterward, and all four Pod identities are revalidated.

Do not claim a baseline merely because the command was printed or because four NetworkPolicy objects exist. A successful run creates new identities.txt, network-policies.yaml, positive-controls.txt, negative-stderr.txt, and kindnet-status.txt files in the pre-existing absolute evidence directory. Seal those artifacts with the source revision, context, time, and run ID. If the command does not complete successfully or any artifact is missing, Day 30 has produced no runtime NetworkPolicy evidence and mesh enrollment must stop.

Why the baseline still needs a mesh

The baseline’s gap is not “Kubernetes has no networking.” It is that Service routing and NetworkPolicy solve different layers from workload-authenticated encrypted transport and application-aware authorization.

NeedBaseline ownerWhat remains unprovedDecision rule
Stable destinationKubernetes Service and EndpointSliceCaller identity, encryption, HTTP intentKeep Services; do not ask a mesh to replace discovery
Reachability segmentationEnforcing NetworkPolicy implementationCryptographic workload identity and mutual authenticationKeep NetworkPolicy as defense in depth; fail the migration if denied-path behavior weakens
Workload nameKubernetes ServiceAccount referencePeer presented that identity on this connectionAdd a mesh only when identity evidence is required
Application policyForge application authorizationUniform transport policy or waypoint-enforced HTTP method/path rulesKeep domain authorization in the app; use mesh policy only for transport and named traffic boundaries

Istio ambient mode uses a per-node Layer 4 proxy called ztunnel for secure workload connectivity; an optional waypoint adds Layer 7 processing without adding sidecars to application Pods (Istio ambient overview). That makes a mesh complementary to, not a replacement for, Services, NetworkPolicy, or application authorization.

Before and after, side by side

Before this lesson, the team had manifests but no falsifiable traffic contract, so a later mesh change could be blamed or praised without comparison. Afterward, one successful sealed run connects the same customer request to Services, endpoints, identities, policies, two positive controls, and one exact denied timeout.

Key takeaways

A mesh added without before-state evidence can hide a regression behind new green objects. Adding one is justified only by a measured gap, not by a component checklist.

  • Freeze the customer path, Service endpoints, ServiceAccounts, and NetworkPolicy behavior before migration.
  • A Service stabilizes changing Pod backends; NetworkPolicy constrains reachability when the network implementation enforces it.
  • Static manifests do not prove enforcement; only the checked-in pre-mesh probe can seal this lesson’s allowed and denied runtime evidence.
  • Ambient ztunnel and an optional waypoint add transport identity, encryption, telemetry, and policy layers; they do not replace application authorization.
  • Reject enrollment when the approved path, denied path, or source-to-runtime mapping lacks fresh evidence.

Checklist

The baseline is complete only when another operator can reproduce both outcomes from the same revision.

  • [ ] Five Services select ready same-named workloads on port 8080
  • [ ] Five Deployments use five same-named ServiceAccounts
  • [ ] Pinned Kind/kindnet substrate and all four NetworkPolicies are recorded
  • [ ] Both positive controls pass before and after evidence-to-generator returns exact timeout status 42
  • [ ] Five new probe artifacts, revision, context, four Pod identities, time, and run ID are sealed