DEV Community

Wajahat Ali Abid
Wajahat Ali Abid

Posted on

Secure Docker Builds with Secrets & Multi-Platform CI

1. Concept: Build Secrets vs. Build Args

When building Docker images, you often need sensitive data (NuGet tokens, NPM keys, API credentials) to fetch dependencies.

  • Build Args (ARG): These are stored in the image's history. Running docker history <image> will reveal these values to anyone with access to the image. Never use these for secrets.
  • Docker Secrets (--secret): These are temporarily mounted into a specific RUN command. They exist only in memory or as a temporary file during that specific layer's execution. They are never committed to the image layers.

2. The Universal Dockerfile Template

This template uses a multi-stage build, a non-root user for security, and the Secret Mount syntax.

# --- Stage 1: Build ---
FROM mcr.microsoft.com/dotnet/sdk:10.0-bookworm-slim AS build-env
WORKDIR /app

# Non-sensitive configuration via ARGs
ARG PACKAGE_SOURCE_URL
ARG SOURCE_NAME
ARG SOURCE_USER

# Copy project files for restoration
COPY *.slnx ./
COPY src/**/*.csproj ./src/

# Use the Secret Mount to safely handle the token
# The id 'app_token' must match the CLI --secret id
RUN --mount=type=secret,id=app_token \
    TOKEN=$(cat /run/secrets/app_token) && \
    dotnet nuget add source "$PACKAGE_SOURCE_URL" \
    --name "$SOURCE_NAME" \
    --username "$SOURCE_USER" \
    --password "$TOKEN" \
    --store-password-in-clear-text && \
    dotnet restore

# Copy remaining source and publish
COPY . .
RUN dotnet publish -c Release -o /app/output --no-restore

# --- Stage 2: Runtime ---
FROM mcr.microsoft.com/dotnet/runtime:10.0-bookworm-slim
WORKDIR /app

# Security: Create a non-root user
# 'useradd' is used instead of 'adduser' for compatibility with 'slim' images
RUN useradd -m -d /app -s /bin/bash appuser

# Copy output and set ownership
COPY --from=build-env --chown=appuser:appuser /app/output .

# Optional: Install diagnostics tools
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

USER appuser
ENTRYPOINT ["dotnet", "MyApplication.dll"]
Enter fullscreen mode Exit fullscreen mode

3. CI/CD Integration

A. GitHub Actions Implementation

GitHub Actions requires you to explicitly map a Secret to an environment variable in the shell before Docker can consume it.

- name: Build Docker Image
  env:
    # Map the GitHub Secret to a shell environment variable
    READONLY_TOKEN: ${{ secrets.ORG_READONLY_TOKEN }}
    DOCKER_BUILDKIT: 1 # Ensure BuildKit is enabled
  run: |
    docker build \
      --secret id=app_token,env=READONLY_TOKEN \
      --build-arg PACKAGE_SOURCE_URL="${{ secrets.SOURCE_URL }}" \
      --build-arg SOURCE_NAME="InternalFeed" \
      --build-arg SOURCE_USER="${{ secrets.SOURCE_USER }}" \
      -t my-app:latest .
Enter fullscreen mode Exit fullscreen mode

B. AWS CodeBuild Implementation

CodeBuild retrieves secrets from AWS Secrets Manager and populates them as environment variables automatically.

version: 0.2

env:
  # Pulls directly from AWS Secrets Manager
  secrets-manager:
    BUILD_TOKEN: "path/to/secret:key_name"

phases:
  build:
    commands:
      - export DOCKER_BUILDKIT=1
      - |
        docker build \
          --secret id=app_token,env=BUILD_TOKEN \
          --build-arg PACKAGE_SOURCE_URL="${SOURCE_URL}" \
          --build-arg SOURCE_NAME="InternalRepo" \
          --build-arg SOURCE_USER="${SOURCE_USER}" \
          -f Dockerfile \
          -t $REPOSITORY_URI:latest .
Enter fullscreen mode Exit fullscreen mode

4. Critical Troubleshooting Guide

Issue Root Cause Solution
adduser: not found Using a Debian "slim" image. Use useradd (the low-level binary) instead of adduser.
Both UserName and Password must be specified A variable passed to dotnet nuget is empty. 1. Ensure ARG is declared after the FROM line. 2. Wrap all vars in double quotes "$VAR".
Secret not found ID mismatch between CLI and Dockerfile. Ensure --secret id=XYZ matches RUN --mount=type=secret,id=XYZ.
Invalid Build Flag BuildKit is disabled. Set DOCKER_BUILDKIT=1 or use docker buildx build.

5. Best Practices Checklist

  • [ ] Multi-stage builds: Always separate the SDK (Build) from the Runtime (Execution) to minimize attack surface.
  • [ ] Non-root execution: Never run your application as root inside the container.
  • [ ] Layer Optimization: Copy .csproj or .slnx files and restore before copying the full source code to take advantage of layer caching.
  • [ ] Secret Hygiene: Never echo secrets to the console. To verify if a secret exists during debug, check its length: echo ${#TOKEN}.
  • [ ] Quotes: Always wrap shell variables in double quotes to prevent word-splitting if a password or URL contains special characters.

Top comments (0)