Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7a62e46
harden Docker API permissions and integration builds
Aug 24, 2026
d6271a2
merge API controls and build-chain hardening into integration
Aug 24, 2026
137e76d
update Docker metadata action to Node 24 runtime
Aug 24, 2026
e3f17e1
merge build action runtime update into integration
Aug 24, 2026
94a1c93
add scoped allow_all container shortcut
Aug 24, 2026
389d5f7
merge scoped allow_all shortcut into integration
Aug 24, 2026
1d51b8f
preserve scoped container lifecycle events
Aug 24, 2026
f530da8
enforce explicit container authorization matrix
Aug 24, 2026
2f16c52
bound caches and harden proxy lifecycle
Aug 24, 2026
a2385ba
reject ambiguous profile configuration
Aug 24, 2026
c1a7971
remove obsolete policy state
Aug 24, 2026
176a147
document and enforce scoped deployment boundaries
Aug 24, 2026
b9dc06e
harden image validation and signing pipeline
Aug 24, 2026
efb7e5e
expand policy and lifecycle regression coverage
Aug 24, 2026
1eee67f
cover scoped network and commit targets
Aug 24, 2026
3b024c2
merge full security audit remediation
Aug 24, 2026
00820c7
make registry login fork safe
Aug 24, 2026
b5fd6d5
merge fork safe registry workflow
Aug 24, 2026
adf8ff6
disable deprecated staticcheck cache runtime
Aug 24, 2026
90c5224
merge current CI runtime configuration
Aug 24, 2026
a2b2357
fix archive upload authorization
Aug 24, 2026
d65cf7f
docs add security migration notes
Aug 24, 2026
ac1b2bb
ci validate multiarch and safe signing
Aug 24, 2026
ce415a8
fix fatal server exit status
Aug 24, 2026
b200757
harden profile parsing and container resolution
Aug 24, 2026
29aee44
fix yaml profile key validation
Aug 24, 2026
e38961d
ci cross compile multiarch binaries
Aug 24, 2026
7e907bf
docs prepare 1.2.0 release migration
Aug 24, 2026
74f6dac
release 1.2.0
Aug 24, 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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
compose.yml
config
README.md
README.fr.md
**/*_test.go
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
65 changes: 60 additions & 5 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- integration
tags:
- "v*"
pull_request:
Expand Down Expand Up @@ -38,16 +39,42 @@ jobs:
- name: Vet
run: go vet ./...

- name: Staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: "2026.2.1"
install-go: false
use-cache: false

- name: Test
run: go test -race ./...

- name: Vulnerability audit
run: go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Build image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: false
platforms: linux/amd64,linux/arm64
provenance: false
sbom: false

publish:
if: github.event_name != 'pull_request'
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -60,32 +87,41 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}

- name: Log in to Docker Hub
if: env.DOCKERHUB_USERNAME != ''
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Prepare optional Docker Hub image
id: dockerhub
run: |
if [ -n "$DOCKERHUB_USERNAME" ]; then
echo "image=$DOCKERHUB_USERNAME/docker-socket-proxy" >> "$GITHUB_OUTPUT"
else
echo "image=" >> "$GITHUB_OUTPUT"
fi

- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
${{ secrets.DOCKERHUB_USERNAME }}/docker-socket-proxy
${{ steps.dockerhub.outputs.image }}
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=integration,enable=${{ github.ref == 'refs/heads/integration' }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}

- name: Build and push production image
id: build
uses: docker/build-push-action@v7
with:
context: .
Expand All @@ -101,3 +137,22 @@ jobs:
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true

- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

- name: Sign published images
env:
DIGEST: ${{ steps.build.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
if [ -z "$TAGS" ]; then
echo "no tags to sign"
exit 0
fi
while IFS= read -r tag; do
[ -n "$tag" ] || continue
cosign sign --yes "$tag@$DIGEST"
done <<EOF
$TAGS
EOF
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
*.log
*.out
coverage.*
docker-socket-proxy
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes are documented here. Release tags use semantic versioning.

## 1.2.0 - 2026-08-24

### Security

- Container inspection now requires `allow_inspect: true` in addition to `containers: true`.
- Creating an exec session with `POST /containers/{id}/exec` now requires both `exec: true` and `post: true`.
- Scoped profiles can no longer perform global image, volume, or network writes because those operations cannot be tied safely to an authorized target container.
- Uploading an archive with `PUT /containers/{id}/archive` requires both `allow_archive: true` and `post: true`.

### Migration

- Add `allow_inspect: true` to every existing profile that calls `GET /containers/{id}/json`.
- Add `exec: true` to every profile that creates exec sessions; `post: true` alone is no longer sufficient.
- Use an unscoped, explicitly privileged profile only when a client genuinely needs global image, volume, or network writes.
- Unknown CLI profile options are rejected during startup rather than being silently ignored. Correct any reported typo before restarting.

Review and apply the migration notes before upgrading, then pin the immutable `1.2.0` tag instead of relying on `latest`.

## 1.1.2

- Previous stable release. See the Git history for its detailed changes.
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM golang:1.26.5-alpine3.24 AS build
FROM --platform=$BUILDPLATFORM golang:1.27.0-alpine3.24@sha256:4c9fe60190a2a3350ddc51de80d0224b8a6698d12bdfc999fee45ea9d6c46dbc AS build

ARG APP_VERSION="dev"
ARG APP_GIT_SHA="unknown"
ARG TARGETOS
ARG TARGETARCH

ENV CGO_ENABLED=0

Expand All @@ -10,11 +12,11 @@ WORKDIR /src
COPY go.mod go.sum ./
COPY src/ ./src

RUN go build -trimpath \
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath \
-ldflags="-s -w -X main.version=${APP_VERSION} -X main.gitSha=${APP_GIT_SHA}" \
-o /out/docker-socket-proxy ./src

FROM gcr.io/distroless/static-debian13:nonroot
FROM gcr.io/distroless/static-debian13:nonroot@sha256:1c2c046bc09ed40fad370b599a0b1ae7987f55b01e247cf27a7c27cd97e5bbc7

COPY --from=build --chown=nonroot:nonroot /out/docker-socket-proxy /usr/local/bin/docker-socket-proxy

Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 cerede2000

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
77 changes: 74 additions & 3 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ La première référence est publiée sur [Docker Hub](https://hub.docker.com/r/

`latest` suit `main`. Chaque release Git `vX.Y.Z` publie également les tags Docker immuables `X.Y.Z` et `X.Y` sur les deux registres.

La branche `integration` publie uniquement le tag mutable `integration`. Elle ne remplace jamais `latest` ni un tag de release.

## Notes de mise à niveau

La release `1.2.0` renforce plusieurs permissions et peut nécessiter une adaptation des profils. L'inspection d'un conteneur demande `allow_inspect: true` ; la création d'une session exec demande à la fois `exec: true` et `post: true` ; enfin, les profils à portée limitée ne peuvent plus effectuer d'écritures globales sur les images, volumes ou réseaux. Les options de profil CLI inconnues sont rejetées afin qu'une faute de frappe ne produise pas silencieusement une politique inattendue.

Consultez la procédure de migration complète dans [CHANGELOG.md](CHANGELOG.md) avant de remplacer une image `1.1.2` ou antérieure, puis épinglez le tag immuable `1.2.0` plutôt que `latest`.

L'image publiée est analysée en continu par [Docker Scout](https://scout.docker.com/reports/org/cerede2000/images/host/hub.docker.com/repo/cerede2000%2Fdocker-socket-proxy). Le rapport est lié ici plutôt que figé dans le README : son résultat suit les mises à jour des vulnérabilités et de l'image.

## Ce qui le différencie
Expand All @@ -37,10 +45,12 @@ Un outil peut donc disposer d'un accès Docker étendu lorsque c'est nécessaire
- Le proxy ne retient que les IP partagées avec ses propres réseaux Docker.
- Les listes, les événements et les opérations ciblant un conteneur respectent la même portée.
- Le cache interne nom / ID de conteneur évite une requête Docker supplémentaire pour les vérifications usuelles.
- Le contrôle local `/version` est accepté sans profil uniquement depuis l'interface loopback. N'utilisez pas le réseau hôte et ne publiez pas le port `2375`.
- Pour les profils à portée limitée, les événements Docker non liés à un conteneur sont volontairement omis car ils ne peuvent pas être rattachés sûrement à une cible autorisée.

## Démarrage rapide

Créez un fichier `profiles.yml`, puis lancez le proxy. Le montage du socket est en lecture seule : les requêtes Docker restent possibles via l'API Unix, mais le fichier socket ne peut pas être remplacé depuis le conteneur.
Créez un fichier `profiles.yml`, puis lancez le proxy. Le montage `:ro` empêche seulement de remplacer le fichier socket Unix ; il ne rend **pas** les appels à l'API Docker accessibles en lecture seule. La politique du proxy constitue la barrière de sécurité.

```yaml
services:
Expand Down Expand Up @@ -142,6 +152,7 @@ traefik:
ping: true
version: true
containers: true
allow_inspect: true
networks: true
events: true
session: true
Expand All @@ -150,6 +161,7 @@ traefik-manager:
ping: true
version: true
containers: true
allow_inspect: true
post: true
allow_restart: true
container_scope: allowlist
Expand Down Expand Up @@ -202,7 +214,7 @@ Chaque famille est désactivée par défaut. Une valeur YAML booléenne (`true`/
| `build` | `/build` |
| `commit` | `/commit` |
| `configs` | `/configs` |
| `containers` | `/containers` |
| `containers` | `/containers` (famille générale ; les sous-routes sensibles restent contrôlées séparément) |
| `distribution` | `/distribution` |
| `exec` | `/exec` |
| `images` | `/images` |
Expand All @@ -217,14 +229,65 @@ Chaque famille est désactivée par défaut. Une valeur YAML booléenne (`true`/
| `tasks` | `/tasks` |
| `volumes` | `/volumes` |

Les écritures (`POST`, `PUT`, `PATCH`, `DELETE`) restent interdites même lorsqu'une famille est activée, sauf si `post: true` est ajouté. Pour les opérations de conteneur, `post` doit être complété explicitement par `allow_start`, `allow_stop` et/ou `allow_restart` selon le besoin. `allow_restarts` est accepté comme alias de `allow_restart`.
Les écritures génériques (`POST`, `PUT`, `PATCH`, `DELETE`) restent interdites même lorsqu'une famille est activée, sauf si `post: true` est ajouté. Les actions ciblées de cycle de vie sont indépendantes de ce droit large et peuvent être accordées avec `post: false`.

| Option conteneur | Route | Nécessite `post` |
| --- | --- | --- |
| `allow_archive` | `/containers/{id}/archive` | GET/HEAD : non ; PUT : oui |
| `allow_changes` | `/containers/{id}/changes` | non |
| `allow_export` | `/containers/{id}/export` | non |
| `allow_inspect` | `/containers/{id}/json` | non |
| `allow_logs` | `/containers/{id}/logs` | non |
| `allow_top` | `/containers/{id}/top` | non |
| `allow_start` | `/containers/{id}/start` | non |
| `allow_stop` | `/containers/{id}/stop` | non |
| `allow_restart` | `/containers/{id}/restart` | non |
| `allow_pause` | `/containers/{id}/pause` | non |
| `allow_unpause` | `/containers/{id}/unpause` | non |
| `allow_kill` | `/containers/{id}/kill` | non |

Toutes ces options valent `false` par défaut. `allow_restarts` reste un alias de `allow_restart` ; contrairement au commutateur groupé de LinuxServer, il n'accorde pas implicitement `stop` ou `kill`. Ces droits doivent être explicitement ajoutés.

La création d'une session exec via `POST /containers/{id}/exec` exige les trois droits explicites `containers: true`, `exec: true` et `post: true`. `allow_all` n'active jamais `exec`.

`GET /containers/{id}/stats` reste volontairement inclus dans le droit général de lecture `containers`. Cette route expose la télémétrie d'exécution, respecte la portée de conteneurs configurée et ne possède pas de commutateur `allow_stats` distinct.

`allow_all: true` est un raccourci groupé mais limité à la portée pour toutes les options `allow_*` du tableau. Ce n'est volontairement **pas** un droit Docker global : il n'active ni `containers`, ni `exec`, ni `post`, ni une autre famille d'API et ne contourne pas les portées de conteneurs. L'envoi d'une archive et les autres écritures génériques nécessitent donc toujours `post: true`. Traitez-le comme un droit à fort impact : `export` peut lire tout le système de fichiers du conteneur et la lecture d'archive peut exposer n'importe quel fichier de la cible.

Exemple minimal limité au cycle de vie :

```yaml
container-operator:
ping: true
version: true
containers: true
post: false
allow_start: true
allow_stop: true
allow_restart: true
allow_pause: true
allow_unpause: true
```

Tous les contrôles ciblés des conteneurs, sans activer les autres familles Docker :

```yaml
container-manager:
containers: true
post: true
allow_all: true
```

`apirewrite` force une version d'API Docker pour un profil, par exemple `apirewrite: "1.53"`.

## Portée des conteneurs

Les noms sont les noms Docker sans le préfixe `/`. Les règles s'appliquent aux listes, événements, inspections, logs, statistiques, exec, opérations réseau et actions ciblées.

### Limites de la portée

La portée conteneur s'applique uniquement lorsqu'une requête Docker peut être rattachée à un conteneur. Pour un profil limité, les opérations globales sur les conteneurs (`create`, `prune`) et les écritures destructrices sur les images, volumes ou réseaux non ciblés sont refusées. Les lectures des familles globales `images`, `volumes` et `networks` ne sont pas filtrées par conteneur. Évitez d'accorder ces familles avec `post: true` sauf si le client administre réellement tout l'hôte.

### Accès large : `all`

`all` est la valeur par défaut. Le profil conserve ses droits sur tous les conteneurs ; utilisez une règle `deny` pour retirer une cible critique.
Expand All @@ -234,6 +297,7 @@ portainer:
ping: true
version: true
containers: true
allow_inspect: true
images: true
networks: true
post: true
Expand All @@ -253,6 +317,7 @@ Les conteneurs absents de `allowed_containers` sont invisibles et inaccessibles.
```yaml
traefik-manager:
containers: true
allow_inspect: true
post: true
allow_restart: true
container_scope: allowlist
Expand All @@ -268,6 +333,7 @@ Les conteneurs de `blocked_containers` sont invisibles et toute opération les v
dockhand:
ping: true
containers: true
allow_inspect: true
events: true
post: true
allow_start: true
Expand All @@ -285,6 +351,7 @@ dockhand:
```yaml
dockhand:
containers: true
allow_inspect: true
events: true
post: true
allow_start: true
Expand Down Expand Up @@ -329,3 +396,7 @@ La runtime `distroless/static-debian13:nonroot` est adaptée à ce modèle : le
go test -race ./...
go vet ./...
```

## Licence

Distribué sous [licence MIT](LICENSE).
Loading