Detection rules › Splunk

Kubernetes Anomalous Inbound Outbound Network IO

Status
experimental
Severity
low
Group by
"k8s.cluster.name", "k8s.node.name", "k8s.pod.name", _time, service
Author
Matthew Moore, Splunk
Source
github.com/splunk/security_content

The following analytic identifies high inbound or outbound network I/O anomalies in Kubernetes containers. It leverages process metrics from an OTEL collector and Kubelet Stats Receiver, along with data from Splunk Observability Cloud. A lookup table with average and standard deviation values for network I/O is used to detect anomalies persisting over a 1-hour period. This activity is significant as it may indicate data exfiltration, command and control communication, or unauthorized data transfers. If confirmed malicious, it could lead to data breaches, service outages, financial losses, and reputational damage.

MITRE ATT&CK coverage

TacticTechniques
ExecutionT1204 User Execution

Rule body splunk

name: Kubernetes Anomalous Inbound Outbound Network IO
id: 4f3b0c97-657e-4547-a89a-9a50c656e3cd
version: 9
creation_date: '2024-01-10'
modification_date: '2026-05-13'
author: Matthew Moore, Splunk
status: experimental
type: Anomaly
description: The following analytic identifies high inbound or outbound network I/O anomalies in Kubernetes containers. It leverages process metrics from an OTEL collector and Kubelet Stats Receiver, along with data from Splunk Observability Cloud. A lookup table with average and standard deviation values for network I/O is used to detect anomalies persisting over a 1-hour period. This activity is significant as it may indicate data exfiltration, command and control communication, or unauthorized data transfers. If confirmed malicious, it could lead to data breaches, service outages, financial losses, and reputational damage.
data_source: []
search: "| mstats avg(k8s.pod.network.io) as io where `kubernetes_metrics` by k8s.cluster.name k8s.pod.name k8s.node.name direction span=10s | eval service = replace('k8s.pod.name', \"-\\w{5}$$|-[abcdef0-9]{8,10}-\\w{5}$$\", \"\") | stats avg(eval(if(direction=\"transmit\", io,null()))) as outbound_network_io avg(eval(if(direction=\"receive\", io,null()))) as inbound_network_io by k8s.cluster.name k8s.node.name k8s.pod.name service _time | eval key = 'k8s.cluster.name' + \":\" + 'service' | lookup k8s_container_network_io_baseline key | eval anomalies = \"\" | foreach stdev_* [ eval anomalies =if( '<<MATCHSTR>>' > ('avg_<<MATCHSTR>>' + 4 * 'stdev_<<MATCHSTR>>'), anomalies + \"<<MATCHSTR>> higher than average by \" + tostring(round(('<<MATCHSTR>>' - 'avg_<<MATCHSTR>>')/'stdev_<<MATCHSTR>>' ,2)) + \" Standard Deviations. <<MATCHSTR>>=\" + tostring('<<MATCHSTR>>') + \" avg_<<MATCHSTR>>=\" + tostring('avg_<<MATCHSTR>>') + \" 'stdev_<<MATCHSTR>>'=\" + tostring('stdev_<<MATCHSTR>>') + \", \" , anomalies) ] | eval anomalies = replace(anomalies, \",\\s$$\", \"\") | where anomalies!=\"\" | stats count values(anomalies) as anomalies by k8s.cluster.name k8s.node.name k8s.pod.name service | rename service as k8s.service | where count > 5 | rename k8s.node.name as host | `kubernetes_anomalous_inbound_outbound_network_io_filter`"
how_to_implement: "To implement this detection, follow these steps:\n* Deploy the OpenTelemetry Collector (OTEL) to your Kubernetes cluster.\n* Enable the hostmetrics/process receiver in the OTEL configuration.\n* Ensure that the process metrics, specifically Process.cpu.utilization and process.memory.utilization, are enabled.\n* Install the Splunk Infrastructure Monitoring (SIM) add-on. (ref: https://splunkbase.splunk.com/app/5247)\n * Configure the SIM add-on with your Observability Cloud Organization ID and Access Token.\n* Set up the SIM modular input to ingest Process Metrics. Name this input \"sim_process_metrics_to_metrics_index\".\n* In the SIM configuration, set the Organization ID to your Observability Cloud Organization ID.\n* Set the Signal Flow Program to the following: data('process.threads').publish(label='A'); data('process.cpu.utilization').publish(label='B'); data('process.cpu.time').publish(label='C'); data('process.disk.io').publish(label='D'); data('process.memory.usage').publish(label='E'); data('process.memory.virtual').publish(label='F'); data('process.memory.utilization').publish(label='G'); data('process.cpu.utilization').publish(label='H'); data('process.disk.operations').publish(label='I'); data('process.handles').publish(label='J'); data('process.threads').publish(label='K')\n* Set the Metric Resolution to 10000.\n * Leave all other settings at their default values.\n* Run the Search Baseline Of Kubernetes Container Network IO Ratio"
known_false_positives: No false positives have been identified at this time.
references:
    - https://github.com/signalfx/splunk-otel-collector-chart
intermediate_findings:
    entities:
        - field: host
          type: system
          score: 20
          message: Kubernetes Anomalous Inbound Outbound Network IO from container on host $host$
analytic_story:
    - Abnormal Kubernetes Behavior using Splunk Infrastructure Monitoring
asset_type: Kubernetes
mitre_attack_id:
    - T1204
product:
    - Splunk Enterprise
    - Splunk Enterprise Security
    - Splunk Cloud
category: cloud
security_domain: network
baselines:
    - Baseline Of Kubernetes Container Network IO

Stages and Predicates

Stage 1: search

| mstats avg(k8s.pod.network.io) as io where `kubernetes_metrics` by k8s.cluster.name k8s.pod.name k8s.node.name direction span=10s

Stage 2: eval

| eval service = replace('k8s.pod.name', "-\w{5}$$|-[abcdef0-9]{8,10}-\w{5}$$", "")

Stage 3: stats

| stats avg(eval(if(direction="transmit", io,null()))) as outbound_network_io avg(eval(if(direction="receive", io,null()))) as inbound_network_io by k8s.cluster.name k8s.node.name k8s.pod.name service _time

Stage 4: eval

| eval key = 'k8s.cluster.name' + ":" + 'service'

Stage 5: lookup

| lookup k8s_container_network_io_baseline key
Lookup table
k8s_container_network_io_baseline
Key field
key

Stage 6: eval

| eval anomalies = ""

Stage 7: search

| foreach stdev_* [ eval anomalies =if( '<<MATCHSTR>>' > ('avg_<<MATCHSTR>>' + 4 * 'stdev_<<MATCHSTR>>'), anomalies + "<<MATCHSTR>> higher than average by " + tostring(round(('<<MATCHSTR>>' - 'avg_<<MATCHSTR>>')/'stdev_<<MATCHSTR>>' ,2)) + " Standard Deviations. <<MATCHSTR>>=" + tostring('<<MATCHSTR>>') + " avg_<<MATCHSTR>>=" + tostring('avg_<<MATCHSTR>>') + " 'stdev_<<MATCHSTR>>'=" + tostring('stdev_<<MATCHSTR>>') + ", " , anomalies) ]

Stage 8: eval

| eval anomalies = replace(anomalies, ",\s$$", "")

Stage 9: where

| where anomalies!=""

Stage 10: stats

| stats count values(anomalies) as anomalies by k8s.cluster.name k8s.node.name k8s.pod.name service

Stage 11: rename

| rename service as k8s.service

Stage 12: where

| where count > 5

Stage 13: rename

| rename k8s.node.name as host

Stage 14: search

| `kubernetes_anomalous_inbound_outbound_network_io_filter`

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
anomaliesne
  • ""
countgt
  • 5
spaneq
  • 10s

Search terms

Bare-string tokens in the SPL search body. Splunk matches each token against _raw (the untyped raw event text) anywhere it appears, not against a specific field. These don't surface in the Indicators table because they aren't predicates on a known field.

StageTerm
1mstats
1avg
1k8s.pod.network.io
1as
1io
1where
1by
1direction
1k8s.cluster.name
1k8s.pod.name
1k8s.node.name
7foreach
7stdev_*