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
4 changes: 3 additions & 1 deletion .agents/skills/adversarial-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ These are the ways this codebase breaks that generic reviewers miss. Read
- **Conventional Commits with a scope** (`<type>(<scope>)[!]: <description>`), enforced by the
`conventional-pre-commit` hook with `--force-scope`. Release notes are generated from them.
- **depguard deny list** in `.golangci.yaml`: `github.com/sirupsen/logrus` outside
`internal/logger`, `github.com/pkg/errors`, and the `github.com/instana/testify` fork. A new
`internal/logger`, `github.com/pkg/errors`, the `github.com/instana/testify` fork, and
`github.com/docker/docker/…` (vulnerable in every version — the Docker SDK is
`github.com/moby/moby/client` + `github.com/moby/moby/api`). A new
import of any of them — or a `//nolint:depguard` to sneak one in — is a finding.

## 4. Adversarial passes — language-agnostic
Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/go-style-guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"fmt"

// Group 2: third-party (everything that is NOT this module)
"github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/unix"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -212,6 +212,7 @@ tags.
| `github.com/sirupsen/logrus` | `github.com/leinardi/swarm-device-access/internal/logger` → `logger.L()` |
| `github.com/pkg/errors` | stdlib `errors` + `fmt.Errorf(...%w...)` |
| `github.com/instana/testify` | `github.com/stretchr/testify` |
| `github.com/docker/docker/…` | `github.com/moby/moby/client` + `github.com/moby/moby/api` (GO-2026-4887, GO-2026-4883) |

---

Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ jobs:
golangci-lint-fmt
golangci-lint-full

# A dedicated job rather than an entry in the pre-commit-hooks list above: reviewdog runs hooks
# only on the files a PR changes, and the audit-deps-go hook only fires on go.mod/go.sum, so new
# code reaching an already-vulnerable API, or a transitively reintroduced banned module, would
# never be scanned there.
audit-deps:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true

- name: Audit Go dependencies
run: make audit-deps

markdownlint:
runs-on: ubuntu-latest

Expand Down
7 changes: 7 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ linters:
desc: Should be replaced by standard lib errors package.
- pkg: "github.com/instana/testify"
desc: It's a fork of github.com/stretchr/testify.
docker-sdk:
deny:
# Trailing slash: matches github.com/docker/docker/... only, not github.com/docker/docker-credential-helpers.
- pkg: "github.com/docker/docker/"
desc: >-
Affected by GO-2026-4887 and GO-2026-4883 in every version, with no fix in this module.
Use github.com/moby/moby/client and github.com/moby/moby/api instead.
dupl:
threshold: 100
funlen:
Expand Down
54 changes: 54 additions & 0 deletions .mk/audit-deps.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
ifndef MK_LOCAL_AUDIT_DEPS_INCLUDED
MK_LOCAL_AUDIT_DEPS_INCLUDED := 1

# Local snippet (NOT part of make-common): check the Go dependencies this repository ships against
# known vulnerabilities, and keep banned modules out of the module graph.
#
# What this is and is not. govulncheck is source-aware: it reports a vulnerable module only when
# the build actually reaches the affected symbol, so it answers "is this repository exposed", not
# "does a vulnerable version appear in the graph". The banned-module check answers the graph
# question for the modules listed in BANNED_GO_MODULES: depguard (.golangci.yaml) only sees this
# repository's own import lines, so it cannot notice a banned module coming back transitively
# through some other dependency. go mod why -m can.
#
# Both need the network, and govulncheck over every package is too slow to run on every Go
# change. The pre-commit hook runs this same target, but only when go.mod or go.sum change; CI runs
# it on every pull request (the audit-deps job in .github/workflows/ci.yaml).
#
# The version is pinned so two runs mean the same thing. Bump it deliberately.

GOVULNCHECK_VERSION ?= v1.8.0
GOVULNCHECK ?= golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)

# github.com/docker/docker is affected by GO-2026-4887 and GO-2026-4883 in every version, with no
# fix in that module; the SDK is github.com/moby/moby/client + github.com/moby/moby/api.
BANNED_GO_MODULES ?= github.com/docker/docker

.PHONY: audit-deps
audit-deps: audit-deps-go audit-deps-banned ## Scan Go dependencies for known vulnerabilities and banned modules (network required)

.PHONY: audit-deps-go
audit-deps-go: ## Run the pinned govulncheck over every Go package
$(GO) run $(GOVULNCHECK) ./...

# go mod why -m prints "(main module does not need module <mod>)" when no package in the build
# (tests included) imports the module; anything else is the import chain that pulls it in.
.PHONY: audit-deps-banned
audit-deps-banned: ## Fail if a module in BANNED_GO_MODULES is reachable from the main module
@status=0; \
for mod in $(BANNED_GO_MODULES); do \
if ! why="$$($(GO) mod why -m "$$mod" 2>&1)"; then \
echo "[audit-deps] go mod why -m $$mod failed:" >&2; \
printf '%s\n' "$$why" >&2; \
status=1; \
elif printf '%s\n' "$$why" | grep -qxF "(main module does not need module $$mod)"; then \
echo "[audit-deps] $$mod: not needed"; \
else \
echo "[audit-deps] banned module $$mod is needed by the main module, via:" >&2; \
printf '%s\n' "$$why" >&2; \
status=1; \
fi; \
done; \
exit $$status

endif # MK_LOCAL_AUDIT_DEPS_INCLUDED
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ repos:
additional_dependencies: ["prettier@3.7.4"]
files: \.(yml|yaml)$
args: ["--write"]
# Dependency-change check, and nothing more. It runs the same target as `make audit-deps` --
# the full source-aware govulncheck scan over every package plus the banned-module check --
# but only when go.mod or go.sum change, which is what limits it: it catches a bad upgrade
# at the moment it is made, and nothing else. It does not catch a CVE disclosed against a
# dependency nobody touched since, and it never fires on a .go change, so new code reaching
# an already-vulnerable API goes unseen here. Widening the trigger is not the fix: it would
# put a multi-minute network scan on every Go commit. The audit-deps CI job runs the same
# target on every pull request.
- id: audit-deps-go
name: go dependency audit
entry: make audit-deps
language: system
pass_filenames: false
require_serial: true
files: ^go\.(mod|sum)$
- repo: https://github.com/adrienverge/yamllint
rev: v1.38.0
hooks:
Expand Down
4 changes: 2 additions & 2 deletions .serena/memories/tech_stack.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Tech Stack

- **Language**: Go 1.26.3 (module `github.com/leinardi/swarm-device-access`)
- **Language**: Go 1.26.8 (module `github.com/leinardi/swarm-device-access`)
- **Build**: Make wrapping `go`; shared `.mk/` snippets from `leinardi/make-common@v1`
- **Key deps**:
- `github.com/cilium/ebpf v0.19.0` — BPF program attach (cgroup v2)
- `github.com/docker/docker v28.5.2` — Docker API client
- `github.com/moby/moby/client v0.6.0` + `github.com/moby/moby/api v1.56.0` — Docker API client and types
- `github.com/godbus/dbus/v5 v5.1.0` — systemd DBus watcher
- `github.com/prometheus/client_golang v1.23.2` — metrics
- `golang.org/x/sys v0.42.0` — unix syscalls
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ make go-build # cross-compiles to Linux into ./dist/ (works from macOS/Win)
make go-test # CGO_ENABLED=1 go test -race ./...
make go-vet
make go-tidy # go mod tidy + go mod verify
make audit-deps # govulncheck + banned-module check (network required); also runs in CI and as a pre-commit hook on go.mod/go.sum changes
make check # pre-commit on all files
make check-stage # pre-commit on staging area only
make docker-build
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ MK_COMMON_DIR := $(REPO_ROOT)/.mk
MK_COMMON_FILES := docker.mk help.mk go.mk password.mk pre-commit.mk

# Repo-local snippets that are NOT in make-common
MK_LOCAL_FILES := docker-run.mk integration-test.mk
MK_LOCAL_FILES := docker-run.mk integration-test.mk audit-deps.mk

MK_COMMON_BOOTSTRAP_SCRIPT := $(REPO_ROOT)/scripts/bootstrap-mk-common.sh

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ The daemon needs host-level privileges: `privileged`, host cgroup namespace,
host PID namespace, host user namespace, the host Docker socket, `/sys` mounted
at `/host/sys`, and `/dev` mounted into the daemon container.

It requires Docker Engine 19.03 or newer (API 1.40): the Docker client it uses
refuses older engines.

Swarm does not allow those runtime options directly on a service. The common
workaround is to deploy a small wrapper service that runs the Docker CLI and
uses the host Docker socket to launch the real privileged daemon container.
Expand Down
14 changes: 6 additions & 8 deletions cmd/swarm-device-access/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"os/signal"
"syscall"

"github.com/docker/docker/client"
"github.com/moby/moby/client"

"github.com/leinardi/swarm-device-access/internal/config"
"github.com/leinardi/swarm-device-access/internal/daemon"
Expand Down Expand Up @@ -98,10 +98,8 @@ func run() int {
// SIGHUP: reload the config file and update hot settings + logger.
go watchSIGHUP(rootCtx, store)

cli, err := client.NewClientWithOpts(
client.WithHost("unix://"+*dockerSocket),
client.WithAPIVersionNegotiation(),
)
// API version negotiation is the client default; it runs lazily on the first request.
cli, err := client.New(client.WithHost("unix://" + *dockerSocket))
if err != nil {
log.Error("docker client init failed", "err", err)

Expand All @@ -113,18 +111,18 @@ func run() int {

isSwarmManager := false

nodeInfo, infoErr := cli.Info(rootCtx)
infoResult, infoErr := cli.Info(rootCtx, client.InfoOptions{})
if infoErr != nil {
log.Warn(
"could not query docker info; assuming worker node (service-label inspection disabled)",
"err",
infoErr,
)
} else {
isSwarmManager = nodeInfo.Swarm.ControlAvailable
isSwarmManager = infoResult.Info.Swarm.ControlAvailable
log.Info("swarm role detected",
"manager", isSwarmManager,
"local_node_state", nodeInfo.Swarm.LocalNodeState,
"local_node_state", infoResult.Info.Swarm.LocalNodeState,
)
}

Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,5 @@ Ensure the host DBus socket is bind-mounted as `-v /run/dbus/system_bus_socket:/
### Daemon cannot connect to Docker

Check the socket path with `-docker-socket`. Default: `/var/run/docker.sock`. On some hosts Docker Desktop uses a different path.

The daemon requires Docker Engine 19.03+ (API 1.40). The `github.com/moby/moby/client` SDK refuses older engines with `API version … is not supported by this client: the minimum supported API version is 1.40`.
19 changes: 6 additions & 13 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module github.com/leinardi/swarm-device-access

go 1.26.3
go 1.26.8

require (
github.com/cilium/ebpf v0.22.0
github.com/docker/docker v28.5.2+incompatible
github.com/godbus/dbus/v5 v5.2.2
github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.6.0
github.com/prometheus/client_golang v1.24.1
golang.org/x/sys v0.48.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -17,31 +18,23 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gotest.tools/v3 v3.5.2 // indirect
)
Loading
Loading