awk ile Apache Hata Loglarından Dinamik IP Kara Liste Oluşturma ve Raporlama

Yıllarca Apache loglarına baktım, analiz ettim, sayısız kez “şu IP’yi engellesek mi?” sorusunu sordum kendime. Zamanla fark ettim ki manuel inceleme bir noktadan sonra anlamsızlaşıyor. Günlük yüz binlerce satır log, elle takip edilebilecek bir şey değil. İşte tam burada awk devreye giriyor ve hayatı kurtarıyor.

Bu yazıda Apache hata loglarından otomatik IP kara listesi oluşturmayı, bu listeyi anlamlı bir rapora dönüştürmeyi ve tüm süreci nasıl işler hale getireceğimizi konuşacağız. Teorik değil, gerçekten üretimde kullandığım yaklaşımlar bunlar.

Apache Hata Log Formatını Anlamak

Her şeyden önce neyle çalıştığımızı bilmek gerekiyor. Apache’nin error.log dosyası şöyle bir format kullanır:

[Thu Jan 18 14:23:45.123456 2024] [authz_core:error] [pid 12345] [client 192.168.1.100:54321] AH01630: client denied by server configuration: /var/www/html/wp-admin

Bu satırda bizi ilgilendiren birkaç alan var: zaman damgası, hata türü, istemci IP adresi ve hata mesajı. awk ile bu alanları çıkarmak için önce ayırıcıyı doğru seçmemiz gerekiyor.

Basit bir test yapalım önce:

awk '{print NF}' /var/log/apache2/error.log | sort -u | head -5

Bu komut, log satırlarındaki alan sayısının ne kadar değişken olduğunu gösterir. Apache hata logları her zaman aynı yapıda gelmez, bu yüzden IP’yi regex ile yakalamak çok daha güvenilir.

IP Adreslerini Çıkarmak: İlk Adım

En temel yaklaşımla başlayalım. “client” kelimesinin ardından gelen IP adresini yakalamak istiyoruz:

awk 'match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {print arr[1]}' 
    /var/log/apache2/error.log | sort | uniq -c | sort -rn | head -20

Bu komut şunu yapar: her satırda [client X.X.X.X kalıbını arar, IP’yi yakalar, tüm IP’leri listeler, sayar ve en çok görünenden başlayarak sıralar. Çıktı şöyle görünür:

   4521 185.220.101.42
   3892 45.155.205.233
   2104 91.108.56.179
    987 194.165.16.72

Solda görünen sayı, o IP’nin log dosyasında kaç kez geçtiğidir. 4000’in üzerinde hata üreten bir IP kesinlikle ilgi gerektiriyor.

Hata Türüne Göre Filtreleme

Her hata aynı ciddiyette değil. 404 hataları bazen meşru kullanıcılardan gelir, ama AH01630 (erişim engellendi) veya AH00128 (geçersiz URI) hataları daha şüpheli. Hata koduna göre filtreleyelim:

awk '/AH01630|AH00128|AH00135/ && match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    count[arr[1]]++
    errors[arr[1]] = errors[arr[1]] " " $0
}
END {
    for (ip in count) {
        if (count[ip] >= 10) {
            print count[ip], ip
        }
    }
}' /var/log/apache2/error.log | sort -rn

Burada bir eşik değeri koyuyoruz: 10 veya daha fazla bu tür hata üretenler listeye giriyor. Bu eşiği ortamınıza göre ayarlamanız gerekebilir. Yoğun trafikli sitelerde 50’ye çekebilirsiniz, küçük sitelerde 5 bile yeterli olabilir.

Zaman Penceresi ile Analiz

Tüm log geçmişine bakmak yanıltıcı olabilir. Belki bir IP iki yıl önce sorun çıkardı ama artık aktif değil. Son 24 saatin loglarına odaklanmak daha anlamlı:

# Bugünün tarihini Apache log formatında al
TODAY=$(date +"%a %b %d" | sed 's/ 0/ /')

awk -v today="$TODAY" '
$0 ~ today && match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    ip = arr[1]
    
    # Hata tipini belirle
    if ($0 ~ /AH01630/) type = "ACCESS_DENIED"
    else if ($0 ~ /File does not exist/) type = "FILE_NOT_FOUND"
    else if ($0 ~ /script not found/) type = "SCRIPT_PROBE"
    else type = "OTHER"
    
    count[ip]++
    types[ip][type]++
    last_seen[ip] = $1 " " $2 " " $3 " " $4
}
END {
    for (ip in count) {
        printf "%5d %s (last: %s)n", count[ip], ip, last_seen[ip]
    }
}' /var/log/apache2/error.log | sort -rn | head -30

Tarih filtreleme kısmı biraz tricky, çünkü Apache’nin log formatındaki ayın başındaki sıfır (Jan 01 vs Jan 1) tutarsız olabiliyor. sed ile bunu normalize ediyoruz.

Dinamik Kara Liste Oluşturma

Artık gerçek işe geldik. IP’leri tespit ettik, şimdi bunları işlenebilir bir kara listeye dönüştüreceğiz. Ben genellikle iki farklı format oluşturuyorum: biri doğrudan iptables için, diğeri Apache’nin mod_authz_host modülü için.

#!/bin/bash
# blacklist_generator.sh

LOG_FILE="/var/log/apache2/error.log"
BLACKLIST_FILE="/etc/apache2/conf-available/blacklist.conf"
IPTABLES_RULES="/tmp/block_rules.sh"
THRESHOLD=50
REPORT_FILE="/var/log/ip_blacklist_report_$(date +%Y%m%d).txt"

echo "# IP Kara Listesi - $(date)" > "$REPORT_FILE"
echo "# Esik Deger: $THRESHOLD hata" >> "$REPORT_FILE"
echo "# Log Dosyasi: $LOG_FILE" >> "$REPORT_FILE"
echo "-------------------------------------------" >> "$REPORT_FILE"

# Ana analiz
awk -v threshold="$THRESHOLD" 
    -v report="$REPORT_FILE" 
    -v blacklist="$BLACKLIST_FILE" 
    -v iptables="$IPTABLES_RULES" '
BEGIN {
    print "#!/bin/bash" > iptables
    print "<RequireAll>" > blacklist
    print "    Require all granted" >> blacklist
}
match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    ip = arr[1]
    count[ip]++
    
    if ($0 ~ /AH01630/) error_type[ip]["ACCESS_DENIED"]++
    if ($0 ~ /AH00128/) error_type[ip]["INVALID_URI"]++
    if ($0 ~ /script not found/) error_type[ip]["SCRIPT_PROBE"]++
    if ($0 ~ /File does not exist/) error_type[ip]["FILE_PROBE"]++
    if ($0 ~ /connect to/) error_type[ip]["PROXY_ATTEMPT"]++
}
END {
    for (ip in count) {
        if (count[ip] >= threshold) {
            # Apache blacklist
            print "    Require not ip " ip >> blacklist
            
            # iptables kuralı
            print "iptables -A INPUT -s " ip " -j DROP" >> iptables
            
            # Rapor satiri
            printf "ENGELLE | %5d hata | %sn", count[ip], ip >> report
        }
    }
    print "</RequireAll>" >> blacklist
}
' "$LOG_FILE"

echo "" >> "$REPORT_FILE"
echo "Rapor tamamlandi: $(date)" >> "$REPORT_FILE"

echo "Kara liste olusturuldu: $BLACKLIST_FILE"
echo "iptables kurallari: $IPTABLES_RULES"
echo "Rapor: $REPORT_FILE"

Gelişmiş Raporlama: Hata Kategorileri ile

Sadece IP listesi yetmez, o IP’nin ne tür saldırılar denediğini de bilmek istiyoruz. Bu bilgi hem olayı anlamaya hem de bir sonraki adımda ne yapacağımıza karar vermeye yardımcı oluyor:

awk '
match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    ip = arr[1]
    total[ip]++
    
    # Hata siniflandirmasi
    if ($0 ~ /wp-admin|wp-login|xmlrpc/) {
        wordpress[ip]++
    } else if ($0 ~ /.php.*?.*=http|base64_decode|eval(/) {
        injection[ip]++
    } else if ($0 ~ /.env|.git|.htpasswd|config.php/) {
        sensitive[ip]++
    } else if ($0 ~ /.sql|backup|dump/) {
        backup_probe[ip]++
    } else if ($0 ~ /AH01630/) {
        access_denied[ip]++
    }
}
END {
    print "IP              | Toplam | WP    | Inject| Sens  | Backup| Diger"
    print "----------------|--------|-------|-------|-------|-------|------"
    
    for (ip in total) {
        if (total[ip] >= 20) {
            other = total[ip] - wordpress[ip] - injection[ip] - sensitive[ip] - backup_probe[ip]
            printf "%-16s| %6d | %5d | %5d | %5d | %5d | %5dn",
                ip, total[ip], wordpress[ip]+0, injection[ip]+0,
                sensitive[ip]+0, backup_probe[ip]+0, other
        }
    }
}' /var/log/apache2/error.log | sort -t'|' -k2 -rn

Bu çıktıya baktığınızda hangi IP’nin ne tür saldırı profili çizdiğini anlık görebiliyorsunuz. WordPress probu çok olan, muhtemelen otomatik bir botnet taraması. Injection denemesi çok olan, daha hedefli bir saldırgan olabilir.

Beyaz Liste Entegrasyonu

Kara listeye meşru IP’leri eklemek felaket olabilir. Bir monitörün IP’sini engellediniz, on dakika sonra telefon çalıyor. Beyaz liste mekanizması şart:

#!/bin/bash
# whitelist kontrollu kara liste olusturucu

WHITELIST="/etc/sysadmin/ip_whitelist.txt"
LOG_FILE="/var/log/apache2/error.log"
THRESHOLD=30

# Beyaz listeyi oku
if [ ! -f "$WHITELIST" ]; then
    echo "Beyaz liste dosyasi bulunamadi: $WHITELIST"
    echo "Ornek icin bos dosya olusturuluyor..."
    cat > "$WHITELIST" << 'EOF'
# Her satira bir IP veya CIDR
# 10.0.0.0/8 - Ic agimiz
10.0.0.0/8
# 192.168.0.0/16 - VPN blogu
192.168.0.0/16
# 203.0.113.50 - Yedekleme sunucusu
EOF
fi

# awk ile analiz ve beyaz liste filtresi
awk -v threshold="$THRESHOLD" -v whitelist_file="$WHITELIST" '
BEGIN {
    # Beyaz listeyi yukle
    while ((getline line < whitelist_file) > 0) {
        if (line !~ /^#/ && line !~ /^[[:space:]]*$/) {
            whitelist[line] = 1
        }
    }
}
match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    ip = arr[1]
    if (!(ip in whitelist)) {
        count[ip]++
    }
}
END {
    for (ip in count) {
        if (count[ip] >= threshold) {
            printf "%d %sn", count[ip], ip
        }
    }
}' "$LOG_FILE" | sort -rn

Not: CIDR bloklarını awk içinde tam olarak kontrol etmek karmaşıklaşıyor. Üretim ortamında bu kısmı ipset veya grepcidr gibi araçlarla tamamlıyorum. awk IP çıkarma ve sayma kısmında, CIDR karşılaştırması harici araçlarda kalıyor.

Saatlik Dağılım Analizi

Bir IP’nin günün hangi saatlerinde aktif olduğunu bilmek de önemli. Gece 3’te yüzlerce hata üretiyorsa, büyük ihtimalle otomatize bir şey:

awk '
match($0, /[...s+w+s+d+s+([0-9:]+)/, time_arr) && 
match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, ip_arr) {
    ip = ip_arr[1]
    split(time_arr[1], t, ":")
    hour = t[1]
    hourly[ip][hour]++
    total[ip]++
}
END {
    for (ip in total) {
        if (total[ip] >= 100) {
            printf "n=== %s (Toplam: %d) ===n", ip, total[ip]
            for (h = 0; h <= 23; h++) {
                bar = ""
                cnt = hourly[ip][sprintf("%02d", h)] + 0
                for (i = 0; i < int(cnt/10); i++) bar = bar "#"
                printf "%02d:00 | %-30s %dn", h, bar, cnt
            }
        }
    }
}' /var/log/apache2/error.log

Bu çıktı size ASCII grafik bir zaman dağılımı verir. Gece yarısı yoğun aktivite gösteren IP’ler hemen göze çarpıyor.

Cron ile Otomatikleştirme ve E-posta Raporu

Tüm bunları elle çalıştırmak yeterli değil. Cron’a ekleyip günlük rapor alalım:

#!/bin/bash
# /etc/cron.daily/apache-blacklist-check
# chmod +x /etc/cron.daily/apache-blacklist-check

LOG_FILE="/var/log/apache2/error.log"
THRESHOLD=100
ADMIN_MAIL="[email protected]"
HOSTNAME=$(hostname -f)

RAPOR=$(awk -v threshold="$THRESHOLD" '
match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
    ip = arr[1]
    count[ip]++
    if ($0 ~ /wp-login|xmlrpc/) wp[ip]++
    if ($0 ~ /.env|.git|config/) sens[ip]++
}
END {
    toplam_engel = 0
    rapor = "APACHE GUVENLIK RAPORU - Sunucu: " ENVIRON["HOSTNAME"] "n"
    rapor = rapor "Tarih: " strftime("%Y-%m-%d %H:%M") "n"
    rapor = rapor "Esik Deger: " threshold " hata/IPnn"
    rapor = rapor "ENGELLENMESI ONERILEN IPLER:n"
    rapor = rapor "================================n"
    
    for (ip in count) {
        if (count[ip] >= threshold) {
            toplam_engel++
            rapor = rapor sprintf("  %-18s %6d hata", ip, count[ip])
            if (wp[ip] > 0) rapor = rapor " | WP-probe:" wp[ip]
            if (sens[ip] > 0) rapor = rapor " | Hassas-dosya:" sens[ip]
            rapor = rapor "n"
        }
    }
    
    rapor = rapor "nToplam engellenmesi gereken IP: " toplam_engel "n"
    print rapor
}' "$LOG_FILE")

# Mail gonder
echo "$RAPOR" | mail -s "[$HOSTNAME] Gunluk Apache Guvenlik Raporu" "$ADMIN_MAIL"

# Kritik esik uyarisi (500'den fazla hata ureten IP varsa acil bildirim)
KRITIK=$(echo "$RAPOR" | awk '$2 >= 500 {print $1}' | wc -l)
if [ "$KRITIK" -gt 0 ]; then
    echo "$RAPOR" | mail -s "KRITIK: [$HOSTNAME] Yuksek Riskli IP Tespit Edildi!" 
        "[email protected]"
fi

Bu scripti /etc/cron.daily/ altına koyduğunuzda her gece otomatik çalışır. mail komutu çalışmıyorsa sendmail veya mutt alternatiflerini kullanabilirsiniz.

Gerçek Dünyadan Bir Senaryo

Geçen yıl yönettiğim bir e-ticaret sitesinde, sabah 7’de beklenmedik bir yavaşlama başladı. Apache loglarına baktığımda dakikada 3000 hata görüyordum. Hemen çalıştırdım:

awk 'match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {count[arr[1]]++}
     END {for(ip in count) if(count[ip]>500) print count[ip], ip}' 
     /var/log/apache2/error.log | sort -rn | head -10

Çıktıda 15 farklı IP, hepsi 500’ün üzerinde hata sayısıyla. Hepsinin /24 bloğuna bakınca aynı alt ağdan geldiğini gördüm. Koordineli bir tarama vardı ortada.

