Preflight
Summary
A Roomote deployment that is not reachable from the public internet cannot obtain
certificates. install.sh offers exactly two TLS modes: letsencrypt, which relies on
HTTP-01 and therefore needs inbound port 80 from the ACME server, and internal, which
issues a self-signed CA that every client has to trust manually.
There is no third option for the case in between — a deployment on a public host whose
Roomote instance is deliberately bound to a private address and reached only over a VPN.
The domain is public and DNS-01 would work; the host simply must not answer on 80/443 for
that name.
Two separate things follow from this, and the second one is not specific to our setup:
- No DNS-01 path exists, so this deployment cannot get a certificate at all.
- Preview certificates are issued on demand, one per hostname. That is the only
option today, and it carries costs that every self-hosted deployment pays — public
Certificate Transparency entries for every preview host, ACME rate limits, and a
round trip on first hit. A deployment that can solve DNS-01 could use a single
wildcard instead, but install.sh currently prevents that.
Problem area
Reverse proxy, TLS, or public URL
Installation method
Docker Compose
Roomote version
v1.4.1
Previous version
not applicable
Environment
- Host OS / architecture: Debian, amd64
- Container runtime: Docker with Compose
- Public URL / reverse proxy: bundled Caddy, bound to a private tunnel address
- DNS: Cloudflare
- Everything else: as shipped by
install.sh
Command or upgrade path
# TLS mode "letsencrypt": Caddy never gets a certificate, because the ACME server
# cannot reach the instance for the HTTP-01 challenge.
deploy/install.sh --domain <internal-domain> --tls-mode letsencrypt
# TLS mode "internal": works, but every client must trust a private CA.
deploy/install.sh --domain <internal-domain> --tls-mode internal
Expected behavior
A deployment that owns a public DNS zone should be able to solve an ACME DNS-01 challenge
and get a real certificate, without being publicly reachable over HTTP.
Actual behavior
Not possible with the shipped images. Two things stand in the way, and neither is a bug —
just a missing seam:
- The Caddy image cannot do DNS-01. Caddy compiles DNS provider modules into the
binary rather than loading them at runtime, so caddy:2.10-alpine has no provider
available no matter what the Caddyfile says.
- The Caddyfile has no place for global TLS configuration.
acme_dns belongs in the
global options block, and there is no environment placeholder there — only
{$ROOMOTE_CADDY_LOCAL_CERTS:}, which selects the internal CA.
What on-demand issuance costs, independent of our case
This part applies to any self-hosted deployment, not just one behind a VPN. The preview
block resolves to tls { on_demand }, so Caddy issues one certificate per preview
hostname on first access:
- Certificate Transparency. Every preview hostname a deployment ever serves is
published in public CT logs. For a self-hosted instance that is a running list of
internal task names, readable by anyone watching the logs.
- ACME rate limits. Issuance is counted per registered domain, so a deployment that
creates many previews can run into the limit — and the failure surfaces to a user
opening a preview, not to an operator running a command.
- Latency on first hit. The first request to each preview host blocks on a
certificate issuance round trip instead of being served immediately.
On-demand is clearly the right default when DNS-01 is not available, and none of this is
fatal. But a deployment that can solve DNS-01 would be better served by a single
wildcard certificate for *.<preview domain> — and today install.sh rewrites
ROOMOTE_CADDY_WILDCARD_TLS_SNIPPET on every run, so that choice cannot be made even by
editing .env.
Workarounds tried
We are running this successfully with four small additive changes, all inert unless the new
variables are set — a deployment that sets nothing keeps the stock image and today's
behaviour exactly. The whole thing is about 35 lines across four files. We are ready to
open a PR; per CONTRIBUTING we are raising the issue first and will wait for an invitation.
image: ${CADDY_IMAGE:-<current pinned image>} in docker-compose.prod.yml, so a
deployment can supply its own Caddy build. Default unchanged.
- A
{$ROOMOTE_CADDY_GLOBAL_TLS:} placeholder in the global options block of the
Caddyfile, plus passing that variable (and CLOUDFLARE_API_TOKEN) through to the
container.
- A small
xcaddy Dockerfile that builds Caddy with a DNS provider module compiled in.
install.sh no longer overwrites ROOMOTE_CADDY_WILDCARD_TLS_SNIPPET when the .env
already sets it, so a deployment can clear it deliberately and get the wildcard
described above. Note env_has_value was not usable here — it treats an intentionally
empty value as unset.
Proposal
We would like to contribute both parts upstream, and we think the second matters more than
the first.
The DNS-01 seam is narrow: two hooks (an overridable Caddy image and a global TLS
placeholder) unblock every deployment that cannot use HTTP-01, and they cost nothing when
unused. Whether Roomote also ships a DNS-enabled Caddy build is a separate call — the hooks
alone are enough for us.
The on-demand behaviour is the part we would raise even if our own deployment were publicly
reachable. Publishing every internal preview hostname to public CT logs, with no way to
turn it off, is a property most operators would not expect from a self-hosted install, and
it is decided today by a single line in install.sh that rewrites itself on every upgrade.
Making that line respect an existing value is a two-line change and takes nothing away from
the default.
We have all four changes running and are ready to open a PR — per CONTRIBUTING we are
waiting for an invitation rather than sending one unsolicited. Happy to cut it to whatever
shape you prefer, split it into two PRs, or hand over the diff for someone on the team to
take from here.
Preflight
Summary
A Roomote deployment that is not reachable from the public internet cannot obtain
certificates.
install.shoffers exactly two TLS modes:letsencrypt, which relies onHTTP-01 and therefore needs inbound port 80 from the ACME server, and
internal, whichissues a self-signed CA that every client has to trust manually.
There is no third option for the case in between — a deployment on a public host whose
Roomote instance is deliberately bound to a private address and reached only over a VPN.
The domain is public and DNS-01 would work; the host simply must not answer on 80/443 for
that name.
Two separate things follow from this, and the second one is not specific to our setup:
option today, and it carries costs that every self-hosted deployment pays — public
Certificate Transparency entries for every preview host, ACME rate limits, and a
round trip on first hit. A deployment that can solve DNS-01 could use a single
wildcard instead, but
install.shcurrently prevents that.Problem area
Reverse proxy, TLS, or public URL
Installation method
Docker Compose
Roomote version
v1.4.1
Previous version
not applicable
Environment
install.shCommand or upgrade path
Expected behavior
A deployment that owns a public DNS zone should be able to solve an ACME DNS-01 challenge
and get a real certificate, without being publicly reachable over HTTP.
Actual behavior
Not possible with the shipped images. Two things stand in the way, and neither is a bug —
just a missing seam:
binary rather than loading them at runtime, so
caddy:2.10-alpinehas no provideravailable no matter what the Caddyfile says.
acme_dnsbelongs in theglobal options block, and there is no environment placeholder there — only
{$ROOMOTE_CADDY_LOCAL_CERTS:}, which selects the internal CA.What on-demand issuance costs, independent of our case
This part applies to any self-hosted deployment, not just one behind a VPN. The preview
block resolves to
tls { on_demand }, so Caddy issues one certificate per previewhostname on first access:
published in public CT logs. For a self-hosted instance that is a running list of
internal task names, readable by anyone watching the logs.
creates many previews can run into the limit — and the failure surfaces to a user
opening a preview, not to an operator running a command.
certificate issuance round trip instead of being served immediately.
On-demand is clearly the right default when DNS-01 is not available, and none of this is
fatal. But a deployment that can solve DNS-01 would be better served by a single
wildcard certificate for
*.<preview domain>— and todayinstall.shrewritesROOMOTE_CADDY_WILDCARD_TLS_SNIPPETon every run, so that choice cannot be made even byediting
.env.Workarounds tried
We are running this successfully with four small additive changes, all inert unless the new
variables are set — a deployment that sets nothing keeps the stock image and today's
behaviour exactly. The whole thing is about 35 lines across four files. We are ready to
open a PR; per CONTRIBUTING we are raising the issue first and will wait for an invitation.
image: ${CADDY_IMAGE:-<current pinned image>}indocker-compose.prod.yml, so adeployment can supply its own Caddy build. Default unchanged.
{$ROOMOTE_CADDY_GLOBAL_TLS:}placeholder in the global options block of theCaddyfile, plus passing that variable (and
CLOUDFLARE_API_TOKEN) through to thecontainer.
xcaddyDockerfile that builds Caddy with a DNS provider module compiled in.install.shno longer overwritesROOMOTE_CADDY_WILDCARD_TLS_SNIPPETwhen the.envalready sets it, so a deployment can clear it deliberately and get the wildcard
described above. Note
env_has_valuewas not usable here — it treats an intentionallyempty value as unset.
Proposal
We would like to contribute both parts upstream, and we think the second matters more than
the first.
The DNS-01 seam is narrow: two hooks (an overridable Caddy image and a global TLS
placeholder) unblock every deployment that cannot use HTTP-01, and they cost nothing when
unused. Whether Roomote also ships a DNS-enabled Caddy build is a separate call — the hooks
alone are enough for us.
The on-demand behaviour is the part we would raise even if our own deployment were publicly
reachable. Publishing every internal preview hostname to public CT logs, with no way to
turn it off, is a property most operators would not expect from a self-hosted install, and
it is decided today by a single line in
install.shthat rewrites itself on every upgrade.Making that line respect an existing value is a two-line change and takes nothing away from
the default.
We have all four changes running and are ready to open a PR — per CONTRIBUTING we are
waiting for an invitation rather than sending one unsolicited. Happy to cut it to whatever
shape you prefer, split it into two PRs, or hand over the diff for someone on the team to
take from here.