Skip to content

feat: add allowed_record_types and enforce append_only as superset check#1

Open
teunis90 wants to merge 1 commit into
mainfrom
feature/record-type-and-append-only-filtering
Open

feat: add allowed_record_types and enforce append_only as superset check#1
teunis90 wants to merge 1 commit into
mainfrom
feature/record-type-and-append-only-filtering

Conversation

@teunis90
Copy link
Copy Markdown
Member

@teunis90 teunis90 commented Mar 24, 2026

New zone-level config options

allowed_record_types (list[str])

Restricts which DNS record types a token may write in a zone. An empty list (the default) means all types are permitted.

append_only (bool)

Prevents any write from removing existing records:

  • DELETE changesets are rejected with HTTP 403.
  • REPLACE changesets are verified against the live PowerDNS state. The incoming records must be a strict superset of what is currently in PowerDNS for that name+type. Any record missing from the payload results in a 403.

Both flags can be combined. Example — a token that may only ever add TXT records globally and can never remove any:

- name: "append-txt-only"
  token_sha512: "..."
  global_read_only: true
  zones:
    - name: ".*"
      regex: true
      allowed_record_types:
        - "TXT"
      append_only: true

Why changetype: REPLACE needed extra protection

PowerDNS's REPLACE changetype overwrites the entire RRset for a given name+type. Without this check, a client could silently drop existing records by sending a REPLACE with a subset of the current records — even with append_only: true. The proxy now fetches the current zone state before forwarding any REPLACE on an append_only zone and rejects the request if records would be lost.

Implementation

Location Responsibility
models.py Added allowed_record_types and append_only fields to ProxyConfigZone
config.pycheck_rrset_allowed() Blocks DELETE when append_only; blocks wrong types when allowed_record_types is set
config.pycheck_append_only_records_intact() Pure helper: compares existing vs incoming record contents, returns False if any would be removed
config.pyensure_rrsets_request_allowed() Gains optional existing_rrsets param; calls the superset check for REPLACE on append_only zones
proxy.pyupdate_zone_rrset() Fetches current zone from PowerDNS when zone.append_only, passes rrsets to ensure_rrsets_request_allowed

All policy logic stays in config.py and is fully unit-testable without mocking HTTP. proxy.py only does the I/O fetch.

Tests

20 new unit tests covering:

  • allowed_record_types: allowed type, mismatched type, multiple types, no restriction
  • append_only: REPLACE allowed, DELETE blocked, DELETE allowed without flag
  • Combined allowed_record_types + append_only: TXT REPLACE allowed, TXT DELETE blocked, wrong type blocked
  • check_append_only_records_intact: no existing RRset, exact same records, superset, drops a record, different type, different name
  • ensure_rrsets_request_allowed with existing_rrsets: superset passes, drop blocked with 403

@teunis90 teunis90 force-pushed the feature/record-type-and-append-only-filtering branch from 661e558 to 2044773 Compare March 24, 2026 12:17
@teunis90 teunis90 changed the title feat: enforce append_only as true superset check on REPLACE feat: add allowed_record_types and enforce append_only as superset check Mar 24, 2026
Two new per-zone options in the proxy config:

**allowed_record_types** (list[str])
Restricts which DNS record types a token may write in a zone.
An empty list (the default) means all types are permitted.

**append_only** (bool)
Prevents any write from removing existing records:
- DELETE changesets are rejected with HTTP 403.
- REPLACE changesets are verified against the live PowerDNS state.
  The incoming records must be a strict superset of what is currently
  in PowerDNS for that name+type. Any missing record results in a 403.

Both flags can be combined. Example: a token that may only ever add TXT
records globally and can never remove any:

  - name: "append-txt-only"
    token_sha512: "..."
    global_read_only: true
    zones:
      - name: ".*"
        regex: true
        allowed_record_types:
          - "TXT"
        append_only: true

Implementation notes:
- check_rrset_allowed() enforces allowed_record_types and blocks DELETE
  for append_only zones (pure, no I/O).
- check_append_only_records_intact() is a new pure helper that compares
  existing vs incoming records by content and returns False if any would
  be lost.
- ensure_rrsets_request_allowed() gains an optional existing_rrsets param.
  When zone.append_only is True, proxy.py fetches the current zone from
  PowerDNS and passes the rrsets list in; all policy decisions stay in
  config.py.
- 20 new unit tests covering allowed_record_types, append_only, combined
  behaviour, and all edge cases of the superset check.
- README updated with documentation for both new zone options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant