CLI tool for syncing route objects to RIPE NCC, ARIN, and RADb, and RPKI ROAs to RIPE and ARIN.
- Python 3.14+
- uv
- 1Password CLI (
op) for credential access
uv syncCopy config.example.yaml and fill in your values. All registry sections (ripe, arin, radb) are optional — include only the registries you use.
ripe:
maintainer: "MAINT-AS12345"
credentials:
db_username: "op://vault/item/username"
db_password: "op://vault/item/password"
rpki_api_key: "op://vault/item/rpki-api-key"
test_db_username: "op://vault/item/test-username" # optional
test_db_password: "op://vault/item/test-password" # optional
sso_emails:
- "admin@example.com"
routes:
- prefix: "192.0.2.0/24"
origin: "AS12345"
description: "Example IPv4 prefix"
- prefix: "2001:db8::/32"
origin: "AS12345"
description: "Example IPv6 prefix"
roas:
- prefix: "192.0.2.0/24"
origin: "AS12345"
max_length: 24
- prefix: "2001:db8::/32"
origin: "AS12345"
max_length: 32
arin:
org_handle: "EXAMPLEORG-1"
credentials:
api_key: "op://vault/item/arin-api-key"
routes:
- prefix: "192.0.2.0/24"
origin: "AS12345"
description: "Example IPv4 prefix"
- prefix: "2001:db8::/32"
origin: "AS12345"
description: "Example IPv6 prefix"
roas:
- prefix: "192.0.2.0/24"
origin: "AS12345"
max_length: 24
- prefix: "2001:db8::/32"
origin: "AS12345"
max_length: 32
radb:
maintainer: "MAINT-AS12345"
contact_email: "admin@example.com"
credentials:
portal_username: "op://vault/item/portal-username"
portal_password: "op://vault/item/portal-password"
mntner_password: "op://vault/item/mntner-password"
routes:
- prefix: "192.0.2.0/24"
origin: "AS12345"
description: "Example IPv4 prefix"
- prefix: "2001:db8::/32"
origin: "AS12345"
description: "Example IPv6 prefix"roas is optional. If omitted, only route objects are synced. ROA sync only manages prefixes explicitly listed — other ROAs in the account are left untouched.
Set delete: true on any route entry to remove it from the registry instead of syncing it:
routes:
- prefix: "192.0.2.0/24"
origin: "AS12345"
delete: trueWhen a radb section is present in the config, every RIPE and ARIN route change (create, update, or delete) is automatically mirrored to RADb. You do not need to list the same prefix in the radb.routes section — duplicates are skipped automatically.
Routes listed exclusively under radb.routes (with no corresponding RIPE/ARIN entry) are still synced to RADb directly.
On update, only fields managed by this tool (route/route6, origin, mnt-by, source, changed, and optionally descr) are modified. All other attributes already present in the registry object (e.g. remarks, admin-c, tech-c) are preserved.
Secrets are fetched from 1Password via the op CLI. The credentials block in the config file specifies the 1Password reference for each secret:
| Field | Used for |
|---|---|
db_username |
RIPE DB REST API username (production) |
db_password |
RIPE DB REST API password (production) |
rpki_api_key |
RIPE RPKI Management API key |
test_db_username |
RIPE test DB username (optional, overrides db_username in test mode) |
test_db_password |
RIPE test DB password (optional, overrides db_password in test mode) |
| Field | Used for |
|---|---|
api_key |
ARIN API key for all IRR and RPKI requests (production) |
test_api_key |
ARIN OTE API key (optional, overrides api_key in test mode) |
The production API key must be linked to a POC with authority over your organization's resources. Create one at ARIN Online → Settings → Security Info → Manage API Keys. OTE keys are created the same way at account.ote.arin.net.
| Field | Used for |
|---|---|
portal_username |
RADb portal email address (HTTP Basic auth) |
portal_password |
RADb portal password (HTTP Basic auth) |
mntner_password |
RADb mntner password for object-level authorization |
References use the op://vault/item/field format. You must be signed in to the 1Password CLI (op signin) before running the tool.
# Dry-run all configured registries (test environments where applicable)
uv run rir-updater config.yaml
# Dry-run a specific registry only
uv run rir-updater config.yaml --registry arin
uv run rir-updater config.yaml --registry ripe --registry radb
# Apply changes to all registries in production
uv run rir-updater config.yaml --production --commit
# Apply changes to ARIN only (production)
uv run rir-updater config.yaml --registry arin --production --commit
# Set up the RIPE test database with objects replicated from production
uv run rir-updater config.yaml --setup-test
# Replicate ARIN production routes and ROAs into the OTE environment
uv run rir-updater config.yaml --setup-ote # dry-run
uv run rir-updater config.yaml --setup-ote --commit # applyRADb always runs against production — --production only affects the RIPE and ARIN sections. ARIN uses its OT&E environment in test mode (reg.ote.arin.net) and production otherwise.
When no --registry flags are given, all registries present in the config are updated. Updates run in order: RIPE → ARIN → RADb. Within each registry, ROAs are published before route objects, because RPKI-aware consumers (such as RADb) reject a route until a covering ROA exists.
A dry-run reports the real ROA diff: it fetches the currently-published ROAs (read-only) and shows the actual number that would be added or deleted, rather than assuming everything is new.
The first time you use --setup-test, the mntner must be created manually via the RIPE web UI at apps-test.db.ripe.net — the API does not allow creating the first mntner programmatically due to a circular person↔mntner dependency. The tool will print instructions if the mntner is not found.
aut-num objects cannot be created in the test DB via the API — all aut-nums require authorization from TEST-DBM-MNT, which is restricted to RIPE staff. --setup-test will warn and continue if aut-num replication fails.
--setup-test only replicates prerequisite objects. Run without --setup-test afterwards to sync route objects and ROAs.
Each object (a route/route6, or the ROA set for a registry) is synced independently. If one object fails with an API error, the failure is reported and the run continues with the remaining objects — a single failure never aborts the whole batch or leaves you unsure which objects were processed.
Failures appear in the summary as ! lines, for example:
! radb route6 2001:db8::/32 FAILED: create radb route 2001:db8::/32 AS64496 failed (400): ...
The command exits non-zero if any object failed, even though the run completed; a fully successful run (or a clean dry-run) exits zero. Because syncs are idempotent, you can simply re-run the same config to retry only the objects that still need changes.
- Output format — RADb's REST API defaults to
text(RPSL) and ignores theAcceptheader, so the tool requests JSON explicitly with a?format=jsonquery parameter on every request. - Transient failures — RADb's API intermittently drops connections or returns 5xx. Idempotent requests are retried automatically with exponential backoff. A create whose response is lost is verified by re-checking existence (not blindly retried), so an object is never duplicated.
uv run ruff check . # lint
uv run ruff format . # format
uv run pytest # testThis project uses a trunk-based workflow. main is the single long-lived
branch; there is no develop branch.
- Branch off
mainfor every change:git switch main && git pull git switch -c my-change - Make your change and keep it green locally:
uv run ruff check . && uv run ruff format --check . && uv run pytest
- Push the branch and open a pull request into
main. - CI runs automatically on the PR (
.github/workflows/ci.ymlruns the lint, format check, and tests above). It must pass. - Get one approving review.
mainis protected: direct pushes are blocked, and a PR needs at least one approval before it can merge. You cannot approve your own PR. - Merge with squash or rebase —
mainrequires linear history, so merge commits are not allowed. Squash is preferred for a single, self-contained change.
Branch protection is enforced by a repository ruleset on main
(required PR + review, required ci status check, linear history, no force-push,
no deletion). Repository admins may bypass these rules when necessary.