v1.1.0 · Open Source · MIT
Remote Docker Agent
We built Portwing to give Drydock a secure foothold on every host. Pair it with sockguard and it never touches the raw Docker socket. Ed25519 auth proves every request, and a structured audit log records everything it touches for export to immutable storage, so you control your fleet without exposing it.

14 reasons your Docker hosts are safer
Security isn't a checkbox — it's every layer. Here's what we built.
- Security
- Default-Deny SocketsecurityPair us with sockguard and we never touch the raw Docker socket. The agent talks to a filtered unix socket instead, so even a fully compromised agent is constrained to an explicit Docker API allowlist enforced at the socket level.
- Ed25519 Per-Client AuthsecuritySigned requests with nonce-based replay protection and no shared secrets on the wire. Enroll client public keys into an authorized_keys file, rotate with SIGHUP, and every enrollment is audited.
- Argon2id Token HashingsecurityPrefer a shared secret? Store only its Argon2id hash via TOKEN_HASH. Generate one with `portwing hash-token`; comparison is constant-time. Plaintext TOKEN is supported for evaluation but the hash is the recommended posture.
- Signed & VerifiablesecurityEvery release ships cosign signatures, per-archive SBOMs, an image SBOM attestation, and SLSA build provenance. Verify the image before you ever trust it with your host — the supply chain is auditable end to end.
- Hardened RuntimesecurityWe ship hardened by default: `read_only`, `cap_drop: ALL`, `no-new-privileges`, and tokens mounted as secrets instead of env vars. The image runs as non-root (UID 65532) out of the box; running as root (`user: "0:0"`) is an explicit opt-in for socket-group compatibility. No security theater.
- Control
- Drydock-NativecontrolWe speak Drydock's wire protocol (portwing/1.0 over WebSocket in edge mode; dd:* SSE events in standard mode) with full watcher-snapshot streaming, letting one Drydock controller manage containers across every host we're deployed on.
- Standard & Edge ModescontrolRun in standard mode behind a TLS reverse proxy (the controller dials in), or use production-supported outbound edge mode for hosts with no inbound port. The stable portwing/1.0 path is gated by real multi-agent reconnect, exec, log-stream, and backpressure soak tests.
- Generic REST AdaptercontrolDon't run Drydock? We work standalone too. Our REST + Bearer-auth surface lets any client list, inspect, start, stop, and stream logs from containers. No controller required.
- Read-Only MCP ServercontrolWe ship a built-in Model Context Protocol server that exposes host and container state to MCP-aware agents. Read-only by design, so your assistant can see everything and change nothing.
- Operations
- Prometheus MetricsoperationsOur zero-dependency `/metrics` endpoint exports host/container telemetry plus edge connection, reconnect, backpressure, and audit health. Explicit `/health` and `/ready` probes keep dependency outages from becoming restart loops.
- Structured Audit LogoperationsEvery action is recorded with a stable structured schema: who asked, what ran, and what the daemon answered. A cursor-based NDJSON path detects overwritten history and restarts before exporters ship records to immutable storage.
- Tiny & Multi-ArchoperationsWe ship as a single static Go binary, ~10 MB, with zero runtime dependencies. amd64, arm64, arm/v7 — runs on the Raspberry Pi at the edge as happily as the server in the rack.
- OpenAPI 3.1 SpecoperationsWe publish the full agent API as a machine-readable OpenAPI 3.1 spec. Generate clients, drive contract tests, and never guess at the wire format.
- Watchtower MigrationoperationsComing from Watchtower? We wrote a migration guide that maps the update-on-a-host workflow onto Portwing + Drydock, so you trade a cron-like updater for an auditable, controllable agent.
Get started in minutes
Add Portwing to your compose file and point Drydock at it — one agent per host.
$ docker run -d \
--name portwing \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e TOKEN=$(openssl rand -hex 24) \
ghcr.io/codeswhat/portwing:1.1.0Heads upTOKEN is visible in docker inspect — fine for a local try, not for production.
# Portwing — standard mode, hardened defaults.
# The image runs as the non-root portwing user (UID 65532). Generate a token
# it can read, and export the numeric group ID of your Docker socket:
# openssl rand -hex 32 > portwing_token.txt
# sudo chown 65532:65532 portwing_token.txt && sudo chmod 0400 portwing_token.txt
# export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
#
# This plaintext example publishes only on host loopback. For remote access,
# configure Portwing TLS before changing this bind, or keep the plaintext
# listener private behind a TLS-terminating reverse proxy.
services:
portwing:
image: ghcr.io/codeswhat/portwing:1.1.0
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
user: "65532:65532" # image default; explicit so it survives image overrides
group_add:
- "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock (see header)}"
ports:
- "127.0.0.1:3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portwing-stacks:/data/stacks
- portwing-state:/data/credentials
environment:
- TOKEN_FILE=/run/secrets/portwing_token
secrets:
- portwing_token
secrets:
portwing_token:
file: ./portwing_token.txt
volumes:
portwing-stacks:
portwing-state:SecretToken delivered as a Docker secret — never in env vars or inspect output. Full getting-started docs →
# macOS (latest stable release)
brew install --cask codeswhat/tap/portwing
# Debian / Ubuntu
sudo apt install ./portwing_1.1.0_linux_amd64.deb
# Fedora / RHEL
sudo rpm --install ./portwing_1.1.0_linux_amd64.rpmVerifiedStable packages are checksummed, keyless-signed, and install-tested. Install and verify →
See it in action!
Recorded from the real CLI: generate a key, boot the agent, sign a request, and watch it reject four kinds of tampering in the audit log.
Recorded from portwing v1.0.0 · local stub
Provenance and redactions
dswsign, a small Go program that calls portwing's own internal/auth (CanonicalMessage, BodyHashHex, NewNonce), the same calls the repo's signRequest test helper makes. Redacted: the private key body in the keygen output, and the machine hostname in the agent's info reply. The code paths shown are unchanged through v1.1.0; only the version strings were updated.Topology · three layers
Edge mode, as it plays out once: the agent dials out over the portwing/1.0 WebSocket, so there is no inbound control port.
- agent → controller: hello
- controller → agent: welcome
- dd:component_sync
- dd:container_sync
Roadmap
Where we've been and where we're headed. Direction, not commitments.
- Post-v1🧭 Demand-DrivenController-managed Portwing upgrade and rollback wavesOptional client-certificate authentication
Polling and intermittent edge transport
- Planned🔐 Enrollment Over TLSEnrollment requires TLS, either native or through a trusted proxy reporting HTTPS, unless ALLOW_INSECURE_ENROLLMENT explicitly opts in. Not in v1.1: the agent still only warns at startup
- v1.1HEAD -> v1.1.0🔏 Audience-Bound SignaturesEd25519 request signature version 3 binds each signature to one agent, so a captured request can't be replayed against another agent that trusts the same key (needs Drydock 1.9 or later)Stable agent ID, generated once and kept in the new STATE_DIR (default /data/credentials); mount /data/credentials and v3 works with no other change
SIGNATURE_MIN_VERSION and the portwing signature-lock command control the downgrade lock on version 2 and legacy signatures
- v1.0🚀 Stable ReleaseStable HTTP API and environment-variable surface under semantic-versioning guaranteesStable agent ↔ controller wire protocol — no breaking changes without a major bump
Threat model review; the HIGH+ CVE gate is already enforced on every release image, and v1.0 keeps itCompetitive review refreshed against primary sourcesFinal v0.9 docs archived from the release tag with byte-for-byte provenance checksThe release candidate passed the Standard/Edge and legacy-floor conformance matrix before GA; each later release needs fresh published-artifact verification
- v0.9.0🔄 Controller-Owned UpdatesDrydock watcher/update execution runs controller-side through Portwing's authenticated Docker proxy in Standard and Edge modesWatcher descriptor declares transport=docker-api, execution=controller, and events=portwing; no remote trigger is advertised
Edge sends component ownership before raw inventory; full feature compatibility requires Drydock v1.6.0-rc.11+ while DrydockCompat remains 1.4.0Edge mode shares the cursor-based NDJSON audit exporter on its explicitly private, unauthenticated operations listenerExec and Docker proxy failures preserve bounded daemon/sockguard error details through the controller pathTagged Portwing × Drydock × Sockguard acceptance matrix passed in standard and edge modes — completed 2026-08-29Controller-backed enrollment, overlapping-key rotation, revocation, and clock-skew recovery exercised against a real Drydock controller — completed 2026-08-29
- v0.8.0🧭 Toward v1.0Edge mode graduated to production support — enabled by default in Drydock 1.6 and gated by a real cross-repo multi-agent load/soakPublished stability policy — semver guarantees for the HTTP API, env-var surface, MCP tools, and DrydockCompat wire contract
Package-manager distribution — a Homebrew tap and apt/rpm packages via the existing GoReleaser pipelineContinuous edge live-tail under the dd: namespace, with cancellation, stdout/stderr framing, and bounded backpressureMode-aware liveness/readiness, edge/backpressure metrics, stable cursor-based NDJSON audit export, and runnable observability examples
- v0.7.0🛡️ Fail-Closed SecurityStandard mode fails closed without credentials — unauthenticated development requires explicit loopback and remote opt-insEd25519 HTTP signature v2 binds the escaped path and exact raw query; legacy signatures are query-free only
Cold Argon2id work is capped agent-wide and per IP; raw-token verifiers retain fixed-size digestsCredential files require regular-file descriptors and safe Unix permissions, checked after openEnrollment bodies are capped and abuse-limited; Compose uploads use root-confined, symlink-resistant writesEdge request correlation, bounded log-follow options, and terminal hello-rejection classificationGenerated CSP and browser security headers cover every exported website and docs route
- v0.6.0🔐 Non-Root & Cross-Repo ParityAgent info surfaced in the Drydock UI — memoryGb, logLevel, and pollInterval reported on every dd:ackEdge-mode container deletion — dd:container_delete_request/response over the WebSocket tunnel
Container environment variables in RuntimeDetails, parsed from Docker's Config.EnvCOMPATIBILITY.md — canonical cross-repo version matrix for Portwing × Drydock × sockguardBREAKING: container image now runs as non-root (UID 65532) by default — root is an explicit opt-inCI egress lockdown — harden-runner egress-policy: block across all jobs in every workflow
- v0.5.0🔐 HardeningRequest and application Prometheus metrics: request counts, duration, in-flight, auth failures, rate limitsAudit ring buffer (AUDIT_BUFFER_SIZE) with an authenticated GET /_portwing/audit endpoint
Pre-auth body cap: signed requests buffered through a 1 MB limit before signature verificationRegistry-auth and proxy query parameters validated before reaching the daemonPrivate-key file-permission check: PRIVATE_KEY_FILE is rejected when group or world accessibleOutbound TLS 1.2 floor, and edge mode now requires PRIVATE_KEY_FILEKubernetes DaemonSet examples for standard and edge mode
- v0.4.0🧪 Quality GatesMonthly one-hour deep fuzz pass, completing the smoke, nightly, and monthly tiersWeekly soak test: resident-set budget under sustained SSE and proxy load
Monthly benchmark tracking: hot-path ns/op and allocs/op kept as a 90-day artifactEdge tunnel test harness covering hello signing, dispatch, and the exec-session lifecycleOrdered edge exec I/O: input that arrives before the exec is up is buffered and replayedOutbound backpressure: bounded write queue, per-frame deadline, slow-consumer evictionDependency scanning standardized on Grype and govulncheck, with the Go toolchain pinned to go1.26.4
- v0.3.0🏷️ Rename & Edge FixesRenamed from Lookout to Portwing: module path, binary, image, headers, and metricsStartup banner with a version, mode, and adapter summary
GoReleaser dockers_v2 migration: one multi-arch OCI index per tag with an image SBOM attestationEdge reconnect backoff restarts from RECONNECT_DELAY after a long-lived session dropsEdge read deadline held at a steady state so a silent controller triggers a reconnectFuzz worker cap so the coordinator keeps a core and the fuzz smoke stops flaking
- v0.2.0🔒 Security & ObservabilityEd25519 per-request signatures with nonce LRU replay protection and SIGHUP key reloadSingle-use key enrollment (ENROLLMENT_TOKEN) with burn-on-first-use and audit logging
Argon2id token hashing (TOKEN_HASH), Bearer auth, and TRUSTED_PROXIES for rate-limit client IPsRead-only MCP server at /_portwing/mcp, plus a headless generic REST adapterPrometheus metrics, structured JSON audit logging, and an OpenAPI 3.1 specCosign keyless signing, CycloneDX SBOMs, and SLSA build provenance on release artifactsHardened Docker Compose examples for standard, edge, and sockguard-layered deployments
- v0.1.0🛡️ FoundationStandard mode: HTTP/SSE inbound server and transparent Docker Engine API proxyEdge mode: outbound WebSocket tunnel to the Drydock platform for hosts that cannot take inbound connections
Drydock adapter with container sync and SSE event stream at /api/eventsToken authentication with timing-safe comparison, plus rate limiting of failed attempts per IPHealth endpoint (/_portwing/health) for Docker HEALTHCHECK directivesMulti-arch image: linux/amd64 + linux/arm64 on a Wolfi base, static CGO-free binary
- └ (initial commit)
How we compare
A sourced snapshot of the four closest remote-agent peers. Partial marks usually mean the capability exists at the controller layer or is optional.
| Feature | Portwing | Portainer | Komodo | Arcane | Hawser |
|---|---|---|---|---|---|
| Inbound + outbound edge modes | Yes | Yes | Yes | Yes | Yes |
| Asymmetric agent identity | Yes | Partial | Yes | Partial | No |
| Documented socket policy layer | Yes | No | No | Partial | No |
| Structured agent-level audit | Yes | Partial | Partial | Partial | Partial |
| Transparent Docker API | Yes | Yes | No | No | Yes |
| Read-only MCP server | Yes | Not documented | Not documented | Not documented | Not documented |
Part of the CodesWhat stack
Portwing is one piece of a small, focused toolkit — each tool does one job, and they compose.

