towerops/lib/towerops/security/ip_whitelist.ex
Graham McIntire 4d73e77f3a
Add 404-based brute force protection system
Implements automated detection and blocking of IPs exhibiting scanning behavior (5+ unique 404s within 1 minute).

Key features:
- Progressive ban escalation (5 min → 1 hour → permanent)
- CIDR range and exact IP whitelisting
- Redis-backed 404 path tracking with 60s TTL
- Cloudflare WAF integration for permanent bans
- Admin UI for whitelist and blocked IP management
- Oban cron jobs for cleanup (expired bans, stale violations)
- ETS caching for whitelist performance

Components:
- Plug: ToweropsWeb.Plugs.BruteForceProtection
- Context: Towerops.Security.BruteForce
- Schemas: IpBlock, IpWhitelist
- Workers: CloudflareBanWorker, ExpiredBanCleanupWorker, StaleViolationCleanupWorker
- Admin UI: /admin/security (superuser only)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 16:31:48 -06:00

52 lines
1.3 KiB
Elixir

defmodule Towerops.Security.IpWhitelist do
@moduledoc """
Schema for tracking IP addresses and CIDR ranges that are exempt from brute force protection.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "ip_whitelist" do
field :ip_or_cidr, :string
field :description, :string
belongs_to :added_by, Towerops.Accounts.User
timestamps(type: :utc_datetime)
end
@doc false
def changeset(ip_whitelist, attrs) do
ip_whitelist
|> cast(attrs, [:ip_or_cidr, :description, :added_by_id])
|> validate_required([:ip_or_cidr])
|> validate_ip_or_cidr()
|> unique_constraint(:ip_or_cidr)
end
defp validate_ip_or_cidr(changeset) do
validate_change(changeset, :ip_or_cidr, fn :ip_or_cidr, value ->
validate_ip_or_cidr_value(value)
end)
end
defp validate_ip_or_cidr_value(value) do
# Check if it's a CIDR range
if String.contains?(value, "/") do
case InetCidr.parse_cidr(value) do
{:ok, _} -> []
{:error, _} -> [ip_or_cidr: "is not a valid CIDR range"]
end
else
# Check if it's a valid IP address
case :inet.parse_address(String.to_charlist(value)) do
{:ok, _} -> []
{:error, _} -> [ip_or_cidr: "is not a valid IP address or CIDR range"]
end
end
end
end