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
93 changes: 62 additions & 31 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
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
# --- Build arguments ---
ARG DOCKER_GID=999
ARG DOCKER_UID=10001
ARG DOCKER_USER_GID=10001

# Install system dependencies
# --- 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 (pinned) ---
ARG HADOLINT_VERSION=2.15.1
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
Expand All @@ -26,9 +43,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 (pinned) ---
ARG TRIVY_VERSION=0.74.0
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in \
Expand All @@ -41,32 +57,47 @@ 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.
# --- Build and install DockSec ---
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.
# Prepare workspace and clean build sources
WORKDIR /github/workspace
RUN rm -rf /src

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

# --- Docker socket group (host GID match) ---
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

# --- DockSec results directory with correct ownership ---
RUN mkdir -p /home/docksec/.docksec/results && \
chown -R docksec:docksec /home/docksec/.docksec

# Bake HOME and DOCKSEC_RESULTS_DIR into the image so the non-root CLI
# always resolves to a writable directory, regardless of how privileges
# are dropped by the entrypoint. Users can still override with
# `docker run -e DOCKSEC_RESULTS_DIR=...`.
ENV HOME=/home/docksec \
DOCKSEC_RESULTS_DIR=/home/docksec/.docksec/results

# --- Entrypoint ---
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"]
33 changes: 28 additions & 5 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
#!/bin/bash
set -e

# Set environment variables from inputs.
# ---------------------------------------------------------------------------
# Privilege dropping (non-root hardening)
# ---------------------------------------------------------------------------
# GitHub Docker container actions MUST run as root to access GITHUB_WORKSPACE.
# For standalone `docker run`, drop to the non-root `docksec` user for
# least-privilege execution. Users can override with `docker run --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."
# Ensure a writable HOME for the non-root user so results go to
# ~/.docksec/results instead of the current working directory.
export HOME=/home/docksec
export DOCKSEC_RESULTS_DIR=/home/docksec/.docksec/results
mkdir -p "$DOCKSEC_RESULTS_DIR"
chown -R docksec:docksec "$HOME/.docksec"
exec gosu docksec "$0" "$@"
fi

# ---------------------------------------------------------------------------
# Set environment variables from Action 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
# DocksecConfig validation and aborts the run with a traceback, even in
# --scan-only mode where no provider is needed at all.
# ---------------------------------------------------------------------------
for var in OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY LLM_PROVIDER LLM_MODEL; do
input_var="INPUT_${var}"
if [ -n "${!input_var}" ]; then
export "${var}=${!input_var}"
fi
done

# Run DockSec
# The inputs are passed as environment variables prefixed with INPUT_
# e.g., inputs.dockerfile becomes INPUT_DOCKERFILE

# ---------------------------------------------------------------------------
# Build the DockSec argument list from Action inputs and CLI args.
# ---------------------------------------------------------------------------
ARGS=()

if [ -n "${INPUT_DOCKERFILE}" ]; then
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