# Ayni /24 blogundaki IP sayisini bul
awk 'match($0, /[client ([0-9]+.[0-9]+.[0-9]+.)([0-9]+)/, arr) {
    subnet[arr[1]]++
}
END {
    for (s in subnet) print subnet[s], s "0/24"
}' /var/log/apache2/error.log | sort -rn | head -10

Evet, tek bir /24 bloğundan 23 farklı IP geliyordu. iptables ile tüm bloğu engelledik ve sorun 2 dakikada çözüldü. awk olmadan bu analizi elle yapmak 30-40 dakika sürerdi.

fail2ban ile Entegrasyon

awk scriptinizi fail2ban ile birleştirirseniz çok daha güçlü bir sistem elde edersiniz. awk‘ı kendi fail2ban filtresi olarak kullanabilirsiniz:

# /etc/fail2ban/filter.d/apache-custom.conf icin regex ornegi
# Once awk ile test edin:

awk '
/[error].*[client/ {
    if ($0 ~ /AH01630/ || $0 ~ /script not found/ || $0 ~ /File does not exist.*wp-/) {
        match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr)
        if (arr[1] != "") {
            count[arr[1]]++
            last_line[arr[1]] = $0
        }
    }
}
END {
    print "fail2ban icin onerilen banlar:"
    for (ip in count) {
        if (count[ip] >= 5) {
            printf "fail2ban-client set apache-custom banip %s  # %d hatan", ip, count[ip]
        }
    }
}' /var/log/apache2/error.log

Bu çıktıyı doğrudan bash‘e pipe edebilir veya review edip onaylayabilirsiniz. Üretimde ben genellikle review adımını atlamamayı tercih ediyorum, otomatik banlama bazen sürprizler çıkarabiliyor.

Performans Notu: Büyük Log Dosyaları

Log dosyası büyükse (birkaç GB) awk‘ın işlem süresi uzayabilir. Birkaç optimizasyon:

  • tail -n 100000 ile son N satırı işleyin, tüm geçmişe bakmak şart değilse
  • Log rotasyonu yapıyorsanız sadece bugünkü dosyayı (error.log) değil, dönemlik analiz için error.log.1 dahil edin
  • zcat error.log.gz | awk '...' ile sıkıştırılmış eski logları da analiz edebilirsiniz
  • Paralel işlem için parallel komutuyla birden fazla log dosyasını aynı anda işleyebilirsiniz
# Tum rotasyonlu loglar dahil analiz
zcat /var/log/apache2/error.log.*.gz | 
    cat - /var/log/apache2/error.log | 
    awk 'match($0, /[client ([0-9]+.[0-9]+.[0-9]+.[0-9]+)/, arr) {
        count[arr[1]]++
    }
    END {
        for (ip in count) if (count[ip] >= 200) print count[ip], ip
    }' | sort -rn

Sonuç

awk, Apache log analizi için hala rakipsiz bir araç. Modern alternatifler (ELK stack, Grafana Loki vs.) çok daha gösterişli arayüzler sunuyor, ama küçük-orta ölçekli ortamlarda ya da ani müdahale gereken anlarda terminale yazacağınız tek satırlık bir awk komutu onları geride bırakabiliyor.

Bu yazıdaki scriptleri doğrudan kopyalayıp kullanmak yerine kendi ortamınıza uyarlamanızı öneririm. Özellikle eşik değerleri, hata kodları ve beyaz liste entegrasyonu ortamdan ortama büyük fark gösteriyor. Bir hafta boyunca sadece raporlama modunda çalıştırın, hangi eşiğin sizin için mantıklı olduğunu görmek için. Sonra otomatik engellemeye geçin.

Son olarak şunu söylemeliyim: kara liste tek başına yeterli değil. Rate limiting, mod_security ve düzgün yapılandırılmış bir WAF ile birlikte kullanıldığında anlamlı bir güvenlik katmanı oluşturuyor. awk bu zincirin hızlı analiz ve raporlama halkasını çok iyi dolduruyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir