ELK Stack — Practice Projects: Exercises & Challenges
Practice Exercises & Challenges
These five challenge exercises extend the projects you built in this chapter. Each exercise builds on an existing project and pushes you to apply what you have learned in a new way.
Try to solve each challenge on your own before looking at the solution.
Exercise 1 — Add GeoIP Enrichment to the Nginx Project
Based on: Project 1 — Nginx Log Monitoring
Challenge
The Nginx project’s Logstash pipeline already includes a geoip filter, but all the traffic comes from localhost / Docker internal IPs, so GeoIP never resolves to a real location.
Your task:
- Modify the Nginx log format to include the
X-Forwarded-Forheader - Create a simple script that sends requests with a spoofed
X-Forwarded-Forheader containing real-world public IPs - Update the Logstash pipeline to extract the IP from
X-Forwarded-Forand apply GeoIP to it - Build a Coordinate Map in Kibana showing request origins around the world
Hints
- Use
curl -H "X-Forwarded-For: 8.8.8.8"to simulate external IPs - In the Nginx log format add
$http_x_forwarded_for - In Logstash, check if the field exists and prefer it over
client_ip - The GeoIP database bundled with Logstash covers most public IPs
Show Solution
Step 1 — Update Nginx log format
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
Step 2 — Create traffic generator script
#!/bin/bash
# generate-geo-traffic.sh
PUBLIC_IPS=(
"8.8.8.8" # US
"1.1.1.1" # Australia
"103.21.244.0" # India
"185.70.41.35" # Norway
"41.57.156.0" # Nigeria
"189.45.67.1" # Brazil
"109.232.227.132" # Germany
"202.14.120.1" # Japan
"5.135.199.0" # France
"46.165.220.0" # Russia
)
for i in $(seq 1 200); do
IP=${PUBLIC_IPS[$((RANDOM % ${#PUBLIC_IPS[@]}))]}
curl -s -H "X-Forwarded-For: $IP" http://localhost:8080/ > /dev/null
sleep 0.1
done
echo "Generated 200 geo-distributed requests"
Step 3 — Update Logstash grok to capture forwarded IP
Update the grok pattern to include the forwarded-for field:
grok {
match => {
"message" => '%{IPORHOST:client_ip} - %{DATA:remote_user} \[%{HTTPDATE:timestamp}\] "%{WORD:http_method} %{URIPATHPARAM:request_uri} HTTP/%{NUMBER:http_version}" %{NUMBER:response_code:integer} %{NUMBER:bytes_sent:integer} "%{DATA:referrer}" "%{DATA:user_agent}" "%{DATA:x_forwarded_for}"'
}
}
# Prefer X-Forwarded-For for GeoIP
if [x_forwarded_for] and [x_forwarded_for] != "-" {
mutate {
copy => { "x_forwarded_for" => "geo_source_ip" }
}
} else {
mutate {
copy => { "client_ip" => "geo_source_ip" }
}
}
geoip {
source => "geo_source_ip"
target => "geoip"
}
Step 4 — Kibana Map
- Create a data view for
nginx-access-* - Add a Maps visualization
- Add layer → Documents → select
nginx-access-* - Set the Geo field to
geoip.location - Use the
response_codefield for color coding
Exercise 2 — Implement Log-Based Alerting for Microservices
Based on: Project 2 — Microservices Logging
Challenge
The microservices project collects logs but does not alert when things go wrong. Your task:
- Create an Elasticsearch Watcher (or Kibana alerting rule) that fires when the payment failure rate exceeds 20% in a 5-minute window
- Create a second alert that fires when any service produces more than 50 ERROR logs in 10 minutes
- Configure the alert to write to a dedicated
alerts-*index (simulating a notification) - Build a Kibana panel that shows alert history over time
Hints
- Use the
_searchAPI with aggregations to prototype the query - Kibana Rules → Create Rule → Elasticsearch query
- For the payment failure rate, aggregate by
log_messageand calculate the ratio - You can also use Logstash to detect error spikes with the
throttlefilter
Show Solution
Alert 1 — Payment failure rate
Kibana Rule configuration:
- Type: Elasticsearch query
- Index:
microservices-payment-service-* - Query:
{
"bool": {
"must": [
{ "match": { "log_message": "Payment failed" } },
{ "range": { "@timestamp": { "gte": "now-5m" } } }
]
}
}
- Threshold: Count > 10 (assuming ~50 total payments in 5 min = 20% failure)
- Check every: 1 minute
- Action: Index connector → write to
alerts-payment-failures
Alert 2 — Error spike detection
- Type: Elasticsearch query
- Index:
microservices-* - Query:
{
"bool": {
"must": [
{ "term": { "log_level": "ERROR" } },
{ "range": { "@timestamp": { "gte": "now-10m" } } }
]
}
}
- Threshold: Count > 50
- Group by:
service_name - Action: Index connector → write to
alerts-error-spikes
Alert 3 — Logstash throttle approach (alternative)
Add to the microservices Logstash pipeline:
if [log_level] == "ERROR" {
throttle {
before_count => 50
after_count => 51
period => 600
key => "%{service_name}"
add_tag => ["error_spike_alert"]
}
}
# In output section:
if "error_spike_alert" in [tags] {
elasticsearch {
hosts => ["http://es01:9200"]
index => "alerts-error-spikes-%{+YYYY.MM.dd}"
}
}
Kibana panel
Create a data view for alerts-* and add a data table panel sorted by @timestamp descending.
Exercise 3 — Scale ECK Deployment with Hot-Warm Architecture
Based on: Project 3 — ELK on Kubernetes with ECK
Challenge
The ECK project uses a single node set. In production, you need a hot-warm-cold architecture to manage costs and performance. Your task:
- Modify the Elasticsearch manifest to have 3 hot nodes and 2 warm nodes
- Create an Index Lifecycle Management (ILM) policy that moves indices from hot to warm after 7 days and deletes after 30 days
- Create an index template that applies the ILM policy to all
k8s-*indices - Verify that new indices are created on hot nodes
Hints
- Use
node.roles: ["data_hot", "ingest"]andnode.roles: ["data_warm"] - Warm nodes can have smaller CPU/memory but larger storage
- Use the Elasticsearch ILM API to create the policy
- Use
_cat/shardsto verify shard placement
Show Solution
Step 1 — Updated Elasticsearch manifest
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elk-project
namespace: elk-project
spec:
version: 8.13.0
nodeSets:
- name: hot
count: 3
config:
node.roles: ["master", "data_hot", "ingest"]
node.store.allow_mmap: false
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 2Gi
cpu: "1"
limits:
memory: 2Gi
cpu: "1"
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
storageClassName: fast-ssd # adjust for your cluster
- name: warm
count: 2
config:
node.roles: ["data_warm"]
node.store.allow_mmap: false
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 1Gi
cpu: 500m
limits:
memory: 1Gi
cpu: 500m
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
storageClassName: standard-hdd # adjust for your cluster
Step 2 — ILM Policy
curl -sk -u "elastic:$ES_PASSWORD" -X PUT "https://localhost:9200/_ilm/policy/k8s-logs-policy" \
-H 'Content-Type: application/json' -d'
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_age": "1d",
"max_primary_shard_size": "10gb"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"allocate": {
"require": { "data": "warm" }
},
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}'
Step 3 — Index Template
curl -sk -u "elastic:$ES_PASSWORD" -X PUT "https://localhost:9200/_index_template/k8s-logs-template" \
-H 'Content-Type: application/json' -d'
{
"index_patterns": ["k8s-*"],
"template": {
"settings": {
"index.lifecycle.name": "k8s-logs-policy",
"index.routing.allocation.require.data": "hot",
"number_of_replicas": 1
}
},
"priority": 200
}'
Step 4 — Verify shard placement
curl -sk -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cat/shards/k8s-*?v&h=index,shard,prirep,state,node"
All primary shards for new indices should be on nodes whose names contain hot.
Exercise 4 — Add Custom Dashboards to the Multi-Source Pipeline
Based on: Project 4 — Multi-Source Log Aggregation
Challenge
The multi-source project routes logs to separate indices but does not have a unified operations dashboard. Your task:
- Create a cross-index dashboard in Kibana that shows a unified view of all three log sources
- Add a saved search that lets operators filter by source type, severity, and time range
- Create a Vega visualization (or Lens) that shows the correlation between Apache error spikes and application errors on a dual-axis chart
- Export the dashboard as an NDJSON file so it can be version-controlled
Hints
- Use a data view with pattern
apache-*,syslog-*,app-* - Use the
_indexfield to distinguish sources in aggregations - For the dual-axis chart, use Lens with a breakdown by index pattern
- Export via Stack Management → Saved Objects → Export
Show Solution
Step 1 — Cross-index data view
- Go to Stack Management → Data Views
- Create: name
all-sources, patternapache-*,syslog-*,app-logs-* - Time field:
@timestamp
Step 2 — Saved search
- Go to Discover
- Select the
all-sourcesdata view - Add filters:
- Add column:
_index(to see the source) - Add column:
severityorresponse_categoryorlevel
- Add column:
- Save as “All Sources — Unified View”
Step 3 — Correlation chart using Lens
- Create a new visualization → Lens
- Data view:
all-sources - X-axis:
@timestampwith date histogram - Left Y-axis: Count of documents where
_indexstarts withapache-error - Right Y-axis: Count of documents where
_indexstarts withapp-logsANDlevel: ERROR - Layer 1: Apache errors (bar chart)
- Layer 2: App errors (line chart on right axis)
- Save as “Error Correlation — Apache vs App”
Step 4 — Export dashboard
# Via Kibana API
curl -s http://localhost:5601/api/saved_objects/_export \
-H "kbn-xsrf: true" \
-H "Content-Type: application/json" \
-d '{"type": ["dashboard"], "includeReferencesDeep": true}' \
-o dashboard-export.ndjson
echo "Dashboard exported to dashboard-export.ndjson"
Or via the UI:
- Go to Stack Management → Saved Objects
- Select the dashboard
- Click Export → include related objects
- Save the
.ndjsonfile in your project repository
Exercise 5 — Build Automated Threat Response
Based on: Project 5 — Security Dashboard
Challenge
The security project detects threats but does not respond to them. Your task:
- Create a Logstash pipeline that monitors the
security-alerts-*index for brute force IPs (more than 10 failed logins in 5 minutes) - When a brute force is detected, write the offending IP to a file (
/tmp/blocked-ips.txt) - Create a script that reads this file and could theoretically add iptables rules (simulate with a log message)
- Build a Kibana panel showing the timeline of automated blocks
- Bonus: Use the Elasticsearch
_update_by_queryto tag all historical events from a blocked IP
Hints
- Use a second Logstash pipeline that reads from Elasticsearch using the
elasticsearchinput - Use the
fileoutput plugin to write blocked IPs - Use the
execoutput plugin to run a script - For the bonus, use
curlwith_update_by_query
Show Solution
Step 1 — Threat response Logstash pipeline
Add to pipelines.yml:
- pipeline.id: threat-response
path.config: "/usr/share/logstash/pipeline-threat/response.conf"
pipeline.workers: 1
Create logstash/pipeline-threat/response.conf:
input {
elasticsearch {
hosts => ["http://es01:9200"]
index => "security-auth-*"
query => '{"query":{"bool":{"must":[{"term":{"security_event":"failed_authentication"}},{"range":{"@timestamp":{"gte":"now-5m"}}}]}}}'
schedule => "*/1 * * * *"
size => 1000
}
}
filter {
# Count failures per source IP
aggregate {
task_id => "%{source_ip}"
code => "
map['attempts'] ||= 0
map['attempts'] += 1
map['source_ip'] = event.get('source_ip')
event.cancel()
"
push_map_as_event_on_timeout => true
timeout => 10
timeout_tags => ['aggregated']
}
if "aggregated" in [tags] {
if [attempts] and [attempts] >= 10 {
mutate {
add_tag => ["auto_block"]
add_field => {
"action" => "block"
"reason" => "brute_force_detected"
"blocked_at" => "%{@timestamp}"
}
}
} else {
drop {}
}
}
}
output {
if "auto_block" in [tags] {
# Write blocked IP to file
file {
path => "/tmp/blocked-ips.txt"
codec => line { format => "%{source_ip}" }
}
# Log the block action to Elasticsearch
elasticsearch {
hosts => ["http://es01:9200"]
index => "security-auto-blocks-%{+YYYY.MM.dd}"
}
# Simulate iptables block (use exec for real scenarios)
stdout {
codec => line { format => "AUTO-BLOCK: Blocking IP %{source_ip} — %{attempts} failed attempts" }
}
}
}
Step 2 — Block script (simulated)
#!/bin/bash
# simulate-block.sh — reads blocked IPs and simulates iptables rules
BLOCKED_FILE="/tmp/blocked-ips.txt"
if [ ! -f "$BLOCKED_FILE" ]; then
echo "No blocked IPs found"
exit 0
fi
while IFS= read -r ip; do
echo "[$(date)] SIMULATED: iptables -A INPUT -s $ip -j DROP"
# In production: sudo iptables -A INPUT -s "$ip" -j DROP
done < "$BLOCKED_FILE"
Step 3 — Kibana panel
Create a data view for security-auto-blocks-* and add:
- Timeline: Line chart of blocks over time
- Blocked IPs table: Data table with
source_ip,attempts,blocked_at - Map: GeoIP locations of blocked IPs
Step 4 (Bonus) — Tag historical events from blocked IP
# Replace BLOCKED_IP with the actual IP
BLOCKED_IP="185.220.101.1"
curl -s -X POST "http://localhost:9200/security-*/_update_by_query" \
-H 'Content-Type: application/json' -d"
{
\"query\": {
\"term\": { \"source_ip\": \"$BLOCKED_IP\" }
},
\"script\": {
\"source\": \"ctx._source.tags = ctx._source.tags != null ? ctx._source.tags : []; ctx._source.tags.add('retroactively_blocked')\",
\"lang\": \"painless\"
}
}"
This tags all historical events from the blocked IP so analysts can review the full activity history.
Summary
| Exercise | Skill Practiced |
|---|---|
| 1 — GeoIP Enrichment | Log format modification, GeoIP filtering, map visualizations |
| 2 — Alerting | Kibana rules, Elasticsearch queries, threshold-based alerting |
| 3 — Hot-Warm Architecture | ILM policies, node roles, index templates, capacity planning |
| 4 — Custom Dashboards | Cross-index views, Lens visualizations, saved object export |
| 5 — Automated Response | Pipeline chaining, aggregate filter, automated threat mitigation |
What’s Next
Congratulations on completing the Practice Projects chapter! You now have hands-on experience with:
- End-to-end ELK pipelines for web servers, microservices, and Kubernetes
- Multi-pipeline Logstash with custom grok patterns
- Security dashboards with threat enrichment and alerting
- Advanced topics like ILM, hot-warm architecture, and automated response
Continue exploring by modifying these projects for your own infrastructure and log sources.