Skip to content

feat: built-in ACME certificate management (DNS-01) - #205

Draft
nd4y wants to merge 1 commit into
remnawave:mainfrom
nd4y:feature/acme
Draft

feat: built-in ACME certificate management (DNS-01)#205
nd4y wants to merge 1 commit into
remnawave:mainfrom
nd4y:feature/acme

Conversation

@nd4y

@nd4y nd4y commented Aug 9, 2026

Copy link
Copy Markdown

Discussion issue per CONTRIBUTING: #206

What this adds

Built-in ACME certificate management: the panel issues TLS certificates for node inbounds over DNS-01, renews them on schedule, and delivers the material to nodes without storing PEM in config profiles. Companion UI PR: remnawave/frontend (linked below).

  • Credentials for DNS providers: Cloudflare, deSEC, DigitalOcean, Gandi, Hetzner, Porkbun, PowerDNS, Vultr — plus CUSTOM (any HTTP DNS broker over an open four-endpoint protocol) and MANUAL (pairs with dns-persist-01). The provider registry lives in the contract, so the backend validates and the UI renders each provider's fields from one source of truth.
  • Certificates ordered from any RFC 8555 CA (Let's Encrypt default, staging first; EAB supported for ZeroSSL/Google-style CAs), wildcards included, ECDSA/RSA key types.
  • Per-node delivery: the PEM is injected into tlsSettings.certificates when the node's config is rendered. Only nodes the certificate is bound to ever see the private key; profiles keep empty certificates arrays. The certificate fingerprint participates in the config hash, so a renewal restarts exactly the affected nodes.
  • Renewal scheduler with backoff and a per-certificate event journal; failures surface in the UI, not only in process logs.
  • Import for certificates issued elsewhere (bought, corporate CA): domains, validity and key type are read from the material; the panel never renews them, replacing the material is one modal.
  • dns-persist-01 is implemented and ready; production CAs do not offer it yet, staging works.

The problem it solves

Serving TLS on nodes today means an external agent (acme.sh/certbot plus glue scripts) that writes PEM into config profiles. That has three structural problems:

  1. Key sprawl — a profile is shared by several nodes, so every node holding the profile gets every private key, including keys for names it does not serve.
  2. Renewal is invisible — the panel does not know a renewal happened; nodes keep serving the old certificate until someone restarts them, and a broken renewal loop is discovered when the certificate expires.
  3. Operational surface — one more container with DNS-provider credentials, its own schedule and its own failure modes, per installation.

With this PR the panel owns the lifecycle end to end: issuance, renewal, delivery, restart of affected nodes, and an audit trail — with private keys scoped to the nodes that need them.

When it applies

  • Multi-node TLS fronts: a shared wildcard plus per-node names, each key visible only to its node.
  • Any DNS host among the eight native providers — or any other DNS host via CUSTOM: the real credential moves to a small broker with a domain allow-list, so an internet-facing panel holds a token that can only create ACME TXT records for permitted names, not edit whole zones.
  • Installations that buy certificates or get them from a corporate CA: import, bind to nodes, replace material when reissued.
  • Not for HTTP-01: the panel deliberately implements DNS-01 (and dns-persist-01) only — nodes routinely sit behind CDNs or answer 443 with Reality/Vision where HTTP-01 cannot land.

Configuration

Deployment: one new environment variable, generated by the bundled CLI. Everything else is runtime, via the UI. Migrations only add acme_* tables.

cli generate-acme-key
# -> ACME_SECRET_KEY=<32 bytes, base64>

It encrypts DNS credentials, ACME account keys and certificate private keys at rest, and is deliberately separate from APP_SECRET: rotating the login secret must not make stored certificates unreadable.

Cloudflare credential (UI: Credentials → Add credential): an API token with Zone:Read + DNS:Edit on the target zones. The Test action verifies the token and lists visible zones before the first order.

CUSTOM credential: base URL + bearer token of any service implementing the broker protocol (POST /v1/dns-01/present, POST /v1/dns-01/cleanup, optional PUT /v1/persist, GET /v1/policy) — specified in docs/acme.md. A reference broker implementation is acme-proxy; its config for the typical "panel may only touch its own names" setup:

listen: ":8080"
ttl_seconds: 60

providers:
  main:
    type: cloudflare            # holds the real DNS credential
    api_token: ${CF_API_TOKEN}  # Zone:Read + DNS:Edit
    zones:
      - example.com

clients:
  panel:
    token: ${CLIENT_TOKEN_PANEL}
    provider: main
    allow:                      # the only names the panel may write
      - "*.edge.example.com"

rate_limit:
  per_client_per_minute: 30

Full operator documentation, including failure semantics and what the feature refuses to do, is in docs/acme.md.

Screenshots

Certificates, nodes-style rows — status, domains, remaining lifetime, DNS credential, node bindings:

certificates

Edit (staging-first defaults, EAB under the fold):

edit modal

Details drawer with the event journal of a failing order:

details drawer

Import of an externally issued certificate:

import modal

Credentials and the registry-driven provider form:

credentials

credential modal

Notes for review

  • Runs in production since 2026-08-08 on a three-node installation (14 certificates: a shared wildcard plus per-node CDN names), including renewals and provider migration on the live system.
  • Migrations are additive; rolling back to a pre-feature image leaves the acme_* tables inert.
  • Feature pages are English-only for now; the menu entry is translated (en/ru/zh/fa). Happy to wire the pages into Crowdin if the feature is wanted.
  • Opening as a draft to hear whether the direction fits upstream before polishing the last mile to your conventions.

@snyk-io

snyk-io Bot commented Aug 9, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

The panel issues TLS certificates for node inbounds itself, renews them on
schedule and injects the material into a node's config as it is rendered, so a
private key only ever reaches the nodes that serve its names - config profiles
are shared, and writing PEM into one hands every key to every node using it.

- credentials for eight native DNS providers (Cloudflare, deSEC, DigitalOcean,
  Gandi, Hetzner, Porkbun, PowerDNS, Vultr), CUSTOM for any HTTP DNS broker and
  MANUAL for dns-persist-01; the provider registry lives in the contract, so
  backend validation and the UI form come from one source of truth
- orders against any RFC 8555 CA: staging-first defaults, EAB, wildcards,
  ECDSA and RSA key types
- per-node delivery with the certificate fingerprint mixed into the config
  hash, so a renewal restarts exactly the affected nodes and profiles keep
  empty certificate arrays
- renewal scheduler with backoff, parallel issuance, and a per-certificate
  event journal that surfaces failures in the UI
- import for certificates the panel did not issue, replaceable in place
- secrets encrypted at rest under ACME_SECRET_KEY, deliberately separate from
  APP_SECRET so rotating the login secret cannot orphan stored certificates

Migrations only add acme_* tables; installations that never open the page are
unaffected. Operator documentation is in docs/acme.md.
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