Portwing

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.

Portwing mascot, a slate and violet toy pigeon
  • 14capabilities
  • ~10 MBbinary
  • 3architectures
  • Sponsor
  • License
  • CI
  • OpenSSF Scorecard
  • OpenSSF Best Practices
  • Maintainability
  • GitHub stars
  • Coverage
Star history chart for CodesWhat/portwing
What it does

14 reasons your Docker hosts are safer

Security isn't a checkbox — it's every layer. Here's what we built.

Get running

Get started in minutes

Add Portwing to your compose file and point Drydock at it — one agent per host.

Quick start · docker run
$ 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.0

Heads upTOKEN is visible in docker inspect — fine for a local try, not for production.

In the real CLI

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.

portwing 1.1.0 · Ed25519 auth

Recorded from portwing v1.0.0 · local stub

Provenance and redactions
Real output from the binary built from the v1.0.0 tag (commit db6c0b1), recorded 2026-10-29, standard mode, against a local stub on a unix socket instead of a Docker daemon. Requests are signed by 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

  1. Drydockcontroller + UI
  2. Portwingagent · Ed25519
  3. Sockguardsocket filter
  4. DockerEngine API

Edge mode, as it plays out once: the agent dials out over the portwing/1.0 WebSocket, so there is no inbound control port.

  1. agent → controller: hello
  2. controller → agent: welcome
  3. dd:component_sync
  4. dd:container_sync
On the horizon

Roadmap

Where we've been and where we're headed. Direction, not commitments.

ReleasedCurrent (HEAD)
roadmapgit log --graph
  1. Post-v1🧭 Demand-Driven
    Controller-managed Portwing upgrade and rollback wavesOptional client-certificate authentication
    Polling and intermittent edge transport
  2. Planned🔐 Enrollment Over TLS
    Enrollment 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
  3. v1.1HEAD -> v1.1.0🔏 Audience-Bound Signatures
    Ed25519 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
  4. v1.0🚀 Stable Release
    Stable 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
  5. v0.9.0🔄 Controller-Owned Updates
    Drydock 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
  6. v0.8.0🧭 Toward v1.0
    Edge 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
  7. v0.7.0🛡️ Fail-Closed Security
    Standard 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
    1. v0.6.0🔐 Non-Root & Cross-Repo Parity
      Agent 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
    2. v0.5.0🔐 Hardening
      Request 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
    3. v0.4.0🧪 Quality Gates
      Monthly 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
    4. v0.3.0🏷️ Rename & Edge Fixes
      Renamed 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
    5. v0.2.0🔒 Security & Observability
      Ed25519 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
    6. v0.1.0🛡️ Foundation
      Standard 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
    7. └ (initial commit)
Why Portwing

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.

FeaturePortwingPortainerKomodoArcaneHawser
Inbound + outbound edge modesYesYesYesYesYes
Asymmetric agent identityYesPartialYesPartialNo
Documented socket policy layerYesNoNoPartialNo
Structured agent-level auditYesPartialPartialPartialPartial
Transparent Docker APIYesYesNoNoYes
Read-only MCP serverYesNot documentedNot documentedNot documentedNot documented
YesPartialNoNot documented
Ecosystem

Part of the CodesWhat stack

Portwing is one piece of a small, focused toolkit — each tool does one job, and they compose.

FAQ

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.