diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 2a72eeced..c54bde37f 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -223,6 +223,20 @@ A deployment that terminated TLS at a proxy but never set `DD_SERVER_TRUSTPROXY` **Migration:** Set `DD_SERVER_TRUSTPROXY` to the number of proxy hops in front of Drydock (e.g. `1`), and make sure the proxy forwards `X-Forwarded-Proto` (and `X-Forwarded-Host`). See [CSRF validation failed (403) behind a reverse proxy](https://getdrydock.com/docs/faq#csrf-validation-failed-403-behind-a-reverse-proxy). +### Implicit reverse-proxy header trust for WebSocket origin checks + +| | | +| --- | --- | +| **Deprecated in** | v1.6.0-rc.3 | +| **Removed in** | v1.6.0-rc.3 (immediate — security fix, no grace period) | +| **Affects** | TLS-terminating reverse-proxy deployments (Traefik, Nginx, NGINX Proxy Manager, Caddy, HAProxy, Synology DSM, …) without `DD_SERVER_TRUSTPROXY` using the log-stream (`/api/v1/log/stream`) or container log-stream (`/api/v1/containers/{id}/logs/stream`) WebSocket endpoints | + +Before rc.3, `isOriginAllowed()` (`app/api/ws-upgrade-utils.ts`) validated a WebSocket upgrade's `Origin` header by comparing host only — the same gap the CSRF check above had before rc.30. rc.3 extends the check to also validate scheme, honoring `X-Forwarded-Proto` / `X-Forwarded-Host` only when Express `trust proxy` is enabled; otherwise the socket's own transport (encrypted or not) decides the expected scheme. Because the forgeable behavior was the vulnerability, it could not be kept alive behind a deprecation window. + +A deployment that terminates TLS at a proxy but never set `DD_SERVER_TRUSTPROXY` will now have WebSocket upgrades to these endpoints rejected with `403`, the same way REST CSRF checks broke in rc.30. + +**Migration:** Set `DD_SERVER_TRUSTPROXY` to the number of proxy hops in front of Drydock (e.g. `1`), and make sure the proxy forwards `X-Forwarded-Proto` (and `X-Forwarded-Host`). See [CSRF validation failed (403) behind a reverse proxy](https://getdrydock.com/docs/faq#csrf-validation-failed-403-behind-a-reverse-proxy). + ### Command trigger process-environment inheritance | | | @@ -248,3 +262,27 @@ Since rc.35 the child environment is a fixed allowlist (`PATH`, `HOME`, `SHELL`, Before rc.35, the HTTP trigger sent requests to any syntactically valid URL, including cloud instance-metadata services (`169.254.169.254` and friends) — an SSRF primitive for anyone able to influence trigger configuration. Requests resolving to link-local/metadata ranges are now rejected before sending. Private-network (RFC-1918) and localhost targets are unaffected — they remain the normal self-hosted case. **Migration:** The rare deployment that genuinely needs a link-local target sets `DD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true` on that trigger. See the [http trigger docs](https://getdrydock.com/docs/configuration/triggers/http). + +### Anonymous-auth grandfather path for upgrades + +| | | +| --- | --- | +| **Deprecated in** | v1.6.0-rc.3 | +| **Removed in** | v1.6.0-rc.3 (immediate — security fix, no grace period) | +| **Affects** | Upgrading instances (an existing `/store/dd.json`) with no authentication configured, or with anonymous auth enabled but not explicitly confirmed | + +Before rc.3, an upgrading instance with no auth strategy configured was let through with anonymous access and a startup warning — the same grandfather path drydock used before v1.4.0 enforced authentication on fresh installs. rc.3 removes it: `app/authentications/providers/anonymous/Anonymous.ts` now throws at startup for an upgrade exactly as it already did for a fresh install, refusing to serve rather than warning and continuing. Because the warn-and-serve behavior was itself the exposure — an open dashboard reachable without credentials — it could not be kept alive behind a deprecation window. + +**Migration:** Set `DD_AUTH_BASIC__USER`/`_HASH` (or configure OIDC) before upgrading, or set `DD_ANONYMOUS_AUTH_CONFIRM=true` to explicitly keep the instance anonymous. See [Authentication](https://getdrydock.com/docs/configuration/authentications). + +### Session cookie renamed `connect.sid` → `drydock.sid` + +| | | +| --- | --- | +| **Deprecated in** | v1.6.0-rc.3 | +| **Removed in** | v1.6.0-rc.3 (immediate — security fix, no grace period) | +| **Affects** | Every authenticated session, Basic and OIDC alike | + +Before rc.3, drydock served sessions under Express's default cookie name, `connect.sid` — the same name countless other Express apps use, making a drydock session fingerprintable and a potential collision risk with another Express app on the same host/path. rc.3 renames the cookie to `drydock.sid`. Because a shared, guessable session-cookie name was itself the exposure, it could not be kept alive behind a deprecation window. + +**Migration:** None required. Existing `connect.sid` cookies are simply no longer recognized, so every user is signed out once on upgrade and needs to log back in. No data is lost. diff --git a/README.md b/README.md index 9f6a265b3..52b28dabf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ > [!WARNING] > **Updating from an older release? Read the upgrade notes first.** Three security-hardening fixes first shipped in **1.4.6** and run through the entire **1.5** line, so anyone updating from a release older than 1.4.6 is affected whatever version they land on (1.4.6, any 1.5.x, or later). They are not deprecations and have no grace period: OIDC now requires `authorization_endpoint` in your provider's discovery metadata, unauthenticated rate-limiting keys on the TCP peer address (shared bucket behind a reverse proxy), and HTTP-trigger proxy URLs must use `http(s)://`. See **[UPGRADE-NOTES.md](UPGRADE-NOTES.md)** before updating. + + +> [!WARNING] +> **Updating to 1.6.0-rc.3 or later?** More security-hardening fixes land with no grace period. An instance with no authentication configured — or with anonymous auth enabled but unconfirmed — now **fails closed** on upgrade, exactly like a fresh install — the container runs, but every API request returns `401`; set `DD_ANONYMOUS_AUTH_CONFIRM=true` or configure `DD_AUTH_BASIC_*`/OIDC before upgrading. The session cookie is renamed `connect.sid` → `drydock.sid`, signing every existing user out once. HTTP notification triggers (plus the Hass webhook and registry icon fetches) now resolve hostnames through a guarded DNS lookup that blocks cloud-metadata/link-local targets and never follow redirects — set `allowmetadata=true` on a specific `DD_NOTIFICATION_HTTP_*` trigger if you legitimately need one. See **[DEPRECATIONS.md](DEPRECATIONS.md#enforced-security-changes-no-deprecation-window)** for full migration guidance. +

📑 Contents

- [📖 Documentation](https://getdrydock.com/docs) @@ -160,7 +165,7 @@ docker run -d \ > > Drydock v1.6 accepts only argon2id Basic auth hashes. Legacy `{SHA}`, `$apr1$`/`$1$`, `crypt`, and plain-text hashes are rejected; regenerate them before upgrading. > Authentication is **required by default**. See the [auth docs](https://getdrydock.com/docs/configuration/authentications) for OIDC, anonymous access, and other options. -> To explicitly allow anonymous access on fresh installs, set `DD_ANONYMOUS_AUTH_CONFIRM=true`. +> Anonymous access must be explicitly confirmed with `DD_ANONYMOUS_AUTH_CONFIRM=true` on new and upgraded instances alike — without it, an instance with no auth configured (or unconfirmed anonymous auth) fails closed and rejects every API request with `401`. The image includes `trivy` and `cosign` binaries for local vulnerability scanning and image verification. diff --git a/app/package-lock.json b/app/package-lock.json index 3a5a2d021..7c7cfac02 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -7067,9 +7067,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/app/package.json b/app/package.json index 58d522c41..1d2e0cbd8 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "drydock-app", "version": "1.6.0", - "description": "Drydock backend — Docker container update manager", + "description": "Drydock backend \u2014 Docker container update manager", "engines": { "node": ">=24.0.0" }, @@ -84,7 +84,7 @@ "vite": "8.1.3", "@babel/core": "7.29.7", "form-data": "4.0.6", - "protobufjs": "7.6.4", + "protobufjs": "7.6.5", "undici": "8.6.0" }, "devDependencies": { diff --git a/apps/web/src/app/data/faq.ts b/apps/web/src/app/data/faq.ts index d70cc582c..708a82332 100644 --- a/apps/web/src/app/data/faq.ts +++ b/apps/web/src/app/data/faq.ts @@ -37,6 +37,6 @@ export const faqItems: Array<{ question: string; answer: string }> = [ { question: "Is Drydock open source, and how do I get started?", answer: - "Drydock is AGPL-3.0 licensed and free to self-host. The fastest path is a single docker run mounting /var/run/docker.sock and setting DD_WATCHER_LOCAL_SOCKET=/var/run/docker.sock — the dashboard is available on port 3000. For production, use Docker Compose with a persistent volume at /store for the database and add a notification trigger such as DD_NOTIFICATION_SLACK_* for update alerts. Authentication is supported via OIDC (Authelia, Auth0, Authentik) or the built-in username/password. Full configuration docs are at getdrydock.com/docs.", + "Drydock is AGPL-3.0 licensed and free to self-host. The fastest path is a single docker run mounting /var/run/docker.sock and setting DD_WATCHER_LOCAL_SOCKET=/var/run/docker.sock plus DD_ANONYMOUS_AUTH_CONFIRM=true for a quick local trial — the dashboard is available on port 3000. For production, use Docker Compose with a persistent volume at /store for the database and add a notification trigger such as DD_NOTIFICATION_SLACK_* for update alerts. Authentication (OIDC via Authelia/Auth0/Authentik, the built-in username/password, or an explicit anonymous opt-in) is required — without one configured the instance fails closed, API requests return 401, and /health returns 503. Full configuration docs are at getdrydock.com/docs.", }, ]; diff --git a/apps/web/src/components/docker-run-snippet.tsx b/apps/web/src/components/docker-run-snippet.tsx index c851564e9..69b1da232 100644 --- a/apps/web/src/components/docker-run-snippet.tsx +++ b/apps/web/src/components/docker-run-snippet.tsx @@ -31,8 +31,13 @@ export function DockerRunSnippet({ {" "}--name drydock \{"\n"} {" "}-v /var/run/docker.sock:/var/run/docker.sock \{"\n"} {" "}-p 3000:3000 \{"\n"} + {" "}-e DD_ANONYMOUS_AUTH_CONFIRM=true \{"\n"} {" "} {SITE_CONFIG.dockerImage} + {" "} + + # open dashboard — add real auth before exposing this + diff --git a/content/docs/current/api/container.mdx b/content/docs/current/api/container.mdx index 0c442b3f2..e7326c57d 100644 --- a/content/docs/current/api/container.mdx +++ b/content/docs/current/api/container.mdx @@ -73,7 +73,14 @@ The gate clock starts from the **earliest point** at which the update is known t The gate clock survives container recreation. A Portainer stack redeploy or `docker compose down && up` gives the container a new Docker ID, but Drydock carries the maturity clock forward via the container's stable identity as long as the same update is still pending. If a different pending tag or digest appears while the previous candidate is still soaking, the clock resets for the new candidate. -When the gate blocks an update the blocker reason is `maturity-not-reached`. The dialog shows days remaining and a `liftableAt` timestamp computed from whichever start timestamp applies. +When the gate blocks an update the blocker reason is `maturity-not-reached`. The dialog shows days remaining and a `liftableAt` timestamp computed from whichever start timestamp applies. The blocker's `details` object also names the clock that produced that countdown: + +| Field | Type | Description | +| --- | --- | --- | +| `details.clockSource` | string | `publishedAt` when the trusted registry publish date won, `detectedAt` when the gate fell back to `updateDetectedAt` | +| `details.clockStartAt` | string (ISO 8601) | The start timestamp the countdown is measured from | + +The UI's Update Status panel renders these two fields (alongside `details.minAgeDays` and `details.remainingMs`) as a single clock-sourced sentence — e.g. "Candidate published Jul 12 — 3 more days until the 7-day minimum" — instead of re-deriving its own countdown. #### `publishedAt` source by registry @@ -564,7 +571,7 @@ Each WebSocket message is a JSON object: ### Authentication -WebSocket upgrade requests require a valid session cookie. Unauthenticated requests receive HTTP 401 during the upgrade handshake. +WebSocket upgrade requests require a valid session cookie. Unauthenticated requests receive HTTP 401 during the upgrade handshake. WebSocket upgrade origin checks must also pass — the `Origin` header's host and scheme must match the server; behind a TLS-terminating reverse proxy this requires `DD_SERVER_TRUSTPROXY` (see [Server configuration](/docs/configuration/server#reverse-proxy-trust-proxy)). ### Rate limiting diff --git a/content/docs/current/api/log.mdx b/content/docs/current/api/log.mdx index 51c2af14a..2b5606111 100644 --- a/content/docs/current/api/log.mdx +++ b/content/docs/current/api/log.mdx @@ -105,7 +105,7 @@ Example frame payload: ### Authentication and limits - Requires an authenticated session (cookie-based auth). -- WebSocket upgrade origin checks must pass. +- WebSocket upgrade origin checks must pass — the `Origin` header's host and scheme must match the server; behind a TLS-terminating reverse proxy this requires `DD_SERVER_TRUSTPROXY` (see [Server configuration](/docs/configuration/server#reverse-proxy-trust-proxy)). - Connection attempts are rate-limited (1,000 upgrades per 15-minute window per identity/IP key). ## Get agent log entries diff --git a/content/docs/current/configuration/actions/update-eligibility.mdx b/content/docs/current/configuration/actions/update-eligibility.mdx index e2c4a4885..8e81e29dd 100644 --- a/content/docs/current/configuration/actions/update-eligibility.mdx +++ b/content/docs/current/configuration/actions/update-eligibility.mdx @@ -42,7 +42,7 @@ If a container is eligible but an automatic update does not run, check that the | `snoozed` | **Snoozed** | Soft | Container's update policy has `snoozeUntil` set to a future timestamp. | Clear the snooze from the container row's **Update policy** menu, or wait for the lift date shown in the panel. | | `skip-tag` | **Tag skipped** | Soft | The remote tag is in the container's `skipTags` list. | Remove the tag from the skip list in the **Update policy** menu. | | `skip-digest` | **Digest skipped** | Soft | The remote digest is in the container's `skipDigests` list. | Remove the digest from the skip list in the **Update policy** menu. | -| `maturity-not-reached` | **Maturing** | Soft | Container's effective `maturityMode` is `mature` and the update has not yet aged past `maturityMinAgeDays` days. The blocker message identifies the active policy tier (`env`, `label`, or `override`). The gate measures elapsed time from the earliest known point the update existed: for Docker Hub and GHCR (including lscr.io), Drydock uses the registry's real publish timestamp (`last_updated` / `updated_at`), so an image already soaked in the registry longer than the threshold clears immediately on first detection. For all other registries (ACR, ECR, GCR, Quay, Harbor, etc.) the OCI image-config `created` field is a build date, not a reliable push date, so the gate falls back to `updateDetectedAt` — Drydock's own wall-clock time when it first recorded the pending update. The gate clock survives container recreation as long as the same candidate remains pending; a changed tag, digest, or mutable-tag image restarts the soak. The candidate remains visible while held, and the detail view shows a live countdown plus the exact local `liftableAt` date and time. | Wait for the gate to lift, use **Update Now**, change the active label/watcher setting, add a UI override, or **Revert** an override to the declarative value. | +| `maturity-not-reached` | **Maturing** | Soft | Container's effective `maturityMode` is `mature` and the update has not yet aged past `maturityMinAgeDays` days. The blocker message identifies the active policy tier (`env`, `label`, or `override`). The gate measures elapsed time from the earliest known point the update existed: for Docker Hub and GHCR (including lscr.io), Drydock uses the registry's real publish timestamp (`last_updated` / `updated_at`), so an image already soaked in the registry longer than the threshold clears immediately on first detection. For all other registries (ACR, ECR, GCR, Quay, Harbor, etc.) the OCI image-config `created` field is a build date, not a reliable push date, so the gate falls back to `updateDetectedAt` — Drydock's own wall-clock time when it first recorded the pending update. The gate clock survives container recreation as long as the same candidate remains pending; a changed tag, digest, or mutable-tag image restarts the soak. The candidate remains visible while held, and the detail view shows a live countdown plus the exact local `liftableAt` date and time. The blocker's `details.clockSource` (`publishedAt` or `detectedAt`) and `details.clockStartAt` (ISO timestamp) identify which clock won; the Update Status panel renders them as the single clock-sourced explanation line instead of a generic countdown. | Wait for the gate to lift, use **Update Now**, change the active label/watcher setting, add a UI override, or **Revert** an override to the declarative value. | | `threshold-not-reached` | **Below threshold** | Soft | The matching trigger's `threshold` (e.g. `major`) is more restrictive than the detected update kind (e.g. `minor`). | Lower the trigger's `THRESHOLD` env var, or wait for an update that meets it. | | `trigger-excluded` | **Trigger excluded** | Soft | Container's `dd.action.exclude` (or legacy `dd.trigger.exclude`) label matches the candidate trigger. | Remove the exclusion from the container label, or rename the trigger so the rule no longer matches. | | `trigger-not-included` | **Trigger filtered** | Soft | Action triggers default to `AUTO=oninclude`, so the container needs an explicit `dd.action.include` label naming the trigger. The label is missing or doesn't list this trigger. | Add `dd.action.include=` to the container's labels, or set `DD_ACTION_DOCKER[COMPOSE]__AUTO=true` on the trigger to fire on every matching container. | diff --git a/content/docs/current/configuration/authentications/index.mdx b/content/docs/current/configuration/authentications/index.mdx index 2de943c58..099363d2a 100644 --- a/content/docs/current/configuration/authentications/index.mdx +++ b/content/docs/current/configuration/authentications/index.mdx @@ -9,7 +9,7 @@ Since v1.4.0, drydock **requires authentication on fresh installs**. If no auth You can enable 1 or multiple authentication strategies using `DD_AUTH_*` env vars. -Authentication protects all API routes and UI views unless anonymous access is enabled. Fresh installs must opt in with `DD_ANONYMOUS_AUTH_CONFIRM=true`; legacy upgrades without configured authentication retain anonymous access with a startup warning. +Authentication protects all API routes and UI views unless anonymous access is enabled. Fresh installs and upgrades alike must opt in with `DD_ANONYMOUS_AUTH_CONFIRM=true` — without it, an instance with no auth configured fails closed: the container starts, but every API call returns `401`, so the dashboard cannot get past login. For OIDC setups, session-cookie policy is controlled by `DD_SERVER_COOKIE_SAMESITE` in [server configuration](/docs/configuration/server). Default `lax` works for most IdP callback flows. ## Login lockout controls @@ -53,20 +53,22 @@ environment: | --- | --- | | Fresh install, no auth configured, no `DD_ANONYMOUS_AUTH_CONFIRM` | Fails closed — all API calls return `401` | | Fresh install, `DD_ANONYMOUS_AUTH_CONFIRM=true` | Anonymous access allowed | -| Upgrade from v1.3.x, no auth configured | Anonymous access allowed with a warning logged at startup | -| Upgrade from v1.3.x, `DD_ANONYMOUS_AUTH_CONFIRM=true` | Anonymous access allowed, no warning | +| Upgrade, no auth configured, no `DD_ANONYMOUS_AUTH_CONFIRM` | Fails closed — all API calls return `401` | +| Upgrade, `DD_ANONYMOUS_AUTH_CONFIRM=true` | Anonymous access allowed | Drydock detects an upgrade by the presence of an existing store file (`/store/dd.json`). If the file exists, the install is treated as an upgrade; if it does not, it is treated as a fresh install. -### Upgrading from v1.3.x +### Upgrading -If you are upgrading from v1.3.x and have no authentication configured, drydock will **not** lock you out. Anonymous access continues to work, but a warning is logged at startup urging you to configure authentication or set `DD_ANONYMOUS_AUTH_CONFIRM=true` to acknowledge the risk and silence the warning. +Upgrades fail closed exactly like fresh installs — there is no more warn-and-continue grace period. If an upgrading instance has no authentication configured (or anonymous auth is enabled but not explicitly confirmed), drydock fails closed — the container runs, but every API request is rejected with `401` and `/health` reports `503` — instead of logging a warning and serving an open dashboard. -To resolve the warning, either: +To restore access, either: 1. **Configure authentication** — set `DD_AUTH_BASIC__USER` and `DD_AUTH_BASIC__HASH` (or use [OIDC](/docs/configuration/authentications/oidc)). 2. **Explicitly opt in to anonymous access** — set `DD_ANONYMOUS_AUTH_CONFIRM=true`. +This release also renames the session cookie (`connect.sid` → `drydock.sid`), which signs out every existing session once on upgrade — Basic and OIDC alike. There's nothing to migrate; users just log back in. + ## Fail-closed auth enforcement Since v1.4.0, drydock enforces **fail-closed** authentication across registries, triggers, and the Docker entrypoint. If auth is configured and fails, drydock raises an error instead of silently falling back to anonymous or unauthenticated access. diff --git a/content/docs/current/configuration/server/index.mdx b/content/docs/current/configuration/server/index.mdx index d5e115f92..43912aaa2 100644 --- a/content/docs/current/configuration/server/index.mdx +++ b/content/docs/current/configuration/server/index.mdx @@ -175,6 +175,7 @@ docker run \ - **CSRF validation failures** (403 errors on POST/PUT/DELETE requests) — drydock sees `http://` internally while the browser sends `Origin: https://...` - **Incorrect security headers** — HSTS and secure cookie flags may not be applied - **Wrong client IPs in logs and rate limiting** — drydock sees the proxy's IP instead of the real client +- **WebSocket connection failures** — log streaming and container log streaming upgrades are rejected because drydock can't confirm the browser's Origin scheme matches Set `DD_SERVER_TRUSTPROXY=1` to trust one proxy hop. Use a higher number if you have chained proxies (e.g. CDN → load balancer → app proxy = `3`). diff --git a/content/docs/current/configuration/triggers/http/index.mdx b/content/docs/current/configuration/triggers/http/index.mdx index 71edc2295..b0d0bfb01 100644 --- a/content/docs/current/configuration/triggers/http/index.mdx +++ b/content/docs/current/configuration/triggers/http/index.mdx @@ -29,6 +29,8 @@ The `http` trigger lets you send container update notifications via HTTP. **Metadata endpoint guard:** Before sending each request, the trigger resolves the target hostname and rejects any address in the cloud metadata / link-local ranges: `169.254.0.0/16` (IPv4, including the AWS IMDSv1 endpoint `169.254.169.254`), `fe80::/10` (IPv6 link-local), and `fd00:ec2::254` (AWS IMDSv2 IPv6). RFC-1918 private ranges (`10.x`, `172.16–31.x`, `192.168.x`) are **not** blocked — self-hosted webhook targets on your local network work normally. Set `ALLOWMETADATA=true` on a specific trigger if you have a legitimate need to reach a link-local address. +**Redirects are never followed.** Independently of `ALLOWMETADATA`, every request is sent with redirect-following disabled — a webhook target that responds with a 3xx (a canonical-URL redirect, an `http://`→`https://` bounce, etc.) fails outright instead of being followed. This closes a redirect-based path around the metadata guard, but it means the endpoint you configure must answer with a final `2xx` directly. + This trigger also supports the [common configuration variables](/docs/configuration/triggers/#common-trigger-configuration). ### Examples diff --git a/content/docs/current/configuration/ui/index.mdx b/content/docs/current/configuration/ui/index.mdx index 8b7de6cb4..ee9e0fc2e 100644 --- a/content/docs/current/configuration/ui/index.mdx +++ b/content/docs/current/configuration/ui/index.mdx @@ -120,6 +120,8 @@ Toggle between modes using the view mode buttons in the toolbar where they are a Containers, Agents, Audit, Security, Servers, and Watchers each expose a column show/hide picker in the toolbar, letting you hide low-priority columns from the table. Hidden columns are persisted per view and have no effect in card mode. +On Containers, a column you still have checked "on" but that responsive sizing folded away to fit the viewport stays checked in the picker with a muted "{column} — hidden to fit" annotation, and the toolbar's "+N" hidden-columns badge tooltip names the auto-hidden columns — the picker never silently claims a column is showing when it isn't. + ### Card Sorting Containers, Agents, and Security cards include a sort-field and ascending/descending control in the toolbar when in card mode, so cards can be ordered the same way table rows can. diff --git a/content/docs/current/configuration/watchers/index.mdx b/content/docs/current/configuration/watchers/index.mdx index 69945b56d..b7ae44c14 100644 --- a/content/docs/current/configuration/watchers/index.mdx +++ b/content/docs/current/configuration/watchers/index.mdx @@ -59,7 +59,7 @@ The default cron is `0 */6 * * *` (every 6 hours). With many watched containers - Hybrid `image:tag@sha256:digest` references (e.g. `docker.io/valkey/valkey:9@sha256:bcf6…`) are handled correctly: drydock preserves both the tag and the digest rather than falling back to digest-only mode, so the human-readable tag is displayed in the UI and the registry router resolves the provider cleanly. - Label-derived watcher fields (`tagFamily`, `tagPinInfo`, `includeTags`, `excludeTags`, `transformTags`, `triggerInclude`, `triggerExclude`) are **re-derived on Docker update events** (e.g. after `docker compose up -d`). Labels set on the recreated container take effect immediately without waiting for the next cron cycle; removing a tag-policy label restores its imgset/watcher default. - Floating semver aliases (e.g. `3.3` and `3.3.0`, which both coerce to the same semver) are excluded from the greater-than check in candidate ranking, so aliases that resolve identically are never reported as upgrades. -- Fully-pinned specific tags (3+ numeric segments, e.g. `1.4.5`, `v1.13.3`) also compare by digest by default, the same as floating tags, so a rebuilt image republished under the same tag is detected. Version climbing across tags for a pinned image is opt-in via `dd.tag.include`, `dd.tag.family=loose`, or the actionable watcher default `DD_WATCHER_{watcher_name}_TAG_FAMILY=loose`. The watcher-wide `loose` setting can make cross-version candidates actionable and therefore eligible for configured action triggers; use it deliberately. Drydock separately computes an informational `updateInsight` — the best newer same-family tag available in the registry — which never triggers an update or notification and never changes `updateAvailable`/`updateKind`. The UI renders that insight as a neutral **Pinned** state with a grey current tag → blue newer tag and a Major/Minor/Patch chip; hover text supplies additional detail. Control the informational channel with `dd.tag.pin.info`, matching-imgset `TAG_PIN_INFO`, or the watcher default `TAG_PIN_INFO`. +- Fully-pinned specific tags (3+ numeric segments, e.g. `1.4.5`, `v1.13.3`) also compare by digest by default, the same as floating tags, so a rebuilt image republished under the same tag is detected. Version climbing across tags for a pinned image is opt-in via `dd.tag.include`, `dd.tag.family=loose`, or the actionable watcher default `DD_WATCHER_{watcher_name}_TAG_FAMILY=loose`. The watcher-wide `loose` setting can make cross-version candidates actionable and therefore eligible for configured action triggers; use it deliberately. Drydock separately computes an informational `updateInsight` — the best newer same-family tag available in the registry — which never triggers an update or notification and never changes `updateAvailable`/`updateKind`. The UI renders that insight as the same Major/Minor/Patch chip used for any other update; the pinned tag itself is indicated separately by a persistent pin glyph on the Tag cell, whose tooltip names `dd.tag.family=loose` / `dd.tag.include` as the remedy. Control the informational channel with `dd.tag.pin.info`, matching-imgset `TAG_PIN_INFO`, or the watcher default `TAG_PIN_INFO`. - A Major insight is not automatically a mismatch: strict family matching preserves prefix, suffix/variant shape, numeric precision, and CalVer rules, but it does not impose a same-major ceiling. By contrast, a Major result shown as an actionable update means an actionable policy such as `dd.tag.family=loose` or `dd.tag.include` is in effect. The Immich `v3.0.2 Major` report in #498 was caused by `dd.tag.family=loose` on only the machine-learning container. Loose mode still preserves suffix variants — for example, `v2.7.5-openvino` can climb to `v3.0.2-openvino`, never bare `v3.0.2` — and that historical cross-suffix bug is regression-covered. diff --git a/content/docs/current/faq/index.mdx b/content/docs/current/faq/index.mdx index 39a6a5bd1..17d992454 100644 --- a/content/docs/current/faq/index.mdx +++ b/content/docs/current/faq/index.mdx @@ -198,6 +198,8 @@ See [Socket proxy permissions](/docs/configuration/watchers#proxy-permissions-by If you see `403 {"error":"CSRF validation failed"}` when clicking buttons like **Recheck for updates**, **Scan**, or any action that sends a POST/PUT/DELETE request, your reverse proxy setup is missing the `DD_SERVER_TRUSTPROXY` configuration. +The same missing configuration also breaks the dashboard's log viewer (`WS /api/v1/log/stream`) and container log streaming (`WS /api/v1/containers/{id}/logs/stream`) — those WebSocket upgrades fail silently (connection refused or closed) rather than surfacing a CSRF error, so a dead log viewer behind a reverse proxy is worth checking here too. + **Why this happens:** Drydock validates that the `Origin` header sent by your browser matches the server's own origin. Behind a TLS-terminating reverse proxy (Traefik, Nginx, Caddy, HAProxy, etc.), the internal connection to drydock uses plain HTTP. Without trust-proxy enabled, drydock thinks it is running on `http://` while your browser sends `Origin: https://...` — the mismatch triggers CSRF protection. **Fix:** Add `DD_SERVER_TRUSTPROXY` to your drydock environment: @@ -327,7 +329,7 @@ This usually happens when the registry has tags from different naming families ( Since v1.4.0, **tag family detection** (`dd.tag.family`) defaults to `strict`, which infers the current tag's prefix, suffix, and segment structure and only considers candidates within the same family. -This applies to floating tags (`latest`, `v3`, `16-alpine`) as well as fully-pinned specific tags (e.g. `1.4.5`, `v1.13.3`) — a pinned container compares by digest by default so a rebuilt image republished under the same tag is still detected, but it will not climb to a different tag on its own. Drydock also computes informational `updateInsight` for pinned containers. The UI shows it as a neutral **Pinned** state with the grey current tag → blue newer tag and a Major/Minor/Patch chip. It never triggers an update or notification and never changes actionable update state. Configure it with precedence `dd.tag.pin.info` → matching imgset `TAG_PIN_INFO` → watcher `TAG_PIN_INFO` → `true`. +This applies to floating tags (`latest`, `v3`, `16-alpine`) as well as fully-pinned specific tags (e.g. `1.4.5`, `v1.13.3`) — a pinned container compares by digest by default so a rebuilt image republished under the same tag is still detected, but it will not climb to a different tag on its own. Drydock also computes informational `updateInsight` for pinned containers. The UI shows it as the same Major/Minor/Patch chip used for any other update; a pinned container that is otherwise up to date reads **Current**. Pin status itself is shown separately — a persistent pin glyph on the Tag cell, whose tooltip names `dd.tag.family=loose` / `dd.tag.include` as the remedy. It never triggers an update or notification and never changes actionable update state. Configure it with precedence `dd.tag.pin.info` → matching imgset `TAG_PIN_INFO` → watcher `TAG_PIN_INFO` → `true`. A Major chip by itself can be a valid informational insight: strict matching preserves the tag's prefix, suffix/variant shape, numeric precision, and CalVer rules, but does not impose a same-major ceiling. If the container instead shows an actionable Major update, inspect its resolved `dd.tag.family`, `dd.tag.include`, matching imgset, and watcher defaults. In #498, the reported Immich `v3.0.2 Major` action was traced to `dd.tag.family=loose` on only the machine-learning container. Loose mode still preserves suffix variants (`-openvino` stays `-openvino`); the older behavior that could select a bare tag from a suffixed reference is fixed and regression-covered. diff --git a/content/docs/current/guides/security/index.mdx b/content/docs/current/guides/security/index.mdx index 4e93a8491..c33fec6a1 100644 --- a/content/docs/current/guides/security/index.mdx +++ b/content/docs/current/guides/security/index.mdx @@ -105,7 +105,7 @@ For remote Docker APIs that require basic, bearer, or OIDC auth, use HTTPS or mT ## Require authentication -Since v1.4.0, authentication is required by default. Fresh installs require authentication configuration before access unless anonymous access is explicitly confirmed with `DD_ANONYMOUS_AUTH_CONFIRM=true`. Existing upgrades that were already anonymous remain accessible with a warning until authentication is configured. +Since v1.4.0, authentication is required by default. Fresh installs require authentication configuration before access unless anonymous access is explicitly confirmed with `DD_ANONYMOUS_AUTH_CONFIRM=true`. Upgrades behave identically to fresh installs — an existing anonymous instance also fails closed — every API request returns `401` — unless `DD_ANONYMOUS_AUTH_CONFIRM=true` is set or authentication is configured. diff --git a/content/docs/current/quickstart/index.mdx b/content/docs/current/quickstart/index.mdx index 72ff99fa8..9d31f803a 100644 --- a/content/docs/current/quickstart/index.mdx +++ b/content/docs/current/quickstart/index.mdx @@ -112,7 +112,7 @@ Use the exact RC tag when reporting a regression. Use `1.6-rc` only when you int Authentication is **required by default** on fresh installs. The examples above use Basic auth with username `admin` and password `password`. See the [auth docs](/docs/configuration/authentications) for OIDC, anonymous access, and other options. -**Behind a reverse proxy?** If a TLS-terminating reverse proxy (Nginx Proxy Manager, Traefik, Caddy, Synology DSM, etc.) sits in front of Drydock, set `DD_SERVER_TRUSTPROXY=1` (the number of proxy hops) — otherwise every manual action in the UI fails with `403 CSRF validation failed`. See [the FAQ](/docs/faq#csrf-validation-failed-403-behind-a-reverse-proxy) for details. +**Behind a reverse proxy?** If a TLS-terminating reverse proxy (Nginx Proxy Manager, Traefik, Caddy, Synology DSM, etc.) sits in front of Drydock, set `DD_SERVER_TRUSTPROXY=1` (the number of proxy hops) — otherwise every manual action in the UI fails with `403 CSRF validation failed`, and WebSocket log streaming (the dashboard log viewer, container logs) fails to connect too. See [the FAQ](/docs/faq#csrf-validation-failed-403-behind-a-reverse-proxy) for details. ## Migrating from WUD diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 3967394fd..df1efd937 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2843,13 +2843,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -6905,9 +6898,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", - "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -6918,7 +6911,6 @@ "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", diff --git a/e2e/package.json b/e2e/package.json index 894a5f88b..e663ead75 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -46,7 +46,7 @@ "ws": "8.21.0", "yaml": "2.8.3", "form-data": "4.0.6", - "protobufjs": "7.6.3", + "protobufjs": "7.6.5", "@opentelemetry/core": "2.8.0", "undici": "7.28.0" } diff --git a/scripts/release-docs-identity.test.mjs b/scripts/release-docs-identity.test.mjs index a3993a04b..026640873 100644 --- a/scripts/release-docs-identity.test.mjs +++ b/scripts/release-docs-identity.test.mjs @@ -332,14 +332,32 @@ test('current and archived docs describe destructive and recovery behavior accur /\*\*Delete\*\*[\s\S]{0,180}?Remove the container from Drydock tracking/u, ); assert.match(actions, /does not delete the runtime container/u); - assert.match( - authentications, - /Authentication protects all API routes and UI views unless anonymous access is enabled\. Fresh installs must opt in with `DD_ANONYMOUS_AUTH_CONFIRM=true`/u, - ); - assert.match( - authentications, - /legacy upgrades without configured authentication retain anonymous access with a startup warning/u, - ); + if (root === 'content/docs/current') { + // v1.6 removed the warn-and-serve grandfather path: upgrades fail closed like fresh installs. + assert.match( + authentications, + /Authentication protects all API routes and UI views unless anonymous access is enabled\. Fresh installs and upgrades alike must opt in with `DD_ANONYMOUS_AUTH_CONFIRM=true`/u, + ); + // Fail-closed means 401-everything from a running container, not a refused start: + // Anonymous's registration throw is caught by the registry fallback and the API + // serves with zero passport strategies (app/registry/index.ts, app/api/auth.ts). + assert.match( + authentications, + /Upgrade, no auth configured, no `DD_ANONYMOUS_AUTH_CONFIRM` \| Fails closed — all API calls return `401`/u, + ); + assert.doesNotMatch(authentications, /refuses to start/u); + assert.doesNotMatch(authentications, /retain anonymous access with a startup warning/u); + } else { + // The 1.5.x archive documents the grandfather path that line actually shipped with. + assert.match( + authentications, + /Authentication protects all API routes and UI views unless anonymous access is enabled\. Fresh installs must opt in with `DD_ANONYMOUS_AUTH_CONFIRM=true`/u, + ); + assert.match( + authentications, + /legacy upgrades without configured authentication retain anonymous access with a startup warning/u, + ); + } assert.match(security, /sbom\?format=\{format\}/u); assert.doesNotMatch(security, /sbom\?format=\\\{format\\\}/u); assert.doesNotMatch(selfUpdate, /zero downtime/iu);