Allowlist, Denylist and switching list entries off

Two ways to change what a block list does without editing it at its origin:

  • the Allowlist and Denylist (dashboard: Filtering → Allowlist, Denylist, and Domains for both together): single names, wildcards and regular expressions you allow or deny, each with its own switch, comment, scope (everyone or client groups) and provenance, like Pi-hole's Domains page or Technitium's Allowed and Blocked zones;
  • switching single entries off in any list, including lists imported from a URL or a ConfigMap (dashboard: the list's page → a source → Entries), like Pi-hole's per-domain switch but per list.

Switching an entry of an imported list off affects only that list: another list that contains the same name still blocks it. To let a name through everywhere, allow it. Both show on the Allowlist: switched-off entries of block lists are listed there as list exceptions (and those of allow lists on the Denylist), so the Allowlist is the one place that shows every name kudurru lets through on purpose.

Entry types

TypeValueMatchesCompiled rule
exactads.example.comthe name onlyads.example.com
wildcard*.example.com (also accepted: example.com, ||example.com^)example.com and every name below it*.example.com
regexan RE2 expression, with or without /…/every name it matches/expression/

Names are lower-cased and IDNA-encoded; a trailing dot is dropped. An exact value that starts with *. is refused (use wildcard). The query log names the compiled rule and the list (ui-allow, ui-deny, ...).

How allow and deny apply

An allow match wins over every block list of the client's policies (and over blocked services); a deny entry blocks like a block list, unless an allow list of the client's policies allows the name.

The entries are stored on BlockLists, one per scope, as structured entries of the source ui (spec.sources[].entries):

ScopeListReferenced from
Allow, everyoneui-allowallowListRefs of every policy of the namespace kudurru may edit
Deny, everyoneui-denyblockListRefs of every policy of the namespace kudurru may edit
Allow, one ClientGroupui-allow-<group> (in the group's namespace)allowListRefs of the policies whose clientGroupRefs name the group
Deny, one ClientGroupui-deny-<group>blockListRefs of those policies

An entry scoped to several groups has one copy in each group's list; the dashboard and the API show and edit it as one entry. The lists are created on demand, managed by the API, and deleted (with their references) when their last entry goes. A group no policy covers cannot be chosen: its clients get the default policy, which other clients share. A group list applies through the group's policies, so a policy that covers several groups applies it to all of them.

The leader keeps these references (every 30 seconds and on every Allowlist or Denylist change): a policy created after the entries gets the everyone lists, a policy that starts naming a group gets the group's lists, and one that stops naming the group loses them. Policies managed by a file (objects.d) or an integration are not edited: the dashboard and GET /api/v1/allowlist (unattached) name them; add the list to their allowListRefs / blockListRefs in the file.

A structured entry:

apiVersion: kudurru.io/v1alpha1
kind: BlockList
metadata: { name: ui-allow, namespace: default }
spec:
  mode: allow
  sources:
    - name: ui
      entries:
        - value: cdn.example.com
          type: exact
          comment: needed by the video player
          created: "2026-10-11T12:00:00Z"
          createdBy: user:alice
          origin: querylog          # manual, querylog, unblock, api or migrated
        - value: "*.example.net"
          type: wildcard
          enabled: false            # kept, not applied
        - value: '^ads?[0-9]*\.'
          type: regex
          origin: unblock
          request: default/ur-0123456789abcdef   # the approved UnblockRequest

Other lists may use entries too (kubectl apply, objects.d); only the ui-* lists appear on the Allowlist and Denylist pages.

Where entries come from: the Add dialog of the pages (manual), the query log's Allow and Deny buttons (querylog, exact, everyone), approved unblock requests (unblock, with the request; see blocking-page.md), the API and kudurru lists allow|deny (api), and the migration below (migrated). Every change writes an audit event (allowlist.add, .update, .delete, .batch; denylist.*) and flushes the names of exact and wildcard entries from the node's response cache; every node recompiles its lists, which also flushes the local edge cache.

Switching entries of any list off

spec.sources[].exclude lists rules of that source that are not compiled. Each value is a rule as the source's format normalises it, which is how the entries browser lists them: ads.example.com for the hosts line 0.0.0.0 ads.example.com (one value per host of a line), ||ads.example.com^, /expression/. The exclusions are part of the list's spec, so they survive refreshes; an exclusion that no longer matches anything in the fetched body (the list dropped the name, or the value is not written the way the format normalises it) is reported in status.sources[].staleExcludes (first 50) rather than silently kept. status.sources[].excluded counts the entries switched off and status.sources[].entries the ones compiled, so the dashboard shows "N of M entries enabled".

spec:
  sources:
    - url: https://example.org/hosts.txt
      exclude: ["tracker.example.com", "ads.example.com"]

For structured entries the switch is entries[].enabled; switching it in the entries browser edits that.

The entries of a URL or ConfigMap source are read from the node's list cache: the leader fetches the body and every node pulls it by digest. A node that has no body yet answers 503 ("the fetched body ... is not on this node yet"); stale: true means the node still serves an older body than the published digest.

List exceptions on the Allowlist and Denylist

Every exclusion of a block list is a row of the Allowlist, every exclusion of an allow list a row of the Denylist (the ui lists themselves excepted): kind exception, origin list, the list and source it lives in, the policies that use the list, and the other lists that still match the name (stillMatchedBy; the exception does not change them). It has a switch and nothing else: on means the entry is switched off in its list (the exception is in effect); switching it off, or deleting it, removes the exclusion, so the list's entry applies again. The Add dialog's scope "Only in list …" creates exceptions: POST /api/v1/allowlist with {values, list, source?} switches the entries of that list (in every source that has the name, or in source) off; a name the list does not contain is refused per value. Filters: kind=entry|exception, scope=list:<ns/name>; q also matches the list name.

File- and Kubernetes-managed lists are read-only through the API (edit the file or the object). The starter list (starter, seeded on first boot, managed by bootstrap) can be tweaked: like any API edit of a first-boot default (decision D84), switching an entry off takes the list over (managed by the API from then on), so a later objects.d declaration of starter is reported as a conflict instead of silently dropping your exclusions.

Migration from 0.1 lists

Before structured entries, the Domains page, the query log and unblock approvals wrote plain lines into an inline source ui of the same lists (example.com, ||example.com^, /re/). The leader converts every such list it may edit once at start (domainlist-migration), and every write to a list converts it too: each line becomes an entry (||x^ and *.x become wildcard *.x) with the list's creation time and origin migrated, or unblock with the request when an approval in force holds the line. Lines that are none of these rules (an @@ exception, a hosts line with several names) stay in an inline source ui-unconverted, so nothing is lost. Lists managed by a file are read in both forms and never rewritten. Approvals made before the migration still remove their entry when they end.

API

Route
GET /api/v1/allowlist, /denylist?namespace=&kind=entry|exception&type=exact|wildcard|regex&enabled=true|false&scope=everyone|<ns/group>|list:<ns/name>&q=&limit=&continue=; items (entries and list exceptions), totals, the lists, unattached policies
GET /api/v1/domainsboth, ?action=allow|deny and the same filters
POST /api/v1/allowlist, /denylist{values[], type?, namespace?, groups?, comment?, enabled?, origin?}, or {values[], list, source?} for list exceptions; refused values in processed.errors
GET, PATCH, DELETE /api/v1/allowlist/{id}PATCH {enabled?, comment?, value?, type?, scope?: everyone|groups, groups?}; a list exception takes only {enabled}
POST /api/v1/allowlist:batch{ids[], op: enable|disable|delete}
GET /api/v1/blocklists/{ns}/{name}/sources/{source}/entriesevery source type; ?q=&enabled=&limit=&continue=; items {value, line, enabled}
PATCH …/entries{enable?, disable?} for every source type, {add?, remove?} for inline sources
POST /api/v1/domains/{allow|deny}/{exact|regex}compatibility: writes everyone entries

Reading needs blocklists.read, writing blocklists.write.

Source: docs/deploy/allowlist.md