DEV Community

Ayi NEDJIMI
Ayi NEDJIMI

Posted on

Implementing IP Reputation Scoring in a Go API

Your API logs show a flood of requests from a single IP — 400 attempts in 30 seconds. By the time your rate limiter kicks in, your database has already taken 400 unnecessary hits. IP reputation scoring lets you make a risk decision before a request ever touches your business logic.

This is not about blocking entire countries or trusting commercial blocklists blindly. It is about building a lightweight, composable scoring layer that combines signals: past behavior, known threat lists, and ASN metadata.

What goes into a reputation score

A reputation score is a number (0–100, higher = riskier) aggregated from several signals:

  • Recent failed requests: failed auth attempts, 429s, 4xx errors from this IP in the last hour
  • Threat intelligence feeds: lists like AbuseIPDB, Emerging Threats, or Spamhaus
  • ASN/organization: requests from hosting providers (AS14061 DigitalOcean) carry more weight than residential ISPs
  • Behavioral velocity: requests-per-minute, unique endpoints hit, unusual timing patterns

Each signal contributes a sub-score. The final score is a weighted sum. You tune the weights based on what a false positive costs your business.

Data structures and scoring logic

Here is a Go struct and scoring function that implements the basics:

package reputation

import (
    "math"
    "time"
)

type IPRecord struct {
    IP             string
    FailedAuths    int
    Total4xx       int
    RequestsPerMin float64
    InThreatFeed   bool
    IsHostingASN   bool
    LastSeen       time.Time
}

type Score struct {
    Value  int    // 0-100, higher = riskier
    Reason string
}

func Calculate(r IPRecord) Score {
    score := 0.0
    var reasons []string

    // Failed auth signals (max 40 points)
    if r.FailedAuths > 0 {
        authScore := math.Min(float64(r.FailedAuths)*4, 40)
        score += authScore
        if r.FailedAuths >= 5 {
            reasons = append(reasons, "repeated_auth_failures")
        }
    }

    // 4xx rate (max 20 points)
    if r.Total4xx > 10 {
        score += math.Min(float64(r.Total4xx-10)*0.5, 20)
    }

    // Velocity (max 20 points)
    if r.RequestsPerMin > 60 {
        score += math.Min((r.RequestsPerMin-60)*0.5, 20)
        reasons = append(reasons, "high_velocity")
    }

    // Threat feed match (30 points flat)
    if r.InThreatFeed {
        score += 30
        reasons = append(reasons, "threat_feed_match")
    }

    // Hosting ASN multiplier
    if r.IsHostingASN {
        score *= 1.3
        reasons = append(reasons, "hosting_asn")
    }

    finalScore := int(math.Min(score, 100))
    reason := "ok"
    if len(reasons) > 0 {
        reason = reasons[0]
    }

    return Score{Value: finalScore, Reason: reason}
}
Enter fullscreen mode Exit fullscreen mode

The weights are opinionated — adjust them to your traffic profile. A failed auth from a hosting ASN that also appears in a threat feed can easily hit 90+. A residential IP with a few stray 404s stays below 20.

Building the middleware

The scorer is most useful as HTTP middleware that short-circuits requests above a threshold:

package middleware

import (
    "fmt"
    "net"
    "net/http"
    "strings"

    "yourapp/reputation"
    "yourapp/store"
)

const BlockThreshold     = 75
const ChallengeThreshold = 50

func ReputationCheck(s store.IPStore) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := extractIP(r)
            record, err := s.Get(ip)
            if err != nil {
                // Fail open — never block on infrastructure errors
                next.ServeHTTP(w, r)
                return
            }

            score := reputation.Calculate(record)
            w.Header().Set("X-Reputation-Score", fmt.Sprintf("%d", score.Value))

            switch {
            case score.Value >= BlockThreshold:
                http.Error(w, "forbidden", http.StatusForbidden)
                return
            case score.Value >= ChallengeThreshold:
                // Upstream can decide to require CAPTCHA or step-up auth
                r.Header.Set("X-Reputation-Challenge", "true")
            }

            next.ServeHTTP(w, r)
        })
    }
}

func extractIP(r *http.Request) string {
    // Only trust X-Forwarded-For if a controlled proxy sets it
    if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
        parts := strings.Split(xff, ",")
        return strings.TrimSpace(parts[0])
    }
    ip, _, _ := net.SplitHostPort(r.RemoteAddr)
    return ip
}
Enter fullscreen mode Exit fullscreen mode

One critical detail in extractIP: only trust X-Forwarded-For if a trusted reverse proxy sets it. If your app is directly exposed, a client can forge any IP in that header and bypass scoring entirely.

Feeding the store and integrating threat feeds

The IP store accumulates signals as requests complete. A Redis-backed store with TTL-expiring counters works well at scale:

// Run this after next.ServeHTTP returns so you have the final status code
func recordOutcome(ip string, statusCode int, s store.IPStore) {
    const ttl1Hour   = 3600
    const ttl1Minute = 60

    if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
        s.IncrField(ip, "failed_auths", 1, ttl1Hour)
    }
    if statusCode >= 400 {
        s.IncrField(ip, "total_4xx", 1, ttl1Hour)
    }
    s.IncrField(ip, "request_count", 1, ttl1Minute)
}
Enter fullscreen mode Exit fullscreen mode

For threat feed lookups, AbuseIPDB offers a free tier. Query it asynchronously on first sight of an IP, then cache the result with a 24-hour TTL — you do not want a synchronous external call on every request:

func checkAbuseIPDB(ip, apiKey string) (bool, error) {
    req, _ := http.NewRequest("GET", "https://api.abuseipdb.com/api/v2/check", nil)
    q := req.URL.Query()
    q.Add("ipAddress", ip)
    q.Add("maxAgeInDays", "30")
    req.URL.RawQuery = q.Encode()
    req.Header.Set("Key", apiKey)
    req.Header.Set("Accept", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return false, err
    }
    defer resp.Body.Close()

    var result struct {
        Data struct {
            AbuseConfidenceScore int `json:"abuseConfidenceScore"`
        } `json:"data"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return false, err
    }
    return result.Data.AbuseConfidenceScore > 25, nil
}
Enter fullscreen mode Exit fullscreen mode

For ASN lookups, MaxMind GeoLite2-ASN ships as a local .mmdb file and resolves in microseconds — no network round-trip needed.

Tuning and avoiding false positives

Before flipping the middleware on in production:

  1. Run in shadow mode first: compute and log scores but do not block anything. Collect a week of data.
  2. Histogram your scores: if 30% of legitimate traffic scores above 50, your weights are off.
  3. Allowlist known IPs: your CI runner, monitoring probes, partner services — seed them with score 0 in the store.
  4. Alert on block rate spikes: a sudden jump in blocks often means a signal source is misbehaving — a threat feed returning stale bulk data or a counter TTL bug.

For a broader hardening checklist covering authentication, headers, and rate limiting, our free security hardening checklists cover these patterns in structured PDF and Excel format.

The takeaway

IP reputation scoring is not a silver bullet. A motivated attacker rotates IPs; a residential botnet scores low on ASN checks. But as a first-pass filter it cuts noise significantly — automated scanners almost always trigger your highest-scoring thresholds within the first 10 requests.

The pattern scales cleanly: start with in-memory counters, graduate to Redis, then layer in external feeds as traffic volume justifies the operational complexity. Keep the fail-open behavior in the middleware. The cost of a false positive — blocking a legitimate user — is almost always higher than the cost of letting one more bot through.


I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.

Top comments (0)