monitoring/crowdsec: add MaxMind GeoLite2-City.mmdb

This commit is contained in:
2026-05-17 13:17:08 +04:00
parent 2e132075b3
commit 1581be1722
8 changed files with 81 additions and 9 deletions
+9
View File
@@ -10,3 +10,12 @@ CROWDSEC_BOUNCER_KEY_FW=
# Leave blank to run without Console (no community blocklist subscriptions). # Leave blank to run without Console (no community blocklist subscriptions).
CROWDSEC_ENROLL_KEY= CROWDSEC_ENROLL_KEY=
CROWDSEC_ENROLL_INSTANCE_NAME= CROWDSEC_ENROLL_INSTANCE_NAME=
############# MaxMind GeoIP #############
# Free account: https://www.maxmind.com/en/geolite2/signup
# After signup: Manage License Keys -> Generate new license key.
# Used by the geoipupdate sidecar to download/refresh GeoLite2-City.mmdb,
# consumed by the exporter for the Country column in the Local bans table.
# Leave blank to disable: exporter still runs, country labels will be empty.
MAXMIND_ACCOUNT_ID=
MAXMIND_LICENSE_KEY=
+1
View File
@@ -1,3 +1,4 @@
/db/* /db/*
/config/* /config/*
/geoip/*
!.gitkeep !.gitkeep
View File
+22
View File
@@ -63,6 +63,9 @@ services:
CROWDSEC_API_KEY: "${CROWDSEC_BOUNCER_KEY_CADDY}" CROWDSEC_API_KEY: "${CROWDSEC_BOUNCER_KEY_CADDY}"
POLL_INTERVAL_SECS: "30" POLL_INTERVAL_SECS: "30"
LISTEN_PORT: "9100" LISTEN_PORT: "9100"
GEOIP_CITY_DB: "/geoip/GeoLite2-City.mmdb"
volumes:
- ./data/geoip:/geoip:ro
depends_on: depends_on:
- crowdsec - crowdsec
logging: logging:
@@ -70,3 +73,22 @@ services:
options: options:
max-size: "50m" max-size: "50m"
max-file: "3" max-file: "3"
geoipupdate:
image: ghcr.io/maxmind/geoipupdate:v7
container_name: crowdsec_geoipupdate
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
environment:
GEOIPUPDATE_ACCOUNT_ID: "${MAXMIND_ACCOUNT_ID}"
GEOIPUPDATE_LICENSE_KEY: "${MAXMIND_LICENSE_KEY}"
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City"
GEOIPUPDATE_FREQUENCY: "24"
volumes:
- ./data/geoip:/usr/share/GeoIP
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "2"
+5 -1
View File
@@ -2,13 +2,17 @@ module crowdsec-exporter
go 1.26.3 go 1.26.3
require github.com/prometheus/client_golang v1.23.2 require (
github.com/oschwald/geoip2-golang/v2 v2.1.0
github.com/prometheus/client_golang v1.23.2
)
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.20.1 // indirect
+4
View File
@@ -12,6 +12,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+34 -4
View File
@@ -4,14 +4,33 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"net/netip"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/oschwald/geoip2-golang/v2"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
var geoDB *geoip2.Reader
func lookupCountry(ip string) string {
if geoDB == nil {
return ""
}
addr, err := netip.ParseAddr(ip)
if err != nil {
return ""
}
rec, err := geoDB.City(addr)
if err != nil || rec == nil {
return ""
}
return rec.Country.ISOCode
}
var ( var (
decisionsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{ decisionsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "crowdsec_decisions_active", Name: "crowdsec_decisions_active",
@@ -31,7 +50,7 @@ var (
decisionRemaining = prometheus.NewGaugeVec(prometheus.GaugeOpts{ decisionRemaining = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "crowdsec_decision_remaining_seconds", Name: "crowdsec_decision_remaining_seconds",
Help: "Remaining seconds before each local decision expires. Only origin=crowdsec is exposed to keep cardinality bounded (CAPI/lists can contain tens of thousands of IPs).", Help: "Remaining seconds before each local decision expires. Only origin=crowdsec is exposed to keep cardinality bounded (CAPI/lists can contain tens of thousands of IPs).",
}, []string{"origin", "ip", "scenario", "type"}) }, []string{"origin", "ip", "country", "scenario", "type"})
lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{ lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "crowdsec_exporter_last_fetch_unix", Name: "crowdsec_exporter_last_fetch_unix",
@@ -106,7 +125,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) {
ips := map[string]map[string]struct{}{} ips := map[string]map[string]struct{}{}
allIPs := map[string]struct{}{} allIPs := map[string]struct{}{}
type localDecision struct { type localDecision struct {
origin, ip, scenario, dtype string origin, ip, country, scenario, dtype string
remaining float64 remaining float64
} }
var locals []localDecision var locals []localDecision
@@ -137,7 +156,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) {
if perr != nil { if perr != nil {
dur = 0 dur = 0
} }
locals = append(locals, localDecision{origin, d.Value, scenario, dtype, dur.Seconds()}) locals = append(locals, localDecision{origin, d.Value, lookupCountry(d.Value), scenario, dtype, dur.Seconds()})
} }
} }
@@ -152,7 +171,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) {
} }
decisionsUniqueIPsTotal.Set(float64(len(allIPs))) decisionsUniqueIPsTotal.Set(float64(len(allIPs)))
for _, l := range locals { for _, l := range locals {
decisionRemaining.WithLabelValues(l.origin, l.ip, l.scenario, l.dtype).Set(l.remaining) decisionRemaining.WithLabelValues(l.origin, l.ip, l.country, l.scenario, l.dtype).Set(l.remaining)
} }
fetchErrors.Set(0) fetchErrors.Set(0)
@@ -175,6 +194,17 @@ func main() {
} }
port := getenv("LISTEN_PORT", "9100") port := getenv("LISTEN_PORT", "9100")
geoDBPath := getenv("GEOIP_CITY_DB", "")
if geoDBPath != "" {
if db, err := geoip2.Open(geoDBPath); err == nil {
geoDB = db
defer geoDB.Close()
log.Printf("[start] GeoIP DB loaded: %s", geoDBPath)
} else {
log.Printf("[warn] GeoIP DB unavailable (%v); country label will be empty", err)
}
}
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
go func() { go func() {
@@ -429,12 +429,14 @@
}, },
"indexByName": { "indexByName": {
"ip": 0, "ip": 0,
"scenario": 1, "country": 1,
"type": 2, "scenario": 2,
"Value": 3 "type": 3,
"Value": 4
}, },
"renameByName": { "renameByName": {
"ip": "IP", "ip": "IP",
"country": "Country",
"scenario": "Scenario", "scenario": "Scenario",
"type": "Type", "type": "Type",
"Value": "Time remaining" "Value": "Time remaining"