Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e289eec
✨ feat(filter): evaluate SecurityOpt label= and systempaths= policies
scttbnsn Jun 11, 2026
05f5385
✨ feat(filter): swarm seccomp/AppArmor confinement mode rails
scttbnsn Jun 11, 2026
d8101b5
πŸ› fix(metrics): clear per-profile in-flight gauges on profile removal
scttbnsn Jun 11, 2026
13778e8
⚑ perf(ratelimit): allocation-free packed token-bucket state
scttbnsn Jun 11, 2026
546d890
πŸ§ͺ test(ratelimit): correct benchmark sentinels for the packed-bucket …
scttbnsn Jun 11, 2026
63f2b58
πŸ“ docs: document the v1.4 roadmap batch β€” SELinux/systempaths, swarm …
scttbnsn Jun 11, 2026
03b24a6
πŸ”§ chore(ci): run the full verify suite on PRs targeting dev/* release…
scttbnsn Jun 11, 2026
dfa1d41
Merge pull request #93 from CodesWhat/feat/v1.4-roadmap-batch
scttbnsn Jun 11, 2026
086466f
πŸ”„ refactor(release): merge main (1.3.0-rc.3 changelog) into dev/v1.4
scttbnsn Jun 11, 2026
3afb785
πŸ“¦ deps: bump github.com/sigstore/sigstore-go
dependabot[bot] Jun 8, 2026
db0c346
πŸ“¦ deps: bump github/codeql-action
dependabot[bot] Jun 11, 2026
3c8776e
πŸ“¦ deps: bump the npm-minor group across 1 directory with 7 updates
dependabot[bot] Jun 10, 2026
7c66c34
πŸ“¦ deps: dedupe package-lock.json after the npm-minor bump
scttbnsn Jun 11, 2026
93800e9
πŸ”’ security(deps): resolve postcss XSS advisory via override spec fix …
scttbnsn Jun 11, 2026
4ea4e26
πŸ”’ security(deps): mirror the postcss override into workspace manifest…
scttbnsn Jun 11, 2026
65368d4
πŸ”§ chore(security): ignore SNYK-JS-POSTCSS-16189065 via .snyk policy (…
scttbnsn Jun 11, 2026
5b95025
Merge pull request #95 from CodesWhat/deps/batch-v1.4
scttbnsn Jun 11, 2026
21e7502
πŸ”„ refactor(release): merge main (v1.3.0 final) into dev/v1.4
scttbnsn Jun 11, 2026
1a75171
✨ feat(presets): add lookout preset and compose example (M2)
scttbnsn Jun 13, 2026
f45bb9e
✨ feat(presets): add lookout-with-exec preset (M3)
scttbnsn Jun 13, 2026
a145f21
✨ feat(presets): add drydock-with-selfupdate preset (M7)
scttbnsn Jun 13, 2026
e9e0bd4
πŸ“ docs(presets): register lookout, lookout-with-exec, drydock-with-se…
scttbnsn Jun 13, 2026
a166b80
πŸ› fix(presets): add logs rule and insecure_allow_read_exfiltration to…
scttbnsn Jun 13, 2026
42d966c
πŸ› fix(presets): correct DOCKER_SOCKET env var in lookout compose example
scttbnsn Jun 13, 2026
d684f0d
πŸ› fix(presets): add insecure_allow_body_blind_writes to lookout-with-…
scttbnsn Jun 13, 2026
5883d16
πŸ› fix(presets): add missing insecure flags to github-actions-runner a…
scttbnsn Jun 13, 2026
f3aae43
πŸ§ͺ test(presets): add TestPresetConfigsPassBuildChain for full validat…
scttbnsn Jun 13, 2026
8801652
Merge pull request #99 from CodesWhat/feat/lookout-presets
scttbnsn Jun 13, 2026
aadc321
πŸ“ docs(presets): align lookout & runner preset docs with shipped configs
scttbnsn Jun 13, 2026
0ea50d5
πŸ“ docs(changelog): backfill v1.4 lookout presets + runner-preset star…
scttbnsn Jun 13, 2026
418a49b
πŸ“¦ deps: refresh dev-tooling + sigstore-go for v1.4 soak (#103)
scttbnsn Jun 16, 2026
7587340
✨ feat(upstream): remote Docker TCP+TLS upstreams with active/passive…
scttbnsn Jun 16, 2026
5a13f76
🎨 style(readme): dark-mode logo variant via <picture> + prefers-color…
scttbnsn Jun 16, 2026
4283762
πŸ“ docs(release): stamp v1.4.0-rc.1 changelog + correct website preset…
scttbnsn Jun 16, 2026
905b69b
🎨 style(readme): ecosystem badge + footer normalization (#98)
scttbnsn Jun 16, 2026
2833b21
πŸ“¦ deps: fold npm-minor group into v1.4 soak (supersedes #102) (#105)
scttbnsn Jun 16, 2026
579711f
πŸ“ docs(changelog): correct v1.4.0-rc.1 dep-refresh bullet to match sh…
scttbnsn Jun 16, 2026
4b5344c
Merge branch 'main' into dev/v1.4
scttbnsn Jun 16, 2026
03e273d
✨ feat(release): cut explicit rc/prerelease tags via release-cut work…
scttbnsn Jun 16, 2026
db6a8fb
πŸ”§ chore(ci): pin golangci-lint to v2.12.2 in ci-verify
scttbnsn Jun 16, 2026
c07faba
πŸ§ͺ test(cmd): explicit return after t.Fatal nil-guards (staticcheck SA…
scttbnsn Jun 16, 2026
46cb3ef
πŸ”§ chore(lint): scope-exclude staticcheck SA5011 in tests; drop per-si…
scttbnsn Jun 16, 2026
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
13 changes: 10 additions & 3 deletions .github/workflows/ci-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
push:
branches: [main, 'release/*']
pull_request:
branches: [main]
branches: [main, 'dev/*']
merge_group:
schedule:
- cron: '30 6 * * 1' # Weekly on Monday at 06:30 UTC
Expand Down Expand Up @@ -80,7 +80,7 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: actions, go, javascript-typescript
queries: security-and-quality
Expand All @@ -96,7 +96,7 @@ jobs:
working-directory: app

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2

dependency-review:
name: "πŸ“¦ Dependency Review"
Expand Down Expand Up @@ -227,6 +227,13 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
# Pin the linter so Go Lint is deterministic. Leaving it unpinned lets
# the action float to a newer golangci-lint whose staticcheck has
# regressed SA5011: it false-positives "possible nil pointer
# dereference" on `if x == nil { t.Fatal(...) }` guards (t.Fatal ends
# the test via Goexit, so the deref below is unreachable). v2.12.2
# matches the local lefthook go-lint version and reports 0 issues.
version: v2.12.2
working-directory: app

go-test:
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/release-cut.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ run-name: "🏷️ Release: Cut β€” manual by ${{ github.actor }}"

on:
workflow_dispatch:
inputs:
release_tag:
description: "Explicit tag to cut, e.g. v1.4.0-rc.1 (supports prereleases the auto-computer cannot emit). Leave blank to auto-compute the next stable version from commit history. Must have a non-empty CHANGELOG entry and must not already exist."
required: false
type: string
default: ""

permissions:
actions: read
Expand Down Expand Up @@ -85,12 +91,27 @@ jobs:
CURRENT_VERSION: ${{ steps.base.outputs.current_version }}
LATEST_TAG: ${{ steps.base.outputs.latest_tag }}
TARGET_SHA: ${{ steps.target.outputs.sha }}
RELEASE_TAG_INPUT: ${{ inputs.release_tag }}
run: |
set -euo pipefail

if [ "${BUMP}" = "auto" ] && [ "${LATEST_TAG}" = "v0.0.0" ]; then
if [ -n "${RELEASE_TAG_INPUT}" ]; then
# Operator-supplied tag (the drydock-style explicit path). Supports
# prereleases like v1.4.0-rc.1 that the auto-computer cannot emit.
release_tag="${RELEASE_TAG_INPUT}"
if ! printf '%s' "${release_tag}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
echo "::error::Invalid release_tag '${release_tag}'. Expected vMAJOR.MINOR.PATCH[-prerelease]."
exit 1
fi
next_version="${release_tag#v}"
case "${next_version}" in
*-*) release_level="prerelease" ;;
*) release_level="explicit" ;;
esac
elif [ "${BUMP}" = "auto" ] && [ "${LATEST_TAG}" = "v0.0.0" ]; then
release_level="patch"
next_version="0.0.1"
release_tag="v${next_version}"
else
mapfile -t vars < <(node scripts/release-next-version.mjs \
--current "${CURRENT_VERSION}" \
Expand All @@ -106,9 +127,9 @@ jobs:
next_version) next_version="${value}" ;;
esac
done
release_tag="v${next_version}"
fi

release_tag="v${next_version}"
if git rev-parse -q --verify "refs/tags/${release_tag}" >/dev/null; then
echo "::error::Tag already exists: ${release_tag}"
exit 1
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/security-grype-weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
- name: Upload Grype SARIF to code scanning
# Code scanning uploads require GHAS, not available on free private repos.
if: always() && github.event.repository.visibility == 'public'
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: ${{ steps.grype.outputs.sarif }}

Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:

- name: Upload Grype source SARIF
if: always() && github.event.repository.visibility == 'public'
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: ${{ steps.grype-src.outputs.sarif }}

Expand Down Expand Up @@ -172,7 +172,7 @@ jobs:

- name: Upload Gosec SARIF
if: always() && github.event.repository.visibility == 'public'
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: gosec-results.sarif

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security-scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ jobs:
publish_results: true

- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: results.sarif
17 changes: 16 additions & 1 deletion .snyk
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Snyk (https://snyk.io) policy file
version: v1.25.0
ignore: {}
ignore:
# next pins postcss 8.4.31 exactly, but npm overrides (root and
# workspace package.json) force ^8.5.15 β€” the installed tree and
# package-lock.json contain no postcss below 8.5.15. Snyk's PR check
# resolves workspace manifests standalone without applying npm
# overrides, so it reports a version that is never installed. Mirror
# copies of this policy live in docs/.snyk and website/.snyk because
# manifest-only projects read policy from the manifest's directory.
SNYK-JS-POSTCSS-16189065:
- '*':
reason: >-
Not installed: npm overrides pin postcss to ^8.5.15; the
lockfile has no 8.4.31. Snyk manifest-only resolution does
not apply npm overrides.
expires: 2026-09-15T00:00:00.000Z
created: 2026-06-11T00:00:00.000Z
patch: {}

# Snyk Code findings cannot be ignored via the `ignore:` section above β€”
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.0-rc.1] - 2026-06-16

### Added

- **`SecurityOpt` SELinux and system-paths directives are now policy-evaluable.** Three opt-in `request_body.container_create` knobs (all default off β€” zero behavior change): `deny_selinux_disable` denies `label=disable` and the legacy `label:disable` colon form (which turn off SELinux confinement); `deny_selinux_label_override` denies `label=user:`/`role:`/`type:`/`level:` SELinux context customization; `deny_unconfined_system_paths` denies `systempaths=unconfined` **and** requests that set `MaskedPaths`/`ReadonlyPaths` to an explicit empty array β€” the Docker CLI translates `--security-opt systempaths=unconfined` into `MaskedPaths: []` client-side, so direct API callers could otherwise clear the masked-path protections without ever sending the SecurityOpt string. Both vectors are covered.
- **Swarm services gained seccomp/AppArmor confinement-mode rails**, completing `ContainerSpec.Privileges` parity with container-create. Three opt-in `request_body.service` knobs (all default off): `deny_unconfined_seccomp` (denies `Privileges.Seccomp.Mode: "unconfined"`), `deny_custom_seccomp_profiles` (denies `Mode: "custom"`, and fail-closed denies a `Seccomp` object carrying a `Profile` blob with no `Mode` β€” an inline profile the proxy cannot vet can encode an allow-everything policy), and `deny_unconfined_apparmor` (denies `Privileges.AppArmor.Mode: "disabled"`, swarm's equivalent of unconfined).
- **Remote Docker TCP+TLS upstreams with active/passive failover (`upstream.endpoints[]`).** Sockguard can now dial a remote Docker daemon over standard Docker mTLS β€” or any mix of local `unix://`/bare-path sockets and remote `tcp://host:port` endpoints β€” with per-endpoint TLS config (`tls.ca_file`/`cert_file`/`key_file`/`server_name`) and insecure opt-ins (`insecure_allow_plain_tcp`, `insecure_skip_tls_verify`). Requests route to the first healthy endpoint in the ordered list; a connect or request failure instantly demotes that endpoint so the next request fails over without retry (Docker writes aren't idempotent). Active connect-level health probes run on a configurable `failover.health_interval`/`health_timeout` schedule, keeping the hot path aware of endpoint state between requests. TLS negotiation lives inside the dialer, so it works transparently across the reverse proxy, exec/attach hijack, and all inspect side-channel paths β€” the rest of the proxy stack is unaware of whether the upstream is local or remote. The intended topology is active/passive redundancy across equivalent daemons (a swarm VIP + managers, an HA pair) β€” all endpoints must address the same logical daemon so daemon-local state (container IDs, exec sessions, owner labels) stays consistent. `DOCKER_HOST`/`DOCKER_TLS_VERIFY`/`DOCKER_CERT_PATH` are auto-detected as a single endpoint when no explicit `endpoints` are configured, and `endpoints`/`failover` are reload-immutable while `request_timeout` stays mutable. Legacy `upstream.socket` continues to work as the default single-local-socket path.
- **Three bundled presets for the Portwing Docker agent and drydock self-update (12 β†’ 15 presets).** `portwing.yaml` covers container lifecycle, image pull/remove, `GET /containers/*/logs` streaming, and event/network/volume/Swarm-service reads with exec denied; `portwing-with-exec.yaml` adds interactive exec (`/containers/*/exec`, `/exec/*/start`, `/exec/*/resize`, `/exec/*/json`). Both Portwing presets disable response redaction so container inspect data forwards intact through the tri-tool topology (sockguard β†’ Portwing β†’ drydock) and set `insecure_allow_read_exfiltration: true` for the logs path. `drydock-with-selfupdate.yaml` extends the drydock preset with the exec paths drydock's self-update finalize callback needs, pinned to the finalize entrypoint argv via `allowed_commands`. A ready-to-run `examples/compose/portwing/` stack ships alongside.

### Fixed

- **Per-profile in-flight gauge series are now deleted when a hot reload removes the profile.** Previously `sockguard_inflight_requests{profile=...}` series for removed profiles persisted at their last value until process restart, misreporting load. Deletion happens after the handler swap, so a request completing on the old chain can at worst briefly re-create the series at a draining value; it is reaped on the next reload.
- **The bundled `github-actions-runner` and `gitlab-runner` presets now load.** Both allowed unpinned exec plus `GET /containers/*/logs` and `POST /containers/*/attach` streaming without the matching acknowledgement flags, so sockguard's startup validator refused to start with either config. Added `insecure_allow_body_blind_writes: true` (runners execute arbitrary job-step commands, not a fixed allowlist) and `insecure_allow_read_exfiltration: true` (job output streams over the logs/attach APIs); both presets document gating the proxy socket to the runner process via `clients.unix_peer_profiles`/`clients.allowed_cidrs`.

### Changed

- **The rate-limit token bucket hot path is now allocation-free.** Bucket state (token count + refill timestamp) packs into a single `atomic.Uint64` (16.16 fixed-point tokens, millisecond timestamp), eliminating the per-admission heap allocation: the hot-path benchmark went from 1 alloc/16 B to 0 allocs/0 B per op (~47 β†’ ~36 ns/op). Two consequences: `limits.rate.burst` now has a validated upper bound of 65535 (configs above it are rejected at startup with a descriptive error; `tokens_per_second` is implicitly bounded the same way since burst β‰₯ tps), and refill granularity is milliseconds rather than nanoseconds β€” negligible for every supported rate.
- **Dependency refresh.** Bumped `sigstore/sigstore-go` v1.2.0 β†’ v1.2.1 (image-trust path; no behavior change) and refreshed the docs/website toolchain β€” Biome 2.4.16 β†’ 2.5.0, Tailwind 4.3.0 β†’ 4.3.1 (docs now in lockstep with website), fumadocs 16.9.3 β†’ 16.10.3, lucide-react 1.17 β†’ 1.18, turbo 2.9.17 β†’ 2.9.18. Lockfile regenerated and deduped so the existing `postcss` override resolves cleanly (`npm audit` reports 0 vulnerabilities).

## [1.3.0] - 2026-06-11

v1.3.0 promotes `1.3.0-rc.3` to stable with no binary delta β€” the release content is the `[1.3.0-rc.2]` and `[1.3.0-rc.3]` entries below, validated during the rc soak behind a live drydock deployment. Headlines: swarm service create/update now enforces the same identity/privilege rails as container create (closing the posture bypass where a service could request a workload shape `/containers/create` would deny), a zero-padded-UID root-user bypass is sealed across container create and exec, a wide-open dedicated admin listener is rejected at validation rather than just warned about, admin endpoint paths are normalized before matching, non-upgrade hijack responses strip hop-by-hop headers, the `signature_path` hot-reload wedge and three silently-ignored `SOCKGUARD_*` env vars are fixed, release images carry real `commit`/`built` metadata, and multi-arch images cross-compile natively.
Expand Down
Loading