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/

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

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