diff --git a/.just/security.just b/.just/security.just new file mode 100644 index 0000000000..28f43a1cc3 --- /dev/null +++ b/.just/security.just @@ -0,0 +1,35 @@ +# Security scanning with Trivy (https://trivy.dev) + +trivy_image := "aquasec/trivy:latest" +trivy_severity := env("TRIVY_SEVERITY", "CRITICAL,HIGH") +trivy_cache_volume := "trivy-cache" +scan_images := env("SCAN_IMAGES", "evstack:local-dev") + +trivy_run := "docker run --rm -v " + trivy_cache_volume + ":/root/.cache/ -e TRIVY_SEVERITY=" + trivy_severity + +# Run all Trivy security scans (filesystem + Docker images) +trivy-scan: trivy-scan-fs trivy-scan-image + +# Scan repo for dependency vulnerabilities, misconfigs, and secrets +trivy-scan-fs: + @echo "--> Scanning repository filesystem with Trivy" + @{{trivy_run}} \ + -v {{justfile_directory()}}:/workspace \ + {{trivy_image}} \ + fs --scanners vuln,misconfig,secret \ + --severity {{trivy_severity}} \ + /workspace + @echo "--> Filesystem scan complete" + +# Scan built Docker images for vulnerabilities +trivy-scan-image: + @echo "--> Scanning Docker images with Trivy" + @for img in {{scan_images}}; do \ + echo "--> Scanning image: $img"; \ + {{trivy_run}} \ + -v /var/run/docker.sock:/var/run/docker.sock \ + {{trivy_image}} \ + image --severity {{trivy_severity}} \ + $img; \ + done + @echo "--> Image scan complete" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dcf30f689..8c20dbeb93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes - Store pending blocks separately from executed blocks key. [#3073](https://github.com/evstack/ev-node/pull/3073) +- **BREAKING:** Docker images for `evm`, `testapp`, and `local-da` now run as non-root user `ev-node` (uid 1000) instead of `root`. Existing volumes or bind mounts with root-owned files may require a `chown` to uid 1000. See the [migration guide](https://ev.xyz/guides/migrate-docker-nonroot). - Fixes issues with force inclusion verification on sync nodes. [#3057](https://github.com/evstack/ev-node/pull/3057) - Add flag to `local-da` to produce empty DA blocks (closer to the real system). [#3057](https://github.com/evstack/ev-node/pull/3057) - Validate P2P DA height hints against the latest known DA height to prevent malicious peers from triggering runaway catchup . [#3128](https://github.com/evstack/ev-node/pull/3128) diff --git a/apps/evm/Dockerfile b/apps/evm/Dockerfile index 009111fce8..fd4717e8d7 100644 --- a/apps/evm/Dockerfile +++ b/apps/evm/Dockerfile @@ -17,10 +17,16 @@ FROM alpine:3.22.2 #hadolint ignore=DL3018 RUN apk --no-cache add ca-certificates curl -WORKDIR /root +RUN addgroup -g 1000 ev-node && \ + adduser -u 1000 -G ev-node -s /bin/sh -D ev-node + +WORKDIR /home/ev-node COPY --from=build-env /src/apps/evm/evm /usr/bin/evm COPY apps/evm/entrypoint.sh /usr/bin/entrypoint.sh -RUN chmod +x /usr/bin/entrypoint.sh +RUN chmod +x /usr/bin/entrypoint.sh && \ + chown -R ev-node:ev-node /home/ev-node + +USER ev-node ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/apps/testapp/Dockerfile b/apps/testapp/Dockerfile index fe3701947c..58258e6e8a 100644 --- a/apps/testapp/Dockerfile +++ b/apps/testapp/Dockerfile @@ -27,8 +27,14 @@ RUN go mod download && cd apps/testapp && go install . # FROM base +RUN groupadd -g 1000 ev-node && \ + useradd -u 1000 -g ev-node -s /bin/sh -m ev-node + COPY --from=builder /go/bin/testapp /usr/bin -WORKDIR /apps +WORKDIR /home/ev-node +RUN chown -R ev-node:ev-node /home/ev-node + +USER ev-node ENTRYPOINT ["testapp"] diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9b1def7546..34753c21fb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -309,6 +309,10 @@ function sidebarHome() { text: "Migrating to ev-abci", link: "/guides/migrating-to-ev-abci", }, + { + text: "Migrate Docker to non-root", + link: "/guides/migrate-docker-nonroot", + }, { text: "Create genesis for your chain", link: "/guides/create-genesis", diff --git a/docs/guides/migrate-docker-nonroot.md b/docs/guides/migrate-docker-nonroot.md new file mode 100644 index 0000000000..8f0f41a7d9 --- /dev/null +++ b/docs/guides/migrate-docker-nonroot.md @@ -0,0 +1,112 @@ +# Migrating Docker Containers to Non-Root User + +Starting with this release, the `evm`, `testapp`, and `local-da` Docker images run as a non-root user (`ev-node`, uid/gid 1000) instead of `root`. This aligns with the `grpc` image, which already ran as non-root. + +If you are running any of these containers with **persistent volumes or bind mounts**, you need to fix file ownership before upgrading. Containers running without persistent storage (ephemeral) require no action. + +## Who is affected + +You are affected if **all** of the following are true: + +- You run `evm`, `testapp`, or `local-da` via Docker (or docker-compose / Kubernetes) +- You use a volume or bind mount for the container's data directory +- The files in that volume were created by a previous (root-based) image + +## Migration steps + +### 1. Stop the running container + +```bash +docker stop +``` + +### 2. Fix file ownership on the volume + +For **bind mounts** (host directory), run `chown` directly on the host: + +```bash +# Replace /path/to/data with your actual data directory +sudo chown -R 1000:1000 /path/to/data +``` + +For **named Docker volumes**, use a temporary container: + +```bash +# Replace with your Docker volume name +docker run --rm -v :/data alpine chown -R 1000:1000 /data +``` + +### 3. Pull the new image and restart + +```bash +docker pull +docker start +``` + +### Kubernetes / docker-compose + +If you manage containers through orchestration, you have two options: + +**Option A: Init container (recommended for Kubernetes)** + +Add an init container that fixes ownership before the main container starts: + +```yaml +initContainers: + - name: fix-permissions + image: alpine:3.22 + command: ["chown", "-R", "1000:1000", "/home/ev-node"] + volumeMounts: + - name: data + mountPath: /home/ev-node +securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 +``` + +**Option B: Set `fsGroup` in the pod security context** + +If your volume driver supports it, setting `fsGroup: 1000` will automatically fix ownership on mount: + +```yaml +securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 +``` + +**docker-compose**: update your `docker-compose.yml` to set the user: + +```yaml +services: + evm: + image: evm:latest + user: "1000:1000" + volumes: + - evm-data:/home/ev-node +``` + +## Verifying the migration + +After restarting, confirm the container runs as the correct user: + +```bash +docker exec id +# Expected: uid=1000(ev-node) gid=1000(ev-node) +``` + +Check that the process can read and write its data directory: + +```bash +docker exec ls -la /home/ev-node +# All files should be owned by ev-node:ev-node +``` + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| `Permission denied` on startup | Volume files still owned by root | Re-run the `chown` step above | +| Container exits immediately | Data directory not writable | Check ownership and directory permissions (`drwxr-xr-x` or more permissive for uid 1000) | +| Application writes to wrong path | Old `WORKDIR` was `/root` or `/apps` | Update any custom volume mounts to target `/home/ev-node` instead | diff --git a/justfile b/justfile index 4259dcc461..805b6e372b 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,7 @@ import '.just/lint.just' import '.just/codegen.just' import '.just/run.just' import '.just/tools.just' +import '.just/security.just' # List available recipes when running `just` with no args default: diff --git a/tools/local-da/Dockerfile b/tools/local-da/Dockerfile index 134eeaa6b1..6fad734e0f 100644 --- a/tools/local-da/Dockerfile +++ b/tools/local-da/Dockerfile @@ -17,9 +17,15 @@ FROM alpine:3.22.2 #hadolint ignore=DL3018 RUN apk --no-cache add ca-certificates curl -WORKDIR /root +RUN addgroup -g 1000 ev-node && \ + adduser -u 1000 -G ev-node -s /bin/sh -D ev-node + +WORKDIR /home/ev-node +RUN chown -R ev-node:ev-node /home/ev-node COPY --from=build-env /src/build/local-da /usr/bin/local-da +USER ev-node + ENTRYPOINT ["/usr/bin/local-da"] CMD ["-listen-all"]