diff --git a/.cargo/Cross.toml b/.cargo/Cross.toml new file mode 100644 index 0000000..47c6a55 --- /dev/null +++ b/.cargo/Cross.toml @@ -0,0 +1,5 @@ +[build] +target-dir = "/target" + +[target.aarch64-unknown-linux-musl] +# cross handles sysroot automatically diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 079d162..4cd8065 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,38 +93,50 @@ jobs: --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Log in to GHCR + # Docker image build, SBOM, and Cosign steps only run in the amd64 job. + # buildx handles multi-arch in a single operation (platforms: linux/amd64,linux/arm64). + - if: matrix.goarch == 'amd64' + name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Go Docker image + - if: matrix.goarch == 'amd64' + name: Build and push Go Docker image (multi-arch) uses: docker/build-push-action@v7 with: context: go file: go/Dockerfile + # Multi-arch: buildx creates a single image index for amd64 & arm64. + # This eliminates race conditions from matrix jobs pushing to the same tag. + # SBOM and Cosign operations then work on the completed index. + platforms: linux/amd64,linux/arm64 push: true build-args: | VERSION=${{ needs.version.outputs.tag }} tags: | ghcr.io/chainsafe/docker-socket-policy-go:${{ needs.version.outputs.tag }} ghcr.io/chainsafe/docker-socket-policy-go:latest - - name: Install syft + - if: matrix.goarch == 'amd64' + name: Install syft run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | \ sh -s -- -b /usr/local/bin v1.42.3 - - name: Generate SPDX SBOM for Go image + - if: matrix.goarch == 'amd64' + name: Generate SPDX SBOM for Go image run: > syft ghcr.io/chainsafe/docker-socket-policy-go:${{ needs.version.outputs.tag }} -o spdx-json --file docker-socket-policy-go.spdx.json - - name: Generate CycloneDX SBOM for Go image + - if: matrix.goarch == 'amd64' + name: Generate CycloneDX SBOM for Go image run: > syft ghcr.io/chainsafe/docker-socket-policy-go:${{ needs.version.outputs.tag }} -o cyclonedx-json --file docker-socket-policy-go.cyclonedx.json - - name: Upload Go SBOMs to release + - if: matrix.goarch == 'amd64' + name: Upload Go SBOMs to release run: | gh release upload "${{ needs.version.outputs.tag }}" \ docker-socket-policy-go.spdx.json \ @@ -132,8 +144,10 @@ jobs: --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: sigstore/cosign-installer@v3 - - name: Sign Go Docker image with Cosign + - if: matrix.goarch == 'amd64' + uses: sigstore/cosign-installer@v3 + - if: matrix.goarch == 'amd64' + name: Sign Go Docker image with Cosign env: COSIGN_EXPERIMENTAL: 1 run: | @@ -143,51 +157,75 @@ jobs: release-rust: needs: [version] runs-on: ubuntu-latest + strategy: + matrix: + rust_target: [x86_64-unknown-linux-musl, aarch64-unknown-linux-musl] + include: + - rust_target: x86_64-unknown-linux-musl + arch_name: amd64 + - rust_target: aarch64-unknown-linux-musl + arch_name: arm64 steps: - uses: actions/checkout@v7 - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: "1.85" + targets: ${{ matrix.rust_target }} + - name: Install cross + run: cargo install cross - name: Build Rust binary - run: cd rs && cargo build --release + run: cd rs && cross build --release --target ${{ matrix.rust_target }} - name: Upload Rust binary to release run: | - cp rs/target/release/docker-socket-policy docker-socket-policy-rs-linux-amd64 + cp rs/target/${{ matrix.rust_target }}/release/docker-socket-policy docker-socket-policy-rs-linux-${{ matrix.arch_name }} gh release upload "${{ needs.version.outputs.tag }}" \ - docker-socket-policy-rs-linux-amd64 \ + docker-socket-policy-rs-linux-${{ matrix.arch_name }} \ --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Log in to GHCR + # Docker image build, SBOM, and Cosign steps only run in the amd64 job. + - if: matrix.arch_name == 'amd64' + name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Rust Docker image + - if: matrix.arch_name == 'amd64' + name: Build and push Rust Docker image (multi-arch) uses: docker/build-push-action@v7 with: context: rs file: rs/Dockerfile + # Multi-arch: buildx creates a single image index for amd64 & arm64. + # This eliminates race conditions from matrix jobs pushing to the same tag. + # SBOM and Cosign operations then work on the completed index. + platforms: linux/amd64,linux/arm64 push: true + build-args: | + VERSION=${{ needs.version.outputs.tag }} tags: | ghcr.io/chainsafe/docker-socket-policy-rs:${{ needs.version.outputs.tag }} ghcr.io/chainsafe/docker-socket-policy-rs:latest - - name: Install syft + - if: matrix.arch_name == 'amd64' + name: Install syft run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | \ sh -s -- -b /usr/local/bin v1.42.3 - - name: Generate SPDX SBOM for Rust image + - if: matrix.arch_name == 'amd64' + name: Generate SPDX SBOM for Rust image run: > syft ghcr.io/chainsafe/docker-socket-policy-rs:${{ needs.version.outputs.tag }} -o spdx-json --file docker-socket-policy-rs.spdx.json - - name: Generate CycloneDX SBOM for Rust image + - if: matrix.arch_name == 'amd64' + name: Generate CycloneDX SBOM for Rust image run: > syft ghcr.io/chainsafe/docker-socket-policy-rs:${{ needs.version.outputs.tag }} -o cyclonedx-json --file docker-socket-policy-rs.cyclonedx.json - - name: Upload Rust SBOMs to release + - if: matrix.arch_name == 'amd64' + name: Upload Rust SBOMs to release run: | gh release upload "${{ needs.version.outputs.tag }}" \ docker-socket-policy-rs.spdx.json \ @@ -195,8 +233,10 @@ jobs: --clobber env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: sigstore/cosign-installer@v3 - - name: Sign Rust Docker image with Cosign + - if: matrix.arch_name == 'amd64' + uses: sigstore/cosign-installer@v3 + - if: matrix.arch_name == 'amd64' + name: Sign Rust Docker image with Cosign env: COSIGN_EXPERIMENTAL: 1 run: | diff --git a/README.md b/README.md index 111733f..73ce79c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ Key features: ## Installation +### Supported Architectures + +docker-socket-policy builds and runs on **amd64** (x86-64) and **arm64** (AArch64) Linux architectures: + +- **Docker Images**: Multi-arch manifest indexes automatically select the correct architecture when pulling. No platform flag needed: + ```bash + docker pull ghcr.io/chainsafe/docker-socket-policy-go:latest + # Pulls amd64 on x86-64, arm64 on ARM machines + ``` + +- **Prebuilt Binaries**: Both amd64 and arm64 variants are published with each release. + +See [docs/reproducible-builds.md](docs/reproducible-builds.md) for verification and per-architecture build instructions. + ### Docker Images Signed, SBOM-attested images are published to GHCR for all three implementations: @@ -36,20 +50,37 @@ Every image is Cosign-signed and ships with SPDX + CycloneDX SBOMs attached to t ### Prebuilt Binaries -Each [release](https://github.com/ChainSafe/docker-socket-policy/releases/latest) attaches a Go binary, a Rust binary, and a TypeScript build archive (plus SBOMs for each): +Each [release](https://github.com/ChainSafe/docker-socket-policy/releases/latest) attaches binaries for amd64 and arm64 architectures, plus SBOMs for each: +**Go** (statically linked ELF binary): ```bash -# Go (statically linked binary) -curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-go -chmod +x docker-socket-policy-go +# amd64 +curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-go-linux-amd64 +chmod +x docker-socket-policy-go-linux-amd64 -# TypeScript (Node 22+ required; archive includes dist/ and node_modules/) -# Replace with the tag shown on the releases page, e.g. v0.2.8 -curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-ts-.tar.gz -tar xzf docker-socket-policy-ts-.tar.gz && node dist/index.js +# arm64 +curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-go-linux-arm64 +chmod +x docker-socket-policy-go-linux-arm64 ``` -The Rust binary is also attached to every release; see the release page for the exact asset name. +**Rust** (statically linked ELF binary with musl): +```bash +# amd64 +curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-rs-linux-amd64 +chmod +x docker-socket-policy-rs-linux-amd64 + +# arm64 +curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-rs-linux-arm64 +chmod +x docker-socket-policy-rs-linux-arm64 +``` + +**TypeScript** (Node 22+ required; archive includes dist/, node_modules/, and package files): +```bash +# Extract and run (platform-independent Node archive) +curl -LO https://github.com/ChainSafe/docker-socket-policy/releases/latest/download/docker-socket-policy-ts-.tar.gz +tar xzf docker-socket-policy-ts-.tar.gz +node dist/index.js +``` To build any implementation from source instead, see [Build All](#build-all) below. @@ -188,12 +219,13 @@ docker pull attacker/malware:latest # denied: image not in allowlist | `--log-file` | `/var/log/docker-socket-policy.log` | Audit log path | | `--readonly` | `false` | Enable read-only mode | -> The TypeScript implementation listens on TCP only (`--listen-tcp`); it does not -> implement `--listen-socket`. All three implementations connect to the Docker -> daemon over a Unix socket only: Go and Rust treat `--docker-host` as a Unix -> socket path, and TypeScript additionally rejects `tcp://`/`http://` schemes -> outright. Connecting to the daemon over TCP would bypass the socket's -> user/group ownership, which is the security boundary. +> **Unix socket security boundary**: Go and Rust support `--listen-socket` for +> binding to a Unix socket, enabling socket-level access control via file +> permissions and Unix groups. TypeScript does not implement `--listen-socket` +> and listens on TCP only (`--listen-tcp`). All three implementations connect +> to the Docker daemon over Unix sockets exclusively; they reject `tcp://` and +> `http://` schemes for `--docker-host`. TCP connections would bypass socket +> ownership-based access control, breaking the security model. ### Systemd Socket Activation diff --git a/docs/reproducible-builds.md b/docs/reproducible-builds.md index 763e77f..ad6ca38 100644 --- a/docs/reproducible-builds.md +++ b/docs/reproducible-builds.md @@ -1,6 +1,6 @@ # Reproducible Builds Verification -This document describes how to verify that docker-socket-policy builds are reproducible and how to verify SBOMs and signatures. +This document describes how to verify that docker-socket-policy builds are reproducible and how to verify SBOMs and signatures. docker-socket-policy is built for multiple architectures: **amd64** and **arm64**. ## Prerequisites @@ -30,46 +30,79 @@ Each target builds twice with `--no-cache` and uses `cmp` to confirm bit-identic ## Verify a Single Build Step by Step +### Build and Verify amd64 Binary + ```bash -# Build Go binary +# Build Go binary for amd64 docker build --no-cache --platform linux/amd64 \ --build-arg VERSION=$(git describe --tags --always --dirty) \ - --output type=local,dest=/tmp/build \ + --output type=local,dest=/tmp/build-amd64 \ -f go/Dockerfile go/ # Check the binary -file /tmp/build/docker-socket-policy +file /tmp/build-amd64/docker-socket-policy # Expected: ELF 64-bit LSB executable, x86-64, statically linked # Generate SBOM -syft scan /tmp/build/docker-socket-policy -o spdx-json > docker-socket-policy.spdx.json -syft scan /tmp/build/docker-socket-policy -o cyclonedx-json > docker-socket-policy.cyclonedx.json +syft scan /tmp/build-amd64/docker-socket-policy -o spdx-json > docker-socket-policy-amd64.spdx.json +syft scan /tmp/build-amd64/docker-socket-policy -o cyclonedx-json > docker-socket-policy-amd64.cyclonedx.json +``` + +### Build and Verify arm64 Binary + +```bash +# Build Go binary for arm64 +docker build --no-cache --platform linux/arm64 \ + --build-arg VERSION=$(git describe --tags --always --dirty) \ + --output type=local,dest=/tmp/build-arm64 \ + -f go/Dockerfile go/ + +# Check the binary +file /tmp/build-arm64/docker-socket-policy +# Expected: ELF 64-bit LSB executable, ARM aarch64, statically linked + +# Generate SBOM +syft scan /tmp/build-arm64/docker-socket-policy -o spdx-json > docker-socket-policy-arm64.spdx.json +syft scan /tmp/build-arm64/docker-socket-policy -o cyclonedx-json > docker-socket-policy-arm64.cyclonedx.json ``` ## Verify SBOMs from a Release +Binary artifacts are available for both amd64 and arm64: + ```bash # Download SBOMs from a release gh release download v0.1.0 --pattern "*.spdx.json" gh release download v0.1.0 --pattern "*.cyclonedx.json" -# Inspect SBOM +# Available binaries: docker-socket-policy-{go,rs,ts}-linux-{amd64,arm64} +ls -la docker-socket-policy-*-linux-* + +# Inspect SBOM (covers all architectures in the release) cat docker-socket-policy-go.spdx.json | jq '.packages[].name' ``` ## Verify Docker Image Signatures +Docker images are published as **multi-arch manifest indexes** that automatically select the correct architecture (amd64 or arm64) when pulling: + ```bash -# Verify Cosign signature (keyless via OIDC) +# Verify Cosign signature on multi-arch image (verifies entire manifest index) cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ ghcr.io/chainsafe/docker-socket-policy-go: + +# Pull for specific architecture (if you need to override auto-detection) +docker pull --platform linux/amd64 ghcr.io/chainsafe/docker-socket-policy-go: +docker pull --platform linux/arm64 ghcr.io/chainsafe/docker-socket-policy-go: ``` ## Verify SBOMs Attached to Docker Images +SBOMs are attached to the multi-arch manifest index, covering all architectures: + ```bash -# List attestations on an image +# List attestations on the multi-arch image (covers amd64 and arm64) cosign verify-attestation \ --type spdx \ ghcr.io/chainsafe/docker-socket-policy-go: @@ -79,6 +112,9 @@ cosign download attestation \ --type spdx \ ghcr.io/chainsafe/docker-socket-policy-go: \ | jq '.payload | @base64d | fromjson' + +# If you need the SBOM for a specific architecture image, download from release artifacts +# Example: docker-socket-policy-go-linux-arm64.spdx.json ``` ## Build from Source diff --git a/go/Dockerfile b/go/Dockerfile index 2bed941..f032c9c 100644 --- a/go/Dockerfile +++ b/go/Dockerfile @@ -1,11 +1,12 @@ -FROM --platform=linux/amd64 stagex/pallet-go@sha256:d7e9e39ab6e9c254f5eacf3ae5496024dc95e4deaf1402a67fda96f2da007558 AS build +FROM stagex/pallet-go@sha256:d7e9e39ab6e9c254f5eacf3ae5496024dc95e4deaf1402a67fda96f2da007558 AS build ARG VERSION=dev ENV SOURCE_DATE_EPOCH=1 ENV CGO_ENABLED=0 ENV GOOS=linux -ENV GOARCH=amd64 + +SHELL ["/bin/sh", "-c"] WORKDIR /app COPY go.mod go.sum ./ @@ -13,16 +14,27 @@ RUN go mod download COPY . . -RUN --network=none \ - go build \ - -mod=mod \ - -trimpath \ - -buildvcs=false \ - -ldflags="-s -w -buildid= -X main.Version=${VERSION}" \ - -o /docker-socket-policy \ - . +RUN --network=none set -e; \ + echo "[DEBUG] TARGETARCH=${TARGETARCH}"; \ + echo "[DEBUG] uname -m=$(uname -m)"; \ + goarch="${TARGETARCH}"; \ + if [ -z "$goarch" ]; then \ + case "$(uname -m)" in \ + x86_64) goarch="amd64" ;; \ + aarch64) goarch="arm64" ;; \ + *) echo "Unsupported native architecture: $(uname -m)"; exit 1 ;; \ + esac; \ + fi; \ + echo "[DEBUG] Detected goarch=$goarch"; \ + GOARCH="$goarch" go build \ + -mod=mod \ + -trimpath \ + -buildvcs=false \ + -ldflags="-s -w -buildid= -X main.Version=${VERSION}" \ + -o /docker-socket-policy \ + . -FROM --platform=linux/amd64 stagex/core-filesystem@sha256:cd3a66471ce1f630fa77d5c9bd9829f9f9fab6302a1aaa64d67b74f1f069b750 AS run +FROM stagex/core-filesystem@sha256:cd3a66471ce1f630fa77d5c9bd9829f9f9fab6302a1aaa64d67b74f1f069b750 AS run COPY --from=build /docker-socket-policy /docker-socket-policy USER 65532:65532 ENTRYPOINT ["/docker-socket-policy"] diff --git a/rs/Dockerfile b/rs/Dockerfile index 6500975..14c2036 100644 --- a/rs/Dockerfile +++ b/rs/Dockerfile @@ -1,10 +1,12 @@ -FROM --platform=linux/amd64 stagex/pallet-rust@sha256:59d4d0c9e232a05ecb99348f7216b521af1b914a430059dbdb9130018f2afde1 AS build +FROM stagex/pallet-rust@sha256:59d4d0c9e232a05ecb99348f7216b521af1b914a430059dbdb9130018f2afde1 AS build ENV SOURCE_DATE_EPOCH=1 ENV CARGO_TARGET_DIR=/target ENV CARGO_INCREMENTAL=0 ENV RUSTFLAGS="-C codegen-units=1 -C target-feature=+crt-static -C strip=symbols --remap-path-prefix=/app=. --remap-path-prefix=/target=target" +SHELL ["/bin/sh", "-c"] + WORKDIR /app COPY Cargo.toml Cargo.lock ./ RUN mkdir src && echo "fn main() {}" > src/main.rs @@ -12,14 +14,27 @@ RUN cargo fetch --locked --target "$(uname -m)-unknown-linux-musl" 2>/dev/null | COPY src/ src/ -RUN --network=none <<-'EOF' - set -eux - triple="$(uname -m)-unknown-linux-musl" - cargo build --frozen --release --target "${triple}" - install -Dm755 "/target/${triple}/release/docker-socket-policy" /docker-socket-policy -EOF +RUN --network=none set -e; \ + echo "[DEBUG] TARGETPLATFORM=${TARGETPLATFORM}"; \ + echo "[DEBUG] uname -m=$(uname -m)"; \ + platform="${TARGETPLATFORM}"; \ + if [ -z "$platform" ]; then \ + case "$(uname -m)" in \ + x86_64) platform="linux/amd64" ;; \ + aarch64) platform="linux/arm64" ;; \ + *) echo "Unsupported native architecture: $(uname -m)"; exit 1 ;; \ + esac; \ + fi; \ + echo "[DEBUG] Detected platform=$platform"; \ + case "$platform" in \ + linux/amd64) triple="x86_64-unknown-linux-musl" ;; \ + linux/arm64) triple="aarch64-unknown-linux-musl" ;; \ + *) echo "Unsupported platform: $platform"; exit 1 ;; \ + esac; \ + cargo build --frozen --release --target "$triple"; \ + install -Dm755 "/target/$triple/release/docker-socket-policy" /docker-socket-policy -FROM --platform=linux/amd64 scratch AS run +FROM scratch AS run COPY --from=build /docker-socket-policy /docker-socket-policy USER 65532:65532 ENTRYPOINT ["/docker-socket-policy"] diff --git a/ts/Dockerfile b/ts/Dockerfile index 0a0735e..7b18698 100644 --- a/ts/Dockerfile +++ b/ts/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=linux/amd64 stagex/pallet-nodejs@sha256:2e09e7bc24546c76afeac30a50404ce4b2d44aa48c4d4950c40304d8bf7e057f AS build +FROM stagex/pallet-nodejs@sha256:2e09e7bc24546c76afeac30a50404ce4b2d44aa48c4d4950c40304d8bf7e057f AS build ENV SOURCE_DATE_EPOCH=1 @@ -10,7 +10,7 @@ COPY . . RUN npx tsc -FROM --platform=linux/amd64 stagex/core-filesystem@sha256:cd3a66471ce1f630fa77d5c9bd9829f9f9fab6302a1aaa64d67b74f1f069b750 AS run +FROM stagex/core-filesystem@sha256:cd3a66471ce1f630fa77d5c9bd9829f9f9fab6302a1aaa64d67b74f1f069b750 AS run COPY --from=stagex/core-musl@sha256:a06cf7b4a7c57313e21ece02d460b7393eb64e0b3ee1de00e1408392ccf283f3 / / COPY --from=stagex/core-libcxx@sha256:414761e604c54982aaf0a33f7aa942652fa3d2de1e4f0802b15fa2413ad7ce71 / / COPY --from=stagex/core-libcxxabi@sha256:7b515659b7c073329e2cd09037eb0c8bad0a76acd1947efe22c7071a380f465b / /