Skip to content

fix(domains): validate against all server IPs and add per-domain DNS validation mode#4659

Open
pparage wants to merge 2 commits into
Dokploy:canaryfrom
pparage:fix/domain-validation-multiple-server-ips
Open

fix(domains): validate against all server IPs and add per-domain DNS validation mode#4659
pparage wants to merge 2 commits into
Dokploy:canaryfrom
pparage:fix/domain-validation-multiple-server-ips

Conversation

@pparage

@pparage pparage commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What is this PR about?

Domain validation (the "Validate" badge on a domain) compared a domain's resolved IPs against a single expected IP: the remote server's stored SSH ipAddress, or the Dokploy host's serverIp. Servers commonly have more than one address (an internal or SSH IP plus a separate public IP that DNS points to), so a correctly configured domain was reported as Invalid. This is the false negative described in #4658.

This PR addresses it in two layers:

Layer 1, automatic detection (the bug fix). Validation now gathers every IP the target server is reachable at: the stored address, the globally scoped interface addresses, and the detected public egress IP. The domain is valid when it resolves to any of them. IPs are collected over SSH for remote servers and locally for the Dokploy host, and any failure to reach the server falls back to the stored address, so behavior never regresses below what it was before.

Layer 2, per domain DNS validation mode. For setups where DNS legitimately does not point at the server itself (a reverse proxy, a load balancer, a floating VIP, Cloudflare Tunnel), each domain gets a small validation mode control:

  • auto: match against the server's detected IPs (default, the Layer 1 behavior).
  • proxy: match against a user provided IP (the proxy or load balancer address).
  • skip: only confirm the domain resolves, without matching an IP.

This generalizes the existing hardcoded CDN special case into an explicit, user driven setting.

Implementation notes

  • validateDomain now takes the full set of expected IPs and passes when the resolved set intersects it.
  • A new getServerIps helper collects the server's addresses (interfaces plus egress), and validateDomainForServer maps the validation mode to the right expected IPs.
  • New validationMode (enum, default auto) and expectedIp columns on the domain table, with a drizzle migration.
  • The domain form gets a validation mode select and a conditional expected IP input.

Testing

  • Added __test__/domain/validate-domain.test.ts covering the multi IP case, a genuine mismatch, and the proxy and skip modes.
  • pnpm --filter server typecheck and pnpm --filter dokploy typecheck pass.
  • Existing domain and traefik suites pass (106 tests), plus the 6 new tests.

Checklist

  • You created a dedicated branch based on the canary branch.
  • You have read the suggestions in the CONTRIBUTING.md file.
  • You have tested this PR in your local instance.

Issues related (if applicable)

closes #4658

… validation mode

Domain validation compared a domain's resolved IPs against a single
expected IP (the server's stored SSH ipAddress, or the Dokploy host's
serverIp). Servers commonly have more than one address (an internal SSH
IP plus a separate public IP that DNS points to), so a correctly
configured domain was reported as Invalid.

Layer 1: validation now gathers every IP the target server is reachable
at (the stored address, globally scoped interface addresses, and the
detected public egress IP) and passes when the domain resolves to any of
them. IPs are collected over SSH for remote servers and locally for the
Dokploy host, falling back to the stored address if the server cannot be
reached.

Layer 2: adds a per-domain DNS validation mode for cases where DNS does
not point at the server itself:
- auto: match against the server's detected IPs (default)
- proxy: match against a user-provided IP (reverse proxy / load balancer)
- skip: only confirm the domain resolves

Refs Dokploy#4658
@pparage pparage requested a review from Siumauricio as a code owner June 17, 2026 17:50
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working enhancement New feature or request labels Jun 17, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 815f07d7e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

domain: input.domain,
validationMode: input.validationMode,
expectedIp: input.expectedIp,
serverId: input.serverId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Authorize server IDs before remote IP collection

When a caller supplies serverId, this path passes it directly into validateDomainForServer, which calls getServerIps and then findServerById/execAsyncRemote; however this procedure only checks domain:read and does not verify that the server belongs to the caller's active organization or is in getAccessibleServerIds. In member/custom-role contexts that can read domains but not servers, a crafted validation request with a known server ID can make Dokploy SSH to that server and return collected IPs in mismatch errors, so the server ID should be derived from an already-authorized service/domain or checked before use.

Useful? React with 👍 / 👎.

Comment thread packages/server/src/services/domain.ts Outdated
}

if (validationMode === "proxy") {
return validateDomain(domain, expectedIp ? [expectedIp] : []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject proxy validation without an expected IP

In proxy mode, an omitted or empty expectedIp is converted to an empty expected-IP list, and validateDomain treats that as resolve-only success. Because the server-side create/update schemas and this validation mutation allow expectedIp to be null/omitted, API or stale clients can save/use validationMode: "proxy" without an IP and get a valid badge for any resolving domain—the same behavior as skip; this branch should fail or require a non-empty expected IP.

Useful? React with 👍 / 👎.

…ected IP in proxy mode

Address review feedback on the domain validation endpoint:

- The validateDomain procedure accepted a client-supplied serverId and
  used it to SSH to that server (via getServerIps) while only checking
  domain:read. A caller could target a server in another organization.
  It now verifies the serverId is in the caller's accessible servers
  before use, mirroring the guard used by the database routers.

- In proxy mode an omitted or empty expectedIp fell through to a
  resolve-only "valid" result, silently behaving like skip. Proxy mode
  now rejects a missing or blank expected IP.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Domain validation reports a false negative when the server has multiple IPs

1 participant