Dokploy / Dokploy/dokploy

Feature request: mark a domain as internal-only, restricted to operator-defined trusted networks

Open
#5,420 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
37.4k
Forks
3k
Avg merge
1d 3h
Merged PRs (30d)
73

Description

The need

A Dokploy host is often reachable from two very different places: the public
internet, and some network the operator already trusts. That trusted side can be
a LAN, a provider's private network between servers, a DMZ, a site-to-site link,
an overlay VPN, or several of those at once. Which one it is does not change the
question being asked:

which services should answer the internet, and which should only answer the
trusted side?

Dokploy currently has no way to express that. Everything published answers on
0.0.0.0, and the only access control available is the BasicAuth middleware
written by utils/traefik/security.ts, plus forward-auth.ts for delegating to
an external service. Neither of those is about where the request came from.

It keeps being asked, in pieces

  • #2615 — distinguish between internal and external IP
  • #2915 / #4715 / #5157 — bind published ports to a specific host IP, or to loopback only
  • #2661 — don't publish the Dokploy and Traefik panels on all interfaces
  • #4312 — reach services over a private overlay rather than the internet

These read as five different features. They are five statements of one missing
concept.

The proposal

Give a domain an exposure mode: public, or restricted to a set of trusted
source ranges.

A restricted domain gets an ipAllowList middleware whose sourceRange the
operator controls. Dokploy does not need to know what produces those addresses —
a LAN, a private interconnect, a DMZ, an overlay. It only needs to know which
ranges the operator calls trusted, and to stop answering everyone else.

The plumbing is mostly there:

  • utils/traefik/middleware.ts already has addMiddleware (:11),
    deleteMiddleware (:30) and writeMiddleware (:109) for the file provider;
  • utils/traefik/file-types.ts already declares the middleware — at :210 as
    ipWhiteList, with sourceRange and ipStrategy at :861.

Two caveats on that last point. The declared name is the Traefik v2 spelling;
traefik-setup.ts:23 pins 3.6.25, where it is ipAllowList. And the type is
declared and never used — searching the repository returns three hits, all inside
file-types.ts itself. So the shape exists, but nothing writes it today.

Managing the trusted ranges

These ranges are data the operator edits, not a constant compiled into Dokploy.
The shape I would suggest is named sets, defined once and referenced by
domains, rather than a free-text list repeated on every domain.

An operator defines a set — "office LAN", "provider private network",
"site-to-site" — and adds, edits or removes CIDRs in it. A domain then chooses:
public, or restricted to one or more sets. When a subnet changes, it is edited in
one place and every domain referencing it is rewritten. With per-domain lists,
the same change means visiting every domain and hoping none was missed — and a
missed one either stops answering or stays open.

Two safeguards follow from that:

  • Deleting a set that is still referenced must be refused, or confirmed
    explicitly.
    Silently turning a restricted domain back into a public one is
    the worst failure this feature could have.
  • Input needs validating. A malformed CIDR produces a broken middleware. More
    importantly, a publicly routable range — 0.0.0.0/0 above all — defeats the
    entire point, and the UI should say so rather than accept it quietly.

Suggested ranges can be offered when creating a set, but as pre-filled
suggestions the operator confirms, never as values that apply on their own.

The parts that need a decision, not a default

Which ranges ship as the suggested default. The RFC1918 blocks
(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are uncontroversial, and IPv6
deployments need fc00::/7 alongside them. 100.64.0.0/10 is tempting because
several overlay networks allocate from it — but it is also what some ISPs use for
carrier-grade NAT, so on those networks it would be a range full of strangers.
It probably belongs in the UI as an opt-in with that warning attached, not in the
default set.

How the client IP is determined. ipStrategy exists for a reason: behind a
CDN or another reverse proxy, the peer address is the proxy, and the real client
sits in X-Forwarded-For. An allowlist configured without thinking about this is
either trivially bypassed or blocks everyone. Traefik offers depth and
excludedIPs; whichever Dokploy picks, the choice should be explicit and
explained in the UI rather than inherited silently.

Where the setting lives. Per domain is the natural place, since that is what
maps to a router. Whether it should also be settable per service, or as an
instance-wide default for new domains, is a product question.

What this would not do

It is not network isolation, and should not be described as one. Traefik
still listens on 0.0.0.0:443; the TCP connection is accepted and TLS terminates
before the middleware answers 403. That is genuinely weaker than not listening at
all, which is what #2915, #4715 and #2661 are asking for. The two are
complementary: an allowlist is a policy on answered requests, port binding is a
policy on what exists. Shipping one should not be used to close the other.

Certificates for non-public hosts stay unsolved. traefik-setup.ts:309 and
:364 both configure the ACME resolver with httpChallenge, which cannot
validate a hostname that the public internet cannot reach. DNS-01 would work, and
two pieces are already in place — utils/traefik/types.ts:60 declares
dnsChallenge, and the DNS provider feature now has credentials for several
registrars — but nothing connects them. That is its own discussion; I mention it
only because a restricted domain runs into it immediately.

Is this a direction the project wants?

Asking rather than opening a PR, since CONTRIBUTING asks for larger features to
be designed with the team first. If the exposure-mode shape looks right I am glad
to work on it. If the project would rather solve this at the port-binding layer
instead, that is a coherent answer too — and arguably the stronger one, since it
addresses the issues an allowlist cannot.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading utils/traefik/middleware.ts and file-types.ts, then inspect traefik-setup.ts for the pinned Traefik version and existing challenge configuration. Before implementation, get agreement on exposure mode versus port binding, then define the named-set lifecycle, CIDR validation, client-IP strategy, and per-domain behavior so completion has an agreed scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
devops, infrastructure, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.