Опирается на правила:
R-SEC-IMG-1…R-SEC-IMG-4иR-SEC-IMG-X1…R-SEC-IMG-X2из Security Style Guide → раздел 4. Container/image-уязвимости.
Важно знать
- Trivy обязателен для всех Docker-образов: сканирует base image, OS-пакеты, Go-зависимости из бинаря.
- HIGH/CRITICAL →
--exit-code 1.--ignore-unfixed— CVE без патча от upstream не блокируют сборку.- Base image для Go:
gcr.io/distroless/static-debian12:nonrootилиalpine:3.20с@sha256:— статически слинкованный Go-бинарь не требует libc.- Digest-pin обязателен —
@sha256:.... Не:latest, не mutable-тег без digest в production.- Non-root user: в distroless:nonroot — UID 65532 встроен, для alpine — явный
adduser; финальная строкаUSER 65532:65532.- HEALTHCHECK в Dockerfile или liveness/readiness probe в K8s — обязательно; без них зависший горутин-пул не детектируется.
- Multi-stage build — единственный правильный паттерн: builder-stage с
golang:1.23-alpine, финальный stage — distroless/static или чистый alpine.
Go-бинарь статически слинкован по умолчанию (CGO_DISABLED=1) — финальный image может быть FROM scratch или distroless/static без libc. Это сокращает attack surface радикальнее, чем в Java: нет JVM, нет JRE-пакетов, нет libc при scratch. Но контейнер всё равно включает OS-слой builder'а, если собрано неправильно.
Multi-stage Dockerfile
Сервис orders-service — типичный паттерн:
FROM golang:1.23-alpine3.20@sha256:<digest> AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o orders-service ./cmd/orders
FROM gcr.io/distroless/static-debian12:nonroot@sha256:<digest>
COPY --from=builder /build/orders-service /orders-service
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/orders-service"]
Ключевые детали:
CGO_ENABLED=0— статическая линковка, нет зависимости от glibc.-trimpathубирает абсолютные пути из отладочных символов (не утекают пути build-машины).-ldflags="-s -w"— стрипит отладочные символы и таблицу символов: бинарь меньше, DWARF нет.distroless/staticне имеет shell, package manager, curl — RCE-атакёр не может установить toolchain.USER 65532:65532— UID встроенного nonroot-пользователя в distroless:nonroot.
Если нужен alpine (для healthcheck через wget или curl):
FROM alpine:3.20@sha256:<digest>
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /build/orders-service /orders-service
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/health/live || exit 1
USER app:app
EXPOSE 8080
ENTRYPOINT ["/orders-service"]
Trivy в CI
R-SEC-IMG-1: сканирование после docker build, до docker push.
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t orders-service:${{ github.sha }} .
- name: Scan with Trivy
run: |
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format sarif \
--output trivy.sarif \
orders-service:${{ github.sha }}
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
- name: Push image
if: success()
run: docker push orders-service:${{ github.sha }}
Параметры:
--exit-code 1— найдено HIGH/CRITICAL → CI red, push не происходит.--ignore-unfixed— CVE, для которых нет патча в upstream, не блокируют; они попадают в SARIF как информационные.--format sarif+ upload → GitHub Security tab, единый дашборд findings.
Trivy сканирует Go-образ на трёх уровнях:
- OS-пакеты (apk, deb) — то, что установлено в alpine/debian-слое.
- Go-зависимости, встроенные в бинарь — Trivy читает SBOM из
go.mod/go.sumвнутри image. - Misconfigurations (
--security-checks config) —USER root,:latest,ADDвместоCOPY.
Дополнительный аргумент --dependency-tree полезен для отладки: показывает, какой модуль тянет уязвимую версию.
Base image и digest-pin
R-SEC-IMG-2: минимальный, известный, закреплён digest.
# distroless/static — оптимально для Go (нет libc, нет shell)
FROM gcr.io/distroless/static-debian12:nonroot@sha256:<digest>
# alpine — если нужен shell для healthcheck или debug
FROM alpine:3.20@sha256:<digest>
# golang builder — только для builder-stage, не финального
FROM golang:1.23-alpine3.20@sha256:<digest> AS builder
Получить digest:
docker pull gcr.io/distroless/static-debian12:nonroot
docker inspect gcr.io/distroless/static-debian12:nonroot --format '{{index .RepoDigests 0}}'
# gcr.io/distroless/static-debian12@sha256:...
Digest обновляется через Renovate — он создаёт авто-PR с новым @sha256: при выходе патча base image (R-SEC-DEP-2).
# Хорошо — digest-pin
FROM gcr.io/distroless/static-debian12:nonroot@sha256:a1b2c3...
# Допустимо — явный тег без digest (обновляется вручную)
FROM alpine:3.20.3
# Плохо — mutable тег, drift возможен
FROM alpine:3.20
# Запрещено
FROM alpine:latest
FROM golang:latest
Non-root user
R-SEC-IMG-3.
В distroless:nonroot non-root пользователь встроен, достаточно:
FROM gcr.io/distroless/static-debian12:nonroot@sha256:<digest>
COPY --from=builder /build/orders-service /orders-service
USER 65532:65532
ENTRYPOINT ["/orders-service"]
Для alpine — создаём пользователя явно:
FROM alpine:3.20@sha256:<digest>
RUN addgroup -S app && adduser -S -u 1000 app -G app
COPY --from=builder /build/product-service /product-service
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/product-service"]
Числовые UID/GID (1000:1000) надёжнее строковых имён — они не зависят от /etc/passwd внутри образа.
В K8s добавляется securityContext:
spec:
containers:
- name: orders-service
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true работает с Go без доработок — бинарь ничего не пишет на диск, если логирует через slog в stdout. Если сервис пишет временные файлы (например, os.CreateTemp), нужен отдельный emptyDir-том.
HEALTHCHECK и K8s probes
R-SEC-IMG-4.
Стандартный подход для chi-роутера — /health/live и /health/ready в отдельном хендлере:
// adapters/in/http/health.go
func RegisterHealthRoutes(r chi.Router) {
r.Get("/health/live", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Get("/health/ready", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
}
HEALTHCHECK в Dockerfile через встроенный бинарь (без wget/curl в distroless):
FROM gcr.io/distroless/static-debian12:nonroot@sha256:<digest>
COPY --from=builder /build/orders-service /orders-service
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD ["/orders-service", "-healthcheck"]
USER 65532:65532
ENTRYPOINT ["/orders-service"]
// cmd/orders/main.go
func main() {
if len(os.Args) > 1 && os.Args[1] == "-healthcheck" {
resp, err := http.Get("http://localhost:8080/health/live")
if err != nil || resp.StatusCode != http.StatusOK {
os.Exit(1)
}
os.Exit(0)
}
// ... основной запуск
}
K8s probes для customer-service:
spec:
containers:
- name: customer-service
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
В K8s достаточно probes — HEALTHCHECK дублируется и может быть опущен.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
USER root или отсутствие USER в финальном stage | R-SEC-IMG-X1 | USER 65532:65532 (distroless) или USER 1000:1000 (alpine) |
:latest или mutable-тег base image | R-SEC-IMG-X2 | digest-pin @sha256:... |
Финальный stage с golang:* вместо distroless/alpine | R-SEC-IMG-2 | multi-stage: builder + минимальный финальный stage |
| Trivy не запущен в CI | R-SEC-IMG-1 | trivy image после build, до push |
Trivy без --exit-code 1 | R-SEC-IMG-1 | fail CI на HIGH/CRITICAL |
Trivy без --ignore-unfixed | R-SEC-IMG-1 | unfixed CVE — отдельная задача, не блокируют |
Без HEALTHCHECK и без K8s probes | R-SEC-IMG-4 | минимум liveness/readiness в K8s |
CGO_ENABLED=1 в builder без статической линковки | R-SEC-IMG-2 | CGO_ENABLED=0 → distroless/static |
Секреты через ENV в Dockerfile | R-SEC-SECRET-X1 | только через os.Getenv из K8s Secret / Vault |
Куда дальше
- Криптография в коде —
crypto/rand, AES-GCM, bcrypt; Go-идиомы. - CVE в зависимостях —
govulncheck,go.sum, Renovate. - Реакция на findings — SLA по severity, suppressions со сроком.
- SAST по коду —
gosec,golangci-lint,staticcheck; SARIF в GitHub. - Секреты в коде и истории —
envconfig, Gitleaks,.env.example.