Drydock
Container update manager — orchestrates across your hosts

Portwing
Security-first remote Docker agent
📍 You're here
Sockguard
Default-deny Docker socket firewall
Frequently asked questions
Common questions about how Portwing works and how to deploy it securely.
What is Portwing and how does it relate to Drydock?
Portwing is a lightweight Go agent that gives Drydock a secure foothold on a Docker host. Drydock is the orchestrator — it manages container update policies, schedules checks, and sends update actions. Portwing is the agent it talks to on each host: it sits next to the Docker socket, proxies the Docker Engine API to Drydock, and handles container lifecycle actions on request. Without an agent on each host, Drydock can only reach hosts it has a direct network path to. Portwing adds two connection options: standard mode (Drydock connects inbound to the agent's HTTP port) and edge mode (the agent dials out to Drydock over a WebSocket, so NAT and firewalled hosts work too).
What are standard mode and edge mode, and which should I use?
Standard mode runs an HTTP/SSE server on port 3000. The Drydock controller connects inbound, calls the agent's inventory endpoints, and holds a long-lived SSE stream for real-time events. Use it when the host is reachable from your Drydock instance. Edge mode flips the control direction: the agent dials out to Drydock over the stable portwing/1.0 WebSocket tunnel, so no inbound control port is published. Its unauthenticated local operations listener must remain private. Use Drydock v1.6.0-rc.11+ with Portwing v0.9.0 or later for the complete controller-owned watcher/update feature path.
How does Ed25519 authentication work and why use it over a shared token?
Ed25519 gives each client a distinct cryptographic identity. Every version 2 request carries a detached signature over the HTTP method, escaped path plus exact raw query string, a SHA-256 body hash, a Unix timestamp, and a 128-bit random nonce. The agent verifies the signature, rejects requests with a timestamp skew greater than 60 seconds, and tracks nonces in an in-memory LRU to block replays within the window. No shared secret is transmitted. Compromising one client's private key leaves all other enrolled clients unaffected. By contrast, a shared token (TOKEN or TOKEN_HASH) is a single credential — rotate or revoke it and every caller is disrupted at once. Ed25519 is the recommended production option; token auth is still supported for evaluation and staged migration. Portwing ships a keygen subcommand: run portwing keygen to generate a keypair and get the authorized_keys line to drop on the agent host.
Is it safe to expose Docker control through Portwing? What's the security model?
Portwing is not a firewall — it is an authenticated transport. Out of the box it authenticates callers and proxies Docker API requests to the daemon. If you want a socket-level allowlist that blocks individual Docker API paths and request body properties (privileged mode, host mounts, capability additions), pair Portwing with sockguard: sockguard writes a filtered unix socket into a shared volume, and Portwing talks to that filtered socket instead of the raw /var/run/docker.sock. The recommended compose file ships with this two-layer configuration pre-wired. All official images run with read_only: true, cap_drop: ALL, no-new-privileges, and Docker secrets instead of inline tokens. Edge mode publishes no inbound control port, but its local operations listener exposes metrics and audit export without inbound auth and must stay on a trusted network.
What does the audit log record and how do I enable it?
Set AUDIT_LOG to a file path, stdout, or stderr. The agent writes one JSON record per line for every security-relevant event: authenticated API calls (api_request), failed auth attempts (auth_failure), rate-limit blocks (rate_limited), Compose lifecycle operations (compose_op), exec tunnel opens (exec_start), and Ed25519 key enrollments (enrollment). Every record includes a UTC nanosecond timestamp, the client IP, HTTP method and path, and an outcome (allowed / denied / error). Portwing also keeps an in-memory ring buffer (default 256) with an authenticated Standard-mode JSON view and cursor-based NDJSON export. Edge mode exposes only the NDJSON export on its private operations listener without inbound auth, so restrict that listener to the trusted exporter or monitoring workload.
What platforms does Portwing run on, and how do I install it?
Portwing ships as a multi-arch container image for linux/amd64, linux/arm64, and linux/arm/v7; as signed deb and rpm packages for those architectures; and as a Homebrew cask for Intel and Apple Silicon Macs. Docker Compose with sockguard remains the recommended hardened deployment, while native packages are convenient for host-managed services and local CLI use. Stable releases update codeswhat/tap/portwing; prereleases do not move that channel. See https://portwing.codeswhat.com/docs/installation for verification, configuration, upgrade, and uninstall instructions.
Is Portwing production-ready?
Portwing v1.1.0 is the current release line of the stable v1 series. GitHub Releases (https://github.com/CodesWhat/portwing/releases) lists what's published. The latest container tag and Homebrew cask follow the latest published stable release. The v1 semver guarantees already apply. The 0.9 line is no longer supported. Pin versions and review the changelog and stability policy. Both standard and edge modes are production supported: edge is gated by a real multi-agent reconnect/load soak with concurrent exec, continuous logs, and controller backpressure. Use Drydock v1.6.0-rc.11+ for full v0.9 watcher/update feature compatibility; the stable wire contract remains DrydockCompat 1.4.0. For the strongest deployment posture, pair either mode with sockguard. Report issues at https://github.com/CodesWhat/portwing/issues and security concerns at security@codeswhat.com.
Do I need sockguard? What does adding it actually change?
Sockguard is optional but recommended for production. Without it, Portwing proxies any Docker API request that passes authentication — which includes container creates with privileged mode, arbitrary bind mounts, or capability additions if a caller requests them. Sockguard sits between Portwing and the Docker socket and applies a default-deny allowlist: every Docker API call is blocked unless an explicit rule in sockguard.yaml permits it (method, path, and request body). The bundled sockguard.yaml tracks sockguard's portwing.yaml preset, which covers container lifecycle, image operations, events, container log reads, and narrow network and volume reads — check https://github.com/CodesWhat/sockguard/blob/main/app/configs/portwing.yaml for the current version. Even if Portwing were fully compromised, sockguard constrains it to that explicit allowlist. The two-layer docker-compose example in the repo pre-wires both containers with a shared filtered socket.