Reach consumers by network, not by a second host bind
CI / test (push) Successful in 16s

The compose file published a second host port on the docker bridge IP so
sibling containers could reach the relay. That is a host-reachable address for
a service whose entire safety story is that it has exactly one, on loopback.

It joins the consumers' existing docker network instead, and is reached there
by name on the container port. Nothing about a consumer's deployment has to
change to use it, and the relay gains no address outside that network.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nirav Patel
2026-08-18 19:25:11 -04:00
co-authored by Claude Opus 5
parent e4d6230dd1
commit 399487862c
3 changed files with 48 additions and 17 deletions
+30 -11
View File
@@ -2,18 +2,25 @@
# keeps nothing, which is deliberate — an outbox of everyone's reminders is a
# liability, and the providers already have delivery logs.
#
# The two bind addresses below are the security boundary of this whole service.
# Neither is routable from outside the machine:
# The bind address below is the security boundary of this whole service. The
# only host port it publishes is on 127.0.0.1 — for a human on the host and for
# host-side consumers. It must never be given a 0.0.0.0 binding, a bridge-IP
# binding, or a cloudflared route. An authenticated relay on the public
# internet is an open spam relay the moment the token leaks, and that token is
# copied into every consumer's environment.
#
# 127.0.0.1 — for a human on the host, and for the health check.
# 172.17.0.1 — the docker bridge, so sibling containers can reach it by an
# address that exists for them. `127.0.0.1` inside a consumer's
# container is that container, so a consumer configured with a
# loopback gateway URL fails every delivery and looks fine.
# Containerised consumers reach it a different way, and this is the part that
# is easy to get wrong: `127.0.0.1` inside a consumer's container is that
# container, so a consumer pointed at a loopback URL fails every delivery while
# looking correctly configured. Instead this joins the docker network its
# consumers already live on and is reached there by name, on the container
# port:
#
# It must never be given a 0.0.0.0 binding or a cloudflared route. An
# authenticated relay on the public internet is an open spam relay the moment
# the token leaks, and that token is copied into every consumer's environment.
# from the host http://127.0.0.1:${CHANNEL_EXIT_PORT}/send
# from a sibling http://channel-exit:8080/send
#
# That is strictly tighter than publishing on the docker bridge IP: it adds no
# host-reachable address at all, and only containers on that network can see it.
services:
channel-exit:
@@ -23,7 +30,9 @@ services:
env_file: .env
ports:
- "127.0.0.1:${CHANNEL_EXIT_PORT:?allocate a port in the 5400-5999 range}:8080"
- "${DOCKER_BRIDGE_IP:-172.17.0.1}:${CHANNEL_EXIT_PORT:?}:8080"
networks:
consumers:
aliases: [channel-exit]
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4).status == 200 else 1)"]
interval: 15s
@@ -37,3 +46,13 @@ services:
options:
max-size: "10m"
max-file: "3"
networks:
# Created by whoever owns the consumers, not by this compose file — the relay
# joins their network rather than asking them onto one of its own, so nothing
# about their deployment has to change to start using it. `external` is what
# makes that true: if the network is missing, this fails to start instead of
# silently creating an empty one nobody else is on.
consumers:
external: true
name: ${CHANNEL_EXIT_NETWORK:?the existing docker network the consumers are on}