diff --git a/CHANGELOG.md b/CHANGELOG.md index 98961e6228..46335c85c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,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). ## v1.0.0-rc.4 diff --git a/Makefile b/Makefile index 1b86d6f70b..7be0a1e977 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ include ./scripts/test.mk include ./scripts/proto.mk include ./scripts/utils.mk include ./scripts/run.mk +include ./scripts/security.mk include ./tools/tools.mk # Sets the default make target to `build`. 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 23ff14e9b0..398bc8b846 100644 --- a/apps/testapp/Dockerfile +++ b/apps/testapp/Dockerfile @@ -27,8 +27,14 @@ RUN go mod download && make 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 f7e631f71c..3149d8afd6 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -305,6 +305,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/scripts/security.mk b/scripts/security.mk new file mode 100644 index 0000000000..dd2fb116e0 --- /dev/null +++ b/scripts/security.mk @@ -0,0 +1,43 @@ +# security.mk - Security scanning with Trivy (https://trivy.dev) + +TRIVY_IMAGE := aquasec/trivy:latest +TRIVY_SEVERITY ?= CRITICAL,HIGH +TRIVY_CACHE_VOLUME := trivy-cache + +# Docker images to scan (space-separated, override or extend as needed) +SCAN_IMAGES ?= evstack:local-dev + +# Common docker run args for Trivy +TRIVY_RUN := docker run --rm \ + -v $(TRIVY_CACHE_VOLUME):/root/.cache/ \ + -e TRIVY_SEVERITY=$(TRIVY_SEVERITY) + +## trivy-scan: Run all Trivy security scans (filesystem + Docker images) +trivy-scan: trivy-scan-fs trivy-scan-image +.PHONY: trivy-scan + +## trivy-scan-fs: Scan repo for dependency vulnerabilities, misconfigs, and secrets +trivy-scan-fs: + @echo "--> Scanning repository filesystem with Trivy" + @$(TRIVY_RUN) \ + -v $(CURDIR):/workspace \ + $(TRIVY_IMAGE) \ + fs --scanners vuln,misconfig,secret \ + --severity $(TRIVY_SEVERITY) \ + /workspace + @echo "--> Filesystem scan complete" +.PHONY: trivy-scan-fs + +## trivy-scan-image: 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" +.PHONY: trivy-scan-image diff --git a/tools/local-da/Dockerfile b/tools/local-da/Dockerfile index 0364731aa8..622dff8db8 100644 --- a/tools/local-da/Dockerfile +++ b/tools/local-da/Dockerfile @@ -19,9 +19,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"]