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
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.github
.gitignore
compose.yml
config
README.md
**/*_test.go
91 changes: 58 additions & 33 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,65 +1,90 @@
name: Build and publish Docker image
name: Test and publish development image

on:
push:
branches:
- main
tags:
- 'v*'
paths:
- src/*
- .github/workflows/docker-image.yml
- Dockerfile
- go.mod
- src/**
pull_request:
paths:
- .github/workflows/docker-image.yml
- Dockerfile
- go.mod
- src/**
workflow_dispatch:

concurrency:
group: docker-dev-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/docker-socket-proxy
PKG_NAME: docker-socket-proxy

jobs:
build-and-push:
test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6

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

- name: Format check
run: test -z "$(gofmt -l .)"

- name: Vet
run: go vet ./...

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

publish:
if: github.event_name != 'pull_request'
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

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

- name: Build and push (linux/amd64 only)
uses: docker/build-push-action@v6
- name: Build and push development image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
build-args: |
VERSION=${{ github.ref_name }}
GIT_SHA=${{ github.sha }}

- name: Prune old container versions (keep last 3)
uses: actions/delete-package-versions@v5
with:
package-name: ${{ env.PKG_NAME }}
package-type: container
min-versions-to-keep: 3
APP_VERSION=dev
APP_GIT_SHA=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
23 changes: 7 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
# =========================
# Stage 1 : build Go binary
# =========================
FROM golang:1.23-alpine AS build
FROM golang:1.26.5-alpine3.24 AS build

ARG APP_VERSION="dev"
ARG APP_GIT_SHA="unknown"

# On désactive les modules pour un projet simple sans go.mod
ENV GO111MODULE=off \
CGO_ENABLED=0
ENV CGO_ENABLED=0

WORKDIR /src

# On copie juste le dossier src/
COPY go.mod go.sum ./
COPY src/ ./src

# Build du binaire
RUN go build -trimpath \
-ldflags="-s -w -X main.version=${APP_VERSION} -X main.gitSha=${APP_GIT_SHA}" \
-o /out/docker-socket-proxy ./src

# =========================
# Stage 2 : image finale
# =========================
#FROM alpine:3.20 si debug
FROM gcr.io/distroless/base-debian12
FROM gcr.io/distroless/static-debian13:nonroot

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

ENV DOCKER_SOCKET_PATH=/var/run/docker.sock \
PROXY_PORT=2375

EXPOSE 2375

# Healthcheck intégré : teste le serveur Go + accès Docker via /version
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["docker-socket-proxy", "healthcheck"]

USER nonroot:nonroot

ENTRYPOINT ["docker-socket-proxy"]
125 changes: 125 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# docker-socket-proxy

Proxy HTTP minimaliste devant le socket Docker. Les clients sont associés à un profil par leur adresse IP et les droits Docker sont refusés par défaut.

## Image de développement

```text
ghcr.io/cerede2000/docker-socket-proxy:dev
```

L'image est publiée pour `linux/amd64` et `linux/arm64` après validation des tests. Elle utilise un binaire Go statique dans Distroless Debian 13 et s'exécute sans privilèges par défaut.

## Configuration

| Variable | Défaut | Description |
| --- | --- | --- |
| `DOCKER_SOCKET_PATH` | `/var/run/docker.sock` | Chemin du socket Docker |
| `PROXY_PORT` | `2375` | Port d'écoute et port utilisé par le healthcheck |
| `PROXY_LISTEN` | vide | Adresse d'écoute complète, prioritaire sur `PROXY_PORT` |
| `SOCKETPROXY_PROFILE_FILE` | `/config/profiles.yml` | Fichier de profils |
| `DISCOVER_INTERVAL` | `30s` | Intervalle de redécouverte |
| `EVENT_DEBOUNCE_DELAY` | `100ms` | Temporisation des événements Docker |

Les options `--listen`, `--socket`, `--profiles`, `--discover-interval` et `--debounce-delay` restent disponibles et sont prioritaires sur l'environnement. Si `--listen` change le port, `PROXY_PORT` doit être renseigné avec le même port pour le healthcheck.

Le compte effectif doit pouvoir lire le socket Docker. Le fichier Compose fournit un exemple avec un UID/GID hôte explicite ; adaptez `user` au propriétaire et au groupe du socket de votre machine.

## Portée et règles par conteneur

Les noms de conteneurs sont exacts et correspondent au nom Docker sans le préfixe `/` (par exemple `container_name: dockman` devient `dockman`). La portée détermine l'accès normal ; `container_rules` ajoute des exceptions par nom.

### Tous les conteneurs — comportement historique

`all` est la valeur par défaut. Le profil conserve les droits Docker qui lui sont accordés sur tous les conteneurs.

```yaml
portainer:
containers: true
images: true
networks: true
post: true
allow_start: true
allow_stop: true
allow_restart: true
container_scope: all
```

### Allowlist — agir seulement sur certaines cibles

Les conteneurs absents de `allowed_containers` sont invisibles et inaccessibles.

```yaml
traefik-manager:
containers: true
post: true
allow_start: true
allow_stop: true
allow_restart: true
container_scope: allowlist
allowed_containers:
- traefik
```

### Blacklist — profil large avec cibles masquées

Les conteneurs de `blocked_containers` sont invisibles et toute opération les visant est refusée.

```yaml
dockhand:
containers: true
events: true
post: true
allow_start: true
allow_stop: true
allow_restart: true
container_scope: blacklist
blocked_containers:
- docker-socket-proxy
```

### Règle `deny` — masquer une cible, quelle que soit la portée

`container_rules` est prioritaire sur `container_scope`. Cette variante est utile avec `all`, ou pour rendre la règle plus explicite.

```yaml
operator:
containers: true
container_scope: all
container_rules:
- name: docker-socket-proxy
access: deny
```

### Règle `readonly` — voir sans pouvoir agir

Une cible en lecture seule reste visible dans les listes et événements. Seules les API de consultation suivantes sont admises : `inspect`, `logs`, `stats`, `top` et `changes`. Les opérations de modification, les exec, les archives et l'attach sont refusés.

```yaml
dockhand:
containers: true
events: true
post: true
allow_start: true
allow_stop: true
allow_restart: true
container_scope: blacklist
blocked_containers:
- docker-socket-proxy
container_rules:
- name: dockman
access: readonly
```

Dans cet exemple, Dockhand peut consulter les logs et statistiques de `dockman`, mais pas le redémarrer ; `docker-socket-proxy` reste entièrement masqué. Les conteneurs non cités conservent les droits du profil.

Une même cible ne peut pas figurer à la fois dans `blocked_containers` et `container_rules`. Les valeurs autorisées pour `access` sont exclusivement `deny` et `readonly`.

Pour toute portée active (`allowlist`, `blacklist`, ou au moins une `container_rules`), les opérations globales de conteneurs (`create` et `prune`) sont refusées. Les règles sont appliquées aussi aux listes de conteneurs et au flux d'événements.

## Développement

```bash
go test -race ./...
go vet ./...
```
8 changes: 8 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ services:
container_name: docker-socket-proxy
user: "107:108"
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
Expand All @@ -16,3 +20,7 @@ services:
- --dockerproxy-watchtower.info=1
- --dockerproxy-watchtower.events=1
restart: unless-stopped

networks:
traefikfront:
external: true
Loading