Skip to content

fix(traefik): renew certificates over 443 only and never publish port 80 - #53

Merged
agentHits merged 22 commits into
AgentHits-Devfrom
feat/traefik-port-80-policy
Oct 9, 2026
Merged

agentHits merged 22 commits into
AgentHits-Devfrom
feat/traefik-port-80-policy

Conversation

@agentHits

Copy link
Copy Markdown
Owner

Summary

Certificates now renew over port 443 only (TLS-ALPN-01). The installer and the panel never publish host port 80, and the static Traefik config uses tlsChallenge.

Servers installed before this change may still have an httpChallenge block in their Traefik config. Removing port 80 without converting that block would stop renewals, so every path that creates or replaces the Traefik container converts it first:

  • fresh installer install, and the installer Traefik update
  • the Traefik component update in the panel
  • saving Traefik ports in the panel
  • server setup when the Traefik container is missing

How the conversion works

  • Only the httpChallenge block is rewritten, to tlsChallenge: {}. Other settings stay as they are.
  • A dated copy of the config is kept next to it, as traefik.yml.bak-<UTC time>.
  • The result is verified before it is written. The awk exit status is checked, and a failed write is restored from the copy.
  • A config path that is a directory, cannot be read, or cannot be checked through sudo stops the step without any change.

Stacking

This PR builds on the panel host stack branch, because it changes code that branch introduces. Before that PR is merged, change this PR's base to the main branch. Otherwise GitHub may close this PR when the old branch is removed.

Operator guide

docs/certificates-and-ports.md explains the rule, what to do when a renewal error appears, and how to check the published ports.

Test plan

  • Server tests (bun run test __test__/server): 233 passed, 19 files.
  • Installer test file: 58 passed. Write-setup test file: 4 passed.
  • bash -n and shellcheck -S warning pass for the installer.
  • biome check and tsc --noEmit pass in both packages.
  • The installer function was run with macOS awk, mawk and busybox. The panel step was run with macOS awk and dash.

Not verified

  • Live servers: nothing was changed on any live server. Conversion runs only when an operator starts an update, a port save or a setup.

🤖 Generated with Claude Code

agentHits and others added 20 commits October 8, 2026 18:13
buildComponentUpdateSteps returns the pre-download gate and the steps of the
selected components. wrapComponentUpdateSteps adds the shell header and the
done marker. buildComponentUpdateScript composes the two with the same output,
so the SSH update path is unchanged, and the panel host update can run the
Traefik steps as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildHostStackRows turns the versions read on the panel host into one row per
component: panel, docker, traefik, postgres and redis. Each row reports its
installed and target version, whether it is outdated, and how it can be
updated: from the Web Server Update dialog (panel), from the panel itself
(traefik, redis), or only in the maintenance window (docker, postgres).
Postgres is compared by major version, and a major change is reported as
needing a migration. Values that cannot be read are unknown, never outdated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateHostStackComponents runs one bash script on the panel host. It replaces
Traefik with the steps the SSH update path already uses, and updates the Redis
service to the pinned image with stop-first and rollback. Redis is pulled
before anything changes. The Redis update waits until the swarm reports the
update as completed and redis-cli answers PONG, within a timeout; a rollback,
a pause, a refused update or a timeout fails the run. A second update is
refused while one is running.

readHostStackRows reads the installed versions with read-only docker commands
and the panel's own update check, which has a timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s page

settings.hostStack returns the rows of the panel host components. It is
limited to local host access (owner or admin) and returns nothing on cloud.
settings.updateHostStack is a subscription that runs the Traefik and Redis
updates on the panel host and streams their logs. It refuses docker, postgres
and panel with the next step, refuses while a panel update or another host
update is running, and writes an audit entry. Both procedures are classified
in the procedure access map; the update is dangerous, like the other host
settings changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "Panel host (this server)" card sits above the server cards. Its Components
button opens the panel host rows with the installed and target versions, the
status and the action for each component. Traefik and Redis can be updated
from the dialog after a confirmation, with a warning that Traefik is recreated
and the admin site is unreachable for a few seconds. Docker and Postgres show
the maintenance-window command, and the panel row opens the Web Server Update
dialog. The card is hidden for users who cannot manage the panel host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ostgres

Docker and Postgres are always updated in the maintenance window, so their
rows keep the reason and the exact installer command even when they are
current. The card can then show the command at any time. Traefik and Redis
rows carry neither, since the panel runs them. A Postgres row whose version
cannot be read gets no command, because the image it would keep is unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rows the panel can update always show a checkbox. It is enabled only when
the component is outdated, so an up-to-date component cannot be selected by
accident. Manual rows show their reason and command, and unknown rows show
the reason they could not be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host components update replaces Traefik and restarts Redis from a script
run by this panel. A panel update restarts the panel container, which would
stop that script in the middle of the change. The update mutation now refuses
while the host components update runs, the same way the host update refuses
while a panel update runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ainer

The installer mounts the socket read-only, but the generated container
for remote servers and the panel host dropped the :ro flag. Keep the same
mount in both places, so an update does not change how Traefik sees the
daemon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel host's Traefik publishes 443 only, but the generated container
and the installer always published 80 as well. A replacement would have
opened port 80 without anyone asking for it. Read the current publication
before the old container stops and reuse it for the new one and for the
restore; a fresh install still publishes 80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings router reads the host stack constant and update guard, so the
module mocks in the settings boundary tests must provide them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontainer did not have it

A replacement container is created with the default bridge network. The
live Traefik had no bridge attachment, so the update added one it did not
have before. Detach bridge after the new container runs, but only when the
container being replaced was not on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty array expanded under set -u fails on bash 3.2, which the macOS
CI runner uses. Use the guarded form, so port 80 is passed only when it is
published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of refusing

The operator update used to stop before any change when the installed
Postgres major version differed from the pinned one. It now prints
"Postgres <N> stays. A major upgrade is a separate migration; this update
does not change it." and continues:

- no Postgres backup, no Postgres pre-pull and no Postgres swap in that run
- Redis, Traefik, the panel and the Docker Engine step run as before
- when the majors match, the backup and the swap behave as before

postgres_ready now checks the image the service actually runs. Checking the
pinned image would never find a kept older Postgres, so the Docker Engine
health check would roll the upgrade back.

Tests: the refusal test now expects the skip line, no backup, no swap and the
other steps in order. A new Docker Engine upgrade test covers a kept Postgres.
The fake docker ps honors the ancestor filter for dokploy-postgres, so the
readiness check against the pinned image fails that test.

Docs: docs/server-maintenance.md is a new operator guide. The install guide
describes the new Postgres behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operator guide now says only port 443 (TCP and UDP) serves domain
traffic, port 80 is never opened, and certificates renew only over 443
through the TLS challenge. The UFW note no longer lists port 80. The note
about the panel's own port 3000 is unchanged. The installer code is not
changed here; that change is on a separate branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Traefik no longer publishes host port 80 from the panel or the
installer. The installer no longer opens it in UFW or the WSL firewall
rule and no longer requires it to be free. The ACME resolver uses
tlsChallenge (TLS-ALPN-01 on port 443) instead of httpChallenge, so
certificate renewal needs only port 443.

- traefik-setup: remove the port 80 publication from the run, create,
  standalone and swarm builders. Stored port lists that still contain
  host port 80 are filtered before the container is built, so a later
  recreate cannot restore it. The web entrypoint stays for plain-HTTP
  routers.
- server-components and server-setup: remove the traefik_http_publish
  branch, the UFW rule for port 80 and the port 80 warning.
- settings router: updateTraefikPorts rejects host port 80.
- install-agenthits.sh: remove TRAEFIK_HTTP_PUBLISH, the -p 80:80/tcp
  flag, the UFW rule and the port 80 preflight. The static config uses
  tlsChallenge. install-agenthits.ps1 drops port 80 from the WSL
  firewall rule and the busy-port check.
- Existing traefik.yml files are not rewritten. The operator guide
  covers migrating a server.
- Docs and CLAUDE.md state the rule and explain renewal errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renewal used the HTTP-01 challenge on port 80. Servers installed before the
port 80 change may still have an httpChallenge block in traefik.yml. Removing
host port 80 without converting that block would stop renewals, so every path
that creates or replaces the Traefik container converts it to tlsChallenge
first:

- fresh installer install, and the installer Traefik update
- the Traefik component update in the panel
- saving Traefik ports in the panel
- server setup when the Traefik container is missing

Only the httpChallenge block is rewritten; other settings stay as they are. A
dated copy of the config is kept next to it, and the result is verified before
it is written. The awk exit status is checked, and a failed write is restored
from the copy. A config path that is a directory, cannot be read, or cannot be
checked through sudo stops the step without any change.

The installer keeps its config path in TRAEFIK_CONFIG_FILE, so the file it
converts is the file the container mounts. Tests cover the installer function
and call order under bash, the panel step under bash and /bin/sh with temp
files, the failing awk, sudo and test cases, and the port-save and server-setup
paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A comment line inside the httpChallenge block no longer ends the removal, and
  the output is rejected if a child line is left indented under tlsChallenge.
- A failed update keeps the converted config instead of restoring the old
  httpChallenge one: the old container works with it because 443 is published,
  and restoring httpChallenge would leave renewal needing a closed port 80.
- The fresh-install path restores the original config when the write fails.
- rm -rf on the config path is limited to a directory named traefik.yml.
- Saving Traefik ports no longer fails for a user without sudo when the config
  is readable and has no httpChallenge; otherwise it still stops without changes.
- The error from a failed conversion names the config and its backup.

Tests cover each case, including a comment inside the block and a failing awk.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- The Traefik step does nothing when the running container already uses the
  target image, so a repeated run (for example after the panel reconnects)
  causes no second outage.
- The update lock is taken synchronously before the audit call and released on
  every exit path, and a panel update cannot start while a host update runs.
- The Postgres row compares the full tag, so 18 against 18.6 shows as needing
  an update. The manual command is shown only for a different major.
- The dialog looks for the done and failed markers in the accumulated log, so a
  marker split across two chunks is not missed.

Tests cover the repeated run, the lock race and release, the row comparison,
and the split marker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…/traefik-port-80-policy

# Conflicts:
#	packages/server/src/setup/server-components.ts
@agentHits
agentHits changed the base branch from feat/panel-host-stack to AgentHits-Dev October 9, 2026 07:08
agentHits and others added 2 commits October 9, 2026 03:08
Checks run only for PRs whose base is AgentHits-Dev, so a PR stacked on another
branch is never checked. The rule keeps every PR on that base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-port-80-policy

# Conflicts:
#	apps/dokploy/__test__/server/server-components.test.ts
#	install-agenthits.sh
#	packages/server/src/setup/server-components.ts
#	packages/server/src/setup/traefik-setup.ts
@agentHits
agentHits merged commit 2757655 into AgentHits-Dev Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant