Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions DEPRECATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

| | |
Expand All @@ -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_<name>_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.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- separate alerts: a blank-line-only gap between blockquotes trips markdownlint MD028 -->

> [!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.

<h2 align="center">đź“‘ Contents</h2>

- [đź“– Documentation](https://getdrydock.com/docs)
Expand Down Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/data/faq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
];
5 changes: 5 additions & 0 deletions apps/web/src/components/docker-run-snippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
{" "}
<span className="text-neutral-600">
# open dashboard — add real auth before exposing this
</span>
</code>
</pre>
</CardContent>
Expand Down
11 changes: 9 additions & 2 deletions content/docs/current/api/container.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion content/docs/current/api/log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=<trigger-name>` to the container's labels, or set `DD_ACTION_DOCKER[COMPOSE]_<NAME>_AUTO=true` on the trigger to fire on every matching container. |
Expand Down
Loading