mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-21 18:13:25 +00:00
c492b26bb1
Add ACME DNS-01 (TXT-record) validation alongside the existing HTTP-01, for internal/isolated clusters with no public port 80 and for wildcard certificates. Opt-in via a global kill-switch (default off); HTTP-01 is byte-for-byte unchanged, with zero agent or rendered-config changes. - Pluggable DNS provider interface (Manual + Cloudflare). Per-account credentials are Fernet-encrypted at rest, verified on save, and never returned by the API or written to logs/events/error_detail. - Non-blocking per-cycle orchestrator: publish (CAS) -> propagation grace (across cycles, no in-loop sleep) -> respond -> finalize/download, with a bounded fresh-order retry chain (1 original + 3 retries) on propagation lag. - Manual flow: user publishes the TXT record and confirms; manual DNS-01 cannot auto-renew unattended (auto-renew forced off and surfaced in the UI). - Migration v8: additive, idempotent columns on letsencrypt_accounts/orders and acme_challenges, plus a new letsencrypt_account_dns_credentials table. - Challenge-type-aware diagnostics (port80/routing/DNS checks skipped for DNS-01) and a DNS-01 event timeline in the order detail. - Frontend: DNS-01 account + credentials management, cert wizard adaptation, order-detail TXT records + verify, orders/renewal Method columns, and a Settings kill-switch. README, release notes, and API docs updated. Implements #35.
54 lines
2.3 KiB
Python
54 lines
2.3 KiB
Python
"""Abstract DNS provider interface for ACME DNS-01 (Issue #35)."""
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Dict, List
|
|
|
|
|
|
class DnsProviderError(Exception):
|
|
"""A DNS provider failure with a SANITIZED, user-safe message.
|
|
|
|
The message must NEVER contain API tokens, request headers, or other secrets — it is
|
|
persisted to acme_order_events / order error_detail and shown in the UI. Raise this (not a
|
|
raw aiohttp/json error) so credentials can't leak into logs or the order timeline.
|
|
"""
|
|
|
|
|
|
class DnsProvider(ABC):
|
|
"""Base class for a pluggable DNS provider.
|
|
|
|
RRset semantics are ADDITIVE: ``add_txt_record`` ensures a (name, value) TXT exists WITHOUT
|
|
removing other values at the same name, and ``remove_txt_record`` deletes ONLY the record
|
|
matching (name, value). This is required because a cert for ``example.com`` + ``*.example.com``
|
|
publishes two distinct values at the SAME name ``_acme-challenge.example.com``.
|
|
"""
|
|
|
|
# Stable machine name (used in DB + API); human label; whether the provider automates publishing.
|
|
name: str = "base"
|
|
label: str = "Base"
|
|
automated: bool = True
|
|
|
|
# Declarative schema the UI renders to collect credentials. Each field:
|
|
# {"key", "label", "type" ("text"|"password"), "required" (bool), "max_length" (int), "help" (str)}
|
|
credential_fields: List[Dict] = []
|
|
|
|
def __init__(self, credentials: Dict[str, str] | None = None):
|
|
self.credentials = credentials or {}
|
|
|
|
@abstractmethod
|
|
async def verify_credentials(self) -> Dict:
|
|
"""Validate the stored credentials against the provider. Returns
|
|
``{"ok": bool, "detail": str}`` (detail is user-safe). Must not raise on auth failure —
|
|
return ``ok=False`` with a sanitized detail; may raise DnsProviderError on transport errors.
|
|
"""
|
|
|
|
@abstractmethod
|
|
async def add_txt_record(self, name: str, value: str) -> None:
|
|
"""Ensure a TXT record (name, value) exists. Idempotent; must not remove other values
|
|
at the same name. Raise DnsProviderError (sanitized) on failure."""
|
|
|
|
@abstractmethod
|
|
async def remove_txt_record(self, name: str, value: str) -> None:
|
|
"""Remove ONLY the TXT record matching (name, value). Tolerate 'already gone'.
|
|
Raise DnsProviderError (sanitized) on a real failure."""
|