# ============================================================ # LokiDeploy - Secure Multi-Stage Dockerfile # ============================================================ # P0 Fixes: # - Non-root user (lokideploy:1000) # - No hardcoded JWT_SECRET (validated at runtime) # - HEALTHCHECK instruction added # - Binary installation verification # P1 Fixes: # - Supply chain risk reduction (--ignore-scripts where applicable) # - File ownership with --chown # - Security environment variables # ============================================================ # --------------------------------------------------------------- # Stage 1: Frontend Builder (Node.js) # --------------------------------------------------------------- FROM node:20-alpine AS frontend-builder WORKDIR /app/frontend # Install dependencies separately to leverage layer caching COPY frontend/package*.json ./ # Use --ignore-scripts to reduce supply chain attack surface RUN npm ci --ignore-scripts && \ npm cache clean --force # Copy source and build COPY frontend/ ./ RUN npm run build # --------------------------------------------------------------- # Stage 2: Backend Builder (Go) # --------------------------------------------------------------- FROM golang:1.24-alpine AS backend-builder RUN apk add --no-cache git ca-certificates tzdata ENV GOTOOLCHAIN=local WORKDIR /app/backend # Download dependencies (cached layer) COPY backend/go.* ./ RUN go mod download # Copy source and build static binary COPY backend/ ./ RUN CGO_ENABLED=0 GOOS=linux go build \ -a -installsuffix cgo \ -ldflags="-s -w -extldflags '-static'" \ -o bin/lokideploy . # --------------------------------------------------------------- # Stage 3: Final Runtime Image # --------------------------------------------------------------- FROM alpine:3.20 # Install runtime dependencies: # ca-certificates: HTTPS TLS verification # openssh-client: SSH key auth for host deployment # curl: kubectl/helm download + runtime checks # bash: Entrypoint script support # wget: HEALTHCHECK (lighter than curl) RUN apk --no-cache add \ ca-certificates \ openssh-client \ curl \ bash \ wget ARG TARGETARCH=amd64 # ---- Install kubectl with checksum verification ---- # P1: curl downloads must have integrity verification ENV KUBECTL_VERSION=v1.31.0 ENV KUBECTL_SHA256_AMD64=e3c5b9c15e96cd8d70b3fdf87f38197d0b92e01fb920b5d8a40a3f4c5cb4e84a ENV KUBECTL_SHA256_ARM64=f9147d84b7dc0630cf4f9d7769c36e65a0885bbc0a34c5dc65b9d93c287d83b9 RUN set -eux; \ if [ "$TARGETARCH" = "amd64" ]; then \ KUBECTL_SHA256="$KUBECTL_SHA256_AMD64"; \ elif [ "$TARGETARCH" = "arm64" ]; then \ KUBECTL_SHA256="$KUBECTL_SHA256_ARM64"; \ else \ echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1; \ fi; \ curl -fsSL -o /tmp/kubectl \ "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl"; \ echo "${KUBECTL_SHA256} /tmp/kubectl" | sha256sum -c -; \ mv /tmp/kubectl /usr/local/bin/kubectl; \ chmod +x /usr/local/bin/kubectl; \ # P1: Verify installation kubectl version --client || { echo "kubectl installation verification failed"; exit 1; } # ---- Install helm with checksum verification ---- ENV HELM_VERSION=v3.16.3 # Helm checksums from official releases (https://github.com/helm/helm/releases) # helm-v3.16.3-linux-amd64.tar.gz ENV HELM_SHA256_AMD64=ad01a74073a785c33121d277100b043246217790d8d30c9f039e9c2dd5769d91 # helm-v3.16.3-linux-arm64.tar.gz ENV HELM_SHA256_ARM64=5bd34b755e58f27f1c4bc8edaef44152d0e7b9f5978e6d0aac394a3c81951498 RUN set -eux; \ HELM_TAR="helm-${HELM_VERSION}-linux-${TARGETARCH}.tar.gz"; \ if [ "$TARGETARCH" = "amd64" ]; then \ HELM_SHA256="$HELM_SHA256_AMD64"; \ elif [ "$TARGETARCH" = "arm64" ]; then \ HELM_SHA256="$HELM_SHA256_ARM64"; \ else \ echo "Unsupported TARGETARCH: $TARGETARCH" >&2; exit 1; \ fi; \ curl -fsSL -o "/tmp/${HELM_TAR}" \ "https://get.helm.sh/${HELM_TAR}"; \ # P1: SHA256 integrity verification echo "${HELM_SHA256} /tmp/${HELM_TAR}" | sha256sum -c -; \ mkdir -p /tmp/helm; \ tar -xzf "/tmp/${HELM_TAR}" -C /tmp/helm; \ mv "/tmp/helm/linux-${TARGETARCH}/helm" /usr/local/bin/helm; \ chmod +x /usr/local/bin/helm; \ rm -rf /tmp/helm*; \ # P1: Verify installation helm version --client || { echo "helm installation verification failed"; exit 1; } # --------------------------------------------------------------- # P0: Create non-root user and group # --------------------------------------------------------------- RUN addgroup -g 1000 lokideploy && \ adduser -u 1000 -G lokideploy -s /bin/sh -D lokideploy # --------------------------------------------------------------- # Working directory with proper ownership # --------------------------------------------------------------- WORKDIR /app RUN chown lokideploy:lokideploy /app # ---- Copy application files with correct ownership (P1) ---- COPY --from=frontend-builder --chown=lokideploy:lokideploy /app/frontend/dist ./dist COPY --from=backend-builder --chown=lokideploy:lokideploy /app/backend/bin/lokideploy ./lokideploy # Create data directory with non-root ownership RUN mkdir -p /app/data && chown -R lokideploy:lokideploy /app/data # Create .kube directory for non-root user (P0: not /root/.kube) RUN mkdir -p /home/lokideploy/.kube && \ chown -R lokideploy:lokideploy /home/lokideploy && \ chmod 700 /home/lokideploy/.kube # ---- Security hardening: remove unnecessary tools ---- RUN rm -rf /tmp/* /var/cache/apk/* # --------------------------------------------------------------- # Security environment variables # --------------------------------------------------------------- ENV DATA_DIR=/app/data ENV SERVER_PORT=8080 ENV KUBECONFIG=/home/lokideploy/.kube/config # HOME must be set for kubectl/helm to find config ENV HOME=/home/lokideploy # NOTE: JWT_SECRET and ENCRYPTION_KEY are NOT set here. # They MUST be provided at runtime via docker-compose env or secrets. # The entrypoint script will enforce this requirement. # ---- Copy and configure entrypoint script ---- COPY --chown=lokideploy:lokideploy docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh EXPOSE 8080 # --------------------------------------------------------------- # P0: HEALTHCHECK - verifies static file serving (no auth needed) # --------------------------------------------------------------- HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD wget --no-verbose --tries=1 --spider \ http://localhost:${SERVER_PORT}/index.html || exit 1 # --------------------------------------------------------------- # P0: Switch to non-root user # --------------------------------------------------------------- USER lokideploy ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["./lokideploy"]