Distroless Docker образы для .NET приложений
Пару лет назад пришлось внедрять «коробочное» решение в одну организацию со строгими политиками безопасности для внешних поставщиков, одна из самых неприятных из которых была — необходимость полного отсутствия любых известных уязвимостей в поставке.
Казалось бы — взять последнюю версию базового образа популярной операционной системы, и все будет хорошо, но в реальной жизни даже в них практически всегда есть известные уязвимости.
Так что единственным вариантом для решения этой проблемы раз и на всегда была сборка собственного базового 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/
А если вам надо собрать такие образы под более старые версии, то можете посмотреть примеры использования моих образов, или собрать свои на их основе.