From 39d0c25548ef659e69f0c32ee5e2485d89f51fb8 Mon Sep 17 00:00:00 2001 From: dmitrii Date: Thu, 23 Jul 2026 14:19:44 +0200 Subject: [PATCH 1/3] Skip recording DNS noise for private IP literals with no pending port Private IP literal hostnames with no pending port (resolver bootstrap noise, network capability probing, etc.) flood the outbound-connections dashboard. Outbound domain blocking and SSRF detection stay fully unconditional either way - only the dashboard hit is skipped. --- .../collectors/DNSRecordCollector.java | 3 +- .../collectors/DNSRecordCollectorTest.java | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index d33c165c..16783623 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -12,6 +12,7 @@ import dev.aikido.agent_api.vulnerabilities.ssrf.SSRFException; import dev.aikido.agent_api.helpers.logging.LogManager; import dev.aikido.agent_api.helpers.logging.Logger; +import dev.aikido.agent_api.vulnerabilities.ssrf.IsPrivateIP; import dev.aikido.agent_api.vulnerabilities.ssrf.StoredSSRFDetector; import dev.aikido.agent_api.vulnerabilities.ssrf.StoredSSRFException; @@ -41,7 +42,7 @@ public static void report(String hostname, InetAddress[] inetAddresses) { for (int port : ports) { HostnamesStore.incrementHits(hostname, port); } - } else { + } else if (!IsPrivateIP.isPrivateIp(hostname)) { // We still need to report a hit to the hostname for outbound domain blocking HostnamesStore.incrementHits(hostname, 0); } diff --git a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java index c7cdd4b3..55558ad9 100644 --- a/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java +++ b/agent_api/src/test/java/collectors/DNSRecordCollectorTest.java @@ -203,6 +203,62 @@ public void testSSRFStillRunsWhenPendingPortIsZero() { assertEquals("Aikido Zen has blocked a server-side request forgery", exception.getMessage()); } + @Test + public void testPrivateIpLiteralWithNoPendingPortNotRecorded() { + Context.set(null); + DNSRecordCollector.report("10.20.11.143", new InetAddress[]{inetAddress2}); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + } + + @Test + public void testPrivateIpLiteralWithNoPendingPortNotRecordedButBlockedInLockdown() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, true, List.of(), true, true, List.of() + )); + Context.set(null); + assertThrows(BlockedOutboundException.class, () -> + DNSRecordCollector.report("10.20.11.143", new InetAddress[]{inetAddress2}) + ); + assertEquals(0, HostnamesStore.getHostnamesAsList().length); + } + + @Test + public void testPrivateIpLiteralWithPendingPortStillRecordedAndBlockedInLockdown() { + ServiceConfigStore.updateFromAPIResponse(new APIResponse( + true, null, 0L, null, null, null, true, List.of(), true, true, List.of() + )); + PendingHostnamesStore.add("10.20.11.143", 443); + Context.set(mock(ContextObject.class)); + + assertThrows(BlockedOutboundException.class, () -> + DNSRecordCollector.report("10.20.11.143", new InetAddress[]{inetAddress2}) + ); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals(443, entries[0].getPort()); + } + + @Test + public void testSsrfStillDetectedForPrivateIpLiteralWithPendingPort() { + ServiceConfigStore.updateBlocking(true); + PendingHostnamesStore.add("169.254.169.254", 80); + Context.set(new EmptySampleContextObject("http://169.254.169.254:80/latest/meta-data/")); + + Exception exception = assertThrows(SSRFException.class, () -> + DNSRecordCollector.report("169.254.169.254", new InetAddress[]{imdsAddress1}) + ); + assertEquals("Aikido Zen has blocked a server-side request forgery", exception.getMessage()); + } + + @Test + public void testNamedHostnameResolvingToPrivateIpWithNoPendingPortStillRecorded() { + Context.set(null); + DNSRecordCollector.report("internal-service.local", new InetAddress[]{inetAddress2}); + Hostnames.HostnameEntry[] entries = HostnamesStore.getHostnamesAsList(); + assertEquals(1, entries.length); + assertEquals("internal-service.local", entries[0].getHostname()); + } + @Test public void testStoredSSRFWithNoContext() throws InterruptedException { ServiceConfigStore.updateBlocking(true); From 0191c2cd3b40fdd9b49de205d5e0d1df0203f74c Mon Sep 17 00:00:00 2001 From: dmitrii Date: Thu, 23 Jul 2026 14:28:25 +0200 Subject: [PATCH 2/3] Explain why private IP literals are skipped here --- .../dev/aikido/agent_api/collectors/DNSRecordCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index 16783623..4e786251 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -43,7 +43,7 @@ public static void report(String hostname, InetAddress[] inetAddresses) { HostnamesStore.incrementHits(hostname, port); } } else if (!IsPrivateIP.isPrivateIp(hostname)) { - // We still need to report a hit to the hostname for outbound domain blocking + // Literal IPs reach this sink without a real DNS call, so skip private ones as noise. HostnamesStore.incrementHits(hostname, 0); } From 6653ed0b2737d143e6ff5c249dd32f3f40e2e9c5 Mon Sep 17 00:00:00 2001 From: dmitrii Date: Thu, 23 Jul 2026 14:31:11 +0200 Subject: [PATCH 3/3] Explain why outbound domain blocking is checked at the DNS sink --- .../dev/aikido/agent_api/collectors/DNSRecordCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java index 4e786251..ed8eb8e5 100644 --- a/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java +++ b/agent_api/src/main/java/dev/aikido/agent_api/collectors/DNSRecordCollector.java @@ -47,7 +47,7 @@ public static void report(String hostname, InetAddress[] inetAddresses) { HostnamesStore.incrementHits(hostname, 0); } - // Block if the hostname is in the blocked domains list + // Checked here, not at the HTTP client, since not all clients are instrumented and port/context isn't always available. if (ServiceConfigStore.shouldBlockOutgoingRequest(hostname)) { logger.debug("Blocking DNS lookup for domain: %s", hostname); throw BlockedOutboundException.get();