I have built a project lab with following tools:
| Component | Tool | |
|---|---|---|
| Hypervisor | VirtualBox | |
| Attacker VM | Kali Linux | |
| Victim VM | Ubuntu Server 22.04 | |
| SIEM VM | Ubuntu Server 22.04 | |
| Log shipper (Linux) | Filebeat | |
| Log processor | Logstash | |
| Search engine | Elasticsearch | |
| UI + Alerting | Kibana |
I performed a brute force SSH attack using Hydra on the victim VM. Although the attacker IP was visible in the logs for the failed SSH login attempts, it was not in a separate structured field. To create said field I used the following add Grok filter in the configuration file of Logstash:
input {
beats {
port => 5044
}
}
filter {
if [agent][type] == "winlogbeat" {
mutate { add_tag => ["windows", "winlogbeat"] }
}
if [agent][type] == "filebeat" {
mutate { add_tag => ["linux", "filebeat"] }
}
# Parse SSH failed password attempts from auth.log
if [log][file][path] == "/var/log/auth.log" {
grok {
match => {
"message" => "%{SYSLOGTIMESTAMP:syslog.timestamp} %{HOSTNAME:observed.hostname} sshd\[%{NUMBER:process.pid}\]: Failed password for %{USER:user.name} from %{IP:source.ip} port %{NUMBER:source.port:int} %{WORD:network.transport}"
}
tag_on_failure => ["_grokparsefailure_sshd"]
}
mutate {
add_field => {
"event.category" => "authentication"
"event.type" => "start"
"event.outcome" => "failure"
}
}
}
}
output {
elasticsearch {
hosts => ["https://192.168.53.40:9200"]
ssl => true
ssl_certificate_verification => false
user => "elastic"
password => "elastic"
index => "lab-logs-%{+YYYY.MM.dd}"
}
}
After that, I created a simple threshold rule to detect this type of attack (source.ip was not available in the group by field so I used source.ip.keyword):
{
"id": "e480dd55-92bf-418e-809e-54cca503ef95",
"updated_at": "2026-06-08T15:33:35.727Z",
"updated_by": "elastic",
"created_at": "2026-06-08T14:32:09.642Z",
"created_by": "elastic",
"name": "SSH Brute Force Detected",
"tags": [],
"interval": "1m",
"enabled": false,
"revision": 1,
"description": "More than 5 failed SSH logins from the same IP within 1 minute",
"risk_score": 75,
"severity": "high",
"license": "",
"output_index": "",
"meta": {
"kibana_siem_app_url": "http://192.168.53.40:5601/app/security"
},
"author": [],
"false_positives": [],
"from": "now-6m",
"rule_id": "9554aea5-59e2-4371-ab9e-cdc025d931d1",
"max_signals": 100,
"risk_score_mapping": [],
"severity_mapping": [],
"threat": [],
"to": "now",
"references": [],
"version": 1,
"exceptions_list": [],
"immutable": false,
"rule_source": {
"type": "internal"
},
"related_integrations": [],
"required_fields": [],
"setup": "",
"type": "threshold",
"language": "kuery",
"index": [
"lab-logs-*"
],
"query": "log.file.path: \"/var/log/auth.log\" and message: \"Failed password\"",
"filters": [],
"threshold": {
"field": [
"source.ip.keyword"
],
"value": 5,
"cardinality": []
},
"actions": []
}
In the Alerts tab, the attack is detected, but the source.ip field is not visible in the table below. However, it does appear when the alert is opened in Investigate in Timeline mode.
How can I solve this problem? Why source.ip without .keyword isn't an available option in group by field of the rule creation?
