Микросервисы и контейнеры для их развертывания являются стандартом в крупных компаниях. Для разработчиков и DevOps-инженеров это удобный подход: он дает больше возможностей и ускоряет процессы.

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

Меня зовут Саша Рахманный, я разработчик в команде информационной безопасности в Lamoda Tech. В этой статье я сравню разные базовые образы для .NET с точки зрения безопасности их компонентов и быстродействия. 

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

Дисклеймер

Актуальность уязвимостей того или иного образа актуальна только на момент публикации статьи. Сравнение производительности проводилось только на тестовом проекте с одинаковыми параметрами запуска и может отличаться на другом железе, операционной системе, версии Dot.Net.

Работа с уязвимостями в контейнерах

Представим, что в процессе разработки приложения мы столкнулись с проблемой: на этапе сборки в Docker-контейнере обнаружена критическая уязвимость. Сборка остановилась на этапе сканирования артефактов.  

Мы пошли смотреть список пакетов с уязвимостями — и выяснили, что на данный момент для них нет ни новых версий, ни хотфиксов для устранения проблем. 

Что делать дальше? 

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

Существует несколько методов работы с этой проблемой:

  1. Избавиться от неиспользуемых пакетов. Один из способов уменьшить атакующую поверхность — удалить из контейнеров на этапе сборки все ненужные пакеты, которые могут содержать уязвимости.

  2. Техническая защита. Внедрение Web Application Firewall, сегментация сети и другие решения, которые помогут защитить систему от известных уязвимостей.

  3. Принятие риска. В некоторых случаях можно принять риски, если потенциальный ущерб от уязвимости ниже стоимости её устранения. Зная о риске, мы можем подключить дополнительные сенсоры и настроить алерты на события в системе мониторинга.

  4. Использование distroless (урезанных) образов. Применение минималистичных образов контейнеров с ограниченным набором компонентов существенно сокращает количество потенциальных уязвимостей.

В этой статье я сосредоточусь на последнем пункте. Сравню несколько базовых образов: покажу, как количество установленных в них пакетов влияют на количество выявленных уязвимостей и быстродействие, сравню результаты. 

Сравнивать образы предлагаю на примере тестового приложения.

Приложение для теста

В качестве тестового приложения используем стандартный шаблон Weather API.

В Program.cs добавим следующий метод для вывода основной информации об окружении:

app.MapGet("/Info", () => new
{
    Username = Environment.UserName,
    OS = Environment.OSVersion,
    IsDevelopment = app.Environment.IsDevelopment(),
    IsProduction = app.Environment.IsProduction(),
    IsStaging = app.Environment.IsStaging(),
    Hosts = File.ReadAllTextAsync("/etc/hosts"),
    Hash = DateTime.UtcNow.ToString("O").GetHashCode()
})
    .WithName("Get Env Information")
    .WithOpenApi();

Контейнер SDK

Официальный образ mcr.microsoft.com/dotnet/sdk:8.0 на момент написания статьи собран на основе контейнера Debian 12 с установленным .NET SDK и включает в себя:

  • .NET CLI

  • .NET runtime

  • ASP.NET Core

Так же есть варианты с Alpine 3.18-3.19, Ubuntu 22.04, CBL-Mariner 2.0.

Microsoft в документации рекомендует использовать этот образ только для этапов разработки или сборки.

Напишем Dockerfile для нашего демо-приложения, используя базовый образ mcr.microsoft.com/dotnet/sdk:8.0.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
ENTRYPOINT ["dotnet", "/app/publish/WebAppDemo.dll"]

Соберем и запустим контейнер:

docker build -f WebAppDemo\Dockerfile -t webappdemo:sdk .
docker run -p 8080:8080 --name webapp-sdk -d webappdemo:sdk

Проверяем работу:

curl http://localhost:8080/info

{"username":"root","os":{"platform":4,"servicePack":"","version":"5.10.102.1","versionString":"Unix 5.10.102.1"},"isDevelopment":false,"isProduction":true,"isStaging":false,"hosts":"127.0.0.1\tlocalhost\n::1\tlocalhost ip6-localhost ip6-loopback\nfe00::0\tip6-localnet\nff00::0\tip6-mcastprefix\nff02::1\tip6-allnodes\nff02::2\tip6-allrouters\n172.17.0.3\t8553cab4aeb7\n","hash":1359993159}

Проверяем размер образа:

docker inspect -f "{{ .Size }}" webappdemo:sdk

Проверим количество установленных пакетов в образе:

syft webappdemo:sdk | grep deb

Так же проверим количество уязвимостей:

trivy image webappdemo:sdk

Что получилось:

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

992 МБ

119

Total: 104 (UNKNOWN: 0, LOW: 80, MEDIUM: 10, HIGH: 13, CRITICAL: 1)

+

+

Для проверки производительности напишем тест, используя Benchmark DotNet:

[IterationCount(200)]
public class RestBenchmark
{
    private HttpClient _client;
    [GlobalSetup]
    public void GlobalSetup()
    {
        _client = new HttpClient();
    }
    [Benchmark]
    public async Task GetInfo()
    {
        var response = await _client.GetAsync("http://localhost:8080/info");
    }
}

И выполним замерив также метрики:

Mean

Container Max Mem Usage

Container Max CPU Usage

434.0 us

307.5 MB

63%

Стоит отметить, что на момент написания статьи ни для одной уязвимости разработчик не выпустил фикс. Так что простой apt-upgrade нам тут ничем не поможет.

Плюсы:

  • Подходит для разработки и сборки. SDK-образ позволяет выполнять различные дополнительные операции, такие как создание дампов работающего приложения через dotnet dump или запуск модульных тестов, что может быть полезно в рамках процесса разработки и отладки.

Минусы:

  • Большой размер образа. Образ SDK имеет большой размер из-за включенных инструментов разработки и зависимостей. Это приводит к увеличенному времени загрузки и использованию большого количества дискового пространства.

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

Multistage с ASP.NET

Рекомендуемый Microsoft способ — использование многоэтапной сборки. Сборка приложения выполняется в контейнере mcr.microsoft.com/dotnet/sdk:8.0, а на последнем этапе мы копируем артефакты в новый слой на базе mcr.microsoft.com/dotnet/aspnet:8.0 командой COPY --from=publish /app/publish.

Данный образ содержит библиотеки, необходимые для запуска asp core, а также shell и пакетный менеджер. Базовый образ aspnet:8.0 собран на базе Debian 12, также есть версии с Alpine 3.18-3.19, Ubuntu 22.04, CBL-Mariner 2.0.

Dockerfile для многоэтапной сборки может выглядеть следующим образом:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebAppDemo.dll"]

Дополнительная рекомендация — мы можем создать специального пользователя, от которого будем запускать приложения без прав root, добавив строку:

USER app

Соберем контейнер, проверим работу и посмотрим, как изменился состав образа:

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

221 МБ

92

Total: 70 (UNKNOWN: 0, LOW: 59, MEDIUM: 8, HIGH: 2, CRITICAL: 1)

+

+

Mean

Container Max Mem Usage

Container Max CPU Usage

391.2 us

287 MB

64%

Уже лучше! Мы сократили размер образа, количество установленных пакетов и количество уязвимостей без патчей.

Плюсы:

  • Уменьшение размера образа. Использование отдельного базового образа для сборки и исполнения позволяет включить в конечный образ только необходимые компоненты, что в итоге уменьшает его размер. Это экономит место и ускоряет процесс развертывания.

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

Минусы:

  • Сложность отладки и тестирования. В контейнере выполнения отсутствуют компоненты .NET SDK, необходимые для тестирования и дебага.

  • Все еще большое количество пакетов, которые часто не нужны приложению. И соответственно, большое количество уязвимостей.

Multistage Chiseled

Осенью 2023 Microsoft представила новый тип контейнера выполнения — .NET Chiseled. Эти контейнеры основаны на Ubuntu 22.04 и предназначены для уменьшения размера и улучшения безопасности образов: из них удалили все, кроме необходимых компонентов. Они также предлагают сокращение CVE за счет уменьшения количества компонентов.

Давайте соберем наше приложение с использованием aspnet:8.0-jammy-chiseled и проверим результат.

Что мы сразу замечаем:

  • Нет менеджера пакетов.

  • Нет sh.

  • Нет root пользователя.

Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebAppDemo.dll"]

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

113 МБ

7

Total: 6 (UNKNOWN: 0, LOW: 5, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

-

-

Mean

Container Max Mem Usage

Container Max CPU Usage

388.7 us

265 MB

66%

Плюсы:

  • Уменьшенный размер образа. Контейнеры .NET Chiseled основаны на Ubuntu 22.04 и содержат только необходимые компоненты, что приводит к значительному сокращению размера по сравнению с полноценными образами.

  • Улучшенная безопасность. За счет удаления всех лишних компонентов контейнеры .NET Chiseled обеспечивают улучшенную безопасность, поскольку это уменьшает поверхность атаки и количество потенциальных уязвимостей.

  • Отсутствие менеджера пакетов, sh и root пользователя. Удаление менеджера пакетов, sh и использование non-root пользователя повышают безопасность образа: это снижает вероятность, что злоумышленники получат доступ к системным ресурсам и выполнят вредоносные действия.

Минусы:

  • Ограниченная функциональность. Использование Сhiseled может привести к ограничениям, особенно если в приложении требуются дополнительные компоненты или библиотеки, которые были удалены из образа.

Установка зависимостей и перенос в среду выполнения

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

В случае с обычным образом мы бы добавили в Dockerfile команду, например:

RUN apt update && apt install -y libgcrypt20 && rm -rf /var/lib/apt/lists/*

Но если мы попробуем это выполнить с образом 8.0-jammy-chiseled, то получим ошибку, связанную с отсутствием apt.

Есть несколько вариантов решения этой проблемы.

Первый — скачать, распаковать и переместить файлы в итоговый образ, пример из документации Microsoft:

# build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
...
RUN wget -O somefile.tar.gz <URL> \
    && tar -oxzf aspnetcore.tar.gz -C /somefile-extracted
...

# final stage/image
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled
...
COPY --from=build /somefile-extracted .
...

Второй вариант — установить нужный пакет через APT и скопировать файлы на последнем этапе.

В случае, если нужный нам пакет устанавливает много файлов в разных директориях или сам имеет зависимости, можно воспользоваться утилитой Chisel https://github.com/canonical/chisel. Chisel — это инструмент для работы с Debian-пакетами, который позволяет создавать минимальные, дополняющие и слабо связанные наборы файлов, основанные на метаданных и содержимом пакета. Эти наборы файлов, или «срезы», представляют собой набор Debian-пакетов с собственным содержимым и зависимостями от других внутренних и внешних срезов. 

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

Посмотрим, как теперь выглядит наш Dockerfile с базовым образом aspnet:8.0-jammy-chiseled, в который мы установим libgcrypt20.

FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM ubuntu:22.04  AS dependency
RUN apt update && \
    apt install -y ca-certificates wget && \
    mkdir /rootfs && \
    wget -O chisel.tar.gz https://github.com/canonical/chisel/releases/download/v0.9.1/chisel_v0.9.1_linux_amd64.tar.gz \
    && tar -oxzf chisel.tar.gz -C / &&\
    ./chisel cut --root /rootfs libgcrypt20_libs

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=dependency /rootfs /
ENTRYPOINT ["dotnet", "WebAppDemo.dll"]

Обратите внимание на фазу dependency. Мы используем базовый образ ubuntu:22.04, так как Aspnet:8.0-jammy-chiseled построен на его основе. Использование более нового образа ubuntu на этом этапе может привести к проблемам совместимости пакетов. 

Создаем отдельный каталог rootfs, в который будут устанавливаться все пакеты с зависимостями. После этого мы скачиваем и запускаем утилиту chisel, указывая, куда и какие пакеты нужно установить. Список доступных пакетов можно найти в https://github.com/canonical/chisel-releases.

На последнем этапе мы копируем все содержимое /rootfs в корень последнего слоя.

Проверим, как изменилось содержимое образа по сравнению с предыдущим билдом:

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

119 МБ

7

Total: 6 (UNKNOWN: 0, LOW: 5, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

-

-

Runtime-deps + self-package + trimmed

Образ mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled представляет собой тип контейнерного образа distroless, который содержит только минимальный набор пакетов, необходимых для запуска .NET приложений без установленного фреймворка (self-contained), с удалением всего остального.

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish -p:PublishTrimmed=true --runtime linux-x64 --self-contained true

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["/app/WebAppDemo"]

Обратить внимание тут стоит на параметры публикации: 

  • -p:PublishTrimmed=true не включает в сборку неиспользуемые библиотеки. При использовании данной опции могут возникнуть проблемы. Например, при динамической загрузке сборок через рефлексию необходимо тщательно тестировать такие приложения.

  • --runtime linux-x64 — целевая платформа.

  • --self-contained true включает в приложение компоненты .NET платформы, приложение можно запускать в системе без .NET-рантайма.

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

44 МБ

7

Total: 6 (UNKNOWN: 0, LOW: 5, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

-

-

Mean

Container Max Mem Usage

Container Max CPU Usage

540.7 us

370 MB

76%

Плюсы:

  • Меньший размер. Это сокращенная версия образа, которая экономит место на диске, позволяет быстрее загружать и разворачивать контейнер.

  • Уменьшенное количество зависимостей. Поскольку образ содержит только минимальный набор пакетов, необходимых для запуска .NET-приложений, это снижает количество пакетов с возможными уязвимостями.

  • Более безопасное приложение. Использование PublishTrimmed=true позволяет исключить из сборки неиспользуемые библиотеки, что уменьшает поверхность атаки и уменьшает возможности эксплойта уязвимостей.

Минусы:

  • Возможные проблемы с динамической загрузкой сборок. Использование опции PublishTrimmed=true может привести к проблемам при динамической загрузке сборок через рефлексию. Это требует тщательного тестирования приложения, чтобы обнаружить и исправить возможные проблемы.

Alpine

Alpine Linux — это легковесный дистрибутив Linux, который часто используется в качестве базового образа для Docker-контейнеров. 

Давайте рассмотрим преимущества Alpine по сравнению со стандартным образом на базе Debian:

  • Размер. Alpine обладает намного меньшим размером, что делает его идеальным выбором для контейнеров. Он содержит только минимальный набор библиотек и утилит, что помогает уменьшить размер образа и ускорить его загрузку.

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

  • Эффективность: Alpine использует musl libc вместо glibc, что позволяет снизить потребление памяти и улучшить производительность. Это особенно полезно для масштабируемых приложений.

Dockerfile для alpine выглядит следующим образом:

FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebAppDemo.dll"]

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

110 МБ

17

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

+

+

Mean

Container Max Mem Usage

Container Max CPU Usage

415.3 us

292 MB

65%

CBL-Mariner distroless (Azure Linux)

CBL-Mariner — это внутренний дистрибутив Linux, разработанный Microsoft для облачной инфраструктуры Azure.

Изменим Dockerfile для использования этого базового образа:

FROM mcr.microsoft.com/dotnet/aspnet:8.0-cbl-mariner-distroless AS base
USER app
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebAppDemo/WebAppDemo.csproj", "WebAppDemo/"]
RUN dotnet restore "./WebAppDemo/WebAppDemo.csproj"
COPY . .
WORKDIR "/src/WebAppDemo"
RUN dotnet build "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebAppDemo.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebAppDemo.dll"]

Результаты:

Размер образа

Количество пакетов

Количество уязвимостей

Shell

Менеджер пакетов

121 МБ

10

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

-

-

Mean

Container Max Mem Usage

Container Max CPU Usage

392.5 us

280 MB

64%

Native AOT + alpine

Эта технология остается сырой и не готовой для использования в проде по нескольким причинам:

  • Использование Native AOT предполагает изменение исходного кода приложения, например, явного указания AppJsonSerializerContext и перечисления каждого dto для него. 

  • Вместо WebApplication.CreateBuilder используется WebApplication.CreateSlimBuilder, который не поддерживает MVC, Blazor, SignalR, многие типы аутентификации.

  • Нет возможности динамически подключать библиотеки в процессе выполнения (обязательная опция trimmed).

  • Мне не удалось с нескольких попыток запустить Native AOT приложение с базовым образом alpine или scratch, а использование runtime-deps chiseled, c которым приложение запустилось, не сокращает количество зависимостей по сравнению с self-package + trimmed.

Выводы

От выбора базового образа зависит, какие уязвимости унаследует от него .NET-приложение. Чем меньше пакетов, тем меньше риск. Кроме этого, легкие образы с минимальным набором компонентов также повышают скорость работы.  

В подавляющем большинстве случаев в продуктивной среде нам не требуется ни менеджер пакетов, ни shell-доступ внутрь контейнера, ни дополнительные утилиты диагностики. И напротив, в среде разработки и тестирования нам эти инструменты необходимы.

Поэтому  существует несколько рекомендаций по сборке и деплою приложений .NET в виде Docker-образов:

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

  • В продакшен-среде использовать итоговый образ с наименьшим количеством зависимостей.

  • Проверять образы контейнеров на этапе сборки на критические уязвимости.

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

Ситуация

sdk

aspnet

aspnet chiseled

runtime-deps chiseled + trimmed

alpine

CBL-Mariner distroless

Разработка приложения

Да

Возможно

Нет

Нет

Возможно

Нет

Модульное тестирование

Да

Нет

Нет

Нет

Нет

Нет

Интеграционное тестирование

Возможно

Да

Да

Да

Да

Да

Pre-Prod/QA

Нет

Да

Возможно

Возможно

Возможно

Возможно

Prod

Нет

Возможно

Да

Возможно

Возможно

Да

Как мы видим из тестов производительности, базовые образы Asp.Net, Asp.Net Chiseled показывают примерно одинаковые метрики производительности, немного медленнее SDK и Alpine. 

Вариант runtime-deps Сhiseled + Self-packaged показал самые худшие показатели производительности. В образе Apline хоть и отсутствуют уязвимости (Trivy не нашел ничего, но утилита grype показала 14 уязвимостей уровня medium), в нем присутствует shell и apk, что увеличивает площадь возможной атаки. 

В CBL-Mariner сканеры trivy, grype, Docker Scout не обнаружили уязвимости, образ также не содержит shell и пакетный менеджер.

Надеюсь, эта шпаргалка поможет сделать правильный выбор!

Комментарии (6)


  1. topright007
    16.04.2024 08:02

    Разрешите побыть адвокатом дьявола :)

    50-метровый образ в проде - круто. 0 уязвимостей на сканере - круто

    Но

    1. это 50 метров - это на hello world. В жизни размер приложения с либами несколько сотен метров. иногда до гигабайта (у нас в основном java+springboot). И размер образа не так важен, как размер последнего часто меняющегося слоя

    2. мы оптимизируем количество срабатываний сканера. А платим за это тем, что тестируем не тот дистрибутив, который будет в проде.


    1. rahmanny Автор
      16.04.2024 08:02
      +4

      Вы правы, дело не в сокращении размера образа на несколько десяток мегабайт. Надо уменьшать количество компонентов в образе, которые не использует приложение, но которыми могут воспользоваться злоумышленники.

      Приведу пример: в одной из библиотек приложения есть уязвимость RCE (удаленное выполнение кода). С установленными в контейнере утилитами, менеджером пакетов не составит труда снять дамп памяти вашего процесса и отправить его на удаленный сервер для последующего анализа. С другой стороны если в контейнере есть только рантайм и ваше приложение вектор атаки сильно сокращается.


  1. kemsky
    16.04.2024 08:02

    Какого рода эти уязвимости, реально ли вообще ими воспользоваться ?


    1. rahmanny Автор
      16.04.2024 08:02

      Если мы говорим о образах с .NET, то MS оперативно выпускает новые версии базовых образов с исправлениями и интересного на данный момент нет.

      Но я просканировал образ, которые собирал более года назад (на основе последней доступной версий mcr.microsoft.com/dotnet/aspnet ) и нашел несколько интересных уязвимостей, которые потенциально могут привести к DoS или влиять на безопасность:

      • CVE-2023-36799 - A vulnerability exists in .NET where reading a maliciously crafted X.509 certificate may result in Denial of Service. This issue only affects Linux systems.

      • CVE-2023-4911 - buffer overflow in ld.so leading to privilege escalation

      • CVE-2023-36054 - krb5: Denial of service through freeing uninitialized pointer

      • CVE-2018-8292 - .NET Core: information disclosure due to authentication information exposed in a redirect

      • CVE-2023-36558 - ASP.NET Security Feature Bypass Vulnerability in Blazor forms


    1. leotsarev
      16.04.2024 08:02
      +1

      Любая зависимость и любая уязвимость усложняют жизнь. Даже если ими не реально воспользоваться, надо тратить время на ручной или полуручной анализ и обоснование этого. В некоторых отраслях мы это делаем «для себя», а в некоторых это может быть основанием для написания формальных документов и отдельное согласование с ИБ.

      Упрощает дело анализ по категориям — отметаем все узявимости, которые имеют вектор атаки в доступом к USB на основании того, что средство контейнеризации и контроль физического доступа исключают этот вектор атаки и т.д., но все равно это время и расходы.

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


  1. amironov
    16.04.2024 08:02

    В свое время тоже интересовался этой темой. Для AOT hello-world получил такой результат (PublishAot, StaticallyLinked, scratch):

    REPOSITORY                      TAG         IMAGE ID      CREATED        SIZE
    localhost/dotnet-web-slim       latest      19c432d03ca3  2 minutes ago  11.3 MB
    localhost/dotnet-web            latest      a089d676d3fb  7 minutes ago  18.1 MB
    

    Для редхатовского ubi-micro такой (PublishSingleFile, PublishTrimmed):

    REPOSITORY                       TAG         IMAGE ID      CREATED         SIZE
    localhost/dotnet-web-ubi-micro   latest      51b29cdc96cb  3 seconds ago   57.4 MB