Skip to content

RFC: IP Allow-List Filtering for Exposed Ports #4048

@nurikk

Description

Problem

Dokploy allows exposing ports for applications and databases, but there is no way to restrict access by source IP. This is a common security need — for example, restricting database ports to office IPs only.

Docker Swarm makes this notoriously difficult:

  1. Docker bypasses host firewalls — published ports go through the FORWARD chain, so ufw/firewalld rules are ignored (Docker docs)
  2. Swarm ingress SNATs traffic — in ingress mode, real client IPs are replaced with internal swarm IPs (10.255.0.x), making IP-based filtering impossible at the application/proxy level (moby/moby #25526)
  3. No built-in solution — Docker provides no native per-port IP filtering

Proposed Solution

Add per-port IP allow-list filtering using iptables DOCKER-USER chain, managed entirely by Dokploy. This works at the kernel level with zero overhead (no proxy, no sidecar).

How it would work

  • New ip_allow_list table: each port (application or database) can have multiple allowed CIDRs
  • Dokploy generates iptables rules in a custom DOKPLOY-FILTER chain (sub-chain of DOCKER-USER)
  • Rules are applied locally via execAsync and on remote swarm nodes via SSH
  • Empty allow-list = port open to all (backward-compatible)
  • Changes apply immediately — no redeploy needed

Example rules

# Host mode — straightforward
iptables -A DOKPLOY-FILTER -p tcp --dport 5432 -s 10.0.0.0/8 -j RETURN
iptables -A DOKPLOY-FILTER -p tcp --dport 5432 -j DROP

# Ingress mode — needs conntrack for port matching after DNAT
iptables -A DOKPLOY-FILTER -p tcp -m conntrack --ctorigdstport 8080 -s 1.2.3.0/24 -j RETURN
iptables -A DOKPLOY-FILTER -p tcp -m conntrack --ctorigdstport 8080 -j DROP

Key Design Decisions — Request for Comments

I'd like community input on these decisions before implementation:

1. Ingress mode: is IP filtering even possible?

In Docker Swarm ingress mode, source IPs are SNATed to internal addresses before packets reach the DOCKER-USER chain. This means -s <real-client-ip> won't match.

Options:

  • A) Only support IP filtering in host mode. Auto-switch ports to host when IP filtering is enabled (loses routing mesh load balancing).
  • B) Support both modes but warn users that ingress mode IP filtering relies on conntrack and may not preserve real source IPs on all setups.
  • C) Use the docker-ingress-routing-daemon approach to recover real IPs in ingress mode (adds complexity).

This is the biggest open question. If SNAT makes ingress-mode filtering impossible, the feature should either force host mode or clearly document the limitation.

2. Scope: which resources should support IP filtering?

  • Applications (per-port via existing ports table) — seems obvious
  • Databases (Postgres, MySQL, MariaDB, MongoDB, Redis via externalPort) — arguably the highest-value target
  • Compose services — ports are user-defined in compose files, much harder to intercept

Current thinking: Applications + Databases. Compose out of scope. Thoughts?

3. Data model for database allow-lists

Application ports have a clean FK (portId). Databases have externalPort as a direct integer field on 5 separate schemas. Options:

  • A) Polymorphic databaseId + databaseType columns (simple but no FK constraint, risk of orphaned rows)
  • B) 5 explicit nullable FK columns (postgresId, mysqlId, etc.) with cascade delete (safe but verbose)
  • C) Add a unified ports table for databases too, then just use portId everywhere (cleanest but bigger refactor)

4. Rule persistence without external services

Current plan: Dokploy applies rules on startup, on CRUD changes, on deploy, and via a periodic reconciliation interval (every ~5 minutes). No systemd services or scripts on disk — all state lives in the DB.

Concerns:

  • Brief unprotected window during flush-and-rebuild. Should we use iptables-restore for atomic swaps?
  • Race conditions between concurrent applies. Need a mutex?
  • If Dokploy crashes, DROP rules persist (could lock users out). Should there be a cleanup on shutdown?

5. IPv6 support

The iptables approach is IPv4-only. Should we also generate ip6tables rules, or declare IPv6 out of scope initially?

6. Allow-list only, or also deny-list?

Current plan: allow-list only (empty = open to all). This follows least-privilege. Is deny-list support needed?

7. UI placement

Current plan: a "shield" icon on each port row that opens an IP allow-list dialog. Databases get the same UI when externalPort is set. Any UX preferences?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions