Phase 1 of Custom DNS Provider Plugin Support: the /api/v1/dns-providers/types endpoint now returns types dynamically from the dnsprovider.Global() registry instead of a hardcoded list. Backend handler queries registry for all provider types, metadata, and fields Response includes is_built_in flag to distinguish plugins from built-ins Frontend types updated with DNSProviderField interface and new response shape Fixed flaky WAF exclusion test (isolated file-based SQLite DB) Updated operator docs for registry-driven discovery and plugin installation Refs: #461
14 KiB
14 KiB
Custom DNS Provider Plugin Support — Remaining Work Plan
Date: 2026-01-14
This document is a phased completion plan for the remaining work on “Custom DNS Provider Plugin Support” on branch feature/beta-release (see PR #461 context in CHANGELOG.md).
What’s Already Implemented (Verified)
- Provider plugin registry:
dnsprovider.Global()registry anddnsprovider.ProviderPlugininterface in backend/pkg/dnsprovider. - Built-in providers moved behind the registry: 10 built-ins live in backend/pkg/dnsprovider/builtin and are registered via the blank import in backend/cmd/api/main.go.
- External plugin loader:
PluginLoaderServicein backend/internal/services/plugin_loader.go (loads.so, validates metadata/interface version, optional SHA-256 allowlist, secure dir perms). - Plugin management backend (Phase 5): admin endpoints in
backend/internal/api/handlers/plugin_handler.gomounted under/api/admin/pluginsvia backend/internal/api/routes/routes.go. - Example external plugin: PowerDNS reference implementation in plugins/powerdns.
- Registry-driven provider CRUD and Caddy config:
- Provider validation/testing uses registry providers via backend/internal/services/dns_provider_service.go
- Caddy config generation is registry-driven (per Phase 5 docs)
- Manual provider type:
manualprovider plugin in backend/pkg/dnsprovider/custom/manual_provider.go. - Manual DNS challenge flow (UI + API):
- API handler: backend/internal/api/handlers/manual_challenge_handler.go
- Routes wired in backend/internal/api/routes/routes.go
- Frontend API/types: frontend/src/api/manualChallenge.ts
- Frontend UI: frontend/src/components/dns-providers/ManualDNSChallenge.tsx
- Playwright coverage exists for manual provider flows: tests/manual-dns-provider.spec.ts
What’s Missing (Verified)
- Types endpoint is not registry-driven yet:
GET /api/v1/dns-providers/typesis currently hardcoded in backend/internal/api/handlers/dns_provider_handler.go and will not surface:- the
manualprovider’s field specs - any externally loaded plugin types (e.g., PowerDNS)
- any future custom providers registered in
dnsprovider.Global()
- the
- Plugin signature allowlist is not wired:
PluginLoaderServicesupports an optional SHA-256 allowlist map, but backend/cmd/api/main.go passesnil. - Sandboxing limitation is structural: Go plugins run in-process (no OS sandbox). The only practical controls are deny-by-default plugin loading + allowlisting + secure deployment guidance.
- No first-party webhook/script/rfc2136 provider types exist as built-in
dnsprovider.ProviderPluginimplementations (this is optional and should be treated as a separate feature, because external plugins already cover the extensibility goal).
Scope
- Make DNS provider type discovery and UI configuration registry-driven so built-in + manual + externally loaded plugins show up correctly.
- Close the key security gap for external plugins by wiring an operator-controlled allowlist for plugin SHA-256 signatures.
- Keep the scope aligned to repo conventions: no Python, minimal new files, and follow the repository structure rules for any new docs.
Non-Goals
- No Python scripts or example servers.
- No unrelated refactors of existing built-in providers.
- No “script execution provider” inside Charon (in-process shell execution is a separate high-risk feature and is explicitly out of scope here).
- No broad redesign of certificate issuance beyond what’s required for correct provider type discovery and safe plugin loading.
Dependencies
- Backend provider registry: backend/pkg/dnsprovider/plugin.go
- Provider loader: backend/internal/services/plugin_loader.go
- DNS provider UI/API type fetch: frontend/src/api/dnsProviders.ts
- Manual challenge API (used as a reference pattern for “non-Caddy” flows): backend/internal/api/handlers/manual_challenge_handler.go
- Container build pipeline: Dockerfile (Caddy built via
xcaddy)
Risks
- Type discovery mismatch: UI uses
/api/v1/dns-providers/types; if backend remains hardcoded, registry/manual/external plugin types won’t be configurable. - Supply-chain risk (plugins):
.soloading is inherently sensitive; SHA-256 allowlist must be operator-controlled and deny-by-default in hardened deployments. - No sandbox: Go plugins execute in-process with full memory access. Treat plugins as trusted code; document this clearly and avoid implying sandboxing.
- SSRF / outbound calls: plugins may implement
TestCredentials()with outbound HTTP. Core cannot reliably enforce SSRF policy inside plugin code; mitigate via operational controls (restricted egress, allowlisted outbound via infra) and guidance for plugin authors to reuse Charon URL validators. - Patch coverage gate: any production changes must maintain 100% patch coverage for modified lines.
Definition of Done (DoD) Verification Gates (Per Phase)
Repository testing protocol requires Playwright E2E before unit tests.
- E2E (first):
npx playwright test --project=chromium - Backend tests: VS Code task
shell: Test: Backend with Coverage - Frontend tests: VS Code task
shell: Test: Frontend with Coverage - TypeScript: VS Code task
shell: Lint: TypeScript Check - Pre-commit: VS Code task
shell: Lint: Pre-commit (All Files) - Security scans:
- VS Code tasks
shell: Security: CodeQL Go Scan (CI-Aligned) [~60s]andshell: Security: CodeQL JS Scan (CI-Aligned) [~90s] - VS Code task
shell: Security: Trivy Scan - VS Code task
shell: Security: Go Vulnerability Check
- VS Code tasks
Patch coverage requirement: 100% for modified lines.
Phase 1 — Registry-Driven Type Discovery (Unblocks UI + plugins)
Deliverables
- Backend
GET /api/v1/dns-providers/typesreturns registry-driven types, names, fields, and docs URLs. - The types list includes: built-in providers,
manual, and any external plugins loaded fromCHARON_PLUGINS_DIR. - Unit tests cover the new type discovery logic with 100% patch coverage on modified lines.
Tasks & Owners
- Backend_Dev
- Replace hardcoded type list behavior in backend/internal/api/handlers/dns_provider_handler.go with registry output.
- Use the service as the abstraction boundary:
h.service.GetSupportedProviderTypes()for the type listh.service.GetProviderCredentialFields(type)for field specsdnsprovider.Global().Get(type).Metadata()for display name + docs URL
- Ensure the handler returns a stable, sorted list for predictable UI rendering.
- Add/adjust tests for the types endpoint.
- Frontend_Dev
- Confirm
getDNSProviderTypes()is used as the single source of truth where appropriate. - Keep the fallback schemas in
frontend/src/data/dnsProviderSchemas.tsas a defensive measure, but prefer server-provided fields.
- Confirm
- QA_Security
- Validate that a newly registered provider type becomes visible in the UI without a frontend deploy.
- Docs_Writer
- Update operator docs explaining how types are surfaced and how plugins affect the UI.
Acceptance Criteria
- Creating a
manualprovider is possible end-to-end using the types endpoint output. /api/v1/dns-providers/typesincludesmanualand any externally loaded provider types (when present).- 100% patch coverage for modified lines.
Verification Gates
- If UI changed: run Playwright E2E first.
- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans.
Phase 2 — Provider Implementations: rfc2136, webhook, script
This phase is optional and should only proceed if we explicitly want “first-party” provider types inside Charon (instead of shipping these as external .so plugins). External plugins already satisfy the extensibility goal.
Deliverables
- New provider plugins implemented (as
dnsprovider.ProviderPlugin):rfc2136webhookscript
- Each provider defines:
Metadata()(name/description/docs)CredentialFields()(field definitions for UI)- Validation (required fields, value constraints)
BuildCaddyConfig()(or explicit alternate flow) with deterministic JSON output
Tasks & Owners
- Backend_Dev
- Add provider plugin files under backend/pkg/dnsprovider/custom (pattern matches
manual_provider.go). - Define clear field schemas for each type (avoid guessing provider-specific parameters not supported by the underlying runtime; keep minimal + extensible).
- Implement validation errors that are actionable (which field, what’s wrong).
- Add unit tests for each provider plugin:
- metadata
- fields
- validation
- config generation
- Add provider plugin files under backend/pkg/dnsprovider/custom (pattern matches
- Frontend_Dev
- Ensure provider forms render correctly from server-provided field definitions.
- Ensure any provider-specific help text uses the docs URL from the server type info.
- Docs_Writer
- Add/update docs pages for each provider type describing required fields and operational expectations.
Docker/Caddy Decision Checkpoint (Only if needed)
Before changing Docker/Caddy:
- Confirm whether the running Caddy build includes the required DNS modules for the new types.
- If a module is required and not present, update Dockerfile
xcaddy buildarguments to include it.
Acceptance Criteria
rfc2136,webhook, andscriptshow up in/dns-providers/typeswith complete field definitions.- Creating and saving a provider of each type succeeds with validation.
- 100% patch coverage for modified lines.
Verification Gates
- If UI changed: run Playwright E2E first.
- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans.
Phase 3 — Plugin Security Hardening & Operator Controls
Deliverables
- Documented and configurable plugin loading policy:
- plugin directory (
CHARON_PLUGINS_DIRalready used by startup in backend/cmd/api/main.go) - optional SHA-256 allowlist support wired end-to-end (from config/env →
NewPluginLoaderService(..., allowedSignatures))
- plugin directory (
- Minimal operator guidance for secure deployment.
Tasks & Owners
- Backend_Dev
- Wire a configuration source for plugin signatures into the
PluginLoaderServicecreation path (currently passednilin backend/cmd/api/main.go). - Prefer a single env var to stay minimal (example format: JSON map of
pluginName→sha256:...). - Add tests covering:
- allowlist reject (plugin not in allowlist)
- signature mismatch
- insecure directory permissions rejection
- Wire a configuration source for plugin signatures into the
- DevOps
- Ensure the plugin directory is mounted read-only where feasible.
- Validate container permissions align with
verifyDirectoryPermissions()expectations.
- QA_Security
- Threat model review focused on
.soloading risks and expected mitigations.
- Threat model review focused on
- Docs_Writer
- Update plugin operator docs to explain allowlisting, signatures, and safe deployment patterns.
Acceptance Criteria
- Plugins can be loaded successfully when allowed, and rejected when disallowed.
- Misconfigured (world-writable) plugin directory is detected and prevents loading.
- 100% patch coverage for modified lines.
Verification Gates
- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans.
Phase 4 — E2E Coverage + Regression Safety
Deliverables
- Playwright coverage for:
- DNS provider types rendering and required-field validation (including plugin types)
- Manual DNS challenge flow regression (existing spec:
tests/manual-dns-provider.spec.ts) - Creating a provider for at least one external plugin type (e.g.,
powerdns) when a plugin is present
- Documented smoke test steps for operators.
Tasks & Owners
- QA_Security
- Add/extend Playwright specs under tests.
- Validate keyboard navigation and form errors are accessible (screen reader friendly) where tests touch UI.
- Frontend_Dev
- Fix any UI issues uncovered by E2E (focus order, error announcements, labels).
- Backend_Dev
- Fix any API contract mismatches discovered by E2E.
Acceptance Criteria
- E2E passes reliably in Chromium.
- No regressions to manual challenge flow.
Verification Gates
- Run Playwright E2E first.
- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans.
Open Questions (Need Explicit Decisions)
- For plugin signature allowlisting: what is the desired configuration shape?
- Option A (minimal): env var JSON map
pluginName→sha256:...parsed by backend/cmd/api/main.go - Option B (operator-friendly): load from a mounted file path (adds new config surface)
- Option A (minimal): env var JSON map
- For “first-party” providers (
webhook,script,rfc2136): are these still required given external plugins already exist?
Notes on Accessibility
UI work in this plan is built with accessibility in mind, but likely still requires manual review and testing (e.g., with Accessibility Insights) as changes land.