Skip to content

feat(network): serviceRef/serviceSelector/entity selectors on NetworkNeighbor - #42

Closed
entlein wants to merge 4 commits into
mainfrom
feat/network-service-selectors
Closed

entlein wants to merge 4 commits into
mainfrom
feat/network-service-selectors

Conversation

@entlein

@entlein entlein commented Aug 23, 2026

Copy link
Copy Markdown

What

Adds Kubernetes-native peer selectors to NetworkNeighbor so a ContainerProfile can allowlist cluster-infrastructure egress/ingress by name (portable, resolved per-cluster) and narrowly, instead of a broad ipAddresses serviceCIDR entry.

New fields (protobuf 10–13, all optional/omitempty):

  • ServiceRefNamespace + ServiceRefName — reference one Service; resolves to its ClusterIP(s) + backing endpoint IPs.
  • ServiceSelector (*LabelSelector) — select Services by label.
  • Entity ("host") — reserved identity no Service can represent (node InternalIP + CNI gateway: kubelet/health probes, node-sourced/masqueraded traffic).

Why

Enforcing node-agent (the SOFIA build) matches an ipAddresses entry as address + port, so the only way to suppress cluster-internal R0011/R0012 today is ipAddresses: [<serviceCIDR>] — which allowlists egress to every ClusterIP on the listed ports and blinds R0011 to lateral movement (measured: one 10.43.0.0/16:80,443 entry hid egress to 18 in-cluster Services incl. the storage API, argocd-server, metrics-server, apiserver). A Service reference / host entity is the narrow, portable expression. Full RCA + design: k8sstormcenter/node-agent#92.

This is the storage half; the node-agent resolver + projection wiring + component test land on k8sstormcenter/node-agent:feat/celnetwork-serviceref (stacked on kubescape#90).

Codegen note

No protoc / go-to-protobuf on the toolchain, so the generated artifacts were hand-edited, each new field mirroring an existing one (DNS string / PodSelector *LabelSelector): generated.pb.go (Marshal/Size/Unmarshal), generated.proto, deepcopy (internal + v1beta1), conversion, zz_generated.openapi.go (SSA pruning), applyconfiguration. Field layout is identical to what codegen would emit; re-running hack/update-codegen.sh on a machine with the toolchain should be a no-op diff.

Tests

Verified without the codegen toolchain via protobuf round-trip (running tests needs no generator):

  • TestNetworkNeighbor_ServiceSelectors_ProtobufRoundtrip — all four fields survive Marshal → Unmarshal.
  • TestNetworkNeighbor_ServiceSelectors_EmptyOmitted — empty fields encode identically to absent (zero wire change for existing profiles).
  • Existing IPAddresses round-trip + softwarecomposition conversion/deepcopy tests stay green.
go test ./pkg/apis/softwarecomposition/...   # ok

Compatibility

Additive, optional, omitempty — existing profiles serialize byte-identically. No behavior change until node-agent consumes the fields.

🤖 Generated with Claude Code

Kubernetes-native peer selectors on NetworkNeighbor so a ContainerProfile
can allowlist cluster-infrastructure egress/ingress portably (by name,
resolved per-cluster) and narrowly, instead of a broad ipAddresses CIDR
over the whole service network — which allowlists every ClusterIP on the
listed ports and blinds R0011/R0012 to lateral movement.
See k8sstormcenter/node-agent#92.

New fields (protobuf 10-13, all optional/omitempty):
- ServiceRefNamespace + ServiceRefName: reference one Service; resolved to
  its ClusterIP(s) + backing endpoint IPs.
- ServiceSelector (*LabelSelector): select Services by label.
- Entity ("host"): reserved identity no Service can represent — node
  InternalIP + CNI gateway (kubelet/health probes, node-sourced traffic).

Regenerated by hand (no protoc/go-to-protobuf on the toolchain), each field
mirroring an existing one (DNS string / PodSelector *LabelSelector):
generated.pb.go Marshal/Size/Unmarshal, generated.proto, deepcopy (internal
+ v1beta1), conversion, openapi schema (SSA pruning), applyconfiguration.

Verified without the codegen toolchain via protobuf round-trip tests:
TestNetworkNeighbor_ServiceSelectors_ProtobufRoundtrip (fields survive
Marshal->Unmarshal) and _EmptyOmitted (empty fields encode identically to
absent — zero wire change for existing profiles). Existing IPAddresses
round-trip + softwarecomposition conversion/deepcopy tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
Verified the hand-regenerated protobuf against the real generator
(build/protoc.Dockerfile -> go-to-protobuf) and corrected two deviations
so a future `hack/update-codegen.sh` produces a zero diff for
NetworkNeighbor:

- scalar fields (ServiceRefNamespace/Name, Entity) are proto2
  optional/non-nullable, so the generator marshals them UNCONDITIONALLY
  (like the existing dns/ipAddress fields), not guarded by len()>0 as the
  first hand-edit had them. Fixed Marshal + Size.
- added the four new fields to NetworkNeighbor.String() (the generator
  emits them; the first hand-edit missed the stringer).

Confirmed by re-running go-to-protobuf: 0 diff in the NetworkNeighbor
Marshal/Size/Unmarshal/String; the only regen delta left is pre-existing
version drift in unrelated messages (IgnoreRule, others), excluded here.

The EmptyOmitted test asserted empty new fields encode identically to
absent — no longer true under unconditional encoding — replaced with
NewFields_Absent (unset fields round-trip empty, existing fields intact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
@entlein

entlein commented Aug 23, 2026

Copy link
Copy Markdown
Author

Verified the hand-regenerated protobuf against the actual generator (build/protoc.Dockerfile → go-to-protobuf, per README) rather than trusting the hand-edit:

  • Ran the generator, diffed → caught two deviations in the first pass: scalar fields were guarded by len()>0 (the generator marshals proto2 optional scalars unconditionally, like dns/ipAddress), and NetworkNeighbor.String() was missing the four fields.
  • Fixed both, re-ran the generator → 0 diff across NetworkNeighbor Marshal/Size/Unmarshal/String. The only regen delta remaining is pre-existing version drift in unrelated messages (IgnoreRule etc., from go-to-protobuf@latest vs the version that built v0.0.303), deliberately excluded.

So hack/update-codegen.sh on a toolchain box will produce a clean diff for this change.

entlein and others added 2 commits August 23, 2026 14:31
…fields

Additive: the consolidated ContainerProfileSpec JSON now carries the four
empty serviceRef/serviceSelector/entity fields. TestConsolidateContainerProfileGolden
regenerated with UPDATE_GOLDEN=1; diff is only the new empty fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
The PreSave IP-collapse groups neighbors by (Type,DNS,selectors) and
rebuilds over-threshold groups into CIDR entries that re-emit only
IP/DNS/selector fields — silently dropping ServiceRef/ServiceSelector/
Entity on any authored serviceRef neighbor swept into a collapsing group.
Hold serviceRef/serviceSelector/entity neighbors out of the collapse
entirely (they carry no aggregatable IPs). Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8UV2B7b6dpDJQci31aC6c
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