Skip to content

feat(cel/network): serviceRef/serviceSelector/host neighbor resolution (stacked on #90) - #93

Open
entlein wants to merge 5 commits into
rebase/celnetworkfrom
feat/celnetwork-serviceref
Open

feat(cel/network): serviceRef/serviceSelector/host neighbor resolution (stacked on #90)#93
entlein wants to merge 5 commits into
rebase/celnetworkfrom
feat/celnetwork-serviceref

Conversation

@entlein

@entlein entlein commented Aug 23, 2026

Copy link
Copy Markdown

What

Resolves serviceRef / serviceSelector / host-entity network neighbors to concrete IPs at projection time, so a ContainerProfile can allowlist cluster-infra egress/ingress by Service name or the host entity instead of a broad ipAddresses serviceCIDR — which allowlists every ClusterIP on the listed ports and blinds R0011/R0012 to lateral movement. RCA + design: #92.

Stacked on #90 (upstream kubescape#902 peer-selectors) — base branch rebase/celnetwork. Pairs with the storage schema PR k8sstormcenter/storage#42 (NetworkNeighbor.{ServiceRef*, ServiceSelector, Entity}).

Changes

pkg/networkpeer (new, 17 tests):

  • resolve.goserviceRef/serviceSelector/host(IP,port,proto) tuples; empty selector fails closed (never a cluster-wide match-all).
  • expand.go — storage NetworkNeighbor → selector-free ipAddresses neighbors (ExpandServiceNeighbors) + WithResolvedServiceNeighbors CP wrapper; fails closed on MatchExpressions / empty matchLabels; never mutates the input.
  • lister.goInformerLister over Service/EndpointSlice/Node listers; ClusterIP ∪ endpoints; host = the agent's own node InternalIP + CNI gateway (carry-correct).

Wiring: ContainerProfileCache.SetServiceLister + WithResolvedServiceNeighbors before both Apply() projection sites; cmd/main.go builds the informer factory (bounded WaitForCacheSync) and installs the lister. nil lister = no-op, so the existing suite is unaffected.

go.mod replaces kubescape/storage with k8sstormcenter/storage@<schema-sha>; fork CI overrides via STORAGE_REF.

Review-driven hardening (fable pass, see #92)

Fixed before opening: fail-open empty/MatchExpressions selectors, HostIPs scoped to the local node, gatewayIP carry, bounded WaitForCacheSync. Storage#42 carries the paired IP-collapse data-loss fix.

Known follow-ups (tracked in #92)

Tests

pkg/networkpeer 17 cases green; containerprofilecache suite unaffected; binary compiles. Component test runs via the fork-ci harness against storage#42's image.

entlein and others added 5 commits August 23, 2026 16:15
…to IPs

Stacked on #90 (upstream kubescape#902 peer-selectors). A ContainerProfile can now
allowlist cluster-infra egress/ingress by Service name or the "host" entity
instead of a broad ipAddresses serviceCIDR (which allowlists every ClusterIP
on the listed ports and blinds R0011/R0012 to lateral movement). See #92.

pkg/networkpeer:
- resolve.go: serviceRef/serviceSelector/host -> (IP,port,proto) tuples; empty
  selector fails closed (never a cluster-wide match-all).
- expand.go: storage NetworkNeighbor -> selector-free ipAddresses neighbors
  (ExpandServiceNeighbors) + WithResolvedServiceNeighbors CP wrapper; fails
  closed on MatchExpressions / empty matchLabels.
- lister.go: InformerLister over Service/EndpointSlice/Node listers; ClusterIP
  + endpoints; "host" = local node InternalIP + CNI gateway (carry-correct),
  scoped to the agent's own node.

Wiring: ContainerProfileCache.SetServiceLister + WithResolvedServiceNeighbors
before both Apply() projection sites; cmd/main.go builds the informer factory
(bounded WaitForCacheSync) and installs the lister. nil lister = no-op, so
every existing test is unaffected.

go.mod replaces kubescape/storage with k8sstormcenter/storage@6f3a2ee6 (the
NetworkNeighbor schema branch); CI overrides this with STORAGE_REF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
Realistic + minimal: no toy target manifests — the peers are the cluster's
own infra Services. A client whose ContainerProfile allowlists egress by
Service NAME (serviceRef default/kubernetes) must NOT fire R0011 for that
egress, while egress to a real UNLISTED Service (kube-dns) MUST still fire
R0011. That contrast is the point of serviceRef over a broad serviceCIDR
entry: narrow + portable, without blinding R0011 to lateral movement (#92).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
Phase 1 passed in CI (serviceRef default/kubernetes allowlisted the apiserver
egress, 0 R0011). Phase 2 failed only because kube-dns:53 is DNS — node-agent
tracks it via dnsmanager (R0005), never R0011, and HTTP-to-:53 times out. Use
the kubescape/storage Service (443, non-DNS, always installed by the CT) as the
unlisted peer instead — the exact case the flux RCA cared about.
…rigger)

curl to kubescape/storage:443 succeeded but cluster-internal ClusterIP egress
isn't R0011-flagged in this suite (node-agent behavior, orthogonal to the
feature; serviceRef still did NOT allow storage — its IP != apiserver's). The
narrowness assertion needs the suite's proven trigger: raw-IP egress to
1.1.1.1:80 (mirrors Test_28c), also the faithful flux analog (R0011 fired for
the external github egress the named-service allowlist didn't cover). Poll with
Eventually for robustness.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant