Distroless Docker образы для .NET приложений

Пару лет назад пришлось внедрять «коробочное» решение в одну организацию со строгими политиками безопасности для внешних поставщиков, одна из самых неприятных из которых была — необходимость полного отсутствия любых известных уязвимостей в поставке.

Казалось бы — взять последнюю версию базового образа популярной операционной системы, и все будет хорошо, но в реальной жизни даже в них практически всегда есть известные уязвимости.

Даже в самом «минимальном» базовом образе alpine могут быть уязвимости (Данный пример как раз просто починится с помощью apk -U upgrade, но кого это волнует)

Так что единственным вариантом для решения этой проблемы раз и на всегда была сборка собственного базового docker образа из «scratch».

Пример такого образа вы можете посмотреть тут https://github.com/RoboNET/dotnet-scratch/blob/main/Dockerfile

Прокомментирую только некоторые моменты


RUN adduser \    
    --disabled-password \    
    --gecos "" \    
    --home "/nonexistent" \    
    --shell "/sbin/nologin" \    
    --no-create-home \    
    --uid "${UID}" \    
    "${USER}"


USER $UID:$UID

Раз уж мы делаем базовый образ в целях безопасности, то надо постараться закрыть как можно большее число векторов атаки. По умолчанию, все команды и ENTRYPOINT в докер образе будет запускаться от root пользователя, так что нам необходимо создать и затем указать, что надо использовать другого пользователя.


RUN apk add --no-cache \
    icu-libs \
    icu-data-full \
    tzdata


COPY --from=globalization-builder /usr/share/icu /usr/share/icu
COPY --from=globalization-builder /usr/share/zoneinfo /usr/share/zoneinfo

По умолчанию alpine не содержит в себе информации о локализации и таймзонах, так что если в вашем приложении необходимо с ними работать, то придется вручную поставить нужные пакеты и потом не забыть скопировать их в свой образ.


ENV ASPNETCORE_URLS=http://+:8080 \
    DOTNET_RUNNING_IN_CONTAINER=true \
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true \
    TMPDIR=/tmp \
    PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools

Чтобы дотнет понимал, что работает в контейнере, и знал нужные ему пути, необходимо указать ряд важных переменных окружения.


RUN dotnet publish -c Release \
    -r linux-musl-arm64 -p:PublishReadyToRun=true \
    -p:PublishTrimmed=true -p:PublishSingleFile=true \
    -o out

Поскольку мы не включаем весь рантайм в приложение, а только необходимые зависимости, нам необходимо собирать наше приложение с включением всего рантайма в билд.

А может ли это пригодиться и для других проектов?

На самом деле этот подход имеет как и плюсы, так и минусы.
Из основных плюсов можно выделить:

  • отсутствие лишних библиотек и бинарных файлов, что уменьшает вероятность взлома приложения через уязвимые компоненты
  • повышение безопасности за счет запуска приложения не от root пользователя, что не позволит как-то вмешаться в оставшиеся зависимости, даже если в нашем приложении как-то смогли выполнить зловредный код через уязвимость
  • уменьшение размера приложения, особенно если не добавлять данные о локализации и таймзонах

Из минусов, с которыми приходилось сталкиваться:

  • приходится поддерживать свой собственный базовый образ
  • сложнее происходит удаленная отладка

И как это использовать?

Если хотите начать использовать distroless и rootless образы, то с 8 версии дотнета microsoft собирать рантайм и зависимости в том числе и в distroless образах, например cbl-mariner https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/8.0/cbl-mariner2.0-distroless/amd64/Dockerfile

И так-же он внес некоторые изменения в поведении по умолчанию, чтобы повысить безопасности приложений, например использование rootless подхода https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/

А если вам надо собрать такие образы под более старые версии, то можете посмотреть примеры использования моих образов, или собрать свои на их основе.

Подписаться на блог
Отправить
Поделиться
Запинить
 193   10 мес   Docker   DotNet