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>
52 lines
1.3 KiB
Elixir
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
|