Changelog

Every release. What shipped. What didn't.

Honest version history. Feature flags, design rollbacks, security fixes — all of it. If we shipped something only to walk it back, you'll see that here too.

v0.722026-08-26

fix Session revocation — a token now dies with its subject

A JWT stayed valid for its full 24 hours no matter what happened to the user behind it. Deleting them did not revoke it, changing their password did not revoke it, and demoting an admin did not revoke it. The account-recovery path was the worst of the five cases: resetting a password invalidated other reset tokens and left the intruder's session running.

Fixed at the request extractor with a per-user session generation, so it applies to every endpoint rather than to the one handler that had guarded against it by hand. Measured in both directions: before the fix, a deleted user's token minted a working API key; after, it is refused.

v0.71.22026-08-19

fix Device enrolment under concurrency

Twenty machines imaged from one Windows image all picked the same device name, and a deterministic retry let exactly one through per round — a 20-machine rollout needed 20 rounds. Now a single atomic insert that picks a random free suffix on retry.

Join-code use limits were enforced by the query and never written by the API, so every code was unlimited-use. Wired end to end, and a use is refunded on any failure after consumption so a bounded code does not shrink on a lost race or a hostname typo.

v0.702026-08-19

feat Corporate device enrolment

Join codes let a machine register itself and wait for an administrator to approve it. The endpoint's DNS is untouched until approval, so waiting costs the employee nothing, and the code grants nothing except the right to ask.

v0.692026-08-18

fix Production-readiness P0s

Change-password never verified the current password — proven on the running build: a wrong password returned 200 and rotated it anyway. Separately, a device whose resolver mapping failed was still admitted and resolved on the global filter set forever, and the config renderer would overwrite a populated resolver config, wiping tenant routing in one command.

v0.682026-08-18

fix External QA report — 10 of 14 findings were real

Webhook event names are validated against an allowlist (any string was accepted, so a typo was a silent dead subscription). Sessions became revocable, and password policy became a breach list rather than composition rules.

The edge access log was recording customer browsing history: an RFC 8484 GET puts the whole DNS question in the query string, and the DoH path is itself the device credential. Both are now redacted at the proxy. Two of the report's three “High” findings were false.

v0.672026-08-17

fix Stop advertising transports that answer nothing

The API now reports DoT and plain DNS as unavailable rather than naming endpoints. Plain DNS was closed in v0.57, and DoT stopped carrying tenant identity at the v0.66 admission cutover. Tenant identity is the DoH path component, and DoT and DoQ have no path.

v0.662026-08-17

fix Admission control — an unknown resolver path is refused

An empty allow-list in the resolver means “all clients”, not “no clients”. Any fabricated DoH path resolved, served with the global filter set — including a deleted device's. Deleting a device returned 200 and revoked nothing.

One module is now the sole writer of that access document, and it refuses rather than guesses: it will not empty the list, will not remove more than half the clients in a single run, and records every decision to a table.

v0.652026-08-15

feat Per-tenant compiled category feeds

v0.642026-08-14

fix Triage of an external QA report, and the defects underneath it

The report's headline finding was self-inflicted by its own test harness, and its tenant-isolation claim was disproved by measurement. What was real and not in the report: a workflow tool that advertised category blocking it never called, wildcard block rules that failed on every call because the API's own documentation promised syntax it rejects, and any device name over 25 characters producing a DoH URL that could never resolve.

v0.632026-08-13

feat Last-known-good blocklist mirror

A major upstream blocklist account disappeared for 36 hours and then came back. Upstream is now an input rather than a dependency: fetch conditionally, validate, store an immutable snapshot named by its own hash, and serve the promoted one. A failed or refused fetch leaves what is being served byte-identical.

v0.622026-08-10

fix SSO sign-in completes in the browser

The OIDC flow was correct end to end and finished by handing the browser a token in a URL fragment that nothing read. Every server-side step logged success and the user got a blank page with a valid session thrown away.

v0.612026-08-10

feat Shadow AI discovery

A detection dictionary of AI services, an endpoint that surfaces unclassified AI-looking traffic, and AI as a blockable category. Detecting a tool and being able to block it are different questions and are now different fields — blocking a shared cloud API host would take unrelated services down with it.

v0.602026-08-09

feat Analytics dashboard bundle

v0.592026-08-08

fix Protection categories become real

Nothing had ever written the readiness column, so all five categories reported unavailable and every attempt to enable one returned 400. The feature was unreachable by construction. Categories are now an operator opt-in, because the resolver's filter list is global and every registered feed slows policy enforcement for every tenant.

v0.582026-08-07

fix Blocklist catalog truth-up

The newly-registered-domain toggle was a live feature backed by nothing; it now reports unavailable and refuses to be enabled. Region-branded entries that pointed at generic lists were deleted, five were repointed against their upstream titles, and a liveness checker records health per list. It found two lists that return HTTP 200 and are useless.

v0.572026-08-06

fix Closed the open resolver on port 53

Plain DNS was being used for reflection: 16.4 million queries for a single record from 7,665 source addresses, peaking at 12,209 queries a minute. It served no customer, because tenant identity requires the DoH path, so it is closed.

v0.562026-08-03

feat Protection profiles

v0.552026-08-01

fix Production safety after a filtering outage

An environment variable that is set but empty is not unset, so the fallback never fired. The resulting URL was schemeless, every filter list failed to install, and the cleanup phase then removed the ones that were working. Tenants resolved unfiltered until it was caught.

Reconcile is now fail-closed, serialised on a database lock, and records every run with counts and a reason — container logs do not survive the recreate that is the first thing anyone does during an incident.

v0.542026-08-01

feat Per-tenant compiled blocklist feed, behind a flag

v0.532026-08-01

fix Platform matrix rebuilt, and two server errors it found

An unparseable date reached the analytics store verbatim, and an absurd hours value produced a negative year. Both now return 400 or clamp.

v0.522026-08-01

test Licence gate, and 46 rescued dashboard assertions

A dependency licence, advisory and provenance check runs as a build step, before the tests, so a policy violation cannot reach a deploy. Also fixed a blocked-services schedule that had never worked.

v0.512026-07-30

feat Setup screen and published client downloads

v0.502026-07-30

feat Endpoint clients for Windows

A local DoH forwarder for Windows 10 and a configurator for Windows 11's built-in DoH client. The DoH URL is the device credential, so no API key goes on an end-user machine: a lost laptop leaks one device identity, revocable by deleting that device.

v0.492026-07-30

feat Client-testable API

Per-credential rate limits, a self-service DNS check endpoint, and coherent version reporting across the API, the spec and the binary.

v0.482026-07-30

fix Tenant-isolation hardening

The DoH identifier is a bearer credential for the data plane, so it is now rotatable: rotation retires the old identifier, mints a new one and rewrites every device URL. Analytics survives, but only for queries logged before the rotation, so a leaked identifier cannot keep injecting rows into the victim's reports.

v0.472026-07-29

fix Deep audit — backups, webhook SSRF, logo XSS, auth rate limiting

Nightly backups had been silently producing nothing for a day: one unquoted value in an environment file killed the script before its first dump, while the API read the same file through a different parser and looked healthy. Webhook targets are tenant-controlled and are now resolved and rejected for private address space, with redirects disabled.

v0.462026-07-29

feat API Keys screen — the one config surface in the dashboard

The dashboard is read-only on purpose. Credential management is the single exception, because without it a self-serve tenant cannot reach the API at all except by reading a token out of browser devtools.

v0.452026-07-29

fix Cross-tenant leak on the tenants endpoint

A scope says what kind of thing a caller may touch, never whose. Listing tenants had only a scope check, so every tenant admin — and a key literally named read-only — could read every tenant's resolver identifier, which is the credential that identifies them on the wire.

v0.442026-07-29

feat Self-serve signup and password reset

v0.432026-07-29

feat Per-tenant rules in Postgres; rule rendering became a pure function

v0.422026-07-28

fix Five defects from a full dashboard sweep

v0.412026-06-08

feat Per-tenant branded block page

v0.402026-06-07

feat Per-tenant OIDC single sign-on

v0.392026-06-07

feat Compliance plumbing

v0.382026-06-06

feat Five-gap DNS-layer sprint

v0.372026-06-05

feat DNS Stamps (sdns://) endpoints

v0.362026-06-05

feat Anycast for the API, Redis auth cache, dashboard bundle

Partially reversed since. Terminating TLS for the API at a third-party edge meant it could see analytics responses and API keys in cleartext, and it overwrote the real caller address in the audit trail. The edge now serves only DNS and the static site.

v0.352026-06-03

fix Security and correctness pass

v0.342026-05-29

feat Blocklist catalog 30 → 86 + marketing-claim integrity pass

Added 56 community blocklists across 5 new categories (AI scrapers, safety, additional regional/language sets). Closes the "smaller catalog than NextDNS" gap that was a real spec-sheet objection.

Integrity pass on the marketing site: removed claims we couldn't back — "ISO 27001 certified" (we're not), "HIPAA BAA on every plan" (we don't offer one), "FedRAMP in process" (we aren't), "4,200+ security teams worldwide" (zero customers today), the fabricated company timeline with the $74M Series B, and the named testimonial from a fictional Director of Security. Replaced with honest current-state copy and a design-partner call. Pre-customer / pre-revenue is the truth; the site now says so.

v0.332026-05-28

feat Hardened headed Playwright + viewer-role test + QA seed

End-to-end test now reads DOM nodes directly (not regex over innerText), asserts exactly 3 QA devices with expected DoH URL shape, clicks the device "copy" button and reads navigator.clipboard back, parses the expanded audit row's <pre> as JSON, and runs a separate viewer-role test that PUT /policies/filtering must 403.

Added scripts/qa_data_seed.sh so the 24h analytics window stays full across days.

v0.322026-05-28

fix Audit table nested-template broke x-for row iteration

Alpine's x-for requires a single root per iteration. A leftover nested <template> meant no rows ever rendered, only the header. Replaced with <tbody> per row (HTML allows multiple tbody per table). Caught by the hardened Playwright spec from v0.33.

v0.312026-05-28

security Fix viewer-role write bypass in require_scope

The scope gate returned Ok unconditionally for any human JWT and assumed "role checks apply instead" — but no policy handler called require_admin. A viewer JWT could PUT /policies/filtering, /policies/custom-rules, etc.

Fixed: human admins still pass every scope; viewers now only pass :read scopes. :write from a non-admin JWT returns 403. Added a regression unit test and the viewer Playwright test from v0.33 that surfaced it.

v0.302026-05-28

fix Per-device queries silently dropped from 9 analytics endpoints + new /version

Since v0.23 introduced per-device DoH (<tenant_uuid>--<slug>), the where_client helper's exact-match check missed every per-device row. Summary, top-domains, top-clients, top-ai-tools, blocked, by-reason, latency, block-rate all undercounted. Switched to LIKE '<tenant_uuid>%'.

Also: new GET /version returns git SHA + build timestamp so ops can verify what's actually running.

v0.292026-05-27

feat Dashboard gains Devices, Threats (DGA+Typosquat), Policies read-only views

Three new read-only screens for surfaces that previously only existed in the API:

  • Devices: per-device DoH URL with copy-to-clipboard button + 24h activity counts
  • Threats: now includes the v0.27 DGA classifier rankings and typosquat detections against your protect-list
  • Policies (read-only): filtering state, default-deny mode, NRD blocking, blocked TLDs, custom rules, protect-list entries, DNS rewrites, blocklist subscriptions

Bundled with the v0.23 device routing fix (AGH rejected / in ClientID; switched separator to --).

v0.282026-05-27

feat Reconcile cron + x-required-scopes + 5 MCP workflow tools

The 5-minute reconcile cron now keeps the AGH global filter pool aligned with per-tenant subscriptions automatically (the v0.25 endpoint was admin-only manual). OpenAPI spec gains x-required-scopes on every secured operation. New high-level MCP workflow tools wrap common multi-step operations (onboard-tenant, triage-typosquat) for AI agents.

v0.272026-05-26

feat Threat-intel v2 — transparent DGA + Typosquat classifiers

Two inline classifiers run at ingest with no vendor feeds:

  • DGA scoring (~280 LoC, src/dga.rs): entropy + ngram + vowel-ratio feature scoring. Surfaces via /analytics/top-dga; tunable threshold (default 0.7); no auto-block.
  • Typosquat detection (~230 LoC, src/typosquat.rs): Damerau-Levenshtein ≤ 2 with homoglyph normalization, length-band pruning. Only scores queries for tenants with a non-empty protect-list. Surfaces via /analytics/top-typosquat.

"Scenario A" — chosen over a vendor-feeds option for explainability + zero per-query cost. Future scenarios B/C remain on the table for higher-cost / higher-recall workloads.

v0.262026-05-26

fix DB-driven feature_flags table (replaces hardcoded filter ids)

The filter-list IDs that drive NRD/TLD/parked toggles were hardcoded in policies.rs. Moved to a feature_flags table so they can be migrated, re-numbered, and disabled per environment without a redeploy.

v0.252026-05-26

feat POST /blocklists/reconcile — drift-correct AGH from Postgres truth

AGH's loaded filters[] is a single global list shared across tenants; per-tenant subscriptions live in Postgres. Reconcile diffs them and applies adds/removes idempotently. Admin-only (tenant 1). Pairs with the cron in v0.28.

v0.242026-05-26

feat Allowlist-only (default-deny) mode

Per-tenant toggle that flips DNS resolution from "block what's bad" to "allow only what's explicitly permitted." Implemented via a catch-all wildcard block rule with the tenant suffix, layered under existing per-tenant allow entries. The "Fortune-500 healthcare desktop" mode.

v0.232026-05-26

feat Per-device identification

Devices are first-class: each gets a unique slug + display name, and AGH ClientID becomes <tenant_uuid>--<slug>. Every analytics endpoint can now answer "which device looked up X?" Pairs with the dashboard Devices view shipped in v0.29.

Note: v0.23 originally used / as the separator, which AGH rejects as an invalid hostname-label character. Fixed in v0.29 to use --; per-device queries were SERVFAIL until then. Caught by end-to-end QA, not by a customer.

v0.222026-05-26

feat HMAC-signed outbound webhooks

Every audit event can fan out to a webhook endpoint. Bodies signed with HMAC-SHA256 over the payload using a per-webhook secret; timestamp + event-name headers; wildcard event matching (policy.*, threat.*, etc.). Backed by a single delivery worker with retry-with-backoff.

v0.212026-05-25

feat Logs anonymization + per-tenant retention + bypass-bypass

Three GDPR/DPDP-flavored controls per tenant:

  • Anonymize client IPs at ingest — drop the source IP from ClickHouse rows for tenants who don't want device-level identification
  • Per-tenant retention (1-365 days) instead of one global TTL
  • Bypass-bypass blocklists: subscribe to the Hagezi DoH-bypass and Tor-exit lists to block DNS-over-HTTPS evasion paths
v0.202026-05-25

feat Curated MCP tool descriptions

All 33 MCP tools now carry hand-written when / why / sharp edges descriptions instead of auto-generated "Executes GET /api/v1/..." fallbacks. Measurably improves how AI agents (Claude, Cursor, Continue) pick the right tool on the first try.

v0.192026-05-25

breaking Dashboard is now read-only

The four configuration screens (Policies, Categories, Users, Settings) are gone. Configuration happens via REST API or via the MCP server — agents do the typing, humans review via the Audit Log.

  • New: Audit Log viewer with actor pills (human session vs specific agent token), filterable by actor type
  • Net −326 LoC of dashboard code
  • Sidebar's "Configure" group now points users at the REST API + MCP endpoint
  • Three follow-up fixes: CSP allowing inline-script removal, structural HTML repair, pre-login 401 noise suppression + post-login hard-reload
v0.182026-05-25

feat Threat-intel toggles — NRD + TLD blocking

One-click toggles wrap existing primitives. Turn on newly_registered_domains to subscribe to the Hagezi NRD blocklist; pass a TLD list to block whole suffixes via wildcard rules.

Future toggles (parked-domain, DGA classifier, typosquat) are reserved in the response shape but return null — they need real classifiers and feeds, landing in a future release.

v0.172026-05-25

feat Per-tenant DNS rewrites

The ControlD-style Redirect feature: override any DNS answer for any domain. Five kinds — A, AAAA, CNAME, NXDOMAIN, REFUSED. Coexists cleanly with custom block/allow rules.

Implemented via AGH's $dnsrewrite modifier joined to the existing per-tenant client tag.

v0.162026-05-25

feat Blocklist catalog expanded 5 → 30 · CSV/NDJSON query log export

  • 25 new curated lists across 14 categories (threat, phishing, ads, tracking, adult, gambling, social, AI, fakenews, telemetry, NRD, TLDs, bypass)
  • New GET /api/v1/analytics/export?format=csv|ndjson for SOC handover and audit prep. Cap 10M rows per call
v0.152026-05-25

feat Full OpenAPI annotation · MCP exclusions for sensitive ops

All 35 API endpoints documented in the OpenAPI 3.1 spec at /api/v1/openapi.json. MCP server uses FastMCP's RouteMap(MCPType.EXCLUDE) to hide eight humans-only operations (login, password change, key mint/rotate/delete, tenant create/delete, user delete) from agents regardless of scope grant.

v0.142026-05-25

feat MCP server live at mcp.olladns.com

Agent-native config plane. FastMCP sidecar auto-generates tools from the OpenAPI spec, forwards the caller's X-API-Key on every upstream call, and respects per-key scopes end-to-end. Both stdio (Claude Desktop, Cursor) and Streamable HTTP transports.

v0.132026-05-25

feat OpenAPI 3.1 spec generation (utoipa)

Single source of truth for the API. Drives MCP tool generation, external SDK codegen, and human-readable docs.

v0.122026-05-24

feat Agent-completeness API gaps closed

  • GET /tenants/me — caller's own tenant (UUID, DoH URL, endpoints)
  • PATCH /tenants/:id — rename + contact_email
  • POST /auth/change-password — self-service, JWT-only
  • fix doh_url no longer leaks :8443
v0.112026-05-24

feat sec Scoped API keys + actor-attributed audit log

Foundation for agent delegation. Twelve resource:action scopes (analytics:read, policies:write, etc.); optional expiry; one-call rotation. Every audit row records actor type (user / api_key / system) and actor id — every config change is now traceable to a specific human session or a specific agent token.

Pre-v0.11 keys are auto-promoted to a legacy:full backstop scope for backward compatibility.

v0.102026-05-24

feat Dashboard SPA · api.olladns.com split

Alpine.js + fetch single-page app on login.olladns.com. API and dashboard now live on separate hostnames with proper CORS.

v0.92026-05-23

sec Hardening · DNSSEC · fail2ban · drop public :53

DNSSEC validation enabled on every resolver. sysctl hardening. Cloudflare token scoped to the single zone. fail2ban watching Caddy 4xx storms. Plain port 53 no longer exposed publicly — DDoS amplification surface eliminated.

Earlier versions (v0.1–v0.8) covered the initial DNS plane, AGH integration, ClickHouse ingest, per-tenant policy/blocklist subscriptions, audit logging, and the original v0.7 switch from Vector to a Rust AGH poller. See the git history for the full record.