cel(network): alert on egress/ingress ports, not only addresses (#80) - #85
Open
ConstanzeTU wants to merge 75 commits into
Open
cel(network): alert on egress/ingress ports, not only addresses (#80)#85ConstanzeTU wants to merge 75 commits into
ConstanzeTU wants to merge 75 commits into
Conversation
… -> ContainerProfile ConvertUserProfilesToContainerProfile produces the single user-defined ContainerProfile equivalent to a legacy user-authored ApplicationProfile + NetworkNeighborhood pair, reusing the existing projectUserProfiles merge onto an empty base. The differential oracle pins the migration contract at the enforcement level: for representative user-defined shapes (opens/exec argv wildcards, HTTP endpoints, egress/ingress + LabelSelector) the ProjectedContainerProfile from the legacy AP+NN overlay path equals the one from using the converted ContainerProfile as the base with no overlay. Behaviour-preserving by construction. Signed-off-by: entlein <einentlein@gmail.com>
…e authoritative base When the user-defined-profile pod label names a ContainerProfile carrying managed-by: User, the cache now uses it directly as the container's base profile (the migrated "new way"), instead of overlaying a legacy ApplicationProfile + NetworkNeighborhood pair. It falls back to the legacy AP+NN pair when no such CP exists, which still fires the existing deprecation signal. - add-time (tryPopulateEntry) and refresh (reconciler) both prefer the user CP, gated on the managed-by: User annotation so a learned CP at the same name is never mistaken for a user-defined one - UserCPRef/UserCPRV bookkeeping mirrors the legacy UserAPRV/UserNNRV RV tracking so the reconciler re-fetches and rebuilds only when the user CP changes Signed-off-by: entlein <einentlein@gmail.com>
Test_28 now creates one ContainerProfile (managed-by: User) carrying the merged exec/syscall + egress/selector surfaces, instead of a separate user-authored ApplicationProfile + NetworkNeighborhood pair — exercising the migrated read-path end to end. Assertions unchanged. Signed-off-by: entlein <einentlein@gmail.com>
…verlay The new path (converted CP as base, no overlay) is ~35% faster and allocates ~27% less than the legacy AP+NN overlay per projection, since it skips the two-object merge. Signed-off-by: entlein <einentlein@gmail.com>
Test_27 (opens R0002, both the regex and the curl-wildcard profile sites), Test_32 (R0040 argv wildcards), and Test_33 (opens wildcard anchoring — the previously-fixed one) now each create a single user-defined ContainerProfile (managed-by: User) carrying the merged exec/open/syscall (+ egress/selector for 32) surfaces, replacing the legacy ApplicationProfile + NetworkNeighborhood pair. Assertions unchanged. Test_28 was ported in an earlier commit. Signed-off-by: entlein <einentlein@gmail.com>
containsDynamicSegment recognised only the one-segment DynamicIdentifier
('⋯'), so a path-surface opens entry bearing the zero-or-more
WildcardIdentifier ('*') — e.g. '/etc/ssl/*' — was routed to Values as if it
were a literal. was_path_opened tolerates this (Values and Patterns are both
matched via CompareDynamic), but it is wrong for any consumer that treats
Values as exact membership, and it drops '*'-only entries a rule needs when
spec.All is false and no prefix/suffix matcher retains them. Recognise both
wildcard markers. Regression test pins '/etc/ssl/*' -> Patterns.
Pre-existing in upstream (identical containsDynamicSegment).
Signed-off-by: Entlein <eineintlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
Test_33's anchoring subtests assert R0002 alerts, but the rule's file-access monitoring is opt-in (monitored prefixes incl. /etc/). Test_33 never applied r0002-files-access-enabled.yaml (Test_27 does via enableR0002ForTest), so R0002 never evaluated the opens and every 'expect alert' case silently passed as a no-alert — invisible because Test_33 had never run in CI. Enable it like Test_27. Verified on a live cluster: /etc/ssl/* correctly alerts on the bare parent /etc/ssl and stays silent on the child /etc/ssl/openssl.cnf. Signed-off-by: entlein <einentlein@gmail.com>
…tations A user-authored profile is authoritative and complete by definition — it should not carry the learning-lifecycle status/completion markers, nor a managed-by annotation. The pod's user-defined-profile label is what declares it user-authored (a signature, added by the signing tooling, is the integrity marker). - read-path/reconciler no longer gate on managed-by: the label-referenced CP is used as authoritative, and the entry State is forced to Completed+Full so the rule engine enforces it despite the absent completion annotation - the converter emits clean CPs (name + namespace + spec only) - fake client models an absent overlay-name fetch (drives the legacy fallback) Signed-off-by: entlein <einentlein@gmail.com>
…mple - Test_28 now loads its user-defined ContainerProfile from a real yaml (resources/containerprofile-user-defined-network.yaml) via a loader helper, so the fixture doubles as the copy-pasteable "how to author a user-defined profile" example. - The example — and all the test CPs — carry only name + spec: the nonsensical learning-lifecycle annotations (status/completion) and the managed-by marker are dropped (node-agent now treats a label-referenced CP as authoritative and forces the enforce-state itself). Signed-off-by: entlein <einentlein@gmail.com>
Migrate the network-wildcards NetworkNeighborhood fixtures into their user-authored ContainerProfile form so users can copy-paste them to author user-defined-profile allow-lists: - per-container spec.egress/ingress (NN's spec.containers[] collapses, one CP document per container; fixture 20 splits into two) - no lifecycle annotations — name only (namespace injected by tooling), matching the clean user-managed CP contract - teaching comments preserved verbatim from the NN fixtures Adds 00-fusioncore-homoglyph-attack.yaml: a pinned single-vendor allow-list and the look-alike (homoglyph) domains its exact-match dnsNames compare rejects (each fires R0005). All 21 documents strict-parse against v1beta1 ContainerProfile and carry zero annotations (verified). Signed-off-by: entlein <einentlein@gmail.com>
The component tests build their user-defined profiles inline as ContainerProfiles (Test_27/32/33) or load a CP yaml (Test_28); the legacy ApplicationProfile / NetworkNeighborhood fixtures they were derived from are no longer referenced by any Go test. Migrate the two CT-relevant ones to their user-authored CP form and delete the orphan: - exec-arg-wildcards-profile.yaml (AP curl-32-overlay) -> containerprofile-exec-arg-wildcards.yaml. CP form mirrors Test_32's inline CP exactly (same execs incl. busybox-symlink + literal-* entries, same syscalls, matchLabels app: curl-32). Demonstrates the exec-arg wildcard grammar for authoring. - known-network-neighborhood.yaml (NN fusioncore-network) -> containerprofile-fusioncore-network.yaml. Clean user-managed CP: name only, no managed-by / status / completion annotations. - user-profile.yaml: deleted. Zero references anywhere; its nginx/server exec surface matches no current test or deployment. Both new documents strict-parse against v1beta1 ContainerProfile and carry zero annotations (verified). CT compiles unchanged (go vet -tags component); no Go test referenced the deleted files. Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
… decommission them -step 1 of many -- I understand that this has a long tail of decommissions Signed-off-by: entlein <einentlein@gmail.com>
…storage etc Signed-off-by: entlein <einentlein@gmail.com>
…icontainer labels, tests not reviewed yet Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
Co-authored-by: Matthias Bertschy <matthias.bertschy@gmail.com> Signed-off-by: Duck <70207455+entlein@users.noreply.github.com>
Signed-off-by: entlein <einentlein@gmail.com>
Apply had assertion-style unit tests but no frozen contract for its full projected output. Add TestApply_Golden: a corpus (execs incl. literal-* and ellipsis args, opens incl. trailing-* and dynamic segments, syscalls, capabilities, endpoints, ingress/egress with CIDR/*-sentinel/DNS, call stacks) projected via the real CompileSpec, frozen to goldens (regenerate with UPDATE_GOLDEN=1). Plus idempotency and spec-hash-stability invariants. This is a characterization/regression freeze, not a two-path differential -- the legacy AP/NN projection was removed, so there is no second implementation to diff. Signed-off-by: entlein <einentlein@gmail.com>
The projection golden corpus set the deprecated DNS/IPAddress singulars (to distinct values) alongside the modern lists. Drop them: the corpus now uses DNSNames/IPAddresses only (still covering DNS name, literal IP, CIDR and the * sentinel). Goldens regenerated from the CP-native corpus. Signed-off-by: entlein <einentlein@gmail.com>
…ace) The rulemanager CEL libraries already read ContainerProfile data but still carried AP/NN naming. Rename applicationprofile -> containerprofile and networkneighborhood -> containerprofilenetwork (dirs via git mv, packages, types, constructors, LibraryName), and unify the CEL function namespace: ap.* and nn.* -> cp.* (cp.was_executed, cp.is_domain_in_egress, ...). Update cel.go registration, the bundled default-rules.yaml, and the network-wildcards test rules to the cp.* namespace so rules compile against the renamed engine. Rule content mirrors the rulelibrary migrate/sbob rename. Signed-off-by: entlein <einentlein@gmail.com>
The CP-form fixtures lived in a parallel network-wildcards-cp/ dir alongside the now-dead network-wildcards/. 'cp' is an implementation detail, not part of what the fixtures are. Remove the dead dir, move the CP fixtures to network-wildcards/ (update the nnlint/fixtures test references), and drop the explanatory README. Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
Resolve the Test_35 collision: upstream added Test_35_ExecTTYFieldTest at the same location as our Test_35_MultiContainerPerContainerBinding. Keep both; renumber ours to Test_36_MultiContainerPerContainerBinding.
Add cluster-free unit coverage for the migration's rule-side surface: - cp.* HTTP evaluators (http.go, was 0%/no file): endpoint/host match, method and prefix/suffix variants, empty-CP, nil-cache error. - isExecInPodSpec + the wasExecuted/wasExecutedWithArgs podspec-exempt fall-through. - containerprofilenetwork matchers: no-match / wildcard-domain / empty-neighborhood. - Declarations smoke+drift guard: compile every cp.* overload, assert the decl set. - profilehelper GetProjectedContainerProfile/GetPodSpec/GetContainerName. - HasFinalApplicationProfile enforce-vs-learn gate (Completed+Full -> enforce). containerprofile 37->69%, containerprofilenetwork 75->80%, profilehelper 95%, HasFinalApplicationProfile 100%. Test-only; no production changes. Signed-off-by: entlein <einentlein@gmail.com>
… racy learning) Test_20/21 hung to the 20-minute global panic in WaitForContainerProfileCompletion*: Test_20's blacklist wait can never succeed (the old completed profile stays matched and blacklisted after the deployment restart), and Test_21's multi-container profile never reached 'completed'. Both raced the natural learn -> daemonset-restart -> deployment-restart -> re-learn cycle. Rewrite to enforce an AUTHORED user-defined ContainerProfile, updated in place: an action not in the profile alerts; the same action, once added, does not. Determinism comes from a positive reload gate -- the update that adds the subject also removes a canary, so the canary starts alerting the moment node-agent reloads the revision (alert-appears signal), confirming the reload before the negative assertion. Single-container workloads; bounded pollers that dump ContainerProfile status on timeout (never a 20m panic); Eventually-style polling on the real alert condition instead of fixed sleeps. Also fixes a latent bug: the old 'no new alert' check filtered on a process_name label that alerts never carry, so it always matched zero and passed regardless; the rewrite keys on comm (process) and rule_id (network). Verified: both pass twice on a live rig (rc5l), 0 'database is locked'. This now tests profile ENFORCEMENT (authored partial->full), not natural learning. Signed-off-by: entlein <einentlein@gmail.com>
… sleep Test_16 slept a hard-coded 175s after the restart-inducing 'service nginx stop' before exec'ing the ls violation (the WaitForContainerProfileCompletion guard was commented out), then asserted the 'Unexpected process launched' alert. On a quiet system that had ~110s of slack; under full-CT concurrency the storage single-writer SQLite contention (database is locked) defers the merged profile's completion-status write past the fixed window, so ls ran against a non-enforcing profile and no alert fired -> the assertion failed (observed as Test_16 red in a full CT with 12 lock events). Replace the fixed sleep with a bounded poll (5m, fail-fast, dumps ContainerProfiles on timeout) for the MERGED ContainerProfile reaching 'completed' after the restart -- keyed on the merged profile only (name has no -<32 hex> suffix), not WaitForContainerProfileCompletion which requires ALL matching profiles completed and would hang on the transient per-instance profiles the restart spawns. Then exec ls and poll (Eventually) for the alert. Validated 3/3 on a live rig; runtime ~241s -> ~90s since it no longer over-sleeps. Signed-off-by: entlein <einentlein@gmail.com>
Signed-off-by: entlein <einentlein@gmail.com>
…known limitation The ephemeral debug container is adopted (section selected, monitor started, tracers attached) but no events from it ever reach the rule engine - verified in CI and interactively on a fresh cluster (zero exec/syscall/capability events while the container demonstrably ran to completion). The ephemeralContainers profile-selection contract stays covered by the cache unit tests; event delivery for ephemeral containers is a container-watcher/ tracer scope issue independent of profile projection, tracked as follow-up. Log the counts and self-signal when tracing starts working so the assertions can be promoted back.
…ance Init-phase enforcement is proven (interactive validation: adoption 3s after deploy, init R0001 fires), but CI runners intermittently take longer than 75s to complete the first adoption; give the init container a 100s runway.
…ner end-of-life Failing tests for the teardown race behind issue #79 (init/ephemeral exec loss): events emitted during a container's life are dropped when processed after the container's removal. - TestProcessEvent_DeliversEventForJustRemovedContainer[_NoPriorEvent]: EventHandlerFactory.ProcessEvent silently drops events whose container has left the live collection (evidence: run 31846699597, Test_48, init container terminal exec at 22:44:26, remove processed 22:44:26, zero R0001; ladder run1 total loss). - TestProjectedProfile_SurvivesContainerRemovalGrace: the projected profile is deleted immediately on the remove callback, so in-flight events lose profile resolution and ProfileDependency=Required rules suppress as profile_incomplete. Both tests fail on current code by design; the fix must provide a removal grace window covering the event pipeline delay.
… at container end-of-life
An event emitted during a container's life can be processed after the
container's removal: the ordered event queue (50ms collection tick +
batching) and the worker pool delay evaluation past teardown. For a
container whose final process performs the exec and exits immediately
(init container with a terminal exec, ephemeral debug container), the
alert-carrying exec event loses this race and is dropped.
Observed failure: run 31846699597, Test_48_MultiSubtypeGroupedProfileDocument,
init container setup (sh -c "sleep 75; /usr/bin/id"): remove processed
22:44:26, terminal exec evaluated afterwards, zero R0001 while 98 R0003
fired during the container's life (assertion: 'id is not in the setup
section (initContainers)', component_test.go:3488). Same-shaped loss for
the ephemeral leg (R0001(debug,id)=0, remove 22:46:15).
Root cause, two drop points on the remove path:
1. EventHandlerFactory.ProcessEvent resolved container info only from the
live container collection plus a lazily-populated cache, silently
dropping events for just-removed containers that never had a prior
event processed.
2. ContainerProfileCache deleted the projected-profile entry immediately
(async) on the remove callback, so rules with ProfileDependency=Required
suppressed in-flight events as profile_incomplete.
Fix: keep container info and the projected profile resolvable for a
10s grace after removal, then evict:
- the factory now receives container lifecycle callbacks, warms its
lookup cache on add, and defers eviction by the grace period;
- the profile cache defers deleteContainer by the grace period and the
reconciler's terminated-eviction honors the same grace (mark on first
Terminated observation, evict on a later tick), so a reconciler tick
landing inside the window cannot reintroduce the race.
Tests: TestProcessEvent_DeliversEventForJustRemovedContainer{,_NoPriorEvent},
TestProcessEvent_RemovedContainerEvictedAfterGrace,
TestProjectedProfile_{SurvivesContainerRemovalGrace,EvictedAfterRemovalGrace},
TestReconciler_HonorsRemovalGraceForTerminatedContainer (all red on the
pre-fix code); TestReconcilerEvictsTerminatedContainer,
TestInitContainerEvictionViaRemoveEvent,
TestMissedRemoveEventEvictedByReconciler updated to the graced contract.
Regression: go test ./pkg/containerwatcher/... ./pkg/objectcache/...
./pkg/rulemanager/... passes (tracers field tests skipped locally: they
require the tracers.tar gadget bundle, unavailable off-CI); -race clean
on both touched packages.
Deterministic rig-side measurement for acceptance tests T4/T5: N repeated init runs (terminal forbidden exec after a configurable runway) and N ephemeral-container runs (terminal whoami+id), each asserting R0001 delivery via node-agent logs. Exits non-zero unless both legs are N/N.
…ntainers Failing tests for the ephemeral total-loss leg of issue #79: the reconciler classifies a container that is absent from all published status lists as reaped, but a just-attached ephemeral container is exactly that (kubelet publishes ephemeralContainerStatuses seconds after the attach), and an init container whose entry carries an empty PodUID hits the same branch while its status has no ContainerID yet. The entry is evicted, nothing re-adds it, and every ProfileDependency=Required rule is suppressed for the container's entire life. Live-cluster evidence: ephemeral container adopted at +1s, reconciler tick 3s later (entries_before=2 entries_after=1), zero alerts of any class over its 75s life while the same pod alerted for other containers. Contract pinned: a container still declared in the pod SPEC without a published status is not reaped; absent from both spec and status is; a termination mark resets when the container is observed alive again.
The reconciler evicted any cache entry whose container was absent from all published status lists once any statuses existed. A just-attached ephemeral container is exactly that: the pod spec already declares it while kubelet publishes its ephemeralContainerStatuses entry seconds later. The freshly-adopted profile entry was evicted on the next tick, nothing re-added it, and every ProfileDependency=Required rule was suppressed for the container's entire life — zero alerts of any class (issue #79 T5, ephemeral 0/N). Init-container entries created before the pod reached the k8s cache (empty PodUID, status ContainerID not yet published) hit the same branch, contributing to the init intermittency. Observed: ephemeral container adopted +1s after attach; reconciler tick 3s later logged entries_before=2 entries_after=1; zero alerts over the container's 75s life while the same pod alerted for its other containers. Fix: absence from published statuses only counts as reaped when the container is also absent from the pod SPEC (containers, initContainers, ephemeralContainers). Additionally, the termination mark introduced with the removal grace now resets when a marked container is observed alive again, so a later genuine termination gets a full grace window. Tests (red pre-fix): TestReconciler_KeepsEphemeralContainerAwaitingStatus, TestReconciler_KeepsInitContainerAwaitingStatusWithEmptyPodUID, TestReconciler_TerminationMarkResetsWhenContainerReappears; negative contract TestReconciler_EvictsContainerRemovedFromSpecAndStatus. Regression: go test -race ./pkg/objectcache/... ./pkg/containerwatcher/v2/ ./pkg/rulemanager/... passes.
…ile cache The ContainerProfileCache reconciler classified any cache entry whose container was absent from the pod's published status lists as reaped and evicted it (reconciler.go isContainerTerminated). But kubelet publishes the status groups incrementally: a just-attached ephemeral container has no ephemeralContainerStatuses entry for several seconds while it is already running and traced, and an entry added before the pod reached the k8s cache carries an empty PodUID, which made the (Name, PodUID) pre-running fallback unreachable for init containers. Eviction is permanent (no re-add path exists), so every ProfileDependency=Required rule (R0001/R0003/R0004) was silently suppressed for the container's whole life: total alert loss for ephemeral containers, intermittent exec-alert loss for init containers (issue #79, CI run 31846699597). Evidence (live rig, issue #79): ephemeral container adopted at +1s, evicted at the next reconciler tick +3s (entries 2->1), exec events verifiably reached ReportEnrichedEvent at +75s and were dropped by the Required-profile gate; the exec gadget's mntns filter map contained the container's mntns the whole time (kernel/tracer exonerated). Fix: - treat absence from the status lists as reaped only when the pod SPEC does not name the container either; a status entry with the same name under a different non-empty ContainerID still evicts (replaced instance) - allow the pre-running (Name, PodUID) fallback to match when the stored PodUID is empty - backfill PodUID from the container runtime metadata when the pod is not yet in the k8s cache at entry-build time New tests fail on the pre-fix code and pass with the fix: TestReconcilerKeepsJustAttachedEphemeralContainer, TestReconcilerKeepsInitContainerWithEmptyStoredPodUID. Regression guards (both-ways green): eviction after published termination, gone from spec+status, replaced instance. Full objectcache, rulemanager and containerprofilemanager suites pass unchanged.
…, match containerName exactly, race-free readiness wait The ladder under-counted to 0/N while the node-agent logs showed 5/5 R0001 for both the init and the ephemeral container: it read only one DaemonSet pod's logs (the workload can land on any node), its grep could not match the alert JSON's containerName field, and its readiness wait raced pod creation.
The gadget rework removed IsResolvedFullPath from path.go while datasource_event.go still guards the fname fallback with it, leaving the tree uncompilable. Restore the helper unchanged; whether the guard is still needed under kernel-side full-path resolution can be decided separately.
Supersedes the IsResolvedFullPath restoration one commit back: upstream kubescape#889 retires the helper entirely and replaces the call-site guard with a plain empty-fpath fallback (the patched gadget resolves full paths kernel-side, so the fragment hazard no longer exists). Take upstream's GetFullPath body, drop the helper again together with its fork-only test, leaving path.go and normalize_path_test.go byte-identical to upstream main.
The backward dentry walk in get_path_str terminates at the mount-tree root of the file's vfsmount. runc >= 1.2 accesses procfs during container init and exec through a private detached mount created via fsopen(2)/fsmount(2); such a mount has no mountpoint (mnt_parent == mnt), so the walk correctly reaches its top with no /proc dentry to prepend and emits prefix-stripped paths such as /1/task/1/fd. The kernel's own d_path reports the same rootless string, so no userspace consumer can recover the prefix afterwards. Detect this termination case by checking the superblock magic of the final dentry: if it is PROC_SUPER_MAGIC the walk ended inside a procfs instance that is the top of its own mount chain, which cannot be the real global root, and the path is canonicalized by prepending proc/. Attached procfs mounts are unaffected because their walk continues through mnt_mountpoint before reaching this branch, and non-procfs detached mounts keep their previous behavior. Verified on kernel 6.1.167 (x86_64): fsopen/fsmount reproducer emits /proc/1/task/1/fd (was /1/task/1/fd), detached procfs root open emits /proc, detached tmpfs root open remains /, regular file paths unchanged, program accepted by the verifier. Refs #81
The 5-byte proc/ prepend decremented buf_off unguarded; a dentry chain longer than the half-buffer could wrap the offset and corrupt the emitted path (masked writes keep it memory-safe; result is garbage, not OOB). Unreachable for PATH_MAX-bounded paths - defense in depth only. On insufficient space the prepend is skipped and the unprefixed path kept.
…metic Proves the u32 wrap condition the guard defends against: an unguarded 5-byte prepend at buf_off < 5 wraps past the half-buffer (garbage path), while buf_off >= 5 lands exactly at buf_off-5 in bounds; the guard skips the prepend untouched below the threshold. Also pins the shipped header: guard present, MAX_PERCPU_BUFSIZE and buf_off initialization unchanged - so the arithmetic assumptions break loudly if the header drifts.
The scrambled-path assertions only ran against nginx in CI; the detached- procfs reproducer (bitnami container init) was never exercised there. Add the bitnami-redis learned leg with a /proc positive control so every CT run provokes and checks the runc procfs open class.
…longer ready budget The bitnami-redis leg timed out on pod readiness in CI - Docker Hub pulls on shared runners are rate-limited and the image is large. Use the public ECR mirror and double the readiness budget.
…ro-demo redis manifest Replace the hand-written fixture with the verbatim helm render of the redis distro deployment (bitnamicharts/redis 27.0.18, digest-pinned image - the tag was removed from the registries, which is what timed the previous leg out). The test applies every rendered document and waits on the statefulset pod and the learned profile completion directly.
Requiring /proc opens in the learned profile raced tracer attach against container start (runc-init opens are only learned when the tracer wins). Replace it: after the profile freezes, read /proc/1/cmdline in the container and require the R0002 alert - no alert means a silent gadget, a scrambled path would fail the path checks.
…t in the positive control The probe swallowed exec errors and matched only R0002; the exec itself (R0001, deterministic - cat is never in the learned profile) is equally valid pipeline evidence.
…ort_protocol_in_{egress,ingress}
Projects each neighbor's addresses grouped with its ports (AddrPortGroup) and matches IP+port+protocol within a single entry; empty/0/nil ports = any-port wildcard. Replaces the address-only degrade.
…ort violation Chart R0011 calls was_address_port_protocol_in_egress(dstAddr,dstPort,proto); Test_28 adds a subtest that curls the NN-allowed IP 162.0.217.171 on :443 (allowed only on TCP/80) and asserts R0011 fires.
ConstanzeTU
force-pushed
the
network-port-alerts
branch
from
August 15, 2026 19:51
0e505ea to
909969c
Compare
…ts stanza = any Fixture allowlists 9.9.9.9 on port 0 and 208.67.222.222 with no ports stanza; two subtests assert neither fires R0011 on any port. Verified on a live cluster.
…ard to loopback-only + internal CT leg
R0011 guard is_private_ip → startsWith('127.') so private-IP peers are evaluated; fixture allowlists cluster DNS (10.96.0.10, any port) and kube-api (10.96.0.1 TCP/443); Test_28 internal_port_violation_R0011 curls 10.96.0.1:80 (allowed only :443) and asserts R0011 fires.
ConstanzeTU
force-pushed
the
network-port-alerts
branch
from
August 16, 2026 18:47
cf1bbe8 to
6174a76
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Egress/ingress port alerting — issue #80
How it used to work (before this change)
was_address_port_protocol_in_{egress,ingress}validated the(port, protocol)arguments and then degraded to address-only —matchIPField(&cp.EgressAddresses, addr)(network.go: "port/protocol projection … out of scope … matchers degrade to address-only"). Consequences:(10.1.2.3, 80, TCP)non-match was not enforced — see chore: deprecate old profiletypes in favor of containerprofiles kubescape/node-agent#864 r3792018550.)!net.is_private_ip(dstAddr), excluding every private-IP peer.How it works after this change
was_address_port_protocol_in_{egress,ingress}matches IP + port + protocol per NetworkNeighbor entry, via a projectedAddrPortGroup{addrs, ports}(sharedobjectcache.ExtractAddrPorts, mirrored in the rule-cache mock). An allowlisted address on a non-allowlisted(port, protocol)fires R0011.port: 0, a nil port, or an omittedports:stanza = any port for that address (silent on any port).!net.is_private_ip(dstAddr)→!dstAddr.startsWith('127.')(loopback-only), so private-IP peers are evaluated. Infra traffic (cluster DNS) stays silent by allowlisting it in the profile.How the tests evidence the new behaviour
Unit —
port_protocol_test.go: per-entry CIDR+port (the fixture-19 contract), protocol case-insensitivity,port: 0/nil/empty-stanza wildcard, ingress symmetry.wildcard_test.go/network_test.go/integration_test.go— which previously pinned the address-only degrade — updated to assert the real port contract (a wrong port on an allowlisted address is now a non-match).Component (
Test_28, kind cluster, run 31965647879):port_violation_different_port_R0011162.0.217.171allowed:80, connect:443internal_port_violation_R001110.96.0.1allowed:443, connect:80Unexpected egress network communication to: 10.96.0.1:80 using TCPport_wildcard_zero_allows_anyport: 0entryport_wildcard_empty_stanza_allows_anyports:stanzaallowed_fusioncore_no_alert,mitm_coredns_poisoningR0011==0)Live A/B (kubescape#864 r3792018550): the identical scenario on the pre-feature image (
test-bff56fa, address-only) stays silent on a port mismatch; on the feature image it fires — with a1.1.1.1control confirming the engine was live.Run tally: 43 pass / 2 fail; the two (
Test_01,Test_22) are the known storage single-writer SQLite contention flake (database is locked), not this change — the internal-IP alert set is exactly{10.96.0.1:80}, no spurious internal alerts.