| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>IRDB — IP Reputation Database, in plain English</title>
- <meta name="description" content="A self-hosted way to share known-bad IP addresses across all your servers and firewalls. One place to collect, one tailored list per system to defend.">
- <link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
- <style>
- :root {
- --bg: #f8fafc;
- --bg-card: #ffffff;
- --bg-soft: #f1f5f9;
- --border: #e2e8f0;
- --text: #0f172a;
- --text-muted: #64748b;
- --text-dim: #94a3b8;
- --accent: #10b981;
- --accent-strong: #059669;
- --accent-soft: #d1fae5;
- --indigo: #6366f1;
- --indigo-strong: #4f46e5;
- --shadow: 0 1px 3px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.04);
- --shadow-lg: 0 10px 25px -5px rgba(15, 23, 42, 0.10), 0 8px 10px -6px rgba(15, 23, 42, 0.05);
- }
- @media (prefers-color-scheme: dark) {
- :root {
- --bg: #020617;
- --bg-card: #0f172a;
- --bg-soft: #1e293b;
- --border: #1e293b;
- --text: #f1f5f9;
- --text-muted: #94a3b8;
- --text-dim: #64748b;
- --accent-soft: #064e3b;
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
- --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
- }
- }
- * { box-sizing: border-box; }
- html { scroll-behavior: smooth; }
- body {
- margin: 0;
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: var(--bg);
- color: var(--text);
- line-height: 1.6;
- -webkit-font-smoothing: antialiased;
- }
- .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
- /* Layout */
- .wrap { max-width: 1100px; margin: 0 auto; padding: 0 1.25rem; }
- .nav {
- position: sticky; top: 0; z-index: 50;
- background: color-mix(in oklab, var(--bg) 85%, transparent);
- backdrop-filter: saturate(180%) blur(8px);
- border-bottom: 1px solid var(--border);
- }
- .nav-inner {
- display: flex; align-items: center; justify-content: space-between;
- padding: 0.85rem 1.25rem; max-width: 1100px; margin: 0 auto;
- }
- .nav-brand { display: flex; align-items: center; gap: 0.6rem; text-decoration: none; color: var(--text); }
- .nav-brand img { width: 28px; height: 28px; }
- .nav-brand strong { font-family: ui-monospace, monospace; letter-spacing: -0.02em; font-size: 1.05rem; }
- .nav-links { display: flex; gap: 1.25rem; align-items: center; font-size: 0.9rem; }
- .nav-links a { color: var(--text-muted); text-decoration: none; }
- .nav-links a:hover { color: var(--text); }
- .nav-cta {
- background: var(--indigo); color: #fff !important;
- padding: 0.45rem 0.9rem; border-radius: 0.4rem; font-weight: 600;
- }
- .nav-cta:hover { background: var(--indigo-strong); }
- /* Hero */
- .hero {
- padding: 4.5rem 0 3rem;
- background: radial-gradient(circle at 70% 20%, color-mix(in oklab, var(--accent) 10%, transparent), transparent 60%);
- }
- .hero-grid { display: grid; grid-template-columns: 1.3fr 1fr; gap: 3rem; align-items: center; }
- @media (max-width: 820px) { .hero-grid { grid-template-columns: 1fr; } }
- .eyebrow {
- display: inline-block; font-size: 0.75rem; font-weight: 600;
- color: var(--accent-strong); background: var(--accent-soft);
- padding: 0.3rem 0.65rem; border-radius: 999px; letter-spacing: 0.05em;
- text-transform: uppercase; margin-bottom: 1rem;
- }
- h1 {
- font-size: clamp(2rem, 4.5vw, 3rem); line-height: 1.1; letter-spacing: -0.02em;
- margin: 0 0 1rem; font-weight: 700;
- }
- .lede { font-size: 1.15rem; color: var(--text-muted); margin: 0 0 1.5rem; max-width: 36rem; }
- .cta-row { display: flex; gap: 0.75rem; flex-wrap: wrap; }
- .btn {
- display: inline-flex; align-items: center; gap: 0.5rem;
- padding: 0.75rem 1.25rem; border-radius: 0.5rem;
- font-weight: 600; font-size: 0.95rem; text-decoration: none;
- transition: transform 0.1s ease;
- }
- .btn:active { transform: translateY(1px); }
- .btn-primary { background: var(--indigo); color: #fff; }
- .btn-primary:hover { background: var(--indigo-strong); }
- .btn-ghost { background: var(--bg-card); color: var(--text); border: 1px solid var(--border); }
- .btn-ghost:hover { background: var(--bg-soft); }
- .hero-shot {
- border-radius: 0.85rem; border: 1px solid var(--border);
- box-shadow: var(--shadow-lg); overflow: hidden; background: var(--bg-card);
- }
- .hero-shot img { width: 100%; height: auto; display: block; }
- /* Sections */
- section { padding: 4rem 0; }
- section.alt { background: var(--bg-soft); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
- h2 {
- font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.015em;
- margin: 0 0 0.75rem; font-weight: 700;
- }
- .section-lede { color: var(--text-muted); max-width: 42rem; margin: 0 0 2.5rem; font-size: 1.05rem; }
- /* Cards / grids */
- .card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }
- @media (max-width: 820px) { .card-grid { grid-template-columns: 1fr; } }
- .card {
- background: var(--bg-card); border: 1px solid var(--border);
- border-radius: 0.85rem; padding: 1.5rem; box-shadow: var(--shadow);
- }
- .card .icon {
- width: 40px; height: 40px; border-radius: 0.5rem;
- background: var(--accent-soft); color: var(--accent-strong);
- display: inline-flex; align-items: center; justify-content: center;
- font-size: 1.25rem; margin-bottom: 1rem;
- }
- .card h3 { margin: 0 0 0.4rem; font-size: 1.1rem; }
- .card p { margin: 0; color: var(--text-muted); font-size: 0.95rem; }
- /* Workflow steps */
- .steps { counter-reset: step; display: grid; gap: 1rem; }
- .step {
- display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; align-items: start;
- padding: 1.5rem; background: var(--bg-card); border: 1px solid var(--border);
- border-radius: 0.85rem; box-shadow: var(--shadow);
- }
- .step-num {
- counter-increment: step;
- width: 40px; height: 40px; border-radius: 50%;
- background: var(--indigo); color: #fff;
- display: flex; align-items: center; justify-content: center;
- font-weight: 700; font-size: 1.05rem;
- }
- .step-num::before { content: counter(step); }
- .step h3 { margin: 0 0 0.35rem; font-size: 1.1rem; }
- .step p { margin: 0; color: var(--text-muted); }
- /* Screenshot showcase */
- .show {
- display: grid; grid-template-columns: 5fr 4fr; gap: 2.5rem;
- align-items: center; margin-bottom: 3rem;
- }
- .show.flip { grid-template-columns: 4fr 5fr; }
- .show.flip .show-img { order: -1; }
- @media (max-width: 820px) {
- .show, .show.flip { grid-template-columns: 1fr; gap: 1.5rem; }
- .show.flip .show-img { order: 0; }
- }
- .show h3 { margin: 0 0 0.6rem; font-size: 1.35rem; letter-spacing: -0.01em; }
- .show p { color: var(--text-muted); margin: 0 0 0.75rem; }
- .show ul { color: var(--text-muted); margin: 0; padding-left: 1.1rem; }
- .show li { margin: 0.25rem 0; }
- .show-img {
- border-radius: 0.75rem; border: 1px solid var(--border);
- box-shadow: var(--shadow-lg); overflow: hidden; background: var(--bg-card);
- }
- .show-img img { width: 100%; height: auto; display: block; }
- /* Install block */
- .install-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: center; }
- @media (max-width: 820px) { .install-grid { grid-template-columns: 1fr; } }
- pre.code {
- background: #0f172a; color: #e2e8f0;
- padding: 1.25rem; border-radius: 0.75rem; overflow-x: auto;
- font-size: 0.85rem; line-height: 1.55; margin: 0;
- border: 1px solid #1e293b;
- }
- pre.code .c { color: #64748b; }
- pre.code .k { color: #6ee7b7; }
- /* Pillars / value props */
- .pill-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
- @media (max-width: 820px) { .pill-grid { grid-template-columns: repeat(2, 1fr); } }
- @media (max-width: 480px) { .pill-grid { grid-template-columns: 1fr; } }
- .pill {
- padding: 1.1rem; background: var(--bg-card); border: 1px solid var(--border);
- border-radius: 0.65rem; text-align: center;
- }
- .pill .num { display: block; font-size: 1.5rem; font-weight: 700; color: var(--accent-strong); }
- .pill .lbl { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; display: block; }
- /* Footer */
- footer {
- border-top: 1px solid var(--border); padding: 2.5rem 0;
- color: var(--text-muted); font-size: 0.9rem;
- }
- .foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 2rem; }
- @media (max-width: 820px) { .foot-grid { grid-template-columns: 1fr; } }
- footer a { color: var(--text-muted); text-decoration: none; }
- footer a:hover { color: var(--text); text-decoration: underline; }
- .foot-brand { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.5rem; }
- .foot-brand img { width: 32px; height: 32px; }
- .foot-brand strong { color: var(--text); font-family: ui-monospace, monospace; }
- footer h4 { color: var(--text); font-size: 0.85rem; margin: 0 0 0.6rem; text-transform: uppercase; letter-spacing: 0.05em; }
- footer ul { list-style: none; padding: 0; margin: 0; }
- footer li { margin: 0.35rem 0; }
- .foot-license {
- margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border);
- font-size: 0.85rem; color: var(--text-dim);
- }
- /* Inline highlights */
- .accent { color: var(--accent-strong); font-weight: 600; }
- /* Zoomable images / diagrams */
- .zoomable {
- cursor: zoom-in;
- transition: transform 0.15s ease, box-shadow 0.15s ease;
- position: relative;
- }
- .zoomable:hover { transform: translateY(-2px); }
- .zoomable:hover .zoom-hint { opacity: 1; }
- .zoomable:focus-visible { outline: 3px solid var(--indigo); outline-offset: 4px; }
- .zoom-hint {
- position: absolute; top: 0.6rem; right: 0.6rem;
- background: rgba(15, 23, 42, 0.78); color: #fff;
- font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
- padding: 0.3rem 0.55rem; border-radius: 999px;
- opacity: 0; transition: opacity 0.15s ease;
- pointer-events: none; backdrop-filter: blur(4px);
- display: inline-flex; align-items: center; gap: 0.3rem;
- }
- /* Flow diagram container */
- .flow-diagram-card {
- border-radius: 0.85rem; border: 1px solid var(--border);
- box-shadow: var(--shadow); background: var(--bg-card);
- padding: 1rem 1rem 0.5rem; margin: 0 auto 2.5rem;
- max-width: 100%;
- }
- .flow-diagram-svg { display: block; width: 100%; height: auto; }
- .flow-diagram-legend {
- display: flex; flex-wrap: wrap; gap: 1.25rem; justify-content: center;
- padding: 0.5rem 0 0.75rem; font-size: 0.85rem; color: var(--text-muted);
- }
- .flow-diagram-legend .lg-item { display: inline-flex; align-items: center; gap: 0.45rem; }
- .flow-diagram-legend .lg-dot {
- width: 14px; height: 3px; border-radius: 2px; display: inline-block;
- }
- .lg-dot.dot-emerald { background: #10b981; }
- .lg-dot.dot-indigo { background: #6366f1; }
- /* Zoom modal */
- .zoom-modal {
- position: fixed; inset: 0; z-index: 100;
- background: rgba(2, 6, 23, 0.9);
- display: none;
- align-items: center; justify-content: center;
- padding: 2rem;
- backdrop-filter: blur(6px);
- }
- .zoom-modal[aria-hidden="false"] { display: flex; }
- .zoom-modal-content {
- position: relative;
- max-width: 96vw; max-height: 92vh;
- display: flex; align-items: center; justify-content: center;
- background: var(--bg-card); border-radius: 0.75rem;
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
- padding: 1rem;
- overflow: auto;
- }
- .zoom-modal-content > img,
- .zoom-modal-content > svg {
- display: block;
- max-width: calc(96vw - 2rem);
- max-height: calc(92vh - 2rem);
- width: auto; height: auto;
- border-radius: 0.5rem;
- }
- .zoom-close {
- position: absolute; top: -3rem; right: 0;
- background: rgba(255, 255, 255, 0.12);
- color: #fff;
- border: 1px solid rgba(255, 255, 255, 0.25);
- width: 40px; height: 40px; border-radius: 50%;
- font-size: 1.4rem; line-height: 1; cursor: pointer;
- display: flex; align-items: center; justify-content: center;
- transition: background 0.15s ease;
- }
- .zoom-close:hover { background: rgba(255, 255, 255, 0.22); }
- @media (max-width: 600px) {
- .zoom-modal { padding: 1rem; }
- .zoom-close { top: 0.4rem; right: 0.4rem; }
- }
- </style>
- </head>
- <body>
- <nav class="nav">
- <div class="nav-inner">
- <a class="nav-brand" href="/about">
- <img src="/assets/logo.svg" alt="">
- <strong>IRDB</strong>
- </a>
- <div class="nav-links">
- <a href="#what">What it does</a>
- <a href="#workflow">How it works</a>
- <a href="#tour">Tour</a>
- <a href="#install">Install</a>
- <a class="nav-cta" href="/login">Sign in</a>
- </div>
- </div>
- </nav>
- <header class="hero">
- <div class="wrap hero-grid">
- <div>
- <span class="eyebrow">IP Reputation Database</span>
- <h1>One shared memory of bad actors — for every server you protect.</h1>
- <p class="lede">
- IRDB collects abuse reports from your web servers, mail servers and intrusion detectors,
- then hands every firewall and proxy a <strong>tailored block list</strong> shaped to its needs.
- One place to gather signal. One list per system to defend. No more copy-paste between machines.
- </p>
- <div class="cta-row">
- <a class="btn btn-primary" href="#workflow">See how it works</a>
- <a class="btn btn-ghost" href="https://git.chiapparini.org/chiappa/irdb">View source on Git</a>
- </div>
- </div>
- <div class="hero-shot zoomable" data-zoom-label="Dashboard">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/Dashboard.png" alt="IRDB dashboard showing real-time activity, top threat categories, and ingest volume.">
- </div>
- </div>
- </header>
- <section id="what">
- <div class="wrap">
- <h2>What IRDB solves</h2>
- <p class="section-lede">
- Most teams already block bad IP addresses — but each server, each firewall, each plugin keeps its own list.
- Lists go stale, knowledge stays trapped in one machine, and an attack already seen by your web server
- walks straight through your mail gateway. IRDB turns that scattered information into one shared,
- up-to-date defence layer.
- </p>
- <div class="card-grid">
- <div class="card">
- <span class="icon">●</span>
- <h3>Collect from anywhere</h3>
- <p>Web servers, mail servers, fail2ban, intrusion detectors, custom scripts — anything that spots an attack can report it through one simple endpoint.</p>
- </div>
- <div class="card">
- <span class="icon">▲</span>
- <h3>Score, don't just list</h3>
- <p>Each report has a weight, an age and a category. IRDB turns those into a reputation score per IP that fades over time, so old noise drops away on its own.</p>
- </div>
- <div class="card">
- <span class="icon">◆</span>
- <h3>Shape lists per consumer</h3>
- <p>Your edge firewall might want strict blocking, your mail relay only spam-related sources. Each consumer gets a list tailored by a named policy you control.</p>
- </div>
- </div>
- </div>
- </section>
- <section class="alt" id="workflow">
- <div class="wrap">
- <h2>How it fits your environment</h2>
- <p class="section-lede">
- IRDB sits in the middle of two flows you already have: machines that <em>see</em> attacks,
- and machines that <em>block</em> them. Below is the same picture, in plain steps.
- </p>
- <div class="flow-diagram-card zoomable" data-zoom-label="How IRDB connects reporters and consumers" role="button" tabindex="0" aria-label="Open enlarged flow diagram">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <svg viewBox="0 0 1120 560" xmlns="http://www.w3.org/2000/svg"
- class="flow-diagram-svg" role="img"
- aria-labelledby="flowDiagramTitle flowDiagramDesc">
- <title id="flowDiagramTitle">How IRDB connects reporters and consumers</title>
- <desc id="flowDiagramDesc">Reporters send abuse reports to IRDB. IRDB scores reports and applies policies. Consumers pull tailored block lists. Operators monitor and refine.</desc>
- <style>
- .fd-node {
- fill: var(--bg-card, #ffffff);
- stroke: var(--border, #e2e8f0);
- stroke-width: 1.5;
- }
- .fd-card {
- fill: var(--bg-soft, #f1f5f9);
- stroke: var(--border, #e2e8f0);
- stroke-width: 1;
- }
- .fd-cluster-title {
- font: 600 17px ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- fill: var(--text, #0f172a);
- }
- .fd-cluster-sub {
- font: 13px ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- fill: var(--text-muted, #64748b);
- }
- .fd-card-text {
- font: 500 14px ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- fill: var(--text, #0f172a);
- }
- .fd-arrow-label {
- font: 600 13px ui-sans-serif, system-ui, sans-serif;
- fill: var(--text-muted, #64748b);
- }
- .fd-arrow { fill: none; stroke-width: 2.5; }
- .fd-arrow.emerald { stroke: #10b981; marker-end: url(#fd-arrow-emerald); }
- .fd-arrow.indigo { stroke: #6366f1; marker-end: url(#fd-arrow-indigo); }
- .fd-link {
- fill: none; stroke: #94a3b8; stroke-width: 2;
- stroke-dasharray: 5 4;
- marker-start: url(#fd-arrow-muted-rev);
- marker-end: url(#fd-arrow-muted);
- }
- .fd-badge { fill: #6366f1; }
- .fd-badge.emerald { fill: #10b981; }
- .fd-badge.invert { fill: #ffffff; stroke: #6366f1; stroke-width: 2; }
- .fd-badge-stroke { stroke: var(--bg-card, #fff); stroke-width: 2; }
- .fd-badge-num {
- font: 700 14px ui-sans-serif, system-ui, sans-serif;
- fill: #ffffff;
- text-anchor: middle;
- dominant-baseline: central;
- }
- .fd-badge-num.indigo-num { fill: #4f46e5; }
- .fd-irdb-bg { fill: #6366f1; }
- .fd-irdb-title {
- font: 700 26px ui-monospace, SFMono-Regular, Menlo, monospace;
- fill: #ffffff;
- text-anchor: middle;
- letter-spacing: -0.02em;
- }
- .fd-irdb-sub {
- font: 13px ui-sans-serif, system-ui, sans-serif;
- fill: rgba(255,255,255,0.85);
- text-anchor: middle;
- }
- .fd-chip {
- fill: rgba(255,255,255,0.18);
- stroke: rgba(255,255,255,0.4);
- stroke-width: 1;
- }
- .fd-chip-text {
- font: 600 12px ui-sans-serif, system-ui, sans-serif;
- fill: #ffffff;
- text-anchor: middle;
- dominant-baseline: central;
- }
- .fd-icon { fill: #94a3b8; }
- </style>
- <defs>
- <marker id="fd-arrow-emerald" viewBox="0 0 12 12" refX="10" refY="6" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
- <path d="M 0 0 L 12 6 L 0 12 z" fill="#10b981"/>
- </marker>
- <marker id="fd-arrow-indigo" viewBox="0 0 12 12" refX="10" refY="6" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
- <path d="M 0 0 L 12 6 L 0 12 z" fill="#6366f1"/>
- </marker>
- <marker id="fd-arrow-muted" viewBox="0 0 12 12" refX="10" refY="6" markerWidth="6" markerHeight="6" orient="auto">
- <path d="M 0 0 L 12 6 L 0 12 z" fill="#94a3b8"/>
- </marker>
- <marker id="fd-arrow-muted-rev" viewBox="0 0 12 12" refX="2" refY="6" markerWidth="6" markerHeight="6" orient="auto">
- <path d="M 12 0 L 0 6 L 12 12 z" fill="#94a3b8"/>
- </marker>
- <symbol id="fd-server-icon" viewBox="0 0 30 30">
- <rect x="0" y="2" width="30" height="9" rx="2" fill="#94a3b8" fill-opacity="0.25"/>
- <rect x="0" y="14" width="30" height="9" rx="2" fill="#94a3b8" fill-opacity="0.25"/>
- <circle cx="5" cy="6.5" r="1.4" fill="#94a3b8"/>
- <circle cx="5" cy="18.5" r="1.4" fill="#94a3b8"/>
- <rect x="11" y="5" width="14" height="1.5" rx="0.5" fill="#94a3b8"/>
- <rect x="11" y="17" width="14" height="1.5" rx="0.5" fill="#94a3b8"/>
- </symbol>
- </defs>
- <!-- Reporters cluster (1) -->
- <g transform="translate(20,80)">
- <rect class="fd-node" x="0" y="0" width="240" height="300" rx="16"/>
- <text class="fd-cluster-title" x="120" y="34" text-anchor="middle">Reporters</text>
- <text class="fd-cluster-sub" x="120" y="54" text-anchor="middle">things that see attacks</text>
- <g transform="translate(20,80)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Web servers</text>
- </g>
- <g transform="translate(20,140)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Mail relays</text>
- </g>
- <g transform="translate(20,200)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Fail2ban / IDS</text>
- </g>
- <g transform="translate(220,-12)">
- <circle class="fd-badge fd-badge-stroke" r="18"/>
- <text class="fd-badge-num">1</text>
- </g>
- </g>
- <!-- Reports arrow (2) -->
- <text class="fd-arrow-label" x="365" y="218" text-anchor="middle">reports</text>
- <path class="fd-arrow emerald" d="M 270 230 L 458 230"/>
- <g transform="translate(365,258)">
- <circle class="fd-badge emerald fd-badge-stroke" r="14"/>
- <text class="fd-badge-num">2</text>
- </g>
- <!-- IRDB center (3) -->
- <g transform="translate(470,140)">
- <rect class="fd-irdb-bg" width="180" height="180" rx="18"/>
- <text class="fd-irdb-title" x="90" y="62">IRDB</text>
- <text class="fd-irdb-sub" x="90" y="84">score engine</text>
- <rect class="fd-chip" x="22" y="118" width="136" height="36" rx="10"/>
- <text class="fd-chip-text" x="90" y="136">policies</text>
- <g transform="translate(155,118)">
- <circle class="fd-badge invert" r="14"/>
- <text class="fd-badge-num indigo-num">3</text>
- </g>
- </g>
- <!-- Block list arrow -->
- <text class="fd-arrow-label" x="755" y="218" text-anchor="middle">tailored block list</text>
- <path class="fd-arrow indigo" d="M 660 230 L 848 230"/>
- <!-- Consumers cluster (4) -->
- <g transform="translate(860,80)">
- <rect class="fd-node" x="0" y="0" width="240" height="300" rx="16"/>
- <text class="fd-cluster-title" x="120" y="34" text-anchor="middle">Consumers</text>
- <text class="fd-cluster-sub" x="120" y="54" text-anchor="middle">things that block</text>
- <g transform="translate(20,80)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Firewalls</text>
- </g>
- <g transform="translate(20,140)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Proxies</text>
- </g>
- <g transform="translate(20,200)">
- <rect class="fd-card" width="200" height="44" rx="10"/>
- <use href="#fd-server-icon" x="10" y="7" width="28" height="28"/>
- <text class="fd-card-text" x="50" y="28">Load balancers</text>
- </g>
- <g transform="translate(220,-12)">
- <circle class="fd-badge fd-badge-stroke" r="18"/>
- <text class="fd-badge-num">4</text>
- </g>
- </g>
- <!-- Operators (5) -->
- <g transform="translate(440,420)">
- <rect class="fd-node" width="240" height="80" rx="16"/>
- <text class="fd-cluster-title" x="120" y="36" text-anchor="middle">Operators</text>
- <text class="fd-cluster-sub" x="120" y="58" text-anchor="middle">monitor · refine · override</text>
- <g transform="translate(220,-12)">
- <circle class="fd-badge fd-badge-stroke" r="18"/>
- <text class="fd-badge-num">5</text>
- </g>
- </g>
- <!-- IRDB ↔ operator dashed connector -->
- <path class="fd-link" d="M 560 332 L 560 410"/>
- </svg>
- <div class="flow-diagram-legend">
- <span class="lg-item"><span class="lg-dot dot-emerald"></span> reports flow in</span>
- <span class="lg-item"><span class="lg-dot dot-indigo"></span> tailored block list goes out</span>
- <span class="lg-item">numbered dots match the steps below</span>
- </div>
- </div>
- <div class="steps">
- <div class="step">
- <div class="step-num"></div>
- <div>
- <h3>Identify your reporters</h3>
- <p>Every server, service or device that can detect an attack is a <strong>reporter</strong> — for example a web server seeing brute-force attempts, a mail relay spotting spam, or an intrusion detector. Each reporter gets a name, a trust weight (how much you believe its reports) and a token.</p>
- </div>
- </div>
- <div class="step">
- <div class="step-num"></div>
- <div>
- <h3>Send reports as events happen</h3>
- <p>When a reporter spots a bad IP, it sends a small message to IRDB: which IP, which kind of attack, and any helpful context. The work for your operations team is one-time: drop a small script or plugin into the reporter, and it streams events from then on.</p>
- </div>
- </div>
- <div class="step">
- <div class="step-num"></div>
- <div>
- <h3>Define policies for who gets what</h3>
- <p>A <strong>policy</strong> describes the kind of list a system should receive — for example, "block anything with high spam score" or "block strict: anything reported for brute force, scanning, or web attacks". You design these once with your team, and adjust thresholds visually at any time.</p>
- </div>
- </div>
- <div class="step">
- <div class="step-num"></div>
- <div>
- <h3>Connect your consumers</h3>
- <p>Every firewall, proxy or load balancer that should defend itself becomes a <strong>consumer</strong>. You assign it a policy and give it a token. From then on, the consumer pulls a fresh, tailored block list at an interval you choose (typically every minute or two).</p>
- </div>
- </div>
- <div class="step">
- <div class="step-num"></div>
- <div>
- <h3>Watch, refine, override</h3>
- <p>The dashboard shows what is being seen and what is being blocked. Operators can promote individual IPs to permanent blocks, allowlist trusted partners to never be blocked by mistake, and inspect the full history of any address. Every change is recorded in an audit log.</p>
- </div>
- </div>
- </div>
- </div>
- </section>
- <section id="tour">
- <div class="wrap">
- <h2>A quick tour of the interface</h2>
- <p class="section-lede">
- The web interface is built for daily use by ops and security teams — clear lists, fast search,
- visual feedback, light and dark mode. Here is what you actually see when you log in.
- </p>
- <div class="show">
- <div>
- <h3>Dashboard — what's happening right now</h3>
- <p>One glance gives you the live picture: how many reports are flowing in, which categories of attack dominate today, which sources are the busiest, and which consumers are actually pulling their lists.</p>
- <ul>
- <li>Real-time activity counts</li>
- <li>Top categories and reporters at a glance</li>
- <li>Health indicators for background jobs</li>
- </ul>
- </div>
- <div class="show-img zoomable" data-zoom-label="Dashboard">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/Dashboard.png" alt="IRDB dashboard">
- </div>
- </div>
- <div class="show flip">
- <div>
- <h3>IP list — search and filter the whole population</h3>
- <p>Find any address by score, country, network owner or attack category. Sort, filter and drill in with one click. Useful for incident response ("did we ever see this IP before?") and for the ops team's monthly reviews.</p>
- <ul>
- <li>Filter by category, country, ASN, score range</li>
- <li>Visible reputation per address, decayed by age</li>
- <li>Quick links into the full per-IP history</li>
- </ul>
- </div>
- <div class="show-img zoomable" data-zoom-label="IP list">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/IP_list.png" alt="IP list view with filters and reputation scores">
- </div>
- </div>
- <div class="show">
- <div>
- <h3>IP details — the full story per address</h3>
- <p>Open any IP to see who reported it, when, for what, with what context. You see the score per category, geographical and network owner information, manual block status, and the complete timeline of events.</p>
- <ul>
- <li>Per-category reputation with decay</li>
- <li>Country and network owner enrichment</li>
- <li>Full audit trail of reports and admin actions</li>
- </ul>
- </div>
- <div class="show-img zoomable" data-zoom-label="IP details">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/IP_details.png" alt="IP details with score breakdown and history">
- </div>
- </div>
- <div class="show flip">
- <div>
- <h3>Policies — design lists visually</h3>
- <p>A policy is just a recipe: which categories matter, and how strict the threshold is. Adjust them with a slider, see immediately how many addresses would be on the resulting list, and ship the change to all matching consumers without a deploy.</p>
- <ul>
- <li>Per-category thresholds with live preview</li>
- <li>Optional inclusion of manual blocks</li>
- <li>Score distribution chart to set thresholds confidently</li>
- </ul>
- </div>
- <div class="show-img zoomable" data-zoom-label="Policy editor">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/Policies_edit.png" alt="Policy editor with thresholds and preview">
- </div>
- </div>
- <div class="show">
- <div>
- <h3>Audit log — full accountability</h3>
- <p>Every administrative action — token created, IP allowlisted, policy edited, user role changed — is recorded with who, when and what. The audit log keeps your team aligned with internal compliance and gives security a clean trail when something needs review.</p>
- <ul>
- <li>Tamper-evident, append-only history</li>
- <li>Filter by actor, action or target</li>
- <li>Configurable retention to match your policy</li>
- </ul>
- </div>
- <div class="show-img zoomable" data-zoom-label="Audit log">
- <span class="zoom-hint" aria-hidden="true">↗ Click to enlarge</span>
- <img src="/assets/screenshots/Audit_log.png" alt="Audit log table">
- </div>
- </div>
- </div>
- </section>
- <section class="alt">
- <div class="wrap">
- <h2>Why teams choose IRDB</h2>
- <p class="section-lede">
- Light to run, simple to integrate, no vendor lock-in.
- </p>
- <div class="pill-grid">
- <div class="pill">
- <span class="num">100%</span>
- <span class="lbl">Self-hosted, your data stays with you</span>
- </div>
- <div class="pill">
- <span class="num">~5 min</span>
- <span class="lbl">From clone to a running stack</span>
- </div>
- <div class="pill">
- <span class="num">1</span>
- <span class="lbl">Endpoint to report — language-agnostic</span>
- </div>
- <div class="pill">
- <span class="num">∞</span>
- <span class="lbl">Reporters and consumers, no per-seat fee</span>
- </div>
- </div>
- </div>
- </section>
- <section id="install">
- <div class="wrap">
- <h2>Quick install</h2>
- <p class="section-lede">
- IRDB ships as a Docker Compose stack. If you have Docker, you have everything you need.
- The default storage is a small embedded database — no external services required to get started.
- </p>
- <div class="install-grid">
- <div>
- <h3 style="margin-top:0;">In one terminal</h3>
- <p style="color:var(--text-muted);">Three commands. Then visit <span class="mono accent">http://localhost:8080</span> to log in.</p>
- <p style="color:var(--text-muted); font-size: 0.95rem;">
- Source code, full documentation and release notes live at
- <a href="https://git.chiapparini.org/chiappa/irdb" class="accent" style="text-decoration:none;">git.chiapparini.org/chiappa/irdb</a>.
- </p>
- </div>
- <div>
- <pre class="code"><span class="c"># 1. clone</span>
- git clone https://git.chiapparini.org/chiappa/irdb
- <span class="k">cd</span> irdb
- <span class="c"># 2. configure (generate a few secrets)</span>
- cp .env.example .env
- $EDITOR .env
- <span class="c"># 3. run</span>
- docker compose up -d
- </pre>
- </div>
- </div>
- </div>
- </section>
- <footer>
- <div class="wrap foot-grid">
- <div>
- <div class="foot-brand">
- <img src="/assets/logo.svg" alt="">
- <strong>IRDB — IP Reputation Database</strong>
- </div>
- <p style="margin:0 0 0.25rem;">A self-hosted central service for collecting abuse reports and distributing tailored block lists.</p>
- </div>
- <div>
- <h4>Project</h4>
- <ul>
- <li><a href="https://git.chiapparini.org/chiappa/irdb">Source code</a></li>
- <li><a href="https://git.chiapparini.org/chiappa/irdb">Documentation</a></li>
- <li><a href="https://git.chiapparini.org/chiappa/irdb">Releases</a></li>
- </ul>
- </div>
- <div>
- <h4>Get started</h4>
- <ul>
- <li><a href="#install">Quick install</a></li>
- <li><a href="#workflow">Integration steps</a></li>
- <li><a href="/login">Sign in</a></li>
- </ul>
- </div>
- </div>
- <div class="wrap foot-license">
- Released under the <strong>Apache License 2.0</strong>. Free for commercial and private use, modification and redistribution.
- Copyright © 2026 Alessandro Chiapparini. See the <span class="mono">LICENSE</span> and <span class="mono">NOTICE</span> files in the repository for details.
- </div>
- </footer>
- <div id="zoom-modal" class="zoom-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="zoom-modal-label">
- <div class="zoom-modal-content" role="document">
- <button type="button" class="zoom-close" aria-label="Close enlarged view">×</button>
- <span id="zoom-modal-label" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)"></span>
- </div>
- </div>
- <script>
- (function () {
- const modal = document.getElementById('zoom-modal');
- if (!modal) return;
- const content = modal.querySelector('.zoom-modal-content');
- const closeBtn = modal.querySelector('.zoom-close');
- const label = modal.querySelector('#zoom-modal-label');
- let lastFocused = null;
- function openZoom(source) {
- // Drop any previously cloned media (keep close button + label).
- Array.from(content.children).forEach((child) => {
- if (child !== closeBtn && child !== label) child.remove();
- });
- const media = source.querySelector('svg, img') || source;
- if (!media) return;
- const clone = media.cloneNode(true);
- if (clone.tagName === 'IMG') {
- clone.removeAttribute('style');
- clone.removeAttribute('class');
- }
- content.appendChild(clone);
- label.textContent = source.getAttribute('data-zoom-label') || 'Enlarged view';
- modal.setAttribute('aria-hidden', 'false');
- document.documentElement.style.overflow = 'hidden';
- lastFocused = document.activeElement;
- closeBtn.focus();
- }
- function closeZoom() {
- modal.setAttribute('aria-hidden', 'true');
- document.documentElement.style.overflow = '';
- Array.from(content.children).forEach((child) => {
- if (child !== closeBtn && child !== label) child.remove();
- });
- if (lastFocused && typeof lastFocused.focus === 'function') {
- lastFocused.focus();
- }
- }
- modal.addEventListener('click', (e) => {
- if (e.target === modal) closeZoom();
- });
- closeBtn.addEventListener('click', closeZoom);
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && modal.getAttribute('aria-hidden') === 'false') {
- closeZoom();
- }
- });
- document.querySelectorAll('.zoomable').forEach((el) => {
- if (!el.hasAttribute('role')) el.setAttribute('role', 'button');
- if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
- el.addEventListener('click', (e) => {
- // Don't trigger when clicking nested links.
- if (e.target.closest('a, button:not(.zoom-close)')) return;
- openZoom(el);
- });
- el.addEventListener('keydown', (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- openZoom(el);
- }
- });
- });
- })();
- </script>
- </body>
- </html>
|