151 lines
10 KiB
JSON
151 lines
10 KiB
JSON
|
|
{
|
||
|
|
"annotations": {"list": [{"builtIn": 1,"datasource": {"type": "datasource","uid": "grafana"},"enable": true,"hide": true,"iconColor": "rgba(0, 211, 255, 1)","name": "Annotations & Alerts","type": "dashboard"}]},
|
||
|
|
"description": "RPKI (Resource Public Key Infrastructure) validation status. Teaches BGP routing security and how RPKI prevents prefix hijacks by validating route origin.",
|
||
|
|
"editable": true,
|
||
|
|
"fiscalYearStartMonth": 0,
|
||
|
|
"graphTooltip": 1,
|
||
|
|
"id": null,
|
||
|
|
"links": [],
|
||
|
|
"panels": [
|
||
|
|
{
|
||
|
|
"content": "## What is RPKI?\n\nRPKI (Resource Public Key Infrastructure) is a cryptographic security framework for BGP routing. It lets IP address holders publish **Route Origin Authorizations (ROAs)** stating which ASNs are authorized to originate their prefixes.\n\n### RPKI Validation States\n| State | Meaning |\n|-------|----------|\n| **Valid** | The route's origin AS matches a ROA for this prefix |\n| **Invalid** | A ROA exists but the origin AS or prefix length does NOT match — this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin — unprotected, can't be validated |\n\n### How to read this dashboard\n- **Valid %** should be as high as possible (target: 100%)\n- **Invalid routes** are critical — they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** — they are not necessarily invalid, just unprotected\n\n> **Lab note:** The RPKI validator table is populated by a cron job in psql-app every 2 hours. If the table shows 0 rows, wait for the cron to run or check `ENABLE_RPKI=1` in docker-compose.yml.",
|
||
|
|
"datasource": {"type": "datasource","uid": "grafana"},
|
||
|
|
"gridPos": {"h": 10,"w": 8,"x": 0,"y": 0},
|
||
|
|
"id": 1,
|
||
|
|
"options": {"content": "## What is RPKI?\n\nRPKI (Resource Public Key Infrastructure) is a cryptographic security framework for BGP routing. It lets IP address holders publish **Route Origin Authorizations (ROAs)** stating which ASNs are authorized to originate their prefixes.\n\n### RPKI Validation States\n| State | Meaning |\n|-------|----------|\n| **Valid** | The route's origin AS matches a ROA for this prefix |\n| **Invalid** | A ROA exists but the origin AS or prefix length does NOT match — this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin — unprotected, can't be validated |\n\n### How to read this dashboard\n- **Valid %** should be as high as possible (target: 100%)\n- **Invalid routes** are critical — they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** — they are not necessarily invalid, just unprotected\n\n> **Lab note:** The RPKI validator table is populated by a cron job in psql-app every 2 hours. If the table shows 0 rows, wait for the cron to run or check `ENABLE_RPKI=1` in docker-compose.yml.","mode": "markdown"},
|
||
|
|
"pluginVersion": "9.1.7",
|
||
|
|
"title": "RPKI Learning Guide",
|
||
|
|
"type": "text"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"description": "Total ROAs (Route Origin Authorizations) loaded from the RPKI validator. If 0, the cron job has not yet run.",
|
||
|
|
"fieldConfig": {
|
||
|
|
"defaults": {
|
||
|
|
"color": {"mode": "thresholds"},
|
||
|
|
"thresholds": {"mode": "absolute","steps": [{"color": "red","value": null},{"color": "yellow","value": 1},{"color": "green","value": 100000}]},
|
||
|
|
"unit": "short"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"gridPos": {"h": 5,"w": 4,"x": 8,"y": 0},
|
||
|
|
"id": 2,
|
||
|
|
"options": {"colorMode": "background","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"text": {}},
|
||
|
|
"targets": [
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"format": "time_series",
|
||
|
|
"rawSql": "SELECT NOW() AS time, COUNT(*) AS \"RPKI ROAs Loaded\" FROM rpki_validator",
|
||
|
|
"refId": "A"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"title": "RPKI ROAs Loaded",
|
||
|
|
"type": "stat"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"description": "Routes with a matching valid ROA — origin AS and prefix length both match.",
|
||
|
|
"fieldConfig": {
|
||
|
|
"defaults": {
|
||
|
|
"color": {"mode": "thresholds"},
|
||
|
|
"thresholds": {"mode": "absolute","steps": [{"color": "red","value": null},{"color": "green","value": 1}]},
|
||
|
|
"unit": "short"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"gridPos": {"h": 5,"w": 4,"x": 12,"y": 0},
|
||
|
|
"id": 3,
|
||
|
|
"options": {"colorMode": "background","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"text": {}},
|
||
|
|
"targets": [
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"format": "time_series",
|
||
|
|
"rawSql": "SELECT NOW() AS time, COUNT(*) AS \"Valid Routes\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nJOIN rpki_validator rv ON rv.prefix >>= r.prefix AND rv.origin_as = ba.origin_as AND r.prefix_len <= rv.prefix_len_max\nWHERE r.iswithdrawn = false AND r.isipv4 = true",
|
||
|
|
"refId": "A"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"title": "RPKI Valid Routes",
|
||
|
|
"type": "stat"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"description": "Routes where a ROA exists but the origin AS does NOT match — high-priority investigation needed.",
|
||
|
|
"fieldConfig": {
|
||
|
|
"defaults": {
|
||
|
|
"color": {"mode": "thresholds"},
|
||
|
|
"thresholds": {"mode": "absolute","steps": [{"color": "green","value": null},{"color": "red","value": 1}]},
|
||
|
|
"unit": "short"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"gridPos": {"h": 5,"w": 4,"x": 16,"y": 0},
|
||
|
|
"id": 4,
|
||
|
|
"options": {"colorMode": "background","graphMode": "none","justifyMode": "auto","orientation": "auto","reduceOptions": {"calcs": ["lastNotNull"],"fields": "","values": false},"text": {}},
|
||
|
|
"targets": [
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"format": "time_series",
|
||
|
|
"rawSql": "SELECT NOW() AS time, COUNT(*) AS \"RPKI Invalid Routes\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nWHERE r.iswithdrawn = false AND r.isipv4 = true\n AND EXISTS (\n SELECT 1 FROM rpki_validator rv\n WHERE rv.prefix >>= r.prefix AND rv.origin_as != ba.origin_as\n )\n AND NOT EXISTS (\n SELECT 1 FROM rpki_validator rv\n WHERE rv.prefix >>= r.prefix AND rv.origin_as = ba.origin_as AND r.prefix_len <= rv.prefix_len_max\n )",
|
||
|
|
"refId": "A"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"title": "RPKI Invalid Routes",
|
||
|
|
"type": "stat"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"description": "Learn: ExaBGP-injected routes (AS 65100) will be NotFound since they use synthetic ASNs not registered in RPKI. Real internet prefixes with valid ROAs will appear as Valid.",
|
||
|
|
"fieldConfig": {
|
||
|
|
"defaults": {
|
||
|
|
"color": {"mode": "palette-classic"},
|
||
|
|
"custom": {"hideFrom": {"legend": false,"tooltip": false,"viz": false}},
|
||
|
|
"mappings": []
|
||
|
|
},
|
||
|
|
"overrides": []
|
||
|
|
},
|
||
|
|
"gridPos": {"h": 10,"w": 10,"x": 0,"y": 10},
|
||
|
|
"id": 5,
|
||
|
|
"options": {"displayLabels": ["percent","name"],"legend": {"displayMode": "list","placement": "bottom"},"pieType": "donut","tooltip": {"mode": "single"}},
|
||
|
|
"targets": [
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"format": "table",
|
||
|
|
"rawSql": "SELECT\n CASE\n WHEN rv_valid.prefix IS NOT NULL THEN 'Valid'\n WHEN rv_any.prefix IS NOT NULL THEN 'Invalid'\n ELSE 'NotFound'\n END AS \"RPKI Status\",\n COUNT(*) AS \"Route Count\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nLEFT JOIN rpki_validator rv_valid\n ON rv_valid.prefix >>= r.prefix AND rv_valid.origin_as = ba.origin_as AND r.prefix_len <= rv_valid.prefix_len_max\nLEFT JOIN rpki_validator rv_any\n ON rv_any.prefix >>= r.prefix AND rv_any.origin_as != ba.origin_as\nWHERE r.iswithdrawn = false AND r.isipv4 = true\nGROUP BY 1\nORDER BY 1",
|
||
|
|
"refId": "A"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"title": "RPKI Validation Status Distribution",
|
||
|
|
"type": "piechart"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"description": "Prefixes that have a ROA but the observed origin AS does not match. These are the most security-critical routes — each one represents a potential hijack or misconfiguration.",
|
||
|
|
"fieldConfig": {
|
||
|
|
"defaults": {"custom": {"align": "auto","displayMode": "auto"}},
|
||
|
|
"overrides": [
|
||
|
|
{"matcher": {"id": "byName","options": "Status"},"properties": [{"id": "custom.displayMode","value": "color-background"},{"id": "mappings","value": [{"options": {"Invalid": {"color": "red","index": 0},"Valid": {"color": "green","index": 1},"NotFound": {"color": "yellow","index": 2}},"type": "value"}]}]}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"gridPos": {"h": 14,"w": 14,"x": 10,"y": 10},
|
||
|
|
"id": 6,
|
||
|
|
"options": {"footer": {"fields": "","reducer": ["sum"],"show": false},"showHeader": true},
|
||
|
|
"targets": [
|
||
|
|
{
|
||
|
|
"datasource": {"type": "postgres","uid": "obmp_postgres"},
|
||
|
|
"format": "table",
|
||
|
|
"rawSql": "SELECT\n r.prefix AS \"Prefix\",\n ba.origin_as AS \"Observed Origin AS\",\n rv.origin_as AS \"Authorized Origin AS (ROA)\",\n 'Invalid' AS \"Status\"\nFROM ip_rib r\nJOIN base_attrs ba ON ba.hash_id = r.base_attr_hash_id\nJOIN rpki_validator rv ON rv.prefix >>= r.prefix AND rv.origin_as != ba.origin_as\nWHERE r.iswithdrawn = false AND r.isipv4 = true\n AND NOT EXISTS (\n SELECT 1 FROM rpki_validator rv2\n WHERE rv2.prefix >>= r.prefix AND rv2.origin_as = ba.origin_as AND r.prefix_len <= rv2.prefix_len_max\n )\nORDER BY r.prefix\nLIMIT 50",
|
||
|
|
"refId": "A"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"title": "RPKI Invalid Routes — Potential Hijacks",
|
||
|
|
"type": "table"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"schemaVersion": 36,
|
||
|
|
"style": "dark",
|
||
|
|
"tags": ["obmp","learning","bgp","rpki","security"],
|
||
|
|
"time": {"from": "now-1h","to": "now"},
|
||
|
|
"timepicker": {},
|
||
|
|
"timezone": "browser",
|
||
|
|
"title": "RPKI Validation Status",
|
||
|
|
"uid": "obmp-learn-04",
|
||
|
|
"version": 1
|
||
|
|
}
|