405 lines
13 KiB
JSON
405 lines
13 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": [
|
||
|
|
{
|
||
|
|
"asDropdown": true,
|
||
|
|
"icon": "external link",
|
||
|
|
"includeVars": true,
|
||
|
|
"keepTime": true,
|
||
|
|
"tags": [
|
||
|
|
"obmp-nav"
|
||
|
|
],
|
||
|
|
"title": "OBMP Dashboards",
|
||
|
|
"type": "dashboards"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"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 \u2014 this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin \u2014 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 \u2014 they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** \u2014 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 \u2014 this route is potentially a hijack |\n| **NotFound** | No ROA exists for this prefix/origin \u2014 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 \u2014 they indicate either a misconfiguration or a prefix hijack\n- Routes with no RPKI data show as **NotFound** \u2014 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 \u2014 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 \u2014 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 \u2014 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 \u2014 Potential Hijacks",
|
||
|
|
"type": "table"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"schemaVersion": 36,
|
||
|
|
"style": "dark",
|
||
|
|
"tags": [
|
||
|
|
"obmp",
|
||
|
|
"bgp",
|
||
|
|
"rpki",
|
||
|
|
"security",
|
||
|
|
"obmp-nav"
|
||
|
|
],
|
||
|
|
"time": {
|
||
|
|
"from": "now-1h",
|
||
|
|
"to": "now"
|
||
|
|
},
|
||
|
|
"timepicker": {},
|
||
|
|
"timezone": "browser",
|
||
|
|
"title": "RPKI Validation Status",
|
||
|
|
"uid": "obmp-learn-04",
|
||
|
|
"version": 1
|
||
|
|
}
|