Self-hosted DNS, DHCP, and IPAM — one control plane, real servers underneath.
A modern, open-source alternative to commercial DDI platforms.
Built in the open by SpatiumNorth, Montréal.
⚠️ Beta software. SpatiumDDI is under active development. Core IPAM / DNS / DHCP / appliance surfaces have stabilised since the2026.04.16-1alpha cut, but expect occasional schema changes between releases and roadmap features that are still in flight. Suitable for labs, homelabs, and pilots in front of non-critical client populations; production deploys in front of business-critical DHCP should pin to a tested release and snapshot Postgres before upgrading. Early adopter feedback is very welcome — open an issue or start a discussion on GitHub.
- Why I built this — the story
- About SpatiumNorth — the company behind it
- Why SpatiumDDI — the elevator pitch
- Privacy: your data stays yours — no telemetry, no analytics, no phone-home
- Support the project — sponsors, tip jar, getting involved
- What's in the box — quick capability tour
- Full feature detail — deep dive on every subsystem
- Screenshots
- Architecture
- Getting Started — three ways to run SpatiumDDI:
- Try the demo in GitHub Codespaces — one-click full stack with seeded data, browser-only, free 60 h/month
- Quick start with the OS appliance ISO — easiest deploy: Debian 13 + embedded k3s + atomic A/B upgrades, guided installer
- Quick start with Docker Compose —
docker compose upon any Docker host, full control - Plus: demo seed · in-place upgrade flow · built-in DNS/DHCP containers · API docs
- Deployment Options
- Documentation
- Project Status
- Contributing
- License
I'm a network engineer. I've spent years working with enterprise DDI platforms, and while they're solid pieces of software, the licensing puts them out of reach for smaller teams, homelabs, and folks who just want to learn how this stuff fits together.
The open source world has excellent standalone tools — NetBox for IPAM, BIND9 / PowerDNS / Technitium for DNS, Kea for DHCP — but nothing that pulls them into a single control plane the way the commercial platforms do. So I started building one on nights and weekends.
If SpatiumDDI ends up being useful to you, that's the whole point. If you want to file an issue, send a PR, or just tell me what's broken, I'd genuinely appreciate it.
SpatiumDDI is built by SpatiumNorth, a Montréal-based software company that builds infrastructure software in the open. The whole product ships under Apache 2.0 — every feature, free wherever you run it — and the company charges for the part that actually costs something: support, deployment and migration, from the people who wrote the code. If your organization wants that, see pricing or get in touch; managed service providers can partner to run it for their clients. The code lives here, in the open, as it always has.
SpatiumDDI started on nights and weekends and homelab hardware, and the software is free and stays free. If it's saving your team work and you want to chip in, there are two ways, depending on who you are.
Individuals — small tips help cover the boring stuff (a domain, a homelab SSD, the occasional cloud VM I spin up to test a deploy topology I don't have locally). No tier system, no perks list, no obligation — just a "thanks, here's a coffee" button if the project saved you an afternoon. Every contribution is genuinely appreciated.
Organizations — if your team runs SpatiumDDI in production, the way to back the project is the support your operators will want anyway: that's what SpatiumNorth sells — support, deployment and migration from the people who wrote the code. See pricing or contact us. Want to sponsor a specific feature or just say hi? Still happy to talk — open an issue.
It runs DNS and DHCP — not just configures them. A modern alternative to Infoblox and EfficientIP: most open-source IPAM tools are pretty dashboards over someone else's /etc/bind/named.conf. SpatiumDDI bundles BIND9 and Kea as first-class service containers; the control plane owns their config, they auto-register, and they keep serving if the control plane is down.
One platform, three surfaces. IPAM tree, DNS zones, DHCP scopes — one UI, one REST API, one source of truth. Hostname changes in IPAM propagate to DNS; reservations propagate to DHCP. No more three-tab reconciliation.
Bring your own servers — or ours. Use the bundled Kea and BIND9, or point SpatiumDDI at your existing Windows DCs and DHCP servers via WinRM. Agentless in both directions — nothing installed on the Windows side.
Built for delegation. Group-based RBAC with LDAP, OIDC, SAML, RADIUS, and TACACS+ (with backup-server failover). Hand a subnet or a zone to a department without handing over root.
API-first. Every UI action is a REST call. Terraform, Ansible, and ad-hoc scripts all speak the same surface. If you can click it, you can automate it.
SpatiumDDI collects no telemetry, no usage analytics, no crash reports, and no account or registration data. There is no phone-home, no license server, no sign-up, and no project-controlled endpoint the software talks to. Everything you put in it — zones, records, leases, subnets, credentials, query logs, audit history — lives in your PostgreSQL on your infrastructure, and the maintainers have no way to see it and do not want it. This README, the documentation site and the web UI use no analytics or tracking scripts.
The software makes no outbound connection you did not configure, with one exception: a daily anonymous check of GitHub for a newer release (an unauthenticated GET; GitHub sees your IP address and nothing about your install — no version, no counts, no identifiers). Turn it off under Settings → Application → Updates → Check for GitHub Releases, or run fully air-gapped — every feature works with no internet access at all. Optional features that do reach third parties (Fingerbank device profiling, the Operator Copilot's LLM provider, Let's Encrypt, blocklist feeds, cloud DNS / integration mirrors, the whois and RBL tools) are off until you configure them, and docs/PRIVACY.md lists exactly what each one sends, to whom, and how to stop it.
One control plane for IPAM, DNS, DHCP — plus the discovery and integrations to keep it honest. No vendor lock-in, no per-IP licence, no agents on Windows.
| Feature | Highlights | |
|---|---|---|
| 🗂 | Hierarchical IPAM | spaces · blocks · subnets · IPv4 + full IPv6 (EUI-64 / random / sequential) · per-IP roles · MAC history · reservation TTL · bulk allocate with name templates · atomic next-available-subnet carve (POST /blocks/{id}/allocate-subnet — race-safe, Terraform-ready) · server-side address pagination / search / sort + cross-subnet IP search (select-all-matches → bulk edit / delete) that survives a busy /16 · hourly utilization-recount self-heal · scheduled reconciliation hygiene alerts (free-but-responding / stale reservation / unknown-MAC squatting) |
| ✂️ | Subnet operations | Split · Merge · Find-free · subnet planner (multi-level CIDR design + transactional apply) — preview-then-commit with typed-CIDR confirm · single Tools dropdown on subnet headers |
| 🧮 | Planning tools | CIDR calculator · address planner (pack /N requests into free space) · aggregation suggestion · free-space treemap |
| 🌐 | DNS | BIND9 or PowerDNS or Technitium container, auto-registering — pick per server group; zones and records are authored identically either way, but split-horizon views and RPZ blocklists are BIND9-only, ALIAS / LUA are PowerDNS-only, and Technitium blocks natively rather than via RPZ · RFC 2136 dynamic updates · per-zone dynamic-update ACLs (authorize external writers by TSIG key or source IP/CIDR, with ingest-back of records they inject) · per-server zone-serial drift + record-level drift report (AXFR the live zone, diff it against the database, per server) · TSIG keys · zone delegation wizard · zone templates · RPZ blocklists with curated catalog · catalog zones (RFC 9432) — producer and consumer on BIND9, producer-only on PowerDNS, not supported on Technitium until #1766 · secondary / stub zones (masters) · SVCB / HTTPS / DNAME records · ALIAS (apex CNAME-equivalent) + LUA (scripted / health-aware answers) on PowerDNS · Response Rate Limiting (RRL) + amplification defenses · query-log analytics (top names / clients / qtypes + per-view breakdown) with NXDOMAIN-spike / query-rate-spike alerting — and, with BIND 9.20 response logging on, the answer as well as the question: rcode + answer count per row, so NODATA is distinguishable from NXDOMAIN and from a query that was refused (opt-in per group; NULL means unrecorded, never NOERROR) · move a server or a zone between server groups — the server move re-elects a primary, purges the old group's queued ops and repoints the appliance firewall; the zone move is preview→commit with three separate acknowledgements, because clearing a view widens exposure rather than narrowing it |
| 🔒 | Encrypted DNS (DoT / DoH / DoQ) | serve DNS-over-TLS (853) + DNS-over-HTTPS (/dns-query) — plus DNS-over-QUIC on Technitium — to local clients and forward upstream over TLS instead of plaintext 53 · BIND9 native, PowerDNS via the dnsdist front, Technitium native (and the only driver that also forwards over HTTPS and QUIC — BIND 9.20 has no client-side HTTP or QUIC transport) · certs from the built-in ACME client or an upload, auto-picked-up on renewal · strict upstream remote-hostname validation that fails closed · per-group, default-off, additive (plain :53 keeps working) |
| 🔐 | DNSSEC | BIND9 inline-signing (BIND owns + auto-rotates keys) + PowerDNS and Technitium online sign / unsign from the zone page · reusable dnssec-policy library — KSK + ZSK algorithm / size / lifetime, NSEC or NSEC3 (iterations · salt length · opt-out) · per-zone public key state + DS export to hand to the parent-zone registrar · manual + automatic rollover · SpatiumDDI never holds private key material — the signer owns and rotates it |
| 🔀 | DNS Views (split-horizon) | per-view zone + record rendering on BIND9 — view_id IS NULL records shared across views, scoped records render only in their view, RPZ / blocklists replicate into each view block |
| ⚖️ | GSLB-lite + GeoDNS steering | health-checked DNS pools — tcp / http / https / icmp / none probes flip A/AAAA records in/out of the rendered rrset; manual enable per member · topology-aware steering — a per-member serving scope (client CIDRs and/or Site) renders as BIND9 geo views composed over split-horizon, evaluated before operator views with a union fallback so a scoped-only pool never blackholes |
| 🔄 | DHCP | Kea container · agentless FortiGate cloud DHCP driver (FortiOS REST, no agent) · group-centric Kea HA (load-balanced or hot-standby) with self-healing peer drift · option templates · 95-entry option-code library · PXE / iPXE provisioning profiles — per-architecture boot-file selection (BIOS · UEFI x64 / ia32 / arm64 · iPXE chainload) rendered as Kea client classes, one reusable profile assigned per scope · DHCPv6 stateful / stateless / SLAAC modes · DHCPv6 prefix delegation (IA_PD / pd-pools + RFC 6603 excluded-prefix) · DUID host reservations · DHCPv6 leases mirrored into IPAM and DDNS, keyed on DUID + IAID · Windows DHCP failover — relationships observed per server, uncoordinated serving of one scope by two Windows servers refused, and relationships created / edited / replicated from SpatiumDDI · per-subnet relay-agent addresses · IPv6 Router Advertisements (radvd rendered per RA-enabled scope + rogue-RA passive sniffer) · fingerbank device-class on the lease list (filterable) · rogue-DHCP detection (opt-in active probe → unexpected-responder alert + allowlist) · fingerprint-driven device policies — compile chosen fingerbank device classes into a real Kea client class with its own option set and lease time, bindable from a pool's class restriction (NAC-lite, no 802.1X, no switch config); ambiguous signatures are excluded by default, counted and listed, because a request list of 1,3,6,15 comes from a doorbell and a rack server alike |
| 🪟 | Windows DNS + DHCP | agentless — RFC 2136 + WinRM, no software on the DC |
| 🧩 | Agentless Technitium | already run Technitium? Point SpatiumDDI at it — paste an API URL + permanent bearer token and the control plane drives its HTTP API directly, nothing deployed. Zone + record CRUD and topology pull; DNSSEC / forwarders / blocklists stay in Technitium's own console. Coexists with the agent-managed container driver — a group is single-driver, so a mixed estate is one group each |
| ☁️ | Cloud DNS | agentless first-class drivers — Cloudflare · Route 53 · Azure DNS · Google Cloud DNS · DigitalOcean · Hetzner · Linode · Vultr · import-existing-zones · client-side multi-value RRset disambiguation |
| 📥 | DNS configuration import | one-shot migration from BIND9 (named.conf archive upload), Windows DNS (live WinRM pull), PowerDNS (live REST pull), and Technitium (live REST pull) · preview-before-commit · per-zone savepoint so partial failures don't abort the batch · provenance stamps (import_source + imported_at) on every imported zone + record |
| 📥 | DHCP configuration import | one-shot migration from Kea (JSON-with-comments upload), Windows DHCP (live WinRM pull), and ISC dhcpd.conf · every scope binds to an IPAM subnet (link-existing or auto-create) · per-scope savepoints + skip / overwrite conflict actions · provenance stamps |
| 📥 | IPAM import (NetBox) | one-shot read-only migration from a live NetBox install (REST v3.x–4.6+) → prefixes / addresses / VRFs / tenants→Customers / sites / VLANs as native IPAM rows · stateless test → preview → commit · per-entity savepoints · per_vrf vs single space strategy · provenance stamps make re-runs idempotent (skip / overwrite) · creds supplied per-request, never persisted |
| 🪟 | Windows cutover | the half the importers stop short of — not a fifth importer: it creates no zones, scopes, pools or records. Per-plan, per-zone and per-scope: parity (diff SpatiumDDI against the live Windows server, classified by why the two differ) · parallel run (replay real queries from the query log at both sides and compare answers) · TTL pre-flight (snapshot and restore exactly) · DHCP lease handover (promote live Windows leases to reservations so a renewing client keeps its address) · the switch, deactivating Windows before activating the managed side, with per-item rollback and a real recovery_estimate_seconds · decommission checklist · a markdown runbook to paste into a change ticket, carrying the Windows-side PowerShell SpatiumDDI deliberately doesn't run for you |
| 📤 | IPAM export | any scope (space / block / subnet) out as CSV · JSON · XLSX — or Print / PDF: a tree report for a space or block (summary counters, block hierarchy indented by nesting depth, every subnet with utilization) or a detail report for a subnet (its facts, custom fields, address table) · paginated with repeating column headers and "Page N of M", so a printout handed to an auditor says whether a page is missing · same scope resolution as the spreadsheet exporters, so the two can't disagree about what a subtree contains |
| 📡 | Multicast group registry | RFC 5771 catalog seeded · per-IPSpace groups + PIM rendezvous-point domains · auto-created enclosing 224.0.0.0/4 / ff00::/8 IPBlock for tree visibility · per-IP collision conformity check · bulk-allocate from RFC 2365 admin-scoped ranges |
| 🔁 | NAT cross-reference | 1:1 / PAT / hide-NAT tracked in IPAM with FK links to live IP rows |
| 📜 | DHCP lease history | forensic trail of every expiry, MAC reassignment, absence-delete |
| 🗑 | Soft-delete trash | 30-day Trash with cascade restore for spaces, blocks, subnets, zones, scopes — a scope's pools + static reservations cascade with it, so one Restore brings the scope back whole |
| ✅ | DNS-name conformance | every hostname / DNS name validated on write against the right standard for its role — RFC 1123 LDH for host names (IDN normalised to xn--, not rejected), the looser RFC 2181 owner rule for records so _acme-challenge / _dmarc / _443._tcp / * stay legal, dotted-label FQDNs for zones · client-supplied lease hostnames are sanitised, never dropped · read-only /diagnostics/name-conformance audits what already exists — validate-on-write never rewrites an existing row |
| Feature | Highlights | |
|---|---|---|
| 🌐 | ASN management | first-class ASN entity · RDAP holder refresh (per-RIR routing via IANA bootstrap) · RPKI ROA pull (Cloudflare or RIPE) with expiry tracking · holder-drift detection with side-by-side diff · alert rules for drift / unreachable / ROA expiry · BGP Footprint tab with RIPEstat (announced prefixes / prefix-overview / routing history) + PeeringDB (peering profile / IXP presence) — REST + MCP tools · in-process TTL cache (RIPEstat 6 h, PeeringDB 24 h) |
| 🤝 | BGP peering + communities | peer / customer / provider / sibling graph between tracked ASNs · BGP communities catalog (RFC 1997 / 7611 / 7999 well-knowns + per-AS extensions, large communities per RFC 8092) |
| 🚨 | BGP prefix-hijack detection | RIPEstat poll (source of truth) + optional RIS Live streaming consumer watching your ASNs' tracked prefixes · bgp_prefix_hijack / bgp_more_specific_announced alerts (RPKI-invalid → critical, unknown → warning) · evidence kept on a pruned victim prefix · outage-safe stale resolution |
| 🔭 | BGP Looking Glass | receive-only GoBGP collector peers with your routers and ingests their live Adj-RIB-In — it never advertises routes back · Sessions + Routes grids with clickable peer / route detail modals (multi-origin possible hijack + anycast headlines, best-path heuristic) · every learned prefix resolved into IPAM / ASN / VRF at ingest, RPKI-validated · VPNv4 / VPNv6 Route-Target matching · six bgp_lg_* alert rule types + dashboard health card · as-path-regexp Query tab + collector-vantage ping / traceroute · optional MetalLB BGP mode advertises the control-plane VIP to the same routers |
| 🛣 | VRFs as first-class | name / RD / import + export RTs / optional ASN linkage · cross-cutting RD/RT validator (warns or 422s on ASN-portion mismatch) · VRF picker on IPSpace + IPBlock modals · auto-backfill from existing freeform fields |
| 📛 | Domain registration tracking | distinct from DNSZone — registrar / registrant / expiry / nameservers / DNSSEC · RDAP refresh (TLD → RDAP-base via IANA bootstrap) · NS-drift, registrar-changed, DNSSEC-status-changed alerts · explicit dns_zone.domain_id linkage with sub-zone tree fallback |
| 🏢 | Customer / Site / Provider | logical ownership entities cross-cutting IPAM / DNS / DHCP / Network · ON DELETE SET NULL cross-references on every existing table so re-tagging is safe · shared pickers + chips wired into every modal |
| 🛤 | WAN circuits | carrier-supplied logical pipe (provider + transport class + bandwidth + endpoints + term + cost) · 9 transport classes including AWS DX / Azure ER / GCP Interconnect cross-connects · soft-deletable (status='decom' is operator-visible end-of-life) · alerts for term-expiring + status-changed |
| 📦 | Service catalog | bundles VRF / Subnet / IPBlock / DNSZone / DHCPScope / Circuit / Site / Overlay into a customer-deliverable · mpls_l3vpn + sdwan + custom kinds in v1 · kind-aware /summary endpoint with L3VPN canonical shape · alerts for term-expiring + resource-orphaned |
| 🌐 | SD-WAN overlays | vendor-neutral overlay topology + routing-policy intent · 6 kinds (sdwan / ipsec / wireguard / dmvpn / vxlan-evpn / gre) · ordered preferred-circuit chain per site · 33 well-known SaaS apps in the catalog · pure read-only /simulate what-if when circuits go down · SVG circular-layout topology view |
| 🚑 | E911 dispatchable location (network.e911) |
SpatiumDDI as a Location Information Server: given a phone's IP, MAC or LLDP chassis + port, answer which room is this device in, right now? from the leases, switch FDB and LLDP data IPAM already collects · Emergency Response Locations as the 31 separate RFC 5139 civic elements, bound to a switch port / subnet / VLAN / device at a fixed precedence · a stale precise answer is refused in favour of a fresh coarse one, and every answer carries its confidence · HELD (RFC 5985) + PIDF-LO for phones and PBXs that already speak it · DHCP options 99 / 123 · civic CSV + LLDP-MED snippets · a location source only — no call routing, no ALI upload (docs) |
Four IP-native domains a generic IPAM doesn't speak. Each is a registry + segmentation documentation + conformity module built on the DDI primitives already here — not a protocol implementation. All four are read-only by construction: SpatiumDDI records what the estate is, and never reads or writes a device object, a control tag, or a study. Off on a fresh install, individually togglable under Settings → Features.
| Domain | What it models | |
|---|---|---|
| 🎚 | AV over IP (network.av) |
Dante / AES67 / SMPTE ST 2110 / NDI / RAVENNA flow descriptors on top of the multicast registry · PTP clock domain per flow · operator-declared reserved ranges per protocol, so an allocation that lands outside the studio address plan is caught at preview time |
| 🏢 | BACnet/IP (network.bacnet) |
building-automation device registry keyed on the internetwork-unique device instance number · BACnet network numbers · per-subnet BBMD designation with BDT / FDT snapshots · the exactly-one-BBMD-per-subnet rule checked in both directions (0 = unreachable across routers, >1 = duplicated broadcasts) |
| 🏭 | Industrial / OT (network.ot) |
PROFINET / EtherNet-IP / Modbus TCP / OPC UA role + vendor + criticality per address · Purdue-level zoning per subnet (Numeric(2,1), so level 3.5 — the DMZ — is representable) · CSV import of engineering-tool exports · devices whose Purdue level contradicts their subnet's zone are flagged |
| 🏥 | DICOM AE registry (network.dicom) |
the institution-wide-unique AE Titles PS3.15 Annex H specifies a registry for and that in practice live in a spreadsheet · directed AE→AE association map behind "what breaks if I renumber this host" · PS3.5-exact title validation (16 bytes, spaces legal as padding) · an AE outlives its host, so decommissioning demotes it to a reservation rather than freeing a name peers still send to. Network identity only — no patient data, ever |
| Feature | Highlights | |
|---|---|---|
| 📡 | SNMP discovery | v1 / v2c / v3 polling of routers + switches → ARP / FDB / interfaces / LLDP neighbours feed back into IPAM |
| 🔦 | IP discovery | opt-in per-subnet ping (unprivileged ICMP + TCP-connect fallback) / ARP sweep → discovered rows for live IPs · three-bucket reconciliation report · stamps the last_seen_at signal |
| ♻️ | Address hygiene | reverse-DNS auto-population (PTR → hostname) · Stale-IP report + one-click bulk-deprecate over the last_seen_at signal · CGNAT (RFC 6598) awareness badge + new-subnet hint |
| 🎯 | Nmap scanner | per-IP / per-subnet (CIDR sweep) / /tools/nmap · live SSE streaming · stamp alive hosts → IPAM |
| 🚧 | Fragile-device "do not probe" | mark an IP space / block / subnet as do not probe with a reason, and every prober SpatiumDDI owns — discovery sweeps, nmap, the network tools — refuses it · ORs down the chain with no per-level opt-out, deliberately unlike the DDNS fields it mirrors: a descendant must not be able to opt a clinical or plant-floor segment back into being swept · one resolver every prober consults, an audited superadmin-only per-request override, and a conformity check that works backwards from the OT / DICOM / BMC registries to find fragile subnets nobody flagged · not behind a feature module — a default-off safety flag protects nobody |
| ⏰ | Wake-on-LAN | send a magic packet from the IP detail modal, the Network Tools page, or the Operator Copilot · server vantage (control plane broadcasts) or appliance vantage (dispatched to a Fleet appliance on the target's segment) · MAC + broadcast resolved server-side · SSRF-denylisted + audited · read:use_network_tools |
| 🗓 | Scheduled Wake-on-LAN | Tools → Wake Schedules — recurring DST-safe cron wake of a whole fleet, targeted by address tags / subnet / subnet tags / explicit hosts · holiday gate (blackout dates + term range) or a subscribed iCal / CalDAV calendar (skip-on-event or only-on-event) · multi-source post-wake verify (ping / tcp / seen / auto) stamps responders into the last_seen_at signal and re-wakes only the hosts that didn't come up — ad-hoc single-host wakes opt into the same verify + bounded re-wake chain · evidence trail per target records what each source actually said, so "down according to what?" has an answer (ping timed out, TCP refused nothing, last seen 3 days ago is a dead box; ping timed out, TCP connected is a contradiction worth a look) · wol_wake_failed alert rule with a per-schedule mute · MAC fallback via ip_mac_history → DHCP lease · hard fan-out cap + auto-tuned stagger · togglable tools.wake_scheduler module |
| 📦 | Packet capture | on-demand tcpdump from Tools → Packet Capture — control-plane container or an appliance's real host NICs (interface dropdown + BPF presets + packet/duration/byte stop conditions) · live progress · keeps the partial on Stop · download .pcap for Wireshark · RBAC-gated (manage_packet_capture) + audited + auto-pruned |
| 🛰 | Device profiling | passive DHCP fingerprinting (scapy + fingerbank) and opt-in auto-nmap on new DHCP lease — what kind of device is on every IP |
| 🏷 | OUI vendor lookup | MAC → vendor names in IP tables, DHCP leases, search filters |
| 🛡 | New-device detection | arpwatch-style alert the moment a never-before-seen MAC appears · trusted-MAC / OUI allowlist · one-click block (writes a DHCP MAC block) · ingests from DHCP lease events + SNMP ARP/FDB + an opt-in L2 ARP/ND sniffer · review queue + "new devices 24h" dashboard KPI · randomised MACs excluded by default · opt-in security.new_device_watch module |
| 🕵 | DNS threat analytics | four independent detections over the query log SpatiumDDI already collects — no extra capture, no response inspection (the BIND9 log records the question, never the answer) · tunneling (iodine / dnscat2 / dnsteal — long high-entropy labels, many unique subdomains under one parent, elevated TXT/NULL/CNAME ratio) · DGA (a crop of implausible registrable names, structurally the inverse of tunneling, so it scores separately) · C2 beaconing (inter-arrival regularity — a callback on a timer barely varies, which almost nothing human-driven does; catches a beacon using one short ordinary name that scores zero on content) · RPZ hit attribution (a blocked lookup isn't a problem, but a host generating thousands of them is an infected machine announcing itself) · Threat tab on Logs + Security dashboard card + alert, deep-linked to the IP · default-off — it reads the names clients look up · needs query logging on at least one DNS group |
| 🚫 | DNSBL / RBL reputation | curated blocklist catalog + a daily sweep over NAT-egress / public / internet_facing / pinned IPs · ip_blocklisted latch alert that auto-resolves on delist or de-scope · Reputation panel on the IP detail modal · security.dnsbl module (no external DNS queries until the sweep switch is on) |
| 🎨 | Dashboards | nine sub-tabs — Overview / IPAM / DNS / DHCP / Network / Integrations / Security / Compliance / Conformity — each backed by a single rollup endpoint under /api/v1/dashboards/, refreshed every 60 s · utilization heatmap · DNS query rate · DHCP traffic · ASN drift + RPKI ROA expiry · circuit alerts · service-catalog orphans · per-mirror integration counts · account lockout state + active sessions + audit-chain verification · platform health card |
| 🔎 | Global search + command palette | Cmd/Ctrl+K from anywhere — 20 resource types across IPAM / DNS / DHCP / network modeling / ownership / compliance, plus go-to-page commands sourced from the sidebar's own nav tree (so the palette cannot drift from the sidebar) · ranked in SQL, before each type's LIMIT — without an ORDER BY the database returns any N matching rows and the exact hit is routinely not among them, which sorting afterwards cannot recover · every hit is filtered through the caller's own read grants, so search can never be the way around a permission the rest of the API enforces · scope chips + recent searches · trigram GIN indexes on the tables that actually grow |
| 📊 | Platform Insights | native Postgres diagnostics + per-container CPU / mem / IO. No extra agents |
| 📈 | InfluxDB push export | ship DNS / DHCP counter deltas and IPAM + lease gauges to InfluxDB v1, v2 or v3 on a per-target interval · counter points carry the agent's own 60 s bucket timestamp, so a backfill lands on the hour the traffic happened · watermarks advance only on a successful write, so a dead collector delays the export rather than punching a hole in it · the connection test is a real single-point write, not a reachability ping — a wrong bucket, org or token answers a GET perfectly well and then rejects every point |
| Source | What's mirrored | |
|---|---|---|
| 🐳 | Docker | networks · optional container IPs |
| ☸️ | Kubernetes | cluster CIDRs · nodes · LoadBalancer VIPs · Ingress → DNS |
| 🖥 | Proxmox VE | bridges · SDN VNets + subnets · VM / LXC NICs (qemu-guest-agent) |
| ☁️ | Cloud (AWS / Azure / GCP) | VPCs → IP blocks · subnets · NIC / public / load-balancer IPs · per-provider picker · paired with the agentless Cloud DNS drivers |
| 🔐 | Tailscale | tailnet devices + synthetic *.ts.net zone |
| 🕸 | NetBird | managed-WireGuard mesh peers + synthetic mesh zone |
| 📡 | UniFi Network | controller sites · networks (VLANs / CIDRs) · clients (with hostnames + MAC) |
| 🛡 | OPNsense | firewall interfaces (LAN / OPT / VLAN → subnets) · VLANs · DHCPv4 leases + static reservations → IPAM |
| 🔥 | Palo Alto PAN-OS / Panorama | address objects + groups → shadow IPAM · NAT rules · zones / interfaces · DHCP leases |
| 🔥 | Fortinet FortiGate | address objects + groups → shadow IPAM · VIPs (DNAT) · interfaces · DHCP leases |
| 🔥 | Cisco Meraki MX | appliance VLANs · DHCP fixed-IP reservations · org policy objects · 1:1 NAT + port-forward |
Every integration above is a read-only mirror. Active block sync is the deliberate exception — a narrow, heavily guarded write path that pushes a real block at the natural enforcement point, so a device that self-assigns a static IP can't walk past a DHCP block.
| Target | How the block is enforced | |
|---|---|---|
| 🛡 | OPNsense | firewall table-alias membership (by IP) — never rule CRUD |
| 📡 | UniFi Network | L2 client quarantine (by MAC) |
| 🔥 | Palo Alto | Dynamic Address Group IP → tag register via the User-ID API — no policy commit |
| 🔥 | Cisco Meraki | per-client built-in Blocked device policy via the Dashboard API |
| 🔥 | Fortinet | credential-free feed inversion — SpatiumDDI serves a token-scoped blocklist.txt the FortiGate polls as an External Threat Feed, so it holds no write credentials on the firewall at all |
Off by default, behind a feature module, a per-target enforcement master
switch, distinct write-scoped credentials, preview + audit on every push,
dedicated RBAC permissions (manage_block_sync /
manage_firewall_enforcement), and two-person approval. SpatiumDDI only
ever removes values it added — never alias members or blocked clients it
doesn't own.
| Feature | Highlights | |
|---|---|---|
| 🔒 | RBAC + external auth | LDAP · OIDC · SAML · RADIUS · TACACS+ with backup-server failover · API tokens with auto-expiry · scoped API tokens (per-permission + resource-scoped — bind a token to one subnet / DNS zone for CI/Terraform credentials) · enrolment QR on the reveal-token modal — the bare token, or a spatiumddi://enrol?… URI carrying host, port, scheme and the TLS certificate fingerprint, so the mobile client's trust-this-certificate prompt becomes a machine check instead of 64 hex characters compared by eye |
| 🛡 | TOTP MFA | 2FA on local logins — QR enrolment via pyotp + qrcode · single-use backup codes · admin force-disable per user (audit-logged) · enrolment also open to SSO accounts so external-auth superadmins can re-confirm sensitive secret reveals (#408) |
| 🔐 | Local-auth hardening | configurable password policy (min length · per-class complexity · history depth · max-age) · account lockout after N failed logins inside a rolling window (default off; opt-in in Settings) · active session viewer + force-logout at /admin/sessions — every login carries a jti claim that resolves to a UserSession row, flip revoked=True to 401 the in-flight token on its next call |
| 🏷 | Subnet classification tags | pci_scope · hipaa_scope · internet_facing first-class boolean columns on every subnet · indexed predicates · compliance roll-up card on Platform Insights · feeds the compliance-change alert + conformity policy filters |
| 🤖 | Operator Copilot (AI) | grounded chat over your live IPAM / DNS / DHCP / Network data — multi-vendor (OpenAI / Anthropic / Azure OpenAI / Gemini / OpenAI-compat for Ollama, vLLM, etc.) with automatic failover · hundreds of tools spanning IPAM (incl. discovery / stale-IP / reconciliation / utilization trends / hygiene findings), DNS (records / pools / blocklists / views / DNSSEC / query stats / drift), DHCP (pools / statics / classes / option templates / PXE / MAC blocks / pool occupancy / rogue responders), network modeling (ASNs / VRFs / circuits / services / overlays / domains), the BGP Looking Glass (sessions / learned routes / as-path + community queries / reverse route lookup for an IP), ownership (customers / sites / providers), admin (users / groups / roles / time-bound grants), reports (top-N), network tools (ping / traceroute / dig / port-test / TLS-cert / whois / MAC-vendor / Wake-on-LAN + wake schedules, runs and calendars), integration mirrors (K8s / Docker / Proxmox / Tailscale / NetBird / UniFi / Cloud / OPNsense / Palo Alto / Fortinet / Meraki — plus the vendor-neutral firewall shadow-IPAM store, active block sync and firewall feeds), appliance fleet (firewall policies / LLDP neighbours / pairing / OS upgrades / upgrade images / etcd snapshots / host-config syslog + SSH + resolver), observability (DNS query / DHCP activity / metrics / Redis stats / global search), compliance (conformity policies + results + framework rollups), maintenance mode, typed-event webhooks (registry + event-type catalog + delivery history), multi-node rolling upgrade state (preflight / runs / lease), Windows-cutover plans (plan status / readiness blockers / live parity check), and Apply-gated write proposals — propose_create_ip_address / propose_create_dns_record / propose_create_dhcp_static / propose_create_alert_rule / propose_run_nmap_scan / propose_run_packet_capture / propose_wake_host / propose_create_wol_schedule / propose_create_lg_peer / propose_archive_session plus conformity / webhooks / DNSSEC / multicast / SNMP-NTP / DNS + DHCP import commits · MCP HTTP endpoint for Claude Desktop / Cursor / Cline · "Ask AI about this" affordances on every resource · per-provider editable system prompt · per-provider tool allowlist · OUI vendor enrichment baked in · live nmap results in chat · per-message token / latency footer · Markdown + GFM tables in replies · daily digest |
| 🔔 | Alerts + forwarding | rule-based alerts · compliance_change rule type (PCI / HIPAA / internet-facing audit-log scanner with 24 h auto-resolve, three disabled seed rules) · DNS query-anomaly rules (dns_nxdomain_spike / dns_query_rate_spike) · IP-hygiene rules (ip_free_but_responding / stale_reservation / unknown_mac_in_static_range) · rogue_dhcp (unexpected DHCP responder) · multi-target syslog (RFC 5424 / CEF / LEEF / RFC 3164) · HTTP webhooks · SMTP email · Slack / Teams / Discord chat |
| 📑 | Conformity evaluations | declarative policy library scheduled against PCI-DSS / HIPAA / SOC2 frameworks · 21 check kinds (has_field · in_separate_vrf · no_open_ports · alert_rule_covers · last_seen_within · audit_log_immutable · voice_segment_not_internet_facing · no_multicast_collision · no_lanwide_control_plane_ports · av_flow_outside_reserved_range · av_flow_no_ptp_domain · bbmd_one_per_subnet · bacnet_duplicate_device_instance · bacnet_vendor_id_unknown · ot_device_crosses_purdue_boundary · ot_zone_missing_purdue_level · fragile_subnet_probed · dicom_ae_default_title · dicom_ae_title_convention · dicom_ae_outside_hipaa_scope · dicom_ae_no_tls) · 19 disabled seed policies, opt-in toggle · pass→fail transitions emit alert events · auditor-facing PDF export with SHA-256 integrity hash · Auditor + Compliance Editor builtin roles |
| 🙋 | Self-service request portal | low-privilege users ask for an IP, subnet, DNS record or DHCP reservation they can't create themselves · a request provisions nothing on its own — an approver who must hold the underlying operation's own permission reviews it against that operation's real preview, and approving runs it, identically to a manual create · not a second state machine: portal rows reuse the approval spine (self-approval block, stale re-preview guard, apply-under-approver, both user IDs in the audit row) · Requester builtin role · default-off governance.requests module · block / subnet / zone / scope fields are searchable pickers filtered to the requester's own read grants, not raw UUID boxes — a scoped requester sees exactly their row and never an estate inventory |
| ⌨️ | Keyboard shortcuts | ? opens a help overlay listing every binding, rendered from one shared map rather than a hand-written copy that goes stale — the global-search handler and its trigger keycap both read the same definition, so retuning a combo moves the handler, the keycap and the help together · stands down inside text inputs and won't stack over an open dialog |
| 🔖 | Saved views | per-user named filter / sort / column presets — "every circuit with this provider whose term expires inside 90 days" becomes one click · set a default per page · personal-only, never shared across users · wired into the Services / Circuits / Sites / Certificates list pages today; other pages opt in with two props |
| 🪝 | Typed-event webhooks | 144 typed events (39 resource namespaces × 3 verbs + 27 special-cased names) · HMAC-SHA256 signed · outbox-backed retry with backoff + dead-letter |
| 🐛 | Diagnostics — captured uncaught exceptions | every uncaught Python exception across API + Celery lands in a queryable internal_error table with fingerprint dedup (sha256 of class + top-2 frames), occurrence counter, last-seen-at bumping, redaction of headers + secret-shaped payload fields, context_json blob capped at 16 KB · admin viewer at /admin/diagnostics/errors with Acknowledge / Suppress (1 h / 1 d / 1 w) / Delete / Submit-bug (pre-filled GitHub-issue template URL) actions · daily prune sweep against the configured retention window |
| 🏷 | Platform-wide tags + filter | tags JSONB columns across IPAM (spaces / blocks / subnets / IPs) · Network modeling (ASNs / VRFs / circuits / services / overlays / customers / sites / providers) · DNS (zones / records) · DHCP (scopes / pools / statics) · ?tag= filter on every REST list endpoint with multi-tag AND/OR semantics · /api/v1/tags/autocomplete ranked by occurrence · tag chips on every list view + clickable pills on the IP detail modal that navigate to a filtered IPAM view |
| 🔏 | TLS certificate monitoring | auto-discover certs serving from managed DNS A/AAAA records + IPAM web / api / lb roles · full-chain probe (leaf → intermediate(s) → root) via pyOpenSSL · expiring / chain-invalid / SAN-mismatch / unreachable / changed alerts · Network → Certificates page + Domain / DNS-zone Certs tabs · "Certs expiring ≤30d" dashboard KPI |
| 🔐 | ACME DNS-01 provider | acme-dns-compatible — certbot / lego / acme.sh issue public certs (wildcards included) |
| 🔐 | ACME embedded client | hand-rolled RFC 8555 — auto-issue a CA-trusted Let's Encrypt cert for SpatiumDDI's own Web UI · DNS-01 over managed + cloud zones · HTTP-01 · manual-TXT fallback · 12 h auto-renewal |
| 📋 | Audit log | every mutation logged, append-only, filterable in the UI · tamper-evident SHA-256 hash chain — every row carries seq + prev_hash + row_hash; verifier walks the table, re-hashes, and pinpoints the first break |
| 🗑 | Soft-delete + 30-day Trash | spaces / blocks / subnets / DNS zones / DNS records / DHCP scopes are recoverable for 30 days · cascade restore via deletion_batch_id (one click brings a subnet's DHCP scopes back together) · global ORM filter hides soft-deleted rows by default · nightly trash_purge Celery task hard-deletes past the retention window |
| 💾 | Backup + restore | full-system backup with passphrase-wrapped secrets.enc + 10 destination kinds (local volume · AWS S3 / S3-compatible · SCP/SFTP · Azure Blob · SMB/CIFS · FTP/FTPS · GCS · WebDAV · NFS · HTTPS PUT/POST) · scheduled cron + retention · selective per-section restore · cross-install secret rewrap so cross-install operators don't hand-copy SECRET_KEY · alembic upgrade head on restore with drift auto-recovery · exclude-secrets diagnostic mode for shareable debug snapshots · proxy archive download · restore drills — scheduled test-restores into a throwaway scratch database that prove an archive is actually restorable, on their own cadence, with a target that has never passed reported as unverified rather than healthy · system.backup_* typed-event fan-out via the existing webhook outbox |
| 🩺 | Support bundle | one-click scrubbed diagnostics archive for a bug report — superadmin, audited, and working on Compose / Kubernetes / appliance alike · secrets are hard-excluded in every mode, including the unscrubbed one (Fernet blobs, password hashes, PEM keys, JWTs, agent PSKs, credentials in a database URL — matched by field name and value shape) · identifiers are pseudonymised HMAC-deterministically off SECRET_KEY, so topology survives: one real /24 lands in one synthetic /24 with the host octet intact and zone grouping preserved, which is what keeps the archive worth reading for a DDI product · preview → review → download, with the decode map on a separate endpoint and never inside the archive · designed on the premise that GitHub attachments are public, so the answer is scrubbing rather than secrecy |
| 🎨 | Operator branding | acceptable-use login banner with an acknowledgement checkbox that gates the SSO buttons too (otherwise the notice is skipped by signing in through the IdP) · custom logo, stored in Postgres rather than on a volume so it survives a multi-node control plane, PNG-only validated by magic bytes (an SVG served same-origin would be stored XSS against anonymous visitors) · coloured DEV / TEST / PROD strip with a contrast warning · a real browser + product title · all of it readable before a session exists via an unauthenticated GET /settings/public allow-list, written superadmin-only because it renders to anonymous visitors |
| 🔁 | Service control | start / stop / restart SpatiumDDI's own services from Admin → Platform Insights → Services on Docker Compose, Helm and the appliance · the capability is answered before anything is attempted — the UI renders the buttons that exist instead of drawing one and learning from its error, because "this deployment cannot do that" and "the daemon is down" need opposite responses · the live inventory is the allowlist, scoped on Compose to the api container's own project label and failing closed if it can't identify itself · off by default except on the appliance, with the env gate and the RBAC as separate switches so each failure is reported as itself |
| 🧹 | Factory reset | per-section "wipe back to defaults" surface for superadmins — 12 sections (IPAM · DNS · DHCP · Network modeling · Integrations · AI · Compliance · Tools · Observability logs · Auth+RBAC · Settings · Everything) · password re-verification + per-section DESTROY-* confirm phrase + in-flight backup mutex + 6 h cooldown · audit anchor that survives audit_log wipes · calling superadmin + built-in roles preserved across every section |
| Path | ||
|---|---|---|
| 🐳 | Docker Compose | docker compose up -d |
| ☸️ | Kubernetes | Helm umbrella chart, OCI-published |
| 🖥 | Bare metal / OS appliance | bare metal today · self-contained appliance ISO for amd64 and arm64, installable onto a RAID1 mirror (beta — Debian 13 + full stack, hybrid USB/CD, see Getting Started) |
The tables above are the elevator pitch. The bullets here are the same surface with the operational detail — what's stored, how it behaves, where the seams are.
-
🗂 Hierarchical IP management — spaces, blocks, subnets, addresses in a visual tree.
- IPv4 + full IPv6 auto-allocation (EUI-64 / random /128 / sequential)
- Per-IP role: host / loopback / anycast / vip / vrrp / secondary / gateway
- Reservation TTL with auto-expiry
- Per-IP MAC observation history
- Bulk allocate a contiguous range with a name template
(
dhcp-{n}/host-{oct3}-{oct4}/web-{n:03d}) — preview → commit, capped at 1024 IPs, skips dynamic DHCP pools, detects FQDN collisions, optionally creates A + PTR records - IP table polish — sticky column headers, shift-click range select, "Seen" recency dot per row (alive / stale / cold / never), subtle gap markers between non-contiguous IPs so a deleted hole doesn't go unnoticed
-
✂️ Subnet operations — preview-then-commit with typed-CIDR confirm.
- Split, Merge, Find-Free workflows
- Surfaced on the subnet detail header and via bulk-action toolbars on the block + space tables
- Bulk-select 1 row to split, 2+ to merge
- Block-detail tables reach parity with the space view — child blocks are bulk-selectable too, so leaf-empty blocks cascade-delete alongside subnets in one shot
-
🧮 Subnet planner + planning tools — design CIDR hierarchies before applying them.
/ipam/plans— draggable multi-level CIDR design surface (root + nested children, arbitrary depth)- Saved as
SubnetPlanrows, validated live as the operator edits, applied in a single transaction - Per-node DNS-group / DHCP-group / gateway bindings (null = inherit, explicit = set + flip inherit off)
- CIDR calculator at
/tools/cidr— pure client-side IPv4 + IPv6 breakdown - Address planner — packs
{count, prefix_len}requests into free space using largest-prefix-first ordering - Aggregation suggestion banner — surfaces clean-merge opportunities (10.0.0.0/24 + 10.0.1.0/24 → /23)
- Free-space treemap toggleable from the Allocation map header — surfaces fragmentation hidden in the 1-D band
-
🗑 Soft-delete + Trash recovery — 30-day Trash with cascade restore.
- Covers IP spaces, blocks, subnets, DNS zones / records, DHCP scopes
- Cascade-stamped batch IDs — one Restore click brings back every dependent row atomically
- Conflict detection on restore guards against clashing with live rows
- Operator-configurable purge sweep (
soft_delete_purge_days, default 30;0= keep forever)
-
🔁 NAT mapping cross-reference — operator-curated rules with FK links to IPAM.
- 1:1 / PAT / hide-NAT supported
- Per-IP modal lists every mapping that touches the address
- Per-subnet "NAT" tab uses Postgres CIDR containment to find every mapping crossing into the subnet's range
-
📜 DHCP lease history — forensic trail of every lease lifecycle event.
- Captures expiry, MAC reassignment, absence-delete
- Operator retention window (default 90 days), daily prune task
-
🌐 Built-in DNS server — BIND9 or PowerDNS or Technitium container, auto-registers, syncs via RFC 2136.
- Three first-class drivers. A server group is BIND9, PowerDNS or Technitium. Zones, records and pools are authored identically whichever you pick and the driver decides how they render — but the three are not feature-equivalent, and the difference is worth knowing before you pick:
- BIND9 only — split-horizon views and RPZ blocklists. The PowerDNS and Technitium drivers both report
views: False/rpz: False; a group on either will not serve them (Technitium has its own native blocking — below — but not RPZ) - PowerDNS only — ALIAS and LUA records (below)
- Technitium only — native DNS-over-QUIC, and encrypted upstream forwarding over HTTPS and QUIC as well as TLS (BIND 9.20 has no client-side HTTP or QUIC transport at all). It also blocks natively from a per-domain set rather than via RPZ, so SpatiumDDI's blocklists map onto that instead — with two documented lossy edges: per-view lists collapse into one flat set, and
is_wildcardis dropped because Technitium blocks a domain and its subdomains by default - Online DNSSEC sign / unsign — PowerDNS and Technitium. BIND9 signs from a rendered
dnssec-policyinstead - Catalog zones — BIND9 does producer and consumer; PowerDNS is producer-only; Technitium is not supported (refused until #1766)
- Technitium ships in two shapes —
technitiumis the container SpatiumDDI deploys and an agent drives over loopback;technitium_apiis agentless, for an install the operator already runs, driven straight from the control plane over its HTTP API with nothing deployed. Zone and record CRUD plus topology pull on the agentless one; DNSSEC, encrypted transports, forwarders and blocklists stay agent-managed. A group is single-driver, so the two live in separate groups - Windows DNS, agentless Technitium and the eight cloud providers are agentless drivers behind the same abstraction — all nine credentialed ones now answer a Test Connection probe on a saved server, so "did that token work?" no longer means waiting for a sync to fail
- BIND9 only — split-horizon views and RPZ blocklists. The PowerDNS and Technitium drivers both report
- Per-server zone-serial drift reporting
- Record-level drift report — per zone, AXFR the live zone from every server in the group and diff it against the database: extra on server (someone edited the host by hand), missing on server (a change never landed), in sync. Strictly read-only — it never applies anything, and the Sync path stays the way you push. A changed value shows as a missing+extra pair, because the diff key includes the value. Split-horizon zones carry a caveat banner: a transfer is addressed by zone name, so the server answers from whichever view matches the control plane's source address. Driver coverage: every driver except PowerDNS, which implements no record pull at all. Windows Path B, the cloud drivers and agentless Technitium (
technitium_api) pull over an API; agent-managed BIND9 and Technitium use a TSIG-signed AXFR, because their agents grant transfer to the group's key rather than to an address (#734) — a group with no TSIG key reports that rather than a bare refusal - PowerDNS-only record types — synthesised at query time by
pdns_server, so they have no BIND9 equivalent:- ALIAS — an apex CNAME-equivalent. Lets
example.comitself point at a cloud load balancer's hostname, which a real CNAME can't do at the apex without breaking the SOA / NS rrset - LUA — scripted answers (health-aware, client-aware, arithmetic). Security boundary worth stating plainly: LUA record scripts execute inside the
pdns_serverprocess, so anyone who can author one has code execution in your DNS daemon. Authoring is gated on the usual DNS write permission — scope that permission accordingly, and treat it as more privileged than authoring an A record
- ALIAS — an apex CNAME-equivalent. Lets
- DNSSEC — two signing models behind one UI, and SpatiumDDI holds no private key material in either
- BIND9 inline-signing — the zone references a
dnssec-policyand BIND generates, rotates and retires keys itself in its key directory. SpatiumDDI stores only the public key state it reads back - PowerDNS online signing — sign / unsign a zone from the zone page (
pdnsutil secure-zonesemantics); pdns signs responses on the fly rather than writing a signed zone file - Technitium online signing — same zone-page sign / unsign, defaulting to ECDSA P256 with NSEC. DS records are assembled from the KSK's own DNSKEY digests and reported with per-key state; Technitium rolls keys on its own schedule, so manual rollover stays BIND9-only
- Reusable policy library — KSK + ZSK algorithm / size / lifetime, and NSEC or NSEC3 with iterations, salt length and opt-out. One policy applies to many zones
- DS export — the delegation-signer record to hand to the parent zone's registrar, which is the step that actually turns validation on. Copy it out of the zone's DNSSEC panel
- Manual and scheduled key rollover, with per-zone key state (active / published / retired) visible per server
- BIND9 inline-signing — the zone references a
- Zone authoring:
- Delegation wizard — auto-stamps NS + glue in the parent zone
- Four starter templates: Email (MX / SPF / DMARC), Active Directory (LDAP / Kerberos / GC SRV), Web (apex + www), k8s external-dns target
- Conditional forwarders as a first-class zone type
- TSIG keys — full CRUD with Fernet-encrypted secrets
- One-shot "copy this secret now" reveal modal
- Rows distribute through the existing
tsig_keysConfigBundle block
- RPZ blocklists — 19-source curated catalog with one-click subscribe + immediate refresh
- Sources: AdGuard, StevenBlack, OISD, Hagezi, 1Hosts, Phishing Army, URLhaus, EasyPrivacy, …
- Content filtering / family filter — a one-click Family filter profile pairs the adult + gambling feeds with the DoH / VPN / proxy bypass lists, because a filter a browser can route around is not one
- SafeSearch enforcement — a built-in template that rewrites each engine to its own filtered endpoint (all 194 Google country domains, the five YouTube hostnames Google documents and no others, Bing including the Edge sidebar entry point, DuckDuckGo / Brave / Ecosia / Pixabay / Qwant, Yandex opt-in)
- Scope a list to a view, not just a server group — which is what makes "filter the kids' VLAN and not the server one" a real setting: the Views tab is full CRUD with an Add subnets… picker that turns IPAM prefixes into
match_clients, and each list carries a scope modal writing group + view assignment in one call. Everything an operator types there (match_clients,match_destinations, the view name — which also becomes a directory on the agent) is validated server-side, because it is interpolated verbatim intonamed.conf: an accepted-but-invalid value does not break one view, it stops the whole group's config converging, silently - Applying a profile assigns it to nothing — auto-scoping would filter the server VLAN along with the kids' one, so assignment stays a deliberate second step
- Rate limiting (RRL) + amplification defenses — BIND9 Response Rate Limiting (responses-per-second / window / slip / qps-scale / exempt-clients) with a
log-onlydry-run, plusminimal-responses/tcp-clients/clients-per-querytoggles; group-level, default-off (no-op until opted in)- RRL drop observability —
RateDropped/RateSlippedcharted as an "RRL drops/s" line on the server Stats tab + a default-offdns_rate_limit_droppingalert (fires when the server is actively shedding a flood) - dnsdist front for PowerDNS — opt-in front container (PowerDNS auth has no RRL of its own) forwarding to pdns: per-source-IP QPS cap (truncate / drop) + sustained-rate dynamic blocking, on a
dns-powerdns-with-dnsdistcompose profile (docker-compose for now)
- RRL drop observability —
- Encrypted transports (DoT / DoH / DoQ) — two independent halves, both per-group and default-off so an existing install renders a byte-identical
named.confuntil you opt in- Inbound — serve DNS-over-TLS (RFC 7858, :853) and DNS-over-HTTPS (RFC 8484,
/dns-query) alongside plain Do53, which is unaffected. Technitium adds DNS-over-QUIC, which defaults to :853 alongside DoT — not a copy-paste slip: DoQ is UDP and DoT is TCP, so they do not collide (the firewall opens both) - Outbound — forward to upstream resolvers over DoT instead of cleartext :53, with
remote-hostnamevalidation that fails closed (SERVFAIL, never a silent downgrade); opportunistic mode available for upstreams that publish no DoT hostname. Technitium also forwards over DoH and DoQ. Encrypted forwarding needs a hostname rather than an IP — there would be nothing to validate the upstream's certificate against — so the group'sforward_tls_hostnameis required and wins over the forwarder IPs - Upstream resolver presets — 16 across 7 providers (Cloudflare, Google, Quad9, Cisco OpenDNS, AdGuard, Mullvad, DNS4EU), each carrying the addresses and the certificate name those addresses actually present, because the two cannot be filled in inconsistently without the group resolving nothing. The unit is the preset, not the brand:
1.1.1.1iscloudflare-dns.comand1.1.1.3isfamily.cloudflare-dns.com, so mixing them breaks exactly like mixing two vendors while looking entirely deliberate — that combination is a 422, as is an encrypted-only upstream selected with Do53. A merely-undocumented hostname is a UI advisory, never a refusal, and manual entry is unconstrained: the catalogue is a convenience, never a whitelist - Certificates reuse the appliance cert store — upload one or issue it from Let's Encrypt with the built-in ACME client; renewals reach the agents automatically. A listener whose cert is deleted degrades to Do53 rather than taking the daemon down
- BIND9 and Technitium serve natively; PowerDNS gets inbound-only via the dnsdist front (pdns auth speaks neither protocol and doesn't forward). On Technitium the agent converts the stored PEM to a PKCS #12 bundle, which is the only format the daemon accepts
- Ports are operator-chosen and flow through to the appliance firewall automatically. DoH defaults to 443, which the appliance rejects because the web UI owns it there — use 8443
- Inbound — serve DNS-over-TLS (RFC 7858, :853) and DNS-over-HTTPS (RFC 8484,
- Named ACLs — reusable address-match lists rendered into
named.confaboveoptions, which is the correctness property rather than a style choice: BIND resolves anaclwhere it is written, so a definition below its first use is an error and not a forward declaration. Nested references are emitted dependency-ordered, and a cycle is refused at the commit by a graph check, sincea → b → ais two individually legal edges no per-field validation can see. Deleting or renaming an ACL something still cites is a 409 naming the citers; an entry-less one renders{ none; }rather than vanishing, because omitting a definition that may already be cited is what takes a whole group's config down - Catalog zones (RFC 9432) — producer / consumer roles auto-derived from the group's primary
- RFC-compliant SHA-1 hashing of zone names
- Operator tools:
- Multi-resolver propagation check (Cloudflare / Google / Quad9 / OpenDNS in parallel) on every record row
- Clickable analytics strip on the Logs page (top qnames + top clients + qtype distribution)
- Per-server detail modal — Overview / Zones / Sync / Events / Logs / Stats / Config tabs + a live
rndc statuspanel — answers "is this server actually running the config we sent?" without SSHing in
- Three first-class drivers. A server group is BIND9, PowerDNS or Technitium. Zones, records and pools are authored identically whichever you pick and the driver decides how they render — but the three are not feature-equivalent, and the difference is worth knowing before you pick:
-
⚖️ DNS pools (GSLB-lite) — health-checked DNS round-robin.
- One DNS name returns one record per healthy + enabled member; members flip in / out of the rrset as state changes
- Health checks:
tcp— open-connection probehttp/https— status-code match with optional TLS verificationicmp— echo-request viaiputils-pingnone— always healthy (for pools that just want manual-enable + multi-RR semantics)
- Per-pool interval (default 30 s), timeout, and consecutive-failure / consecutive-success thresholds so single flapping checks don't churn records
- Operator UX:
- Top-level
/dns/poolspage — every pool across every zone with live health summary - Per-zone Pools tab on the zone detail page
- Manual enable / disable per member, like a load-balancer pool
- Top-level
- Driver-agnostic — members render as regular A/AAAA records via the normal record pipeline, so BIND9 + Windows DNS serve them unchanged
- Tradeoff (UI-warned) — TTL races. DNS is cached client-side; a member dropping out doesn't take effect until TTL expires. This is not a real L4/L7 load balancer. Default TTL is 30 s with an inline pointer to the LB-mapping roadmap item.
-
🔄 DHCP server management — Kea container + agent with lease tracking.
- Group-centric HA (hot-standby + load-balancing) with live state reporting
- Self-healing peer-IP drift
- Supervised daemons for crash-loop-safe restarts
- Scope authoring:
- 95-entry RFC 2132 + IANA option-code library with autocomplete on the custom-options row (search by code or name, description shown inline)
- Named option templates (group-scoped, e.g. "VoIP phones", "PXE BIOS clients") — apply to a scope in one click; apply is a stamp not a binding, so later template edits don't propagate
- PXE / iPXE provisioning profiles — netboot a mixed-architecture estate without hand-writing Kea client classes
- A profile is group-scoped and reusable; a scope selects one via
pxe_profile_id. It carries thenext_server(TFTP / HTTP boot host) and N architecture matches - Each match pairs a vendor-class + DHCP arch-code filter with the boot file that architecture should pull —
undionly.kpxefor BIOS,ipxe.efifor UEFI x64, the arm64 or ia32 equivalents, or a chained iPXE config URL - iPXE chainloading is the case that makes this worth modelling: the classic two-stage boot needs a class guarded on
dhcp.user-classmatching the iPXE signature, so the second request (from iPXE itself) is answered with a script URL instead of the binary it already loaded — otherwise the client loops - Profiles can be disabled rather than deleted; a disabled profile renders no classes at all, so an operator can A/B-test boot files and roll back by flipping one switch
- A profile is group-scoped and reusable; a scope selects one via
-
🪟 Windows Server DNS + DHCP — agentless management of existing Windows DCs.
- RFC 2136 + WinRM for DNS
- Near-real-time WinRM lease-mirroring for DHCP
- No software installed on the Windows side
-
📥 DNS configuration importer — one-shot migration tool that turns existing zone data into native SpatiumDDI zones + records.
- Four sources, one canonical IR + commit pipeline:
- BIND9 — upload a
.zip/.tar.gzof thenamed.conftree; the parser walksincludedirectives, resolves zone files via four strategies (relative path, absolute path,directoryoption, search), toleratesview {}blocks, and feeds the same canonical-zone shape the other sources do - Windows DNS — live pull over WinRM via the existing
WindowsDNSDriver; honours system zones (TrustAnchors /_msdcs.*) by routing them through a dedicated branch instead of trying to migrate them - PowerDNS — live pull over the authoritative REST API (
X-API-Keyauth, hard cap of 5000 zones / 60 s socket timeout); hoists SOA from rrset content, splits MX / SRV priority into the dedicated columns, drops disabled records + DNSSEC + LUA / ALIAS with distinct warnings - Technitium — live pull over the console REST API (token auth); only
Primaryzones import, with secondary / stub / forwarder / catalog reported as warnings rather than minted as rows SpatiumDDI would then serve authoritatively. Technitium returns structuredrDatawhose field names differ from its own write API and which renders numeric rdata as enum names, so the importer inverts that back to wire values (DANE-EE/SPKI/SHA2-256→3 1 1 <hex>), passing unknown enum members through unchanged
- BIND9 — upload a
- Preview-before-commit on every source — operator sees the conflict picker (overwrite / skip / merge) before any rows are written
- Per-zone savepoint commit so a failure on zone N rolls back N but keeps zones 1..N-1 — no all-or-nothing import abort
- Provenance stamping —
import_source+imported_atcolumns ondns_zone+dns_recordflag everything that came in from the importer (UI surfaces a chip) - Tabbed admin page at
/admin/dns/import— one tab per source (plus a Cloud tab that pulls from a registered cloud-DNS server row), all rendered by a shared preview panel + commit-result panel - Once imported, SpatiumDDI is the source of truth — there is no continuous two-way mirror
- Four sources, one canonical IR + commit pipeline:
-
🪟 Windows → SpatiumDDI cutover — the importers land a copy of the Windows estate and prove nothing about what happens next: the Windows server is still running, still authoritative, and still the thing clients actually talk to. This is the other half of the journey, and it is emphatically not a fifth importer — it creates no zones, scopes, pools or records. Its only writes are TTL reductions on a zone SpatiumDDI already owns, reservations synthesised from live Windows leases, and the
is_activeflag on a managed scope.- The unit of work is a plan — a source Windows DNS and/or DHCP server, a target server group, and a list of items. One item is one zone or one scope. Items are independent: they can be cut over on different days and each rolls back on its own. There is no big-bang step anywhere
- Phase 1 — parity. Diff each object against the live Windows server, classifying every difference by why the two sides differ:
value_mismatch/drifted_since_import/never_imported/intentionally_diverged. A changed record pairs into one decision rather than the missing+extra pair the drift report produces. A Path-B PowerShell pull can't emit CAA / TLSA / SSHFP, so those reportnot_comparedrather than missing; an unparseable response reportsunverifiedrather than "everything diverged" - Phase 2 — parallel run. Replay recently-observed queries from the query log against both sides and compare answers, so parity is demonstrated against production traffic rather than asserted from config equality. Queries go out with RD=0, so a cached answer from somewhere else can't stand in for the server under test
- Phase 3 — the switch. A TTL pre-flight that snapshots the originals once and restores them exactly; a DHCP lease handover that promotes live Windows leases to reservations, so a renewing client keeps the address it already holds instead of meeting a Kea with an empty lease database; then the switch itself, which deactivates the Windows scope before activating the managed one and puts the old one back if the new side fails to come up
- Phase 4 — a 15-item decommission checklist, three of them advisory-evaluated from SpatiumDDI's own data (DC SRV registration, reverse-zone ownership, zone-transfer ACLs — the three an operator is most likely to get wrong). None is ever auto-ticked
- Readiness blockers, and one refusal that
forcecan't bypass — an AD-integrated zone set to "Secure only" dynamic updates is a hard block, because GSS-TSIG is unimplemented and a domain controller that can't register its SRV records turns a DNS migration into a domain outage. It fails closed: a dynamic-update mode we can't interpret on an AD-integrated zone is treated as Secure - A markdown runbook to paste into a change ticket — deliberately not a summary of what SpatiumDDI already did. It carries the Windows-side PowerShell SpatiumDDI won't run for you (lowering the authoritative TTLs resolvers actually cache — lowering only our side is worse than lowering neither, because it promises a five-minute rollback while the world holds hour-long answers), the order of operations, and a rollback with a real number attached
- Behind the
migration.cutoverfeature module (ships disabled), superadmin on every endpoint — a cutover is strictly more dangerous than an import, so inventing a grantable permission for it would be a weaker posture than the surface it extends
-
📡 Multicast group registry — IPv4 + IPv6 multicast groups as first-class entities.
- RFC 5771 IANA registry seeded as platform-provided rows (e.g.
224.0.0.1All-Hosts,224.0.0.5OSPF, …) - Operator catalog of business-defined groups (per-IPSpace) with description, owner, scope (link-local / admin-local / org-local / global)
- PIM rendezvous-point domains —
pim_rp_domaintable tracking RP routers + group ranges they serve - IPAM tree integration — creating a group in an IPSpace auto-creates the enclosing
224.0.0.0/4(v4) orff00::/8(v6) IPBlock when none exists; a startup hook backfills blocks for pre-existing groups so the upgrade is seamless - Tree rendering — multicast IPBlocks render with a violet 📡 Radio icon in both the tree row and BlockDetailView identity row; inside a multicast block, a "Multicast Groups" panel surfaces the streams whose addresses fall within the block's CIDR (queries
multicast_groupdirectly — no mirror IPAddress rows) - Click-through opens the multicast page pre-scoped to the IPSpace via
?space=<uuid> - Bulk-allocate from RFC 2365 admin-scoped ranges with name templating
- Per-IP collision conformity check — flags addresses that overlap a known multicast registration before allocation
- RFC 5771 IANA registry seeded as platform-provided rows (e.g.
-
🌐 ASN management — first-class autonomous-system entity.
- Data model:
asntable with BigIntegernumber(full 32-bit range), auto-derivedkind(public / private per RFC 6996 + RFC 7300), auto-derivedregistry(RIR — arin / ripe / apnic / lacnic / afrinic) from a hand-curated IANA delegation snapshot - RDAP holder refresh — per-RIR routing via IANA bootstrap; per-row Refresh button + scheduled hourly task (
asn_whois_interval_hours, default 24 h) - RPKI ROA pull — Cloudflare or RIPE NCC source (operator-tunable via
rpki_roa_source); cached for 5 min in-memory so a sweep of 50 ASNs makes one HTTP call; per-row Refresh RPKI button - Holder-drift diff viewer —
previous_holderpersisted on every refresh so the WHOIS tab can render a side-by-side without consulting the audit log - Alert rules —
asn_holder_drift,asn_whois_unreachable,rpki_roa_expiring,rpki_roa_expired - BGP Footprint tab — what the rest of the internet sees for this AS, alongside what you've recorded. RIPEstat supplies announced prefixes, prefix-overview and routing history; PeeringDB supplies the peering profile and IXP presence. Read-only, REST + MCP, with an in-process TTL cache (RIPEstat 6 h, PeeringDB 24 h) so an open tab doesn't hammer either service
- Detail page tabs: WHOIS · RPKI ROAs · BGP Footprint · BGP Monitoring · Learned Routes (only when the Looking Glass module is on) · BGP Peering · Communities · IP Spaces / Blocks · Alert History
- Data model:
-
🤝 BGP peering + communities — operator-curated relationship graph + community catalog.
- Peerings —
bgp_peeringtable withpeer | customer | provider | sibling; both endpoints FK ON DELETE CASCADE; unique on(local, peer, relationship_type). Form lets the operator pick either side as "local"; modal normalises to canonical shape on submit Router.local_asn_idFK — stamps which AS a router originates routes from- Communities catalog — 7 RFC 1997 / 7611 / 7999 well-knowns seeded as platform rows (no-export, no-advertise, no-export-subconfed, local-as, graceful-shutdown, blackhole, accept-own); per-AS catalog with
kindvalidation (standard/regularASN:N/largeASN:N:M) - "Use on this AS" button per standard row pre-fills the form with the well-known value
- Peerings —
-
🛣 VRFs as first-class entities — replaces the freeform
vrf_name/route_distinguisher/route_targetstext fields on IPSpace.- Data model:
vrftable with name, description, optionalasn_idFK, RD (with format validation), split import / export RT lists, tags, custom_fields ip_space.vrf_id+ip_block.vrf_idFKs ON DELETE SET NULL- Cross-cutting RD / RT validator — each
ASN:Nentry whose ASN portion does not matchvrf.asn.numberproduces a non-blocking warning;vrf_strict_rd_validationtoggle escalates to 422 IPBlock.vrf_warningflags when a block's pinned VRF differs from its parent space's VRF (intentional in hub-and-spoke designs but worth a heads-up)- VRF picker on the New / Edit IPSpace and Create / Edit IPBlock modals (replaces the freeform text inputs)
- Migration backfills existing freeform values into VRF rows so nothing is lost
- Data model:
-
📛 Domain registration tracking — distinct from DNSZone (records SpatiumDDI serves vs. registry-side metadata).
- Data model:
domaintable tracking registrar / registrant / expiry / DNSSEC status / nameservers - RDAP refresh — TLD → RDAP-base lookup driven by the IANA bootstrap registry (
data.iana.org/rdap/dns.json), cached 6 h; routes.com→rdap.verisign.com/com/v1/, etc. - Nameserver drift — operator-pinned expected list vs. registry-advertised list, with a side-by-side diff panel
- Alert rules —
domain_expiring(severity escalation aroundthreshold_days),domain_nameserver_drift,domain_registrar_changed,domain_dnssec_status_changed - Per-row expiry countdown badges (green > 90 d / amber 30–90 d / red < 30 d / dark-red expired)
- Explicit
dns_zone.domain_idlinkage with sub-zone suffix-match fallback —test.example.comshows up underexample.com's linked-zones tab;example.com.aucorrectly does NOT
- Data model:
-
🏢 Customer / Site / Provider — three first-class logical ownership rows that cross-cut IPAM / DNS / DHCP / Network.
Customer— soft-deletable; account number / contact info / status (active / inactive / decommissioning) / tagsSite— hierarchical viaparent_site_id; unique-per-parentcode(NULLS NOT DISTINCT for top-level deduping); kinds (datacenter / branch / pop / colo / cloud_region / customer_premise) + free-form region labelProvider— kinds (transit / peering / carrier / cloud / registrar / sdwan_vendor) + optionaldefault_asn_idFK- Cross-reference FKs added on subnet / ip_block / ip_space / vrf / dns_zone / asn / network_device / domain / circuit / network_service / overlay_network — every column is
ON DELETE SET NULLso re-tagging is safe and operators never lose data - Shared
CustomerPicker/SitePicker/ProviderPicker(with optional kind filter) + matching Chip components plug into every IPAM / DNS / circuit / overlay create + edit modal
-
🛤 WAN circuits — carrier-supplied logical pipe distinct from the equipment that lights it up.
- Data model:
circuittable withprovider_id(RESTRICT), optionalcustomer_id(SET NULL), 4 endpoint refs (a/z-end site + subnet, all SET NULL),transport_classenum (mpls / internet_broadband / fiber_direct / wavelength / lte / satellite / direct_connect_aws / express_route_azure / interconnect_gcp), asymmetricbandwidth_mbps_down/bandwidth_mbps_up,term_start_date/term_end_date,monthly_cost+ 3-letter ISO 4217 currency - Soft-deletable —
status='decom'is the operator-visible end-of-life flag; row stays restorable for "what carrier did Site-X use in 2024?" audits - List page at
/network/circuitswith bulk-action table + tabbed editor modal (General / Endpoints / Term + cost / Notes) + colour-coded term-end badge - Alert rules —
circuit_term_expiring(severity escalates aroundthreshold_days),circuit_status_changed(only fires onsuspended/decomtransitions; auto-resolves after 7 d)
- Data model:
-
📦 Service catalog — bundles network resources into a customer-deliverable.
NetworkServiceis one row per thing the operator delivers; polymorphicNetworkServiceResourcejoin row binds to VRF / Subnet / IPBlock / DNSZone / DHCPScope / Circuit / Site / OverlayNetwork- Kinds in v1:
mpls_l3vpn(with hard at-most-one-VRF rule + soft warnings for missing VRF, fewer than 2 edge sites, edge subnet's enclosing block in a different VRF) andcustom.sdwanlit up alongside the SD-WAN overlay roadmap. Future kinds reserved in the column:mpls_l2vpn/vpls/evpn/dia/hosted_dns/hosted_dhcp - Kind-aware
/summaryendpoint — L3VPN view returns canonical VRF + edge sites + edge circuits + edge subnets + warnings - Reverse lookup —
GET /by-resource/{kind}/{id}returns every service referencing a given resource - Alert rules —
service_term_expiring(mirrors circuit shape),service_resource_orphaned(sweep over join rows whose target was deleted; auto-resolves on detach) - List page at
/network/services(bulk-action table) + tabbed editor modal (General / Resources / Term + cost / Notes / Summary)
-
🌐 SD-WAN overlays — vendor-neutral source of truth for overlay topology and routing-policy intent.
- Vendor config push (vManage / Meraki Dashboard / FortiManager / Versa Director) and real-time path telemetry are explicitly out of scope — those stay NCM / observability concerns
- Data model:
overlay_network(six kinds: sdwan / ipsec_mesh / wireguard_mesh / dmvpn / vxlan_evpn / gre_mesh),overlay_site(m2m binding sites with role hub / spoke / transit / gateway, edge device, loopback subnet, orderedpreferred_circuitsjsonb — first wins, fall through on outage),routing_policy(priority + match-kind + match-value + action + action-target + enabled),application_category(curated SaaS catalog seeded with 33 well-known apps — Office365 / Teams / Zoom / Slack / Salesforce / GitHub / AWS / Azure / GCP / SIP voice / OpenAI / Anthropic / …) /topologyendpoint — nodes (sites + roles + device + loopback + preferred-circuits) + edges (site pairs whosepreferred_circuitslists overlap;shared_circuitsis the intersection so the UI can colour by transport class) + policies/simulateendpoint — pure read-only what-if; body specifiesdown_circuits, response shows per-site fallback resolution + per-policy effective-target withimpactedflag and human-readable note- List page at
/network/overlays+ detail page with five tabs: Overview / Topology (SVG circular layout with role-coloured nodes + transport-coloured edges) / Sites / Policies (priority-ordered with per-kind editors) / Simulate
-
📡 SNMP discovery — v1 / v2c / v3 polling via standard MIBs.
- MIBs walked: IF-MIB, IP-MIB, Q-BRIDGE-MIB, LLDP-MIB
- Surfaces interfaces, ARP, FDB, LLDP neighbours
- Per-IP switch-port + VLAN visibility in IPAM
- Neighbours tab on each device
-
🎯 Nmap scanner — on-demand scans from the browser.
- Per-IP "Scan with Nmap" launcher · per-subnet "Scan with nmap"
in the IPAM Tools dropdown (pre-fills CIDR target +
subnet_sweeppreset) · standalone/tools/nmappage for ad-hoc targets - Presets: quick, service+version, service+OS, OS, default-scripts, subnet_sweep (-sn ping sweep capped at /16 worth of hosts), UDP top-100, aggressive, custom
- Live SSE output streams while the scan runs; results render single-host or multi-host (CIDR) summaries
- Stamp alive hosts → IPAM action on a CIDR scan claims
responding IPs as
discoveredrows withlast_seen_atset;Copy alive IPsfor clipboard handoff - History page with bulk-delete (cancels in-flight scans + drops terminal ones) and a 3-tab right panel (Live / History / Last result) that auto-switches as a scan completes
- Per-IP "Scan with Nmap" launcher · per-subnet "Scan with nmap"
in the IPAM Tools dropdown (pre-fills CIDR target +
-
🛰 Device profiling — answer "what kind of device is on every IP" without asking. Two layers feeding one consolidated panel in the IP detail modal.
- Passive — DHCP fingerprinting. scapy
AsyncSnifferthread on the DHCP agent reads option-55 / option-60 / option-77 / client-id from every DISCOVER + REQUEST, batches per-MAC, ships to the control plane - Enrichment — fingerbank. Optional API key in Settings → IPAM →
Device Profiling turns raw signatures into Type / Class / Manufacturer
(
HP iLO,Aruba AP,Cisco IP Phone 8841,iOS device, …); 7-day cache; works offline-degraded - Active — auto-nmap on new DHCP lease. Per-subnet opt-in toggle picks a preset; refresh-window dedupe (default 30 days) means churning Wi-Fi leases don't fan out; per-subnet 4-scan concurrency cap
- "Re-profile now" button on the IP detail modal for ad-hoc rescan
- Default-off everywhere — IDS-aware (nmap is loud; passive collection
needs
cap_add: NET_RAW)
- Passive — DHCP fingerprinting. scapy
-
🎨 Dashboard-at-a-glance — nine sub-tabs: Overview / IPAM / DNS / DHCP / Network (ASN drift + RPKI expiry + circuit alerts + service orphans) / Integrations (per-mirror counts + last-sync staleness) / Security (lockout state + active sessions + audit-chain status + MFA enrolment) / Compliance (PCI / HIPAA / internet-facing flag counts) / Conformity (per-framework status + auditor PDF download).
- Platform health card (API / Postgres / Redis / workers / beat)
- Live DNS query rate + DHCP traffic charts — self-contained, no Prometheus needed
- Sources: BIND9 statistics-channels + Kea
statistic-get-all
- Sources: BIND9 statistics-channels + Kea
- Subnet utilization heatmap
- Live activity feed
-
📊 Platform Insights admin page — native diagnostics, no extra agents.
- Postgres: DB size, cache hit ratio, WAL position, slow queries via
pg_stat_statements, table sizes, idle-in-transaction watch - Containers: per-container CPU / memory / network / IO from the local Docker socket
- Postgres: DB size, cache hit ratio, WAL position, slow queries via
-
🏷 IEEE OUI vendor lookup — opt-in MAC vendor display.
- Surfaces in IP tables and DHCP leases
- Filter-by-vendor support
-
🧩 Read-only integrations — auto-mirror cluster / hypervisor / overlay state into IPAM.
- Kubernetes — CIDRs, nodes, LoadBalancer VIPs, Ingress → DNS
- Docker — networks, optional container IPs
- Proxmox VE — bridges, SDN VNets + subnets, VMs, LXC guests (runtime IPs via QEMU guest-agent); one row per cluster
- Tailscale — device mirror + synthetic
*.ts.netDNS zone - NetBird — managed-WireGuard mesh peers (OS / version / groups / connection state) + optional synthetic mesh DNS zone; one row per NetBird deployment, operator-supplied management URL (self-hosted or cloud)
- UniFi Network — per-controller sites, networks (VLAN ID + CIDR → IPAM subnets), connected clients (hostname + MAC + IP); one row per controller
- Palo Alto PAN-OS / Panorama — address objects + groups → a vendor-neutral shadow IPAM store with a two-way drift report, NAT rules → live NAT mappings, opt-in zones / interfaces → subnets and DHCP leases → addresses; one row per
vsysor Panoramadevice-group - Fortinet FortiGate — address objects + groups → shadow IPAM, VIPs (destination NAT), opt-in interfaces + DHCP leases; one row per VDOM, over the FortiOS REST API
- Cisco Meraki MX — appliance VLANs → subnets, DHCP fixed-IP reservations, org policy objects → shadow IPAM, 1:1 NAT + port-forward, opt-in clients; one row per organization, over the cloud Dashboard API (rate-limit aware)
- One-click setup guides per integration
- Opt-in VNet-CIDR inference from guest NICs (for SDN deployments where PVE is L2-only)
- Per-endpoint "Discovery" modal — which VMs aren't reporting IPs + copy-ready fix hints
- Settings toggle gates each; per-target sync interval + on-demand Sync Now
- Supernet auto-creation for RFC 1918 / CGNAT ranges keeps the tree tidy
-
🚫 Active block sync — the one write path, and the deliberate exception to the read-only stance. SpatiumDDI could already see a rogue DHCP responder or an unknown MAC and starve it of a lease, but a device that self-assigns a static IP walked straight past that. Block sync pushes a real block at the natural enforcement point.
- A SpatiumDDI-owned block set (IP or MAC, with reason / source / optional auto-expiry) that an idempotent reconciler converges onto every armed target — and lifts when a block is disabled, expires, or is deleted
- OPNsense — firewall table-alias membership (by IP); never rule CRUD
- UniFi — L2 client quarantine (by MAC)
- Palo Alto — Dynamic Address Group
IP → tagregister via the User-ID API; no policy commit, enforced near-instantly - Cisco Meraki — per-client built-in
Blockeddevice policy; the cloud applies it immediately, no on-prem deploy - Fortinet — the feed inversion: instead of SpatiumDDI holding write credentials on your firewall, it serves a token-scoped
blocklist.txtthat the FortiGate polls as an External Threat Feed. Zero write credentials on the device - Convergence is non-destructive — SpatiumDDI only ever removes values it added, never alias members or blocked clients it doesn't own
- Guardrails: off by default behind a feature module, a per-target enforcement master switch (independent of the mirror), distinct write-scoped credentials, a per-target preview diff, full audit on every push, dedicated
manage_block_sync/manage_firewall_enforcementpermissions, and two-person approval - The New Devices review queue's Block action grows an "also quarantine upstream" option
-
🔒 Group-based RBAC + external identity — multi-protocol auth.
- LDAP, OIDC, SAML, RADIUS, TACACS+
- Backup-server failover for every protocol
- Delegate IP ranges and zones by role
- API tokens with auto-expiry
- Scoped API tokens —
scopesJSONB column onapi_tokenlists the resource_types the token is allowed to touch (vs. inheriting all of the user's permissions). Permission-name granularity (subnet:read,subnet:admin,*for full inheritance). Authorization enforces scope intersection — token can do at most what the scope set allows AND what the user has permission for. - Resource-scoped API tokens (#374) — a
resource_grantslist binds a token to specific instances ({action, resource_type, resource_id}, resource_type ∈subnet/dns_zone) so a leaked CI / Terraform secret can only touch the one subnet or zone it was minted for. The binding only ever narrows the owner: the effective permission is the intersection of the owner's RBAC and the token grants, validated at create time to be a subset of what the issuer holds (you can't mint a token more powerful than yourself). Enforced per-row at the IP-create/edit/delete + DNS-record CRUD handlers.
-
🛡 TOTP MFA — second factor on local logins; SSO accounts can also enrol to re-confirm sensitive secret reveals (appliance kubeconfig, pairing codes, agent keys, SNMP community) when they have no local password (#408).
- Enrolment flow: Settings → Security → "Enable MFA" → scan QR (
pyotp+qrcodelibraries) → enter 6-digit code → backup codes shown once - Login flow gains a second step when MFA is enabled — JWT pre-token issued on username+password, exchanged for full token after TOTP code or backup code accepted
- Backup codes are single-use and persisted hashed
- Admin can force-disable MFA per user (audit-logged)
- Enrolment flow: Settings → Security → "Enable MFA" → scan QR (
-
🏷 Subnet classification tags — first-class compliance flags on every subnet.
pci_scope/hipaa_scope/internet_facingboolean columns, each individually indexed (partial indexWHERE col = true) so the auditor's "show me every PCI subnet" filter hits an index without competing- List filters across the IPAM page + the API
- Compliance dashboard at
/admin/complianceshows the three buckets side-by-side - Feeds the compliance-change alert + conformity policy filters described below
-
🛂 Compliance change alerts — reactive: catch every mutation against PCI / HIPAA / internet-facing scope.
- New
compliance_changerule type with two params:classification(which Subnet flag the rule watches) andchange_scope(any_change/create/delete) - Audit-log scanner runs on the existing 60 s alert tick. Watermark column on the rule baselines to
now()on first run so historical audit history doesn't retro-page operators when a rule is first enabled - Resolves IP-address / DHCP-scope audit rows back to their parent subnet for classification lookup; deletes fall back to
audit_log.old_value.subnet_idso a delete still resolves the originating subnet - One event per matching audit row, auto-resolves after 24 h, fans through the existing audit-forward syslog / webhook / SMTP targets
- Three disabled seed rules ship at first boot: PCI scope changes, HIPAA scope changes, internet-facing scope changes — operator opts in by toggling enabled
- New
-
📑 Conformity evaluations — proactive: prove steady state and produce the auditor PDF.
- Declarative
ConformityPolicyrows pin acheck_kindagainst a target set (subnet / IP address / DNS zone / DHCP scope / platform). Beat-driven engine ticks every 60 s and runs every enabled policy on itseval_interval_hourscadence (default 24 h). On-demand re-eval viaPOST /conformity/policies/{id}/evaluate - 16 check kinds:
has_field— non-empty value on a named target column (e.g. PCI subnet must havecustomer_id)in_separate_vrf— subnet's effective VRF holds only classification-matched siblings (no PCI ↔ non-PCI mixing)no_open_ports— latest nmap scan within N days didn't expose forbidden ports (warnwhen no recent scan; never silent-pass)alert_rule_covers— at least one enabled alert rule of the named type covers this scope (positive coverage signal — confirms the reactive #105 channel is wired)last_seen_within— IP / subnet recency check (catches rows that should be decommissioned)audit_log_immutable— platform-level positive-presence signal for the auditor checkboxvoice_segment_not_internet_facing— a voice / VoIP-classified subnet must not also carry theinternet_facingflagno_multicast_collision— no two multicast group registrations in an IPSpace claim the same addressno_lanwide_control_plane_ports— no appliance exposes k3s control-plane ports (etcd / kubelet / apiserver) to the LANav_flow_outside_reserved_range— an AV-over-IP flow sits outside every multicast range declared for its protocol (not applicable when no range is declared — absence of policy isn't a violation)av_flow_no_ptp_domain— advisory; an AV flow with no PTP clock domain recorded (PTP misconfig is the top AoIP failure mode)bbmd_one_per_subnet— a BACnet-bearing subnet has exactly one BBMD; fails on 0 (devices unreachable across routers) and on >1 (duplicated broadcasts)bacnet_duplicate_device_instance— no two BACnet devices share an internetwork-unique device instance numberbacnet_vendor_id_unknown— advisory; BACnet devices reporting vendor id 0 or none (usually a cloned or misconfigured controller)ot_device_crosses_purdue_boundary— an OT device's Purdue level differs from the level its subnet is zoned forot_zone_missing_purdue_level— advisory; a subnet carrying OT devices but declaring no OT zonefragile_subnet_probed— works backwards from the OT / DICOM /bmc-role registries: a subnet holding devices known to be probe-fragile that nobody marked do-not-probedicom_ae_default_title— no DICOM AE is still on a vendor default title (AE_TITLE,DICOM_SCP, …)dicom_ae_title_convention— AE Titles match an operator-supplied regex, so the estate's naming scheme is enforceabledicom_ae_outside_hipaa_scope— AEs sit in subnets actually flaggedhipaa_scopedicom_ae_no_tls— AEs are recorded as TLS-enabled
- 19 disabled seed policies covering PCI-DSS / HIPAA / SOC2 + the vertical registries: PCI dedicated VRF, PCI owner_assigned, PCI no admin ports, PCI alert coverage, PCI no stale IPs, HIPAA dedicated VRF, internet-facing alert coverage, voice-not-internet-facing, multicast-collision-free, audit log immutable, no LAN-wide control-plane ports, AV flows in declared range, AV flows record PTP domain, one BBMD per subnet, unique BACnet device instances, plausible BACnet vendor ids, OT devices match subnet Purdue level, OT subnets declare a zone, fragile subnets marked do-not-probe
pass→failtransitions emitAlertEventrows against the policy's wired alert rule when set, so conformity drift surfaces in the existing alerts dashboard- Append-only
ConformityResulthistory indexed twice (by policy and by resource) so both natural drilldowns hit an index — answers "every result for this policy" + "every policy that touched this resource" in O(log n) - Auditor-facing PDF export via
reportlab— per-framework summary table, per-policy section with pass / warn / fail counts, enumerated failing rows with diagnostic JSON pretty-printed beneath, trailer with a SHA-256 hash over(result_id, status)tuples so the auditor can verify post-generation tampering.GET /conformity/export.pdfwith optional?framework=filter - New
conformitypermission resource type plus two new built-in roles: Auditor (read-only) suitable for an external auditor account, Compliance Editor (admin) for the team that authors and tunes policies - Frontend
/admin/conformitypage with per-framework summary cards, policies table (toggle / re-eval / edit / delete inline), filterable results panel with diagnostic JSON drill-in. Platform Insights gains a Conformity card with deep-link
- Declarative
-
🗑 Soft-delete + 30-day Trash — accidental deletes are recoverable; the
Deletebutton moves rows to a holding area, not the void.- Scope:
IPSpace,IPBlock,Subnet,DNSZone,DNSRecord,DHCPScoperows inherit aSoftDeleteMixin(deleted_at,deleted_by_user_id,deletion_batch_id). IP addresses are intentionally NOT soft-deletable — they cascade-delete with their parent subnet, and the parent subnet is the recoverable unit - Global ORM filter — a
do_orm_executeevent listener injectsModel.deleted_at IS NULLinto every SELECT touching one of these models, so the rest of the codebase doesn't need to remember. Callers that need to see the trash opt in viaexecution_options(include_deleted=True) - Cascade-aware restore — when you delete a subnet its DHCP scopes are stamped under the same
deletion_batch_id; one click on Restore brings the whole batch back atomically, with a pre-flight conflict check (rejects 409 when a live row would clash on a uniqueness key) - Admin page at
/admin/trashlists soft-deleted rows newest-first with type / since / substring filters and "Restore" / "Delete permanently" per row. Sidebar entry under Admin - Nightly purge —
trash_purgeCelery beat task hard-deletes rows older thanPlatformSettings.soft_delete_purge_days(default 30; set 0 to disable forever). The retention window is operator-tunable in Settings → Security - Endpoints:
GET /admin/trash·POST /admin/trash/{type}/{id}/restore·DELETE /admin/trash/{type}/{id}(hard-delete a soft-deleted row before the purge sweep)
- Scope:
-
📋 Audit log + tamper-evident hash chain — append-only, SHA-256 chained, machine-verifiable.
- Every mutation across IPAM / DNS / DHCP / Network / auth / ownership / integrations writes an
AuditLogrow before the response is returned. Filterable in the UI by user / action / resource type / time range; full row diff (old_value/new_value/changed_fieldsJSONB) so an audit shows you exactly what changed - Hash chain — each row carries
seq(monotonically-increasing position),prev_hash(the previous row's hash), androw_hash = sha256(prev_hash || canonical_json(row)). Abefore_flushSQLAlchemy listener takes a Postgres transaction-scoped advisory lock so concurrent transactions can't interleave their "fetch previous hash, hash my row, write it" sequence — you can't fork the chain by racing - Verifier —
verify_chainwalks the table inseqorder, recomputes the hash for each row, and returns the first break withreason=row_hash_mismatch(someone edited the row's content) orreason=prev_hash_mismatch(someone deleted or inserted a row mid-stream). One verification pass shows the offending row + the position in the chain - Conformity hookup — the
audit_log_immutableconformity check kind runs the verifier on its scheduled tick and emits apass/failresult, so the auditor's PDF export carries a positive-presence signal that nothing has been tampered with since the last evaluation - Backfill migration —
d92f4a18c763_audit_chain_hashpopulatesseq,prev_hash, androw_hashfor every existing row in chronological order on upgrade, so the chain is unbroken from day one
- Every mutation across IPAM / DNS / DHCP / Network / auth / ownership / integrations writes an
-
💾 Backup + restore — full-system snapshot with passphrase-wrapped secrets and 10 destination kinds.
- Format — single
.ziparchive carryingmanifest.json(app version, schema head, hostname, dump format),database.dump(custom-formatpg_dump),secrets.enc(PBKDF2-HMAC-SHA256 + AES-256-GCM envelope wrapping the install'sSECRET_KEY), andREADME.txt. The operator passphrase NEVER lands on disk or in the API logs - Ten destination kinds —
local_volume(filesystem path mounted as a docker / k8s volume),s3(AWS + S3-compatible: MinIO, Wasabi, Backblaze B2, Cloudflare R2 — viaendpoint_url),scp(SFTP with password OR PEM private key, three host-key check modes),azure_blob(shared-key OR connection-string),smb(NTLM with optional domain + SMB3 encryption),ftp(plain ftp / ftps_explicit / ftps_implicit + passive/active + verify_tls toggle),gcs(service-account JSON key),webdav(Nextcloud / ownCloud / mod_dav / IIS WebDAV via PUT/GET/PROPFIND/DELETE — no SDK dep),nfs(an NFSv4 or v3 export, spoken in userspace — no kernel mount, so it works on the appliance too),https_put(PUT or POST to an HTTPS receiver — Artifactory / Nexus generic repositories, a presigned URL — with none / bearer / basic / header auth; write-only, so no listing, restore-from-destination or drill). SameBackupDestinationABC + module registry; the kind picker reflects onGET /backup/targets/kindsso adding a new driver requires no frontend changes - Scheduled cron + retention — 5-field UTC cron with friendly presets (hourly / 6h / 12h / daily 02:00 / 04:00 / weekly Sun 03:00 / monthly 1st 03:00) + custom; retention as
keep_last_nORkeep_for_days; per-target last-run state surfaced inline; 60s beat sweep withlast_run_status='in_progress'per-target mutex - Selective restore — operators tick which sections to restore (IPAM only / DNS only / etc.) on both upload-restore and restore-from-destination flows. 17-section catalog mapping all 110 schema tables; volatile sections (DHCP leases, DNS query log, DHCP activity log, nmap scan history, metric samples) skipped by default. TRUNCATE … RESTART IDENTITY CASCADE +
pg_restore --data-only --table=…in one transaction, with the closure's foreign keys set aside and checked on the way back, so it needs no superuser (#1693);platform_internal(alembic_version + oui_vendor) always rides along - Cross-install secret rewrap — restoring onto an install with a different
SECRET_KEYrewraps every Fernet-encrypted column (22 columns across 16 tables + thebackup_target.configJSONB blob). Same-install restores short-circuit. Closes the manualSECRET_KEYcopy step that Phase 1 needed - Alembic upgrade-on-restore + drift recovery — restore detects schema-version skew and runs
alembic upgrade headautomatically when the source is on an older head. Drift-recovery branch handles the case where the dump's alembic_version row is stale relative to the dump's own schema (operator stamped head later, then restored an older backup): canonical "table already exists" error pattern is detected and recovered viaalembic stamp head - Exclude-secrets diagnostic mode — checkbox on the build form switches to plain-format dump and post-processes the SQL text in memory to scrub every Fernet-encrypted column +
__enc__:-prefixed JSONB field. Live database is never touched. For sharing snapshots with support / consultants without leaking integration credentials - Restore from any archive at any destination — pick destination → expand drawer → click Restore icon. Driver fetches the bytes via
download(filename); standard restore path takes over. PlusGET /backup/targets/{id}/archives/{filename}/downloadfor proxy-download (operator pulls a remote archive without ever holding the destination's credentials) and…/archives/latest/downloadfor one-shot "give me the newest" automation - Audit + observability — every backup creates / fails / restore-performed lands an audit row;
system.backup_completed/system.backup_failed/system.restore_performedtyped events fire via the existing webhook event-outbox + HMAC-signed POST + retry pipeline - Restore drills — scheduled proof that the archives are actually restorable, which is the failure nobody discovers until recovery day. A drill downloads a target's newest archive, replays it into a throwaway scratch database, runs a fixed assertion set (archive readable and a supported format version · the stored passphrase opens
secrets.enc· the dump replays · the alembic head is one this build knows · core tables are populated · the audit chain verifies), then drops the scratch database. The live database is never written to — the scratch name is generated from a fixed prefix plus random hex, never from operator input, and is checked against the live database name before anything runs. Deliberately not built on the operator restore path, which takes a pre-restore safety dump and disposes the live engine's pool: correct for a deliberate restore, wrong for an unattended background check- Cadence is separate from the backup schedule, because replaying a full dump costs far more than taking a backup — weekly drills against nightly backups is the expected shape
- Bounded and self-cleaning: a size ceiling (
SPATIUM_DRILL_MAX_DB_BYTES, 20 GiB default) so a verification job can never fill the production volume, a stale-mutex escape so a killed worker can't stop a target drilling forever, and a reaper that drops orphaned scratch databases with no backend attached restore_drill_failedalert fires only on a verdict about the archive — never on "the drill couldn't run at all", which would train operators to ignore the rule. A target with drills switched off reports as unverified, not healthy- Restore Drills tab on the Backup admin page,
GET/POST /backup/drills(list · get · run-now · readiness), 2 Copilot tools
- Endpoints:
POST /backup/create-and-download·POST /backup/restore·POST /backup/targets(CRUD + run-now + test) ·POST /backup/targets/{id}/archives/restore·GET /backup/targets/{id}/archives/{filename}/download·GET /backup/targets/{id}/archives/latest/download·GET /backup/drills. UI lives at Administration → Backup
- Format — single
-
🧹 Factory reset — per-section "wipe back to defaults" surface for superadmins.
- 12 sections mapped from the operator's mental model: IPAM · DNS · DHCP · Network modeling · Integrations · AI/Copilot · Compliance · Tools · Observability logs · Auth+RBAC · Settings+branding · Everything. Per-section confirm phrase (
DESTROY-IPAM/DESTROY-DNS/ … /FACTORY-RESET-ALL) typed exactly to commit - Three dispatch kinds:
truncate(9 sections — straightTRUNCATE … RESTART IDENTITY CASCADE),auth_rbac(partial wipe preserving the calling user, every other superadmin, and built-in roles),settings_reset(DELETE platform_settings; recreated with model defaults). Tables intentionally untouchable:alembic_version,oui_vendor,backup_target,feature_module,event_outbox,internal_error,audit_forward_target— the schema head, OUI cache, recovery path, module toggles, and audit-forward shipping channels survive every reset - Hard guardrails server-side: superadmin gate · fresh bcrypt password re-check (NOT bearer-token check) · exact-match per-section confirm phrase · refuses 409 when any backup target is mid-run · Redis lock against concurrent resets · 6-hour cooldown · audit anchor written via fresh AsyncSessionLocal post-truncate so the trail of evidence survives an
audit_logwipe ·system.factory_resettyped event fans through the existing webhook outbox - Pre-flight backup as warn-only with override — if no enabled backup target exists,
POST /system/factory-reset/executereturns 412 unless the operator passesacknowledge_no_backup=true. The Backup admin tab surfaces the warning + checkbox up front - UI lives as a third tab on the Backup admin page (after Manual + Destinations) — backup snapshots state, factory reset wipes it, two ends of the same lifecycle. Per-section cards in a 2-col grid + red-bordered "Reset everything" card. Modal gates the password field on a green-border phrase match
- 12 sections mapped from the operator's mental model: IPAM · DNS · DHCP · Network modeling · Integrations · AI/Copilot · Compliance · Tools · Observability logs · Auth+RBAC · Settings+branding · Everything. Per-section confirm phrase (
-
🤖 Operator Copilot — AI assistant grounded in your live IPAM / DNS / DHCP / Network data. Hosted-API or fully on-prem (Ollama). One provider config, hundreds of tools, real conversations about your network.
Provider + model
- Multi-vendor — OpenAI, Anthropic (Claude), Azure OpenAI, Google Gemini, plus OpenAI-compat (Ollama, OpenWebUI, vLLM, LM Studio, llama.cpp server, LocalAI, Together, Groq, Fireworks). Add multiple providers in priority order; orchestrator picks the highest-priority enabled one
- Automatic failover chain — on transient failure (5xx / timeout / rate-limit) the orchestrator walks remaining providers; first successful chunk wins. Permanent errors (4xx / auth) surface immediately
- Per-provider system prompt override — admin-editable inside the AI Provider modal; baked-in default is also viewable inline so you can fork it. Snapshotted onto each session at creation so live edits don't break in-flight chats
- Per-provider tool allowlist — new "Tools" tab on the AI Provider modal, category-grouped checkbox list with "write" badges on
propose_*rows. NULL = "use whatever the registry has"; non-empty list pins exactly those tools. Right call for small Ollama models that struggle with a large tool set, kiosk providers limited to read-only, and per-provider compliance posture - Reasoning-channel fallback —
qwen3.5/ DeepSeek-R1 / o1 / o3 family that route their answer toreasoninginstead ofcontentare handled transparently by the driver - Ollama context-window forwarding — driver forwards
options.num_ctx/num_predict/extra_bodyso Ollama respects the configured context window. Operators can also setOLLAMA_CONTEXT_LENGTHenv var on the server side (recommended); without one or the other, Ollama silently truncates to 2048 tokens and small models hallucinate tool names from a half-cut tool list
Tool registry (hundreds of tools — highlights below)
Each tool is gated by both the
feature_moduleit belongs to (integrations.unifioff → UniFi tool disappears from the registry) and an admin-controlled per-tool allowlist at Admin → AI → Tools, so operators can trim what the model can see without touching code. Every tool can also be flipped per-provider via the AI Provider modal's Tools tab — the right call for small Ollama models that struggle with hundreds of tool schemas.The per-category bullets below are a representative tour, not an exhaustive enumeration — the canonical inventory is
backend/app/services/ai/tools/(one module per category, every@register_tool(...)decorator is a live entry) and the live/api/v1/ai/toolsendpoint on a running install. Categories not surfaced here (multicast, host-config firewall / LLDP / syslog / SSH, …) round out the full registry.- IPAM —
list_ip_spaces,list_ip_blocks,list_subnets,get_subnet_summary,find_ip(returns MAC + vendor),find_ip_addresses(cross-subnet paginated search),find_by_tag,count_ipam_resources,find_devices_by_vendor,count_devices_by_vendor,propose_create_ip_address. Name-or-UUID resolution onspace_id/block_idso the model can pass"home"directly without a UUID-lookup hop - DNS —
list_dns_server_groups,list_dns_zones,list_dns_views,list_dns_records(cross-zone substring search),export_dns_zone_records(one zone's records as the rows of its CSV / XLSX export),list_dns_pools(GSLB pools + per-member health),list_dns_blocklists(RPZ rows + sync state),list_blocklist_templates(the catalog's inline templates + profiles, e.g. SafeSearch and Family filter),list_resolver_presets(each upstream's addresses with the certificate name they present, which is what a DoT forwarder has to match),query_dns_records,forward_dns,reverse_dns,propose_create_dns_record - DHCP —
list_dhcp_server_groups,list_dhcp_servers,list_dhcp_scopes,list_dhcp_pools(dynamic / excluded / reserved),list_dhcp_statics(MAC → IP reservations),list_dhcp_client_classes,list_dhcp_option_templates,list_pxe_profiles,list_dhcp_mac_blocks,find_dhcp_leases(returns MAC + vendor),find_ra_subnets/find_observed_ra_routers/count_rogue_ra_routers/propose_allowlist_ra_router(IPv6 RA + rogue-RA),propose_create_dhcp_static - Network modeling —
list_asns+get_asn(RDAP holder, RPKI ROAs, BGP peerings),find_bgp_hijacks/count_bgp_hijacks/find_tracked_prefixes/propose_allowlist_bgp_origin(prefix-hijack detection),list_domains(registrar / expiry / DNSSEC / NS drift),list_vrfs(RDs + RTs + ASN linkage),list_circuits(transport + bandwidth + cost + endpoints),trace_circuit_impact(down-circuit blast radius across services + sites),list_network_services+get_network_service_summary(service-catalog deliverables — MPLS L3VPN, etc.),list_overlay_networks+get_overlay_topology(SD-WAN sites + policies),list_application_categories(RFC 4594 DSCP catalog),list_network_devices,find_switchport,ping_host,list_nmap_scans,get_nmap_scan_results,propose_run_nmap_scan - BGP Looking Glass —
find_bgp_lg_sessions(peer state + uptime),find_bgp_routes/count_bgp_routes/get_bgp_route(the collector's live RIB, filterable by prefix, origin ASN, community, or a Cisco / Juniper as-path regexp),find_bgp_route_for_ip(which learned prefix covers this address),find_vrf_learned_routes(Route-Target matched VPNv4 / VPNv6 routes),find_multicast_bgp_reachability,propose_create_lg_peer - Scheduled Wake-on-LAN —
find_wol_schedules/get_wol_schedule/count_wol_schedules,preview_wol_schedule_targets(which hosts would this fire wake, right now),find_wol_runs(history + per-host verify rollup),find_wol_wake_failures/count_wol_wake_failures(hosts that didn't come up, with the per-source evidence trail),find_wol_calendars/get_wol_calendar/find_wol_calendar_events,propose_create_wol_schedule/propose_run_wol_schedule_now/propose_set_wol_schedule_enabled - Ownership —
list_customers,list_sites,list_providers,get_customer_summary(per-customer rollup of subnets / blocks / spaces / circuits / services / ASNs / zones / domains / overlays in one call) - Admin —
list_users,list_groups,list_roles(superadmin-gated inline; the orchestrator returns an error dict for non-admins) - Appliance fleet config —
find_snmp_settings,find_ntp_settings,find_pairing_codes. All three superadmin-gated; pairing codes also redact the cleartext code + sha256 hash, only the last two digits ever leave the database. Nopropose_*write companions by design — the create response for a pairing code carries the cleartext code, which we don't want in chat transcripts - Backup + factory-reset —
list_backup_targets(every configured destination with last-run state, schedule, retention;configblob deliberately omitted so destination credentials stay out of the LLM context),list_backup_archives_at_target(calls the driver'slist_archivesso the result matches the Backup admin Archives drawer),find_backup_audit_history(windowed timeline of backup_created / target-run-success/failed / backup_restored / factory_reset_performed audit rows),get_backup_health(per scheduled target: ok / failed / stale / stuck, the same decision as thebackup_failed/backup_stalealerts). All superadmin-gated. Nopropose_*writes by design — restore + factory-reset are password-gated + confirm-phrase-gated, an LLM intermediary in "should I restore?" adds friction without value - Integration mirrors —
list_kubernetes_targets,list_docker_targets,list_proxmox_targets,list_tailscale_targets,list_netbird_targets,list_unifi_targets,list_cloud_targets,list_opnsense_targets,list_panos_targets,list_fortinet_targets,list_meraki_targets(each tagged with the matchingintegrations.*module so disabling the integration removes the tool in lock-step with the sidebar entry; credentials never enter the response) - Firewall objects, block sync + feeds —
find_firewall_objects/count_firewall_objects(vendor-neutral over the Palo Alto / Fortinet / Meraki shadow-IPAM store, filterable bysource_kind),find_network_blocks/count_network_blocks,list_firewall_feeds, and the default-disabledpropose_create_network_blockwrite proposal. Gated on thesecurity.block_sync/security.firewall_feedsmodules; write-scoped target credentials never enter the response - Ops, observability + audit —
list_alerts,list_alert_rules,get_audit_history,audit_walk(paginated chronology),current_state(platform health snapshot),query_dns_query_log,query_dhcp_activity_log,query_logs,get_dns_query_rate/get_dhcp_lease_rate(24-bucket timeseries),global_search,lookup_whois_asn/lookup_whois_domain/lookup_whois_ip,tls_cert_check,find_nonconforming_names(audit pre-existing hostnames / record owners / zone names against the DNS standards),help_write_permission,find_agents_with_config_failures(which DNS / DHCP / looking-glass agents rejected their last config and reverted — the one state wherestatus, the health check andlast_seen_atall read normal while the saved config is live nowhere),find_branding_settings,get_support_bundle_preview(default off — a broad read),propose_create_alert_rule,propose_archive_session - Reputation (DNSBL) —
find_blocklisted_ips,count_blocklisted_ips,find_dnsbl_lists(curated catalog + per-list sweep state),propose_pin_ip_for_dnsbl(add an IP to the monitored set). Gated on thesecurity.dnsblmodule - Compliance —
list_conformity_policies(per-framework registry filter),find_conformity_results(append-only evaluation history),get_conformity_summary(per-framework rollup of policy + result counts) - Typed-event webhooks (superadmin-gated) —
list_webhooks(registry; secrets NEVER returned,secret_setboolean only),get_webhook_event_types(the full typed-event vocabulary so the LLM can validateevent_types[]references before proposing a subscription),find_webhook_deliveries(outbox history — state / attempts / last_error / last_status_code) - Rolling upgrade orchestrator —
find_upgrade_preflight,find_upgrade_runs,find_upgrade_lease— read state of the multi-node A/B slot rolling upgrade so the LLM can answer "what's in flight?" without operator copy-paste - Windows cutover (superadmin-gated, tagged
migration.cutover) —find_cutover_plans,find_cutover_plan_status,count_cutover_blockersread plan and item state straight from the database.find_cutover_parity_checkis the one default-off member of the set, because it runs a live WinRM pull against the Windows server rather than reading a stored report - Write proposals (Apply-gated, default-off, double-validated in the Tool Catalog UI) — every
propose_*returns a planned diff first; the operator clicks Apply in the chat drawer to actually write. Apply lands an audit row withvia=ai_proposalso the trail distinguishes operator vs. AI-driven mutations
MCP integration
- MCP HTTP endpoint at
/api/v1/ai/mcpexposes the full read-only tool set so external MCP clients (Claude Desktop, Cursor, Cline, Continue.dev) can drop SpatiumDDI in as a tool source — no Copilot UI required
Chat surface
- Floating chat drawer — slide-in panel with sessionStorage-backed state (close + reopen lands on the same conversation, draft text survives), Markdown + GFM tables + code blocks, blinking caret during streaming. Opens via the floating "Ask AI" button, the Cmd-K palette entry, or the per-row "Ask AI about this" affordances on every IPAM / DNS / DHCP / alerts row
- Per-message footer — token-count + copy + info popover (sent timestamp, tokens in / out, latency, role) on every assistant reply; matches the OpenWebUI footer pattern
- Daily token + cost chip — live in the drawer header; refetches automatically when you delete chats
- Multi-select session history — checkbox column on every history row; "Select all" + "Delete N" + "Delete all" toolbar; bulk delete fans out per-id and updates the daily tally on success
- Live nmap proposal results — when a
propose_run_nmap_scanApply lands, the proposal card pollsGET /nmap/scans/{id}every 2 s and renders the full results table (alive flag, open ports + service / version, OS guess, CIDR-host list) inline once status flips tocompleted - Custom prompts library — operator-curated templates persisted per platform; built-in starter pack (Find unused IPs, Audit recent changes, Summarize subnet utilization, Triage open alerts)
- Cmd-K palette "Ask AI" entry — top entry in the global palette, pre-fills with the current page's context
Reliability + safety
- Per-turn dedup loop guard — if the model emits the exact same tool call twice in a turn (a known failure mode of smaller open-weight models), the orchestrator skips re-execution and feeds back a synthetic warning telling the model the result is already in context
- Tool-not-found auto-correction — when the model hallucinates a tool name, the error response includes the full list of real tool names + a hint, so the next iteration self-corrects rather than giving up
- Scope rules in the system prompt — Copilot is explicitly not a general-purpose coding assistant; refuses code-generation requests outside narrow platform-config contexts
- Audit everything — every tool call, every Apply, every chat turn writes through to the append-only audit log
Token / cost observability + per-user caps
- Per-request usage tracked in
ai_chat_message; pricing table covers the major hosted models; per-user daily token + cost caps; AI usage card on Platform Insights aggregates the last 7 days by provider + model - Daily digest — optional 0900 local Operator Copilot summary fired through audit-forward / SMTP / webhook channels
Self-host with Ollama in five minutes
# On the Ollama host: docker run -d --gpus all -p 11434:11434 \ -e OLLAMA_CONTEXT_LENGTH=32768 \ -e OLLAMA_KEEP_ALIVE=30m \ -v ollama:/root/.ollama --name ollama ollama/ollama:latest docker exec ollama ollama pull qwen3.5:latest
Then in SpatiumDDI: Admin → AI Providers → New →
kind: openai_compat,base_url: http://<ollama-host>:11434/v1,default_model: qwen3.5:latest, save → click the floating "Ask AI" button.OLLAMA_CONTEXT_LENGTHis required — Ollama defaults to 2048 tokens which silently truncates the system prompt + tool schemas; the result is a model that hallucinates tool names. We recommendqwen3.5:latestfor tool calling on the small open-weight class. -
🔔 Alerts + audit forwarding — multi-target delivery with pluggable wire formats.
- Rule-based alerts framework (subnet utilization, server unreachable)
- Multi-target syslog (UDP / TCP / TLS), HTTP webhook, SMTP email, Slack / Teams / Discord chat
- Wire formats: RFC 5424 JSON, CEF, LEEF, RFC 3164, JSON lines
- Per-target filters
-
🪝 Typed-event webhooks — curated automation surface for downstream consumers.
- 144 typed events covering every resource × verb (e.g.
subnet.created,dns.zone.updated,ip.allocated) - HMAC-SHA256 signed POSTs with reserved
X-SpatiumDDI-*headers - Outbox-backed at-least-once delivery with exponential backoff + dead-letter
- Per-subscription manual retry, custom headers, and one-time secret reveal
- 144 typed events covering every resource × verb (e.g.
-
🔐 ACME DNS-01 provider —
acme-dns-compatible HTTP surface.- certbot / lego / acme.sh issue public certs (wildcards included)
- For any FQDN delegated to a SpatiumDDI-managed zone
-
📋 Full audit trail — every mutation logged, append-only.
- Viewable in the UI with per-column filters
- 🚀 Flexible deployment — same control plane, multiple paths.
- Docker Compose
- Kubernetes — Helm umbrella chart, OCI-published
- Bare metal
- OS appliance ISO — beta (Debian 13 + k3s + full stack pre-installed, dedicated
/appliancemanagement hub (Fleet · Cluster · Firewall · Logs & Diagnostics · Maintenance · Network & Host) with TLS cert upload, GitHub releases, atomic A/B + rolling cluster upgrades, and host config — SNMP / NTP / LLDP / timezone driven from the UI; declarative fleet firewall — per-role nftables policy with posture presets, staged-preview diffs, an enforcement master switch that cuts LAN-wide etcd / kubelet exposure, and source-scopable Web UI; multi-node control-plane HA — promote appliances to a 3/5/7-node cluster with CloudNativePG + Redis Sentinel + a MetalLB VIP; see Getting Started +docs/deployment/APPLIANCE.md)
Click any image to open the full-size version.
| Dashboard | IPAM |
|---|---|
![]() |
![]() |
| Utilisation, VLAN, DNS & DHCP status at a glance | Hierarchical space / block / subnet tree with per-IP DNS sync |
| DNS | DHCP | VLANs |
|---|---|---|
![]() |
![]() |
![]() |
| Zones, records, server groups | Scopes, pools, static reservations | Routers & VLANs linked to subnets |
Control plane — FastAPI + PostgreSQL 16 + Redis 7 + Celery. Single source of truth for everything (IPAM tree, DNS records, DHCP scopes, auth, audit log). Exposes a REST API; the web UI and any Terraform / Ansible / CLI / MCP integration all speak the same API. Runs single-node by default; on the OS appliance it can scale to a 3/5/7-node HA cluster — replicated PostgreSQL (CloudNativePG), Redis Sentinel, k3s embedded-etcd quorum, and a MetalLB control-plane VIP. The Operator Copilot (multi-vendor LLM + hundreds of tools in the registry + MCP HTTP endpoint) is grounded in this same live data.
Data plane — four independent shapes (mix freely):
-
Agented, on-prem (BIND9 or PowerDNS or Technitium, plus Kea) — one container per service. Each bakes in a sidecar agent (
spatium-dns-agent/spatium-dhcp-agent) that (1) bootstraps with a PSK or pairing code → rotating JWT, (2) long-polls/configwith an ETag (woken over a Redis pub/sub channel), (3) caches the last-known-good bundle on disk so the service keeps serving if the control plane is unreachable — and reverts to it, quarantines the bad etag and reports that upward when a bundle renders config the daemon rejects, because a reverted agent otherwise keeps serving and keeps heartbeating while the zone or scope you saved is live nowhere, (4) drains record / config ops over loopback (nsupdate + TSIG for BIND9; REST API for PowerDNS / Technitium; REST Control Agent for Kea). Structural changes reload the daemon; record changes do not. The DNS engine is mutually exclusive per server group. -
Agentless, on-prem (Windows DNS, Windows DHCP, Technitium) — no software on the managed host. The control plane speaks directly: RFC 2136 over UDP/TCP 53 (DNS record writes + AXFR), WinRM + PowerShell over 5985/5986 (DNS zone CRUD, DHCP lease / scope reads), and for
technitium_apithe server's own HTTP API with a permanent bearer token. Credentials are Fernet-encrypted on the server row. -
Agentless, cloud DNS (Cloudflare, Route 53, Azure DNS, Google Cloud DNS, plus DigitalOcean / Hetzner / Linode / Vultr) — first-class drivers that call the provider's REST API / SDK directly, with import-existing-zones. No agent, no daemon.
-
Read-only integration mirrors (Kubernetes, Docker, Proxmox VE, Cloud AWS/Azure/GCP, Tailscale, NetBird, UniFi, OPNsense, Palo Alto, Fortinet, Meraki) — scheduled pulls reconcile external state into IPAM; never written back. The single exception is active block sync, an opt-in, off-by-default enforcement path that pushes a SpatiumDDI-owned block set (and only that) to an OPNsense alias / UniFi client / Palo Alto DAG / Meraki client policy, or serves it as a feed the firewall polls.
On the OS appliance, a host-side spatium-supervisor (Ed25519 identity, pairing-code onboarding, heartbeat long-poll) orchestrates the local k3s node — it picks up role assignments, atomic A/B OS upgrades, and firewall + host-config (SNMP / NTP / LLDP / syslog / SSH) from the control plane and enforces them on the host.
The driver abstraction is backend-neutral — services speak to DNSDriver / DHCPDriver, never to BIND9 / PowerDNS / Technitium / Kea / PowerShell / cloud-SDK specifics.
Tech stack: Python 3.12 · FastAPI · SQLAlchemy 2.x (async) · PostgreSQL 16 · Redis 7 · Celery · React 18 · TypeScript · Tailwind · shadcn/ui · pywinrm · dnspython · cloud provider SDKs · Docker · Kubernetes + Helm · k3s (appliance)
⚠️ SpatiumDDI is beta. The alpha cut on2026.04.16-1(first release) and the project has since stabilised across IPAM / DNS / DHCP / appliance surfaces. Commands and APIs may still shift between releases.
📘 For the full setup order (servers → zones/scopes → subnets → addresses) see docs/GETTING_STARTED.md. For Windows DC integration see docs/deployment/WINDOWS.md.
One click brings up a full SpatiumDDI stack in a fresh Codespace, builds the images from main, runs migrations, and seeds realistic IPAM / DNS / DHCP / network-modeling demo data so every screen has something to look at. Sign in with admin / admin.
The demo Codespace runs in DEMO_MODE — abusable surfaces are server-side locked: nmap, packet capture, the AI Copilot, every read-only integration mirror (Kubernetes / Docker / Proxmox / Tailscale / NetBird / UniFi / Cloud / OPNsense / Palo Alto / Fortinet / Meraki), active block sync and firewall feeds, webhook subscriptions, backup target creation, factory reset, and password change all return 403. IPAM / DNS / DHCP CRUD on the seeded data stays open so you can play with it.
Cold start is ~5–8 minutes (image build) on a 4-core machine; the Codespace's free-tier hours come from your own GitHub account, and trashing the data only affects your own copy. To start fresh, delete the Codespace and click the badge again.
The fastest way to a working SpatiumDDI install. Boot a single ISO, answer a handful of wizard prompts, and you're on HTTPS with the full stack running in 2-5 minutes. No Docker setup, no Kubernetes setup, no Python virtualenv. The ISO is a self-contained Debian 13 image with k3s embedded — a single-binary Kubernetes distribution — so the same SpatiumDDI containers that ship as a Helm chart deploy on the appliance via HelmChart CRDs. Operators get a real Kubernetes node without managing one.
New to Kubernetes? Don't let that stop you. You never have to touch it — the guided installer, the
/applianceweb UI, and one-click atomic upgrades hide k3s completely; there are nokubectlordockercommands to learn. If you're curious why we picked embedded k3s over plain Docker Compose — flexibility, growing a single box into a real high-availability cluster, and never hand-running container commands — here's the plain-English rationale: Why k3s, not Docker Compose?
- Single-node k3s cluster (~70 MB static binary, no daemons to chase).
- SpatiumDDI control plane (API, frontend, worker, beat, PostgreSQL, Redis, migrate Job) running as Pods.
- HTTPS on
:443from first boot — a self-signed cert with the host's globally-scoped IPs in the SAN list. Replace with your own pasted PEM / CSR-on-server / Let's Encrypt cert whenever you're ready. - A dedicated
/appliancemanagement hub that handles everything you'd otherwise need SSH for: a live Cluster health dashboard (pod inspection + log streaming + node vitals + etcd snapshots), TLS cert manager, atomic A/B OS slot upgrades, firmware-pending banner, fleet management for remote agents, NTP / SNMP host config, a realtime firewall-log viewer, journalctl viewer, self-test runner, one-click diagnostic bundle, maintenance mode, host reboot / shutdown. - A Talos-style operations cockpit on the appliance's physical / serial console — a KPI ribbon (cluster / etcd / Postgres / API / platform / slot), live vitals, pod health, a journalctl tail, and an F7 health drill-down with guarded recovery actions — useful when the web UI is down.
- Atomic A/B slot OS upgrades: every appliance has two
identical root partitions; an upgrade dd's the new image
into the inactive one, reboots, and auto-reverts on
/health/livefailure. Worst case is one wasted reboot. - Scale to a 3/5/7-node HA control plane by promoting more appliances from the Fleet UI — embedded-etcd quorum, replicated PostgreSQL (CloudNativePG) + Redis Sentinel, and a MetalLB control-plane VIP, all configured from the web UI (see below).
-
Pre-built: grab the ISO for your architecture from the latest release:
Architecture ISO Firmware x86-64 (Intel/AMD) spatiumddi-appliance-<version>-amd64.isoBIOS or UEFI arm64 (Apple Silicon, Graviton, Ampere) spatiumddi-appliance-<version>-arm64.isoUEFI only The arm64 image is UEFI-only because AArch64 has no BIOS boot path — there is no legacy firmware mode to fall back to, and the installer refuses rather than completing an install that could never boot. Both are also published under un-versioned names (
spatiumddi-appliance-<arch>.iso) thatreleases/latest/download/…always points at. -
Nightly: every night that
mainmoves, a pre-release taggednightly-YYYY.MM.DDships the same artifacts a real release does — container images, the appliance ISO and the A/B slot image, assembled by the same reusable workflow the release runs, so a break in the assembly surfaces the next morning rather than at the cut. Kept for 7 days. Useful for testing a fix before it's tagged; not for production — nightlies are never tagged:latest. -
Build from source:
make appliance-dev-isoproduces an ISO inappliance/build/. AddAPPLIANCE_ARCH=linux/arm64for the arm64 image — on an Apple Silicon Mac that is a NATIVE build, which is also the quickest way to test the appliance locally (UTM, Apple Virtualization backend). Seedocs/deployment/APPLIANCE.mdfor prerequisites.
-
Attach the ISO as a CD-ROM in your hypervisor (Proxmox / VMware / Hyper-V / QEMU), or
ddit to a USB stick for bare metal. amd64 or arm64 (UEFI only).Hard floor (installer refuses below this): 32 GiB disk — the A/B atomic-upgrade layout needs two 8 GiB OS slots that each carry the full container image set for air-gapped boot, and
/varmust hold the k3s image store + database + logs. (The floor was 24 GiB; it was raised to 32 because the Control plane's first boot otherwise tipped the kubelet DiskPressure threshold into an eviction storm — issue #312.)Recommended per role:
Role vCPU RAM Disk Control plane (api + worker + frontend + Postgres + Redis + k3s etcd) 4 8 GiB 40 GiB SSD Appliance (DNS / DHCP agent box) 2 4 GiB 32 GiB SSD Each control-plane HA node sizes the same as a single control plane (4 vCPU / 8 GiB) — every member runs a full api / worker / Postgres replica / Redis. SSD strongly preferred for the etcd + Postgres write path. When you pick the Control plane role on a box below the recommended 40 GiB / 8 GiB, the installer shows a sizing warning before proceeding.
-
Boot. The installer wizard asks for:
- Role — Control plane (the required first install: control plane on this box + the k3s etcd seed; DNS / DHCP are off at install and enabled later per node from the Fleet UI) or Appliance (DNS / DHCP agent box; pairs against a remote control plane and can later be promoted to join the control-plane cluster).
- Target disk, hostname, admin password, network (DHCP or static), timezone.
- For Appliance, also control plane URL and a bootstrap method — see "Joining DNS / DHCP agents" below.
-
The installer partitions, installs GRUB, and reboots. First boot generates secrets, bakes a self-signed cert, and starts k3s + the helm-controller. The helm-controller reconciles the baked HelmChart CR; in 30-90 s every spatium pod is in Running state.
Browse to https://<appliance-ip>/. Accept the self-signed
cert warning (you'll replace it through Appliance → Web UI
Certificate as soon as you're logged in). Sign in with
admin / admin — you'll be forced to set a real password on
first login.
The default port is :443 (HTTPS); :80 redirects to it. If
you replace the cert with Let's Encrypt or a corp-CA cert, the
nginx pod reloads automatically — no operator action needed
beyond pasting the new cert in the UI.
For distributed deployments (control plane on one box, DNS and DHCP on others), install the Appliance role on each agent box. Each agent needs a bootstrap secret to register with the control plane. Two ways to provide it:
Point agents at the VIP, not a node IP. When the installer asks for the control-plane URL, and the control plane is (or will become) a multi-node HA cluster, use the MetalLB control-plane VIP — not any single node's address. An agent pinned to one node's IP loses its control plane whenever that node is down, even though the cluster is healthy on the survivors. (Control-plane cluster members need no such care — their supervisor automatically heartbeats the in-cluster API Service rather than any fixed node IP.)
Pairing code (recommended). Easy to type, even over an IPMI / serial console.
- On the control plane, open Appliance → Pairing and click New pairing code. Pick the agent kind (DNS / DHCP / DNS + DHCP for combined boxes), an optional server group, and an expiry (15 min default).
- The UI shows an 8-digit code with a live countdown.
- On the agent appliance's installer, pick "Pairing code" at the Bootstrap method prompt and type the 8 digits. The agent redeems the code for the real key on first boot and registers itself.
Bootstrap key (advanced). For re-installs, air-gapped sites with a saved key, or when a pairing code expired before you got to the installer. Reveal the 64-char hex key on the control plane via Settings → Security → Agent bootstrap keys and paste it.
The agent appliance's console dashboard shows a Pairing row — green ✓ when registered, yellow while in progress, red with a regenerate-the-code hint on failure.
Once paired, the agent shows up on Appliance → Fleet.
The operator approves the row (signs the agent's cert),
then picks roles (dns-bind9 / dns-powerdns /
dns-technitium / dhcp — DNS engines are mutually
exclusive). The supervisor on the
agent labels the k3s node (spatium.io/role-<role>=true)
and the matching DaemonSet schedules the role pod. Total
time from click to running pod: typically 25-35 s.
A single appliance is a one-node control plane. To survive a node loss, promote more appliances into the control-plane cluster from Appliance → Fleet → Manage control plane cluster…. Pick the boxes to add and confirm — embedded-etcd HA wants an odd member count, so you grow 1 → 3 → 5 → 7 (the API refuses a batch that would land on an even total).
Promotion is hands-off. The supervisor on each promoted node does a full k3s cluster-identity reset and rejoins the seed's embedded-etcd cluster; within ~60 s the data layer scales itself to the new member count:
- PostgreSQL (CloudNativePG) grows from one instance to a primary + streaming replicas with automatic failover.
- Redis runs in Sentinel mode — each member pairs a
redis-server with a sentinel that elects a master and fails
over; the app resolves the live master via a
sentinel://URL. - api / frontend / worker spread to one replica per node.
One Web UI, one address. Set a MetalLB L2 address pool + a floating control-plane VIP in Appliance → Network & Host; the frontend Service moves onto the VIP so the UI (and every agent heartbeat) hits one stable address regardless of which node is up. The self-signed Web UI cert auto-grows its SAN list to cover every member's hostname + IP and the VIP, so it validates on any node — unless you've uploaded your own cert, which is never touched.
Roll back by demoting members the same way (the etcd seed can't be demoted, and demoting to an even count is refused). A control-plane node can't be revoked until it's been demoted — revoking a live etcd member would break quorum. Control-plane workloads are pinned to control-plane nodes by a per-role node label, so promoting a DNS-only appliance never accidentally schedules Postgres onto it.
Multi-node HA shipped in #272; the live shake-out validated a 1 → 3 promote end-to-end. See
docs/deployment/APPLIANCE.mdfor the architecture + the HA topologies indocs/deployment/TOPOLOGIES.md.
The Appliance section in the sidebar opens a management hub with six tabs:
- Fleet — the heart of it: a roster of every registered
appliance (control-plane nodes + service agents). Approve /
reject pending pairings, assign DNS / DHCP roles + server
groups, watch per-service health, schedule per-box atomic
A/B OS slot upgrades + reboots, re-key, and delete. Promote
/ demote control-plane cluster members and set the MetalLB
control-plane VIP from the roster drilldown (see HA above).
A left sidebar holds the fleet-wide config surfaces:
Pairing codes (single-use onboarding codes), Rolling
Upgrade (coordinated one-node-at-a-time OS+app upgrade of
the whole cluster — preflight → CNPG switchover → drain →
slot apply → reboot → health-gate → uncordon → chart
image.tagbump → migrate Job; air-gap mirror PVC or GitHub-release source; Halt / Resume / Abort — plus the GitHub release catalog), Upgrade images (air-gap slot-image mirror), Web UI Certificate (pasted PEM + key, in-server CSR, or Let's Encrypt — nginx reloads in ~15 s), NTP (chrony, propagates to every agent), and SNMP (v2c community + CIDR allowlist, or v3 USM). - Cluster — the k3s cluster underneath: a Pods view (running workloads with per-pod restart + SSE live logs) and etcd snapshots (disaster-recovery state + guided single-node restore), both driven off the in-cluster kube-API via the api pod's ServiceAccount. (Appliance host only.)
- Firewall — declarative per-role / per-appliance
nftables policy compiled into each node's drop-in, with a
staged-preview diff of any node's effective merged ruleset,
an enforcement master switch that cuts LAN-wide etcd /
kubelet exposure, and a realtime firewall-log viewer.
(Module-gated on
appliance.firewall.) - Logs & Diagnostics — host-log viewer, the self-test runner (DNS resolution + kube-API reachability + pod health + role presence), and a one-click diagnostic bundle (secrets redacted). (Appliance host only.)
- Web UI reachability self-check — the appliance answers
the one question a self-
curlcan't: would a new, off-box TCP connection to 443 (and 80) actually be accepted? Curling your own LAN IP ridesiif lo acceptand succeeds even when the port is firewalled, which is how "the UI is down" turns into an hour of confirming healthy pods. It reads the live nftables ruleset at first boot, every 5 minutes, and after every firewall apply. An accept scoped byweb_ui_allowed_cidrsreports scoped, never blocked — a hardened appliance must not be reported as broken — and anything the model can't reason about degrades to indeterminate rather than a confident wrong answer. Only a genuine block renders: bold red on the console next to the Web UI URL it invalidates. (Appliance host only.) - Maintenance — maintenance-mode toggle (drains DNS / DHCP traffic before host work) plus reboot / shutdown with confirmation. (Appliance host only.)
- Network & Host — hostname, DNS resolvers, IPv4 / IPv6 mode (DHCP or static), the nftables drop-in editor, SSH-key upload, proxy config, console-mode selector, and a reboot-pending banner. (Appliance host only.)
On docker / k8s control planes the host-only tabs hide; Fleet stays so operators can still drive remote appliance agents.
The console cockpit on the appliance's physical or
serial console shows a 6-box KPI ribbon (cluster / etcd /
Postgres / API / platform / slot), live vitals, pod health,
and a journalctl tail. F-keys give you local-login (F1),
htop (F2), kubectl get pods -A (F3 — pod log viewer),
nmtui for networking (F4), confirmed reboot / shutdown
(F5 / F6), and a health drill-down with guarded recovery
actions (F7). The post-boot console (dashboard / verbose
dashboard / plain login) is selectable from Appliance →
Network & Host.
Stuck without a working web UI? SSH in as the OS admin user
you created during install. kubectl is on PATH (with bash
completion + a k alias); kubectl get pods -A is the
fastest "what's broken" diagnostic. journalctl -u spatiumddi-firstboot shows the first-boot setup log; kubectl -n spatium logs <pod> for any pod's stdout/stderr.
The appliance is beta — see issue #134
- #183 for the roadmap and
docs/deployment/APPLIANCE.mdfor the full design, build pipeline, k3s architecture, and known limitations.
If you'd rather run on existing Docker infrastructure, the same SpatiumDDI containers ship as a docker-compose stack. Useful for dev work, small single-host production where you want full control of the OS, or environments where Kubernetes overhead is unwanted.
git clone https://github.com/spatiumnorth/spatiumddi.git
cd spatiumddi
cp .env.example .env
# Required env vars in .env:
# POSTGRES_PASSWORD=<set this>
# SECRET_KEY=$(openssl rand -hex 32)
# DNS_AGENT_KEY=$(openssl rand -hex 32) # needed if running the DNS container
docker compose pull
docker compose run --rm migrate
docker compose up -dOpen http://localhost:8077 and log in with admin / admin (you're forced to change the password on first login).
To populate a fresh install with a representative dataset (DNS group + zones + records, DHCP scope + pool, ASNs + BGP peerings, VRFs, IP space + blocks + subnets + ~30 IPs, SNMP-stubbed network devices, VLANs, domains, custom fields, IPAM templates, alert rules, and a few shared AI prompts) — useful for screenshots, demos, or kicking the tyres on the AI Copilot:
python3 scripts/seed_demo.py http://localhost:8000 admin <your-password>Idempotent — re-running the seed swallows 409s and PATCHes existing rows so foreign-key pointers converge as new entities are added in later releases. Any other refused request is printed as it happens and listed again at the end, and the script then exits 2 instead of reporting Seed complete. Out of scope: AI providers (secrets), webhooks (per-deployment URLs), audit-forward targets, API tokens — those need real credentials from you.
SpatiumDDI ships every component (api, worker, beat, frontend,
dns-bind9, dhcp-kea) at the same tag. Releases up to the bridge are
CalVer (YYYY.MM.DD-N); from 1.0.0 they are SemVer
(MAJOR.MINOR.PATCH), and every SemVer release is newer than every
CalVer one. The image tag is controlled by SPATIUMDDI_VERSION in
your .env.
Reaching 1.0.0 from a CalVer release. Upgrade to the bridge — the last CalVer release — first. The code that decides whether a version is newer runs in the version you upgrade from, and only the bridge and later know that
1.0.0comes after2026.x: an older install is never offered 1.0.0, and its upgrade preflight refuses it. The 1.0.0 release notes name the bridge.
Track latest (default — your .env ships with SPATIUMDDI_VERSION=latest):
cd spatiumddi
git pull # refresh docker-compose.yml + .env.example for any new fields
docker compose pull # fetch the newest images
docker compose run --rm migrate # apply any new alembic migrations (idempotent — no-op if up to date)
docker compose up -d # recreate api/worker/beat/frontend on the new imagesPin to a specific release (recommended for production — reproducible, no surprise upgrades):
Pin with the current
docker-compose.yml: it pulls fromghcr.io/spatiumnorth/, where every release tag is published. A copy taken from a release tag up to and including2026.09.04-1, or from a checkout older than the organization move (#1100), namesghcr.io/spatiumddi/instead, which now answersdenied—git pullfirst, or changeghcr.io/spatiumddi/toghcr.io/spatiumnorth/in your copy.
# In your .env:
SPATIUMDDI_VERSION=2026.06.25-1
# Then:
docker compose pull
docker compose run --rm migrate
docker compose up -dBump the pinned version when you're ready to upgrade and re-run the same three commands.
Notes:
- Take a backup before upgrading. Click
Administration → Backup → Build + downloadand save the archive somewhere off the host (or pick a configured remote destination'sRun now). If the upgrade goes sideways the archive is your one-click rollback. Ten destination kinds ship today — local volume / S3 / SCP / Azure Blob / SMB / FTP / GCS / WebDAV / NFS / HTTPS PUT — plus selective per-section restore + cross-install secret rewrap + alembic upgrade-on-restore. See the in-app page for the security-model details. docker compose run --rm migrateruns alembic against your current schema — safe to run every upgrade. It exits as a no-op if there are no new migrations.- Downgrades are not supported. Database migrations are forward-only; if a release introduces a schema change you can't roll back to a tag that predates it without restoring a database backup. Always snapshot Postgres before a major-version upgrade you're not sure about.
- Watch the CHANGELOG.md entry for your target version for any release-specific upgrade notes (e.g. "operators on Kea HA must read this before upgrading").
- The sidebar shows the running version in the bottom-left corner and surfaces an
update availablebadge when a newer GitHub release exists — the version probe runs hourly.
The managed-service containers ship under Compose profiles — opt in when you want them:
docker compose --profile dns-bind9 up -d # BIND9 (default)
docker compose --profile dns-powerdns up -d # PowerDNS (issue #127)
docker compose --profile dns-technitium up -d # Technitium (issue #746)
docker compose --profile dns-bind9 --profile dhcp up -d # BIND9 + DHCP--profile dns still works as a back-compat alias for dns-bind9. Pick whichever DNS driver matches the server group on the control plane — every driver-specific feature (PowerDNS ALIAS / LUA, Technitium DoQ and encrypted-HTTPS/QUIC forwarding, BIND9 views and RPZ) gates on every server in the group running that driver.
Or set COMPOSE_PROFILES=dns-bind9,dhcp in your .env so plain docker compose up -d enables both automatically.
That starts dns-bind9 bound to host port 1053 (udp + tcp), dns-powerdns on 5453, or dns-technitium on 6053. The agent registers with the control plane automatically using DNS_AGENT_KEY from your .env and appears in the UI under DNS → Server Groups.
Upgrading from a release that used port 5353? The DNS host port default changed from
5353to1053in release2026.05.11-1because 5353 is the well-known mDNS port and collides with avahi (default-on in Ubuntu desktop / Fedora / most lab distros). Either point your clients at the new port (dig -p 1053 …), or pin the old behaviour withDNS_HOST_PORT=5353in your.envand recreate the container.
Create a zone + record in the UI, then verify with dig:
dig @127.0.0.1 -p 1053 <your-record>.<your-zone> A +short
dig @127.0.0.1 -p 1053 -x <your-ip> +short # reverse (PTR)Record changes propagate to BIND9 via RFC 2136 — typically sub-second, no daemon restart. Zone / ACL / view changes trigger a config reload.
Production: point the agent at your real control plane, expose 53/udp + 53/tcp, and run one container per DNS server you want in the cluster. All servers in a group share the same TSIG key for dynamic updates.
Turn it on per group under DNS → Server Groups → Options → Encrypted
transports, after linking a TLS certificate (upload one, or issue it
from Let's Encrypt under Appliance → Web UI Certificate). Both
listeners are additive — plain DNS on :53 keeps working.
The listener port lives in the database; what you change to make it reachable depends on how you deploy:
| Deployment | What to do |
|---|---|
| OS appliance | Nothing. The DNS workload is hostNetwork and the supervisor opens the firewall port automatically when you enable a listener. |
| Docker Compose | Add the overlay: -f docker-compose.dns-encrypted.yml. It publishes 1853→853 (DoT) and 8443→443 (DoH); tune with DNS_DOT_HOST_PORT / DNS_DOH_HOST_PORT, and if you changed the port in the UI match it with DNS_DOT_PORT / DNS_DOH_PORT. |
| Kubernetes / Helm | Set dnsBind9.dotPort / dnsBind9.dohPort (appliance chart) or per-server dotPort / dohPort (umbrella chart). |
docker compose -f docker-compose.yml \
-f docker-compose.dns-encrypted.yml \
--profile dns-bind9 up -dThe ports live in an overlay rather than the base file because Compose
can't publish conditionally — binding 853/443 unconditionally would break
up -d on any host already using them, for a feature that ships off.
# DoT
dig +tls +tls-hostname=dns.example.com @127.0.0.1 -p 1853 www.example.com A +short
# DoH
dig +https=/dns-query +tls-hostname=dns.example.com @127.0.0.1 -p 8443 www.example.com A +shortDoH and port 443. 443 is the RFC 8484 default and is what the UI starts with, but on an appliance the web UI already owns it — the API rejects
443there, so pick8443and hand clients a DoH URL with the port in it. On Compose there's no clash: the frontend publishes8077→80and never binds 443, and the container-side 443 is private to the DNS container.
PowerDNS: pdns Authoritative speaks neither protocol, so DoT/DoH runs
on the dnsdist front — bring up both the dns-powerdns and
dns-powerdns-with-dnsdist profiles and point clients at the front
(5853 / 5444). The standalone docker-compose.agent-dns-powerdns.yml
has no front, so encrypted transports aren't available there.
BIND9 and Technitium need no sidecar — both serve natively. Upstream forwarding is where they part: BIND9 forwards over DoT only (9.20 has no client-side HTTP or QUIC transport), Technitium forwards over DoT, DoH and DoQ, and pdns doesn't forward at all.
The FastAPI backend auto-generates OpenAPI / Swagger:
| Path | What |
|---|---|
http://localhost:8077/api/docs |
Swagger UI — try endpoints directly from the browser |
http://localhost:8077/api/redoc |
ReDoc — cleaner reference layout |
http://localhost:8077/api/openapi.json |
Raw OpenAPI 3 spec (for code generators) |
The same document is attached to every release as openapi.json, so an out-of-repo client can generate against an exact server version instead of whatever main happens to be:
curl -LO https://github.com/spatiumnorth/spatiumddi/releases/download/<tag>/openapi.json
make openapi VERSION=<tag> # reproduces the identical bytes locallyTwo things the published document guarantees, both because a generated client fails quietly rather than loudly: nullable properties are the plain schema with the property absent from required — not OpenAPI 3.1's null union, whose unmodellable arm makes strict generators drop the whole property with a warning — and timestamps are RFC 3339 with exactly three fractional digits, since the six Python emits (and the none it emits on a whole second) are what most generated decoders reject. See docs/API.md §2.
Every UI action is a REST call, so anything you do in the UI you can do via curl, Terraform, or your own client. Log in to the UI first to obtain a bearer token, then use Authorization: Bearer <token>.
docker compose exec api python - <<'EOF'
import asyncio
from sqlalchemy import update
from app.core.security import hash_password
from app.db import AsyncSessionLocal
from app.models.auth import User
async def reset():
async with AsyncSessionLocal() as db:
await db.execute(update(User).where(User.username == "admin")
.values(hashed_password=hash_password("NewPass!"), force_password_change=True))
await db.commit()
asyncio.run(reset())
EOF- Docker 24+ and Docker Compose v2, or
- Kubernetes with Helm: CI tests the charts on Kubernetes 1.36 with Helm 4 (k3s 1.36 on the appliance); the umbrella chart declares
kubeVersion: >=1.31, or - The SpatiumDDI OS appliance on bare metal or a VM (see APPLIANCE.md); running directly on a host OS without Docker is planned, not implemented
| Method | Use case | Status |
|---|---|---|
| Docker Compose | Dev, small single-host production | ✅ Supported |
| Kubernetes + Helm | Multi-node production, scalable | ✅ Umbrella chart (charts/spatiumddi, published OCI to ghcr.io/spatiumnorth/charts/spatiumddi). See docs/deployment/KUBERNETES.md |
| Bare metal / VM (Ansible) | On-prem without containers | 📋 Planned — no Ansible playbooks yet. For bare-metal today use Docker Compose on a host or the OS appliance: see docs/deployment/BAREMETAL.md |
| OS Appliance (ISO / qcow2) | Easiest deploy, air-gapped, dedicated /appliance management hub |
🔄 Beta — Debian 13 + embedded k3s + full stack as HelmChart CRs, hybrid USB/CD, installer wizard, atomic A/B slot upgrades, in-UI TLS / releases / pods / logs / diagnostics / maintenance. Build with make appliance-dev-iso. See docs/deployment/APPLIANCE.md + issues #134 / #183 |
Full docs at www.spatiumddi.com — republished automatically on every push to main.
| Document | Description |
|---|---|
| Getting Started | Recommended setup order — from server groups down to allocating an IP |
| Changelog | What changed in each release, including upgrade notes |
| Architecture | System topology, control plane / data plane split, agent contract, HA design |
| Data Model | Database models grouped by domain, key relationships, shared conventions |
| REST API | API conventions — pagination, filtering, error format, auth, versioning |
| Development Guide | Coding standards, lint/test stack, CI gate, migration workflow |
| IPAM Features | IP space, block, subnet, address management |
| DHCP Features | DHCP server management — Kea, Windows DHCP |
| DNS Features | DNS zones, views, server groups, blocking lists, Windows DNS, PowerDNS, Technitium, Cloud DNS |
| Integrations | Read-only mirrors — Kubernetes, Docker, Proxmox, Cloud, Tailscale, NetBird, UniFi, OPNsense, Palo Alto, Fortinet, Meraki — plus active block sync + firewall feeds (the one write path) |
| Migration / Import | One-shot DNS + DHCP + NetBox importers (BIND9 / Windows / PowerDNS / Technitium / Kea / ISC dhcpd), plus the guided Windows → SpatiumDDI cutover |
| ACME DNS-01 | acme-dns-compatible provider for Let's Encrypt / public-CA cert issuance |
| Vertical network awareness | AV-over-IP (Dante / AES67 / SMPTE 2110), BACnet/IP device-instance registry + BBMD conformity, Industrial-OT inventory + Purdue zoning, DICOM AE Title registry + peer-association map — plus the un-gated fragile-device do_not_probe flag |
| Auth & Permissions | LDAP, OIDC, SAML, RADIUS, TACACS+, roles, scoped permissions |
| Permissions (RBAC) | Permission grammar, builtin roles, wildcards, group-scoped access |
| System Admin | Health dashboard, backup, notifications |
| Observability | Logging, metrics, alerting |
| Deployment Topologies | Six reference topologies — single VM through HA cloud + on-prem hybrid — with diagrams |
| Windows Server Setup | WinRM, service accounts, firewall — Windows-side checklist |
| DNS Agent Design | Agent protocol, auto-registration, config sync |
| DNS Driver Spec | BIND9 + PowerDNS + Technitium (agent-managed and agentless) + Windows DNS + cloud (Route 53 / Azure DNS / Cloudflare / Google) driver internals |
| DHCP Driver Spec | Kea + Windows DHCP driver internals |
| Docker Compose | Compose setup, ports, first-time setup, TLS, HA, password reset |
| Kubernetes | Umbrella Helm chart walkthrough — HPA, Ingress / LoadBalancer, CloudNativePG + Redis Sentinel HA |
| Bare Metal | Bare-metal / VM paths — Docker Compose on a host, OS appliance (the Compose Patroni overlay is unsupported in 1.0) |
| Appliance Deployment | OS appliance ISO — base OS selection, build pipeline, first-boot orchestration, /appliance management hub spec |
| Troubleshooting | Recovery recipes — deleted agent rows, password reset, subnet-delete refused |
| Third-Party Components | Every bundled engine, library and OS package — license, artifact it ships in, and why it's there |
| Phase | Focus | Status |
|---|---|---|
| Phase 1 | Core IPAM, auth, user management, audit log, Docker Compose | ✅ Done — LDAP/OIDC/SAML + RADIUS/TACACS+, group-based RBAC, bulk-edit, inheritance, mobile-responsive UI, and full IPv6 /next-address (EUI-64 + random /128 + sequential) all shipped |
| Phase 2 | DHCP (Kea), DNS (BIND9), DDNS, zone/subnet tree UI | ✅ Done — DNS, Kea DHCPv4, subnet-level DDNS, agent-side Kea DDNS, block/space DDNS inheritance, per-server zone serial reporting all shipped |
| Phase 3 | DNS views, server groups, blocking lists, VLAN/VXLAN, system admin, Kea HA | ✅ Done — DNS features + health dashboard + alerts framework + group-centric Kea HA (self-healing peer-IP drift + supervised daemons) + DNS Views end-to-end split-horizon + multi-node control-plane HA (CloudNativePG + Redis Sentinel + MetalLB VIP, operator promote/demote) all shipped |
| Phase 4 | OS appliance, Terraform provider, SAML, backup/restore, ACME | 🔄 SAML + full backup/restore + factory-reset + OS appliance beta (Debian 13 ISO + embedded k3s + Helm orchestration, /appliance management hub with TLS upload + CSR-on-server, GitHub release apply, kubeapi-driven Pods tab + live SSE logs, host log viewer + self-test + diagnostic bundle, maintenance mode + reboot, web first-boot wizard, atomic A/B slot upgrades, multi-node rolling cluster upgrade with CNPG switchover + lease-mutex + preflight + air-gap mirror PVC, consolidated Cluster tab (Pods + etcd + live SSE health dashboard), realtime firewall-log viewer, Talos-style console cockpit) + the ACME DNS-01 provider (acme-dns-compatible, for external certbot / lego / acme.sh clients) + the ACME embedded client (hand-rolled RFC 8555 — auto-issues a CA-trusted Let's Encrypt cert for SpatiumDDI's own Web UI via DNS-01 / HTTP-01, with auto-renewal) all landed. Terraform / Ansible providers still pending |
| Phase 5 | Multi-tenancy, IP request workflows, advanced reporting | 📋 Planned |
See CHANGELOG.md for the per-release feature list and CLAUDE.md for the authoritative spec.
Contributions are welcome.
- Please read CONTRIBUTING.md before opening a PR. Every commit needs a DCO sign-off (
git commit -s), explained there - Good first tasks are tagged on the issue tracker
- Design discussion happens in GitHub Discussions
Thanks to everyone who has opened a pull request against SpatiumDDI.
| Contributor | Area | |
|---|---|---|
| @mzac | Maintainer | |
| @amoona6 | Appliance unattended installer, firewall sole-etcd-member fix, control-plane node-loss HA recovery | |
| @tristanbob | authlib.jose → joserfc migration, pairing-code prune fix |
|
| @Cmonnich | Technitium DNS driver | |
| @waza-ari | Agentless FortiGate cloud DHCP driver | |
| @stefanriegel | Multi-node appliance field testing and fixes: Technitium zone types and blocklists, cloud DNS health, DHCP server groups, blocklist feed memory, alert evaluator | |
| @containerguy | Hetzner DNS driver on the Hetzner Cloud API |
Opened a PR and not listed? That is an oversight, not a judgement — please say so on the issue tracker and it will be fixed. This list is maintained by hand, so it is added to as part of merging each contributor's first PR.
Released under the Apache 2.0 License.
Bundled components (BIND9, PowerDNS, Technitium, ISC Kea, k3s, and the appliance's Debian userland) are distributed under their own licenses. See NOTICE for the attribution manifest, and Third-Party Components for the full catalogue — what each component is, which artifact it ships in, and what its license means in practice.
Built with ❤️ by SpatiumNorth and the SpatiumDDI community · docs · spatiumnorth.com




