Skip to content
Open
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
78 changes: 47 additions & 31 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
FROM python:3.12-slim

# Pinned tool versions. Unpinned installs make the image non-reproducible and
# let an upstream release change scan results without a DockSec change.
ARG TRIVY_VERSION=0.74.0
ARG HADOLINT_VERSION=2.15.1
ARG DOCKER_GID=999
ARG DOCKER_UID=10001
ARG DOCKER_USER_GID=10001

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
curl \
git \
ca-certificates \
gosu \
&& rm -rf /var/lib/apt/lists/*

# Install Hadolint.
# -f is required: without it curl exits 0 on an HTTP error and writes the error
# page to the destination, producing a "successful" build with a text file where
# the binary should be.
# Docker CLI (GPG-signed APT repository)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg \
&& install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc \
&& chmod a+r /etc/apt/keyrings/docker.asc \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends docker-ce-cli \
&& rm -rf /var/lib/apt/lists/* \
&& docker --version

# Linter install
ARG HADOLINT_VERSION=2.15.1
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
Expand All @@ -26,9 +41,8 @@ RUN set -eux; \
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-${hadolint_arch}"; \
chmod +x /usr/local/bin/hadolint

# Install Trivy from the release tarball rather than the install script, so the
# download is version-addressed and a redirect or a moved script cannot silently
# change what lands in the image.
# Scanner install
ARG TRIVY_VERSION=0.74.0
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
Expand All @@ -41,32 +55,34 @@ RUN set -eux; \
tar -xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy; \
rm -f /tmp/trivy.tar.gz

# Fail the build if either tool is missing or not executable. The previous
# install steps could fail silently, shipping an image whose every scan errored
# with "Missing required tools: trivy".
RUN trivy --version && hadolint --version
# Fail the build if any tool is missing
RUN trivy --version && hadolint --version && docker --version

# Build and install from a dedicated directory. /github/workspace is where the
# Action mounts the user's repository, so installing there both shipped the
# DockSec source to every user and left files that a bind mount then shadowed.
WORKDIR /src
COPY . .

# Install DockSec with the AI extra: the image backs the GitHub Action, which
# exposes AI analysis inputs, so a scan-only install would make those inputs fail.
RUN pip install --no-cache-dir ".[ai]"

# Fail the build if the CLI itself is not importable and runnable.
RUN docksec --version

# The Action mounts the repository being scanned here; it must start empty.
# Leaving /src first so the build sources can be removed rather than left behind.
WORKDIR /github/workspace
RUN rm -rf /src

# Copy and set permissions for entrypoint
RUN groupadd -g ${DOCKER_USER_GID} docksec && \
useradd -m -u ${DOCKER_UID} -g docksec -s /sbin/nologin -c "DockSec App User" docksec

RUN if getent group ${DOCKER_GID} >/dev/null; then \
existing=$(getent group ${DOCKER_GID} | cut -d: -f1); \
usermod -aG "$existing" docksec; \
else \
groupadd -g ${DOCKER_GID} docker_host && \
usermod -aG docker_host docksec; \
fi

RUN mkdir -p /home/docksec/.docksec/results && \
chown -R docksec:docksec /home/docksec/.docksec

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN chmod +x /entrypoint.sh && \
chown docksec:docksec /entrypoint.sh && \
chown -R docksec:docksec /github/workspace

# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]
16 changes: 16 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
#!/bin/bash
set -e

# ---------------------------------------------------------------------------
# Privilege dropping (non-root hardening)
# ---------------------------------------------------------------------------
# GitHub Docker container actions MUST run as root to access GITHUB_WORKSPACE.
# For standalone `docker run`, we drop to the non-root `docksec` user for
# least-privilege execution. Users can override with `--user`.
#
# Detection:
# - GITHUB_WORKSPACE is set -> GitHub Action mode -> stay root
# - UID != 0 -> already non-root -> continue
# - otherwise -> standalone -> drop to docksec
if [ "$(id -u)" = "0" ] && [ -z "${GITHUB_WORKSPACE:-}" ]; then
echo "Standalone run detected: dropping privileges to 'docksec' user."
exec gosu docksec "$0" "$@"
fi

# Set environment variables from inputs.
# Only export values that were actually provided: exporting an empty string is
# not the same as leaving a variable unset. An empty LLM_PROVIDER fails
Expand Down
52 changes: 52 additions & 0 deletions tests/smoke-test-nonroot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Smoke tests for non-root container hardening.
#
# These tests exercise the entrypoint.sh privilege-drop logic. They do NOT
# use --entrypoint to override the entrypoint, because doing so would bypass
# the very code path being tested.
set -euo pipefail

IMAGE="${IMAGE:-docksec:smoke-test}"
DOCKER_GID="$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo 999)"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

cleanup() {
docker rmi -f "$IMAGE" >/dev/null 2>&1 || true
}
trap cleanup EXIT

echo "==> Building image (DOCKER_GID=$DOCKER_GID)..."
docker build \
--build-arg "DOCKER_GID=$DOCKER_GID" \
-t "$IMAGE" "$REPO_ROOT"

echo "==> Test 1: standalone run must drop privileges (entrypoint.sh)"
OUT="$(docker run --rm "$IMAGE" --version 2>&1)"
echo " $OUT" | head -3
if ! echo "$OUT" | grep -q "dropping privileges to 'docksec'"; then
echo "FAIL: entrypoint.sh did not drop privileges in standalone mode" >&2
exit 1
fi
echo " OK: standalone run drops privileges"

echo "==> Test 2: CLI works as non-root"
docker run --rm "$IMAGE" --version >/dev/null
echo " OK: --version"
docker run --rm "$IMAGE" --help >/dev/null
echo " OK: --help"

echo "==> Test 3: GitHub Action mode (GITHUB_WORKSPACE set) must NOT drop"
GH_OUT="$(docker run --rm -e GITHUB_WORKSPACE=/github/workspace "$IMAGE" --version 2>&1)"
echo " $GH_OUT" | head -3
if echo "$GH_OUT" | grep -q "dropping privileges"; then
echo "FAIL: GH Action mode should NOT drop privileges" >&2
exit 1
fi
echo " OK: GH Action mode stays root"

echo "==> Test 4: explicit --user override"
docker run --rm --user 0 "$IMAGE" --version >/dev/null
echo " OK: --user 0 override works"

echo ""
echo "All smoke tests passed."
Loading