Image hardening
Container images must be small, reproducible, traceable, and safe to run with least privilege. This guide defines our baseline for production images and aligns image construction with the image and Dockerfile controls in Section 4 of the CIS Docker Benchmark.
This is an image-build standard. Customers remain responsible for hardening their container runtime, hosts, orchestrator, network, and deployment policies. Describe an image as CIS Docker Benchmark Section 4 aligned only when the applicable controls have been assessed and any exceptions are documented. Do not make an unqualified claim that the complete deployment is CIS compliant.
Required practices
Use trusted and immutable base images
- Use official or otherwise approved images from a trusted registry.
- Use a currently supported release and track its end-of-support date.
- Pin every build and runtime base to its multi-platform SHA-256 digest.
- Keep the human-readable tag next to the digest so maintainers can see the expected version.
- Review and update pinned digests regularly. Digest pinning prevents an unexpected change; it does not deliver security updates automatically.
FROM golang:1.25.14-bookworm@sha256:<builder-image-digest> AS builderNever use floating tags such as latest for production builds.
Use multi-stage builds
Compilers, source code, package managers, and build tools belong in a builder stage, not in the image delivered to a customer. Copy only the files required at runtime into the final stage.
For a Go service, copy go.mod and go.sum before application source. This
allows dependency downloads to be cached independently while ensuring that a
source-code change does not invalidate the dependency layer.
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
go mod download && \
go mod verify
COPY main.go ./
COPY internal/ ./internal/Prefer explicit COPY instructions over COPY . .. Explicit copies prevent
credentials, repository metadata, local binaries, and unrelated source files
from entering the build context or cache.
Protect the build context
Every repository that builds an image must have a .dockerignore, even when
the Dockerfile uses explicit copies.
.git
.github
.devcontainer
.vscode
.env
.env.*
coverage.out
*.test
mainAdd project-specific generated data, local state, test output, and credentials to this list. Never rely on a multi-stage build alone to protect sensitive files: ignored final-stage files can still be sent to the builder and retained in its cache.
Keep dependency verification enabled
Do not set GOSUMDB=off globally. Public Go dependencies must continue to use
the public checksum database. Scope private modules with GOPRIVATE, which
also sets the default behavior for GONOPROXY and GONOSUMDB.
ENV GOPRIVATE=github.com/uug-ai/*Continue to commit go.sum, use -mod=readonly, and run go mod verify.
Never pass credentials as build arguments
Docker ARG and ENV are not secret transports. Credentials can be exposed
through image metadata, build history, logs, or cache. If a clean build needs
access to a private module, use a
BuildKit secret mount.
RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \
git config --global \
url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf \
"https://github.com/" && \
go mod download && \
go mod verify && \
rm -f /root/.gitconfigSupply the secret from CI without writing it to the repository:
docker buildx build \
--secret id=github_token,env=GITHUB_TOKEN \
.Use a short-lived, read-only token with access only to the required modules. When all dependencies are public, do not provide a token.
Produce a static Go binary when possible
A static binary allows the final image to omit libc and an operating-system distribution. Use reproducible build flags and verify both supported architectures.
ARG TARGETOS=linux
ARG TARGETARCH
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" \
go build \
-mod=readonly \
-trimpath \
-buildvcs=false \
-tags timetzdata,netgo \
-ldflags="-s -w -buildid=" \
-o /out/main \
.Do not set CGO_ENABLED=0 without testing the resulting application. Database
drivers, Kafka clients, image-processing libraries, and hardware integrations
may require CGO or shared libraries. When they do, use a minimal, supported,
digest-pinned distroless runtime containing only the required libraries.
Prefer an empty or distroless runtime
Use scratch for a verified static binary:
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/ca-certificates.crt
COPY --from=builder --chown=65532:65532 /out/main /app/main
USER 65532:65532
WORKDIR /app
EXPOSE 8080/tcp
ENTRYPOINT ["/app/main"]scratch is a special empty base. It contains no operating system, shell,
package manager, libc, or debugging tools. Copy CA certificates when the
service makes TLS connections. Compile timezone data into the Go binary or copy
it explicitly when the application requires it.
Use a minimal distroless image instead when the application requires dynamic
libraries. Do not install a shell, curl, a compiler, or a package manager in
the final image merely for convenience.
Run as a fixed non-root identity
- Set
USERto an explicit numeric UID and GID. - Use a high, unprivileged ID consistently;
65532:65532is our default for images without an/etc/passwd. - Copy runtime files with explicit ownership.
- Keep application files read-only for the runtime identity.
- Write only to documented data or temporary paths.
- Listen on ports above 1024.
Numeric identities work in scratch without a user database because the Linux
kernel runs the process directly with those IDs.
Do not add capabilities or special permission bits
A normal service image must not contain setuid or setgid executables, and its
application binary must not have file capabilities. In particular, do not add
CAP_NET_BIND_SERVICE when the service can listen on an unprivileged port such
as 8080.
Runtime platforms can override image defaults, so customers must still enforce capability dropping, non-root execution, privilege-escalation prevention, and a read-only root filesystem in their deployment policy.
Add health checks only when they are valid
Do not weaken a minimal image by installing curl or a shell only to implement
a health check. A scratch or distroless image should expose a health-check
subcommand in the application or include a purpose-built static probe.
The check must:
- Exercise a meaningful readiness or liveness signal.
- Have a bounded timeout.
- Return a non-zero status when unhealthy.
- Avoid credentials and external dependencies where possible.
Do not publish a health check that starts a second copy of the service or tests the wrong port. If the customer orchestrator owns health checking, document the endpoint, port, timeout, and expected response as part of the image contract.
Add traceable metadata
Add standard OCI labels and populate release-specific values from CI:
ARG VERSION=dev
ARG REVISION=unknown
ARG CREATED
LABEL org.opencontainers.image.title="service-name" \
org.opencontainers.image.source="https://github.com/uug-ai/service-name" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}" \
org.opencontainers.image.vendor="UUG.AI"Publish immutable image digests alongside readable version tags.
CI and release controls
Hardening is not complete when the Dockerfile builds. CI must:
- Build every supported architecture from the same source revision.
- Scan the final image for operating-system and application dependency vulnerabilities.
- Scan the repository, Dockerfile, and image for embedded secrets and misconfiguration.
- Fail the release for prohibited findings. At minimum, critical and high vulnerabilities require remediation or an approved exception with an owner and expiry date.
- Generate an SPDX or CycloneDX software bill of materials.
- Generate build provenance rather than disabling it.
- Sign the final multi-platform image with Cosign or an equivalent signing system.
- Retain the scan reports, SBOM, provenance, signature, source revision, base image digest, and final image digest as release evidence.
Scanning must be release-blocking. A scanner configured to report findings but always exit successfully is informational, not an enforcement control.
Image contract
Document the following for customers:
- Image registry, version tag, immutable digest, and supported architectures.
- Numeric UID and GID.
- Entrypoint and exposed ports.
- Required environment variables and secrets, without example credentials.
- Required CA certificates and timezone behavior.
- Writable paths and expected volume mounts.
- Health and metrics endpoints.
- Expected outbound connections.
- SBOM, signature-verification instructions, and vulnerability policy.
The customer is responsible for runtime controls such as seccomp or AppArmor, capability dropping, read-only filesystem enforcement, secrets injection, resource limits, network policy, and orchestrator configuration.
Review checklist
Before releasing an image, verify:
- Builder and runtime bases are approved, supported, and digest-pinned.
- The final image contains only required runtime files.
-
.dockerignoreexcludes credentials, VCS metadata, and generated files. - No credential is passed through
ARG,ENV, a copied file, or a layer. - Public dependency checksum verification remains enabled.
- Private modules are scoped with
GOPRIVATE. - The application runs as a fixed numeric non-root UID/GID.
- No shell, package manager, unnecessary utility, capability, setuid file, or setgid file is present.
- TLS certificates and timezone data are included only when required.
- Health-check behavior is valid and documented.
- All supported architectures build and pass tests.
- Vulnerability, secret, and configuration scans enforce the release policy.
- SBOM, provenance, signature, and immutable image digest are published.
- Any CIS control exception is documented with justification, owner, and expiry date.