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
999 changes: 224 additions & 775 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/assets/codeswhat-logo-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/assets/codeswhat-logo-original.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions docs/content/docs/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ non-loopback address requires the separate
`ALLOW_UNAUTHENTICATED_REMOTE=true` acknowledgement.
</Callout>

The examples below publish port 3000 on host loopback only. For remote access, configure Portwing TLS before widening that bind, or keep the plaintext listener private behind a TLS-terminating reverse proxy.

---

## 1. Plaintext token
Expand All @@ -29,7 +31,7 @@ docker run -d --name portwing \
--group-add $(stat -c '%g' /var/run/docker.sock) \
-v /var/run/docker.sock:/var/run/docker.sock \
-e TOKEN="$TOKEN" \
-p 3000:3000 \
-p 127.0.0.1:3000:3000 \
ghcr.io/codeswhat/portwing:latest
```

Expand All @@ -53,7 +55,7 @@ docker run -d --name portwing \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /run/secrets/portwing-token:/run/secrets/portwing-token:ro \
-e TOKEN_FILE=/run/secrets/portwing-token \
-p 3000:3000 \
-p 127.0.0.1:3000:3000 \
ghcr.io/codeswhat/portwing:latest
```

Expand All @@ -78,7 +80,7 @@ docker run -d --name portwing \
--group-add $(stat -c '%g' /var/run/docker.sock) \
-v /var/run/docker.sock:/var/run/docker.sock \
-e TOKEN_HASH="$HASH" \
-p 3000:3000 \
-p 127.0.0.1:3000:3000 \
ghcr.io/codeswhat/portwing:latest
```

Expand Down Expand Up @@ -148,7 +150,7 @@ docker run -d --name portwing \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/portwing/authorized_keys:/etc/portwing/authorized_keys:ro \
-e AUTHORIZED_KEYS=/etc/portwing/authorized_keys \
-p 3000:3000 \
-p 127.0.0.1:3000:3000 \
ghcr.io/codeswhat/portwing:latest
```

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ For auth-method selection and key-enrollment flows, see [Authentication](/authen
| Variable | Default | Description |
|---|---|---|
| `PORT` | `3000` | TCP port the HTTP server listens on. |
| `BIND_ADDRESS` | Standard: `0.0.0.0`; Edge: `127.0.0.1` | IP address the HTTP/operations listener binds to. Edge mode defaults to loopback because its operations routes have no inbound authentication; explicitly use a broader address only on an isolated monitoring network. |
| `BIND_ADDRESS` | Standard: `0.0.0.0`; Edge: `127.0.0.1` | IP address the HTTP/operations listener binds to. Edge mode defaults to loopback because its operations routes have no inbound authentication, and it refuses a non-loopback address unless `ALLOW_UNAUTHENTICATED_REMOTE=true` is set; use a broader address only on an isolated monitoring network. |
| `DRYDOCK_URL` | — | WebSocket URL for Edge mode (e.g. `wss://drydock.example.com`). When set, Portwing operates in Edge mode and connects outbound to Drydock. A plaintext `http://`/`ws://` scheme is rejected at startup unless `ALLOW_INSECURE_EDGE_URL=true`. |
| `ALLOW_INSECURE_EDGE_URL` | `false` | Dangerous opt-in to connect Edge mode to a plaintext `http://`/`ws://` `DRYDOCK_URL`. The controller's identity is otherwise verified by TLS alone, so this is for trusted local testing only. |
| `TLS_CERT` | — | Path to server TLS certificate (Standard mode). Must be set together with `TLS_KEY`; partial configuration is a startup error. |
Expand Down
7 changes: 6 additions & 1 deletion docs/content/docs/connection-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Otherwise

Portwing runs a plain HTTP server on `PORT` (default `3000`). The Drydock controller connects inbound and drives the session: it calls `GET /api/containers`, `GET /api/watchers`, and `GET /api/triggers` during the handshake, then holds a long-lived SSE stream on `GET /api/events` for real-time container events. All Docker Engine API paths are transparently proxied through the agent.

Standard mode is active whenever `DRYDOCK_URL` is **not** set.
Standard mode is active whenever `DRYDOCK_URL` is **not** set. The controller authenticates with the `X-Dd-Agent-Secret` shared secret. Built-in TLS (`TLS_CERT` / `TLS_KEY`) accepts TLS 1.2 and later with modern cipher suites.

### Required networking

Expand Down Expand Up @@ -95,6 +95,8 @@ Edge mode is active when **both** of the following are true:

Setting `DRYDOCK_URL` without `PRIVATE_KEY_FILE` is rejected at startup with a fatal error — the Drydock controller endpoint is Ed25519-only, so the agent enforces `PRIVATE_KEY_FILE` at startup rather than looping forever on a rejected hello. `TOKEN` and `AUTHORIZED_KEYS` are not accepted for edge mode.

Edge mode's trust in the controller is TLS-only and one-directional. The signed hello lets the controller verify Portwing, but nothing verifies the controller's identity in return, so a plaintext `http://` or `ws://` `DRYDOCK_URL` would let an on-path attacker complete the handshake and drive dockerd. Portwing fails closed on a plaintext `DRYDOCK_URL` unless `ALLOW_INSECURE_EDGE_URL=true` is set, which is for trusted local testing only. Always use `https://` or `wss://` against a real controller.

### Key environment variables

| Variable | Default | Purpose |
Expand Down Expand Up @@ -123,6 +125,9 @@ On each connection attempt the agent:
3. Waits up to `WELCOME_TIMEOUT` seconds for the controller's `welcome` response.
4. On success, sends component descriptors (`dd:component_sync`) before the
initial container inventory (`dd:container_sync`), then sends host metrics.
The component descriptors declare watcher ownership as controller-owned, so
controller-owned Docker calls travel as correlated WebSocket `request` /
`response` messages.

### Reconnection and keepalive

Expand Down
2 changes: 2 additions & 0 deletions docs/content/docs/drydock-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ edge agent adapter does not yet surface that reason to the user; today it
receives the frame but discards `reason` before it reaches Drydock's own
exec error display.

Sockguard only includes the detailed reason in its `reason` field when the preset sets `deny_verbosity: verbose`. Under the default `minimal` verbosity the surfaced message is the generic `request denied by sockguard policy`.

**Drydock-specific (`dd:` namespace):**

| Type | Direction | Purpose |
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/standalone-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e ADAPTER=generic \
-e TOKEN=my-secret \
-p 3000:3000 \
-p 127.0.0.1:3000:3000 \
ghcr.io/codeswhat/portwing:latest
```

Expand Down
2 changes: 1 addition & 1 deletion scripts/package-release-config-test-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ prefix_version="${release_version}0"
readme_prefix_backup="${fixture}/README.md.prefix-version-backup"
cp "${fixture}/README.md" "${readme_prefix_backup}"

awk -v from="VERSION=${release_version}" -v to="VERSION=${prefix_version}" '
awk -v from="portwing_${release_version}_" -v to="portwing_${prefix_version}_" '
!changed && index($0, from) {
sub(from, to)
changed = 1
Expand Down
6 changes: 3 additions & 3 deletions scripts/package-release-config-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,11 @@ require_file "docs/assets/star-history-dark.svg" \
# grep for the img tag would be satisfied by any other occurrence in the README,
# and naming hosts one at a time only ever catches the ones already known.
star_section="$(awk '
/<a id="star-history"><\/a>/ { collecting = 1 }
/id="star-history"/ { collecting = 1 }
collecting { print; if ($0 == "---") exit }
' README.md)"
if [ -z "${star_section}" ]; then
echo 'FAIL: the README must keep the star-history section (no <a id="star-history"></a> anchor found in README.md)' >&2
echo 'FAIL: the README must keep the star-history section (no id="star-history" heading found in README.md)' >&2
failures=$((failures + 1))
fi
# The whitespace before src is load-bearing: "src=" as a bare substring is also
Expand Down Expand Up @@ -825,7 +825,7 @@ fi
require_text "README.md" "brew install --cask codeswhat/tap/portwing" "the repository landing page must advertise Homebrew installation"
require_text "README.md" "/docs/installation" "the repository landing page must link the full package guide"
# shellcheck disable=SC2016 # The documented shell command expands VERSION.
require_text "README.md" 'release.yml@refs/tags/v${VERSION}' "public verification instructions must bind signatures to the selected tag"
require_text "docs/content/docs/verification.mdx" 'release.yml@refs/tags/v${VERSION}' "public verification instructions must bind signatures to the selected tag"
require_text "RELEASING.md" "HOMEBREW_TAP_TOKEN" "maintainer release docs must name the tap publishing credential"
require_text "RELEASING.md" "verify-native-packages" "maintainer release docs must describe the native package gate"
require_text "website/src/components/get-started.tsx" "codeswhat/tap/portwing" "the website must advertise the Homebrew cask"
Expand Down
2 changes: 1 addition & 1 deletion scripts/standard-mode-bind-config-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ require_publications "examples/docker-compose.with-sockguard.yml" "yaml" 1 \
"the canonical standard-mode sockguard Compose example"
require_publications "README.md" "yaml" 1 \
"the README copy of the standard-mode sockguard example"
require_publications "README.md" "docker run" 4 \
require_publications "README.md" "docker run" 1 \
"README standard-mode docker run instructions"
require_publications "docs/content/docs/getting-started.mdx" "yaml" 2 \
"getting-started copies of the standard-mode Compose examples"
Expand Down
Loading