Некоторые языки программирования (например, Go и Zig) позволяют собрать приложение без каких-либо зависимостей, в том числе отвязаться от libc, тем самым создание distroless-контейнера на Go становится тривиальной задачей. Но эта же особенность может быть применена не только для создания контейнера, но и для запуска такого приложения в VM или на реальном хосте не используя какой-либо дистрибутив Linux, а используя только ядро Linux и само приложение, построенное с помощью Go (или, например, Zig). Такая возможность позволяет избавиться от дополнительных зависимостей, которые добавляют потенциальные риски с точки зрения атаки на цепочку поставок (supply chain attack).

Рассмотрим простейший пример веб-сервера на Go:

package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("hello-world"))
    })
    println("Server started at port 8080")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

Далее в качестве хоста для подготовки образа используется Ubuntu 24.04.3 x86_64.

Сборка и запуск kernel + initramfs

Сборка с отвязкой от libc и подготовка initramfs-образа:

#CGO_ENABLED=0 убирает зависимость от libc, ldflags убирают отладочную информацию
CGO_ENABLED=0 go build -ldflags='-w -s' server.go
mkdir rootfs
cp ./server rootfs/init # ядро Linux ищет файл /init в rootfs

# сборка initramfs
cd rootfs
find . | cpio -H newc -o | gzip -9 > ../initramfs.cpio.gz
cd ..

Ядра из generic-дистрибутивов (Debian/Ubuntu, RHEL-based, Alpine) не подойдут для запуска конкретно этого приложения потому что они собраны таким образом, что драйвера сетевых карт (включая virtio-net) не включены в ядро (не являются built-in), а собраны модулями, а чтобы загрузить модуль нужно, чтобы init (из initramfs) умел это делать, в нашем же случае init - это тривиальный веб-сервер на Go, который не умеет грузить модули. Также нужно, чтобы ядро было собрано с CONFIG_IP_PNP, чтобы задачу назначения IPv4/IPv6-адреса можно было возложить на само ядро, в противном случае надо делать то, что делают тулы типа ip/ifconfig.

# получаем исходники ядра Linux любым удобным способом
git clone https://github.com/torvalds/linux -b v6.16 --depth=1

# ставим компилятор и прочие инструменты для сборки ядра Linux
sudo apt install gcc-x86-64-linux-gnu gcc make flex bison libelf-dev bc

# задаем целевую архитектуру и префикс поиска компилятора (можно не делать если не планируется компилировать под другие платформы)
export ARCH=x86_64
export CROSS_COMPILE=x86_64-linux-gnu-

cd linux
make kvm_guest.config # базовый конфиг для работы как гость kvm

./scripts/config --enable CONFIG_PRINTK_TIME # добавление времени в логи ядра, для отладки

# для работы initramfs(initrd)
./scripts/config --enable CONFIG_SHMEM
./scripts/config --enable CONFIG_TMPFS
./scripts/config --enable CONFIG_DEVTMPFS
./scripts/config --enable CONFIG_BLK_DEV_INITRD

# для использования устройства с ФС FAT вместо initramfs
./scripts/config --enable CONFIG_NLS_CODEPAGE_437
./scripts/config --enable CONFIG_NLS_ISO8859_1
./scripts/config --enable CONFIG_VFAT_FS

# для возможности работы в UEFI-среде, не нужно если не планируется загрузка как EFI-приложение
./scripts/config --enable CONFIG_EFI
./scripts/config --enable CONFIG_EFI_STUB

make olddefconfig # устанавливает незаданные значения по умолчанию

# пример отключения фич ядра, которые не нужны для указанного выше примера веб-сервера
./scripts/config --disable CONFIG_WIRELESS
./scripts/config --disable CONFIG_WLAN
./scripts/config --disable CONFIG_NAMESPACES
./scripts/config --disable CONFIG_ETHERNET
./scripts/config --disable CONFIG_IPV6

make -j$(nproc)
# на выходе получаем образ ядра arch/x86_64/boot/bzImage

Предложенный .config ядра не претендует на минималистичность (в нем можно довольно много отключить), но вполне пригоден для экспериментов в qemu с использованием kvm

qemu-system-x86_64 -nographic -enable-kvm -cpu host -m 64 \
 -kernel arch/x86_64/boot/bzImage  \
 -initrd initramfs.cpio.gz \
 -netdev user,id=net0,hostfwd=tcp::8080-:8080 -device virtio-net,netdev=net0 \
 -append "console=ttyS0 ip=10.0.2.15::10.0.2.2:255.255.255.0:::none:" 

Здесь происходит следующее: qemu использует одно ядро (с помощью kvm) и 64МБ памяти, делает примерно то же самое, что загрузчики типа grub/u-boot (загружает initrd по нужному адресу и запускает ядро), также в ядро передается, что консоль будет в виртуальном серийном порту и что ядро должно настроить ip-адрес 10.0.2.15/24 на (единственном) virtio-net интерфейсе. Примерно за одну секунду ядро загрузится и запустит приложение (тривиальный web-сервер)

[    0.606419] IP-Config: Complete:
[    0.606927]      device=eth0, hwaddr=52:54:00:12:34:56, ipaddr=10.0.2.15, mask=255.255.255.0, gw=10.0.2.2
[    0.607437]      host=10.0.2.15, domain=, nis-domain=(none)
[    0.607729]      bootserver=255.255.255.255, rootserver=255.255.255.255, rootpath=
[    0.608310] Freeing unused kernel image (initmem) memory: 1548K
[    0.608806] Write protecting the kernel read-only data: 14336k
[    0.609333] Freeing unused kernel image (text/rodata gap) memory: 1760K
[    0.609806] Freeing unused kernel image (rodata/data gap) memory: 1912K
[    0.610064] Run /init as init process
Server started at port 8080

Проверка веб-сервера (с хост-машины):

user@host:~$ curl http://localhost:8080 -D -
HTTP/1.1 200 OK
Date: Thu, 18 Sep 2025 20:24:49 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8

hello-world

Сборка kernel + initramfs в EFI-приложение

Если планируется работа на реальном хосте и хост поддерживает UEFI (что вполне актуально не только для x86_64, но и для современных arm64 и riscv64), то собрать EFI-приложение можно следующим образом:

sudo apt install systemd-boot-efi systemd-ukify # для сборки EFI-приложения
sudo apt install ovmf # поддержка запуска EFI-приложений в qemu (x86_64)

# построение EFI-образа
ukify build \
    --linux=arch/x86_64/boot/bzImage \
    --initrd=initramfs.cpio.gz \
    --cmdline="console=ttyS0 ip=10.0.2.15::10.0.2.2:255.255.255.0:::none:" \
    --os-release="distroless golang app" \
    --output=golang-web.efi

# создание  структуры каталогов для загрузки EFI-приложения
mkdir -p esp/EFI/BOOT
cp golang-web.efi esp/EFI/BOOT/BOOTX64.EFI

# запуск EFI-образа (диск с файловой системой FAT эмулируется с помощью qemu)
qemu-system-x86_64 -nographic -enable-kvm -cpu host -m 128 \
  -netdev user,id=net0,hostfwd=tcp::8080-:8080 -device virtio-net,netdev=net0 \
  -bios /usr/share/qemu/OVMF.fd \
  -drive file=fat:rw:esp/,format=raw

Вопросы подписи EFI-приложений выходят за рамки данной статьи

Загрузка приложения с файловой системы вместо initramfs

initramfs занимает память, от него можно избавиться, если загрузка осуществляется с накопителя (например с файловой системой FAT)

cp ./server fatfs/sbin/init # копируем собранное Go-приложение как /sbin/init

qemu-system-x86_64 -nographic -enable-kvm -cpu host -m 64 \
 -kernel arch/x86_64/boot/bzImage  \
 -netdev user,id=net0,hostfwd=tcp::8080-:8080 -device virtio-net,netdev=net0 \
 -drive file=fat:rw:fatfs/,format=raw,if=none,id=disk -device virtio-blk-pci,drive=disk \
 -append "console=ttyS0 ip=10.0.2.15::10.0.2.2:255.255.255.0:::none: root=/dev/vda1"

Аналогично можно поступить с EFI-приложением, т.е. не добавлять initramfs при сборке EFI-приложения и добавить в cmdline ядра root=/dev/vda1

Пример подготовки distroless на реальном устройстве Orange Pi RV2 (riscv64)

Уходя из мира qemu в реальный мир железа, особенно за пределы типовых x86_64 устройств, начинаются нюансы. В качестве примера distroless-системы возьмём плату Orange Pi RV2 на базе SoC SpaceMIT X-60. Как это часто бывает в мире микрокомпьютеров, наработки по коду ядра не заапстримлены, как и u-boot и toolchain (компилятор), в дополнение к этому еще накладываются прошивки (блобы), которые тоже надо добавлять в distroless-систему. Вендор этой платы предоставляет образ Ubuntu 24.04 (riscv64), в котором ядро и initramfs собраны из репозитория вендора.

Беглый осмотр файла /boot/config-6.6.63-ky и вывода lsmod говорит о том, что большая часть драйверов встроена (built-in) в ядро, т.е. можно попробовать использовать ядро вендора без пересборки самому (хотя рабочая инструкция имеется). Попытка это сделать, подложив собранное под riscv64 Go-приложение в качестве /init внутри initramfs (или в качестве /sbin/init отключив initramfs) привела к такой ошибке

[    5.158776] remoteproc remoteproc0: powering up rcpu_rproc
[    5.164404] remoteproc remoteproc0: Direct firmware load for esos.elf failed with error -2
[    5.172729] remoteproc remoteproc0: request_firmware failed: -2
[    5.178700] ky-rproc c088c000.rcpu_rproc: rproc_boot failed

Ядро пытается загрузить прошивку процессора esos.elf, не может найти файл, после чего дальше процесс загрузки не идет и возникает множество ошибок. Данная проблема решается тем, что нужно положить файл esos.elf в /lib/firmware внутри initramfs (сам файл лежит в /lib/firmware/esos.elf в образе от вендора), после чего проблема с инициализацией CPU решается и ядро грузится дальше.

# записываем образ ubuntu24.04 от вендора платы на SD-карту и монтируем его в /path/to/vendor_fs
mkdir -p initramfs/lib/firmware # готовим структуру каталогов initramfs
cp /path/to/vendor_fs/lib/firmware/esos.elf initramfs/lib/firmware/ # копирование прошивки
cd initramfs
find . | cpio -H newc -o | gzip -9 > ../initramfs-fw.cpio.gz # создание initramfs в формате cpio.gz

# поскольку использует u-boot и initrd в формате u-boot initramfs, то создаем uInitrd инструментом mkimage из пакета u-boot-tools
mkimage -A riscv -O linux -T ramdisk -C gzip -d ../initramfs-fw.cpio.gz  ../uInitrd
cp ../uInitrd /path/to/vendor_fs/boot/uInitrd # заменяем uInitrd от вендора платы на свой

# Поскольку ядро Linux не умеет монтировать по UUID (этим занимается /init из initramfs, которого теперь нет), то прописываем rootdev явным образом (SD карта это /dev/mmcblk0)
# diff boot/orangepiEnv.txt.old boot/orangepiEnv.txt 
5c5
< rootdev=UUID=b615f740-5087-40b8-af5a-0a7ffa0b83f7
---
> rootdev=/dev/mmcblk0p1

Также добавляем дополнительные аргументы cmdline ядра, чтобы назначить ip-адрес на сетевой интерфейс и явным образом задать init (поскольку в initramfs его не будет)

echo "extraargs=ip=192.168.1.10::192.168.1.4:255.255.255.0::eth0:none: init=/fbapp" >> /path/to/vendor_fs/boot/orangepiEnv.txt

В качестве приложения рассмотрим пример веб-сервиса, который принимает png-файл в http-запросе и выводит его на экран (через интерфейс framebuffer)

Исходник
package main

import (
	"fmt"
	"image"
	"image/png"
	"net/http"
	"sync"
	"syscall"

	"github.com/d21d3q/framebuffer"
	"golang.org/x/image/draw"
)

func main() {
	fbpath := "/dev/fb0"
	// Создание устройства fb0, нужно если что /dev не примонтирован (зависит CONFIG_DEVTMPFS_MOUNT и опции ядра devtmpfs.mount)
	mode := uint32(syscall.S_IFCHR | 0600)
	major := 29 // см. https://www.kernel.org/doc/Documentation/fb/framebuffer.txt
	minor := 0 // первый фреймбуфер
	dev := int((major << 8) | minor)
	err := syscall.Mknod(fbpath, mode, dev)
	if err != nil { // Игнорируем ошибку если fb0 уже есть
		fmt.Println(fbpath, err)
	} else {
		println(fbpath, "created (mknod)")
	}

	// Открываем фреймбуфер как FrameBuffer для прямого доступа
	fb, err := framebuffer.OpenFrameBuffer(fbpath, syscall.O_RDWR)
	if err != nil {
		panic(err)
	}

	// Получаем информацию о экране
	varInfo, err := fb.VarScreenInfo()
	if err != nil {
		panic(err)
	}

	// Получаем размеры фреймбуфера
	fbWidth := int(varInfo.XRes)
	fbHeight := int(varInfo.YRes)

	// Проверяем формат пикселя, последующий предполагает именно RGBA
	if varInfo.BitsPerPixel != 32 {
		panic("Framebuffer не в формате RGBA (32 bpp)")
	}

	// Создаем промежуточное изображение для масштабирования один раз
	dst := image.NewRGBA(image.Rect(0, 0, fbWidth, fbHeight))

	// Получаем прямой доступ к пикселям фреймбуфера
	pixels, _ := fb.Pixels() // под капотом это mmap

	// Мьютекс для последовательной обработки запросов
	var mu sync.Mutex

	// Настраиваем HTTP сервер
	http.HandleFunc("/upload", func(w http.ResponseWriter, req *http.Request) {
		if req.Method != http.MethodPut {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}
		defer req.Body.Close()

		// Блокируем мьютекс для последовательной обработки
		mu.Lock()
		defer mu.Unlock()

		// Декодируем PNG из тела запроса
		img, err := png.Decode(req.Body)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		// Масштабируем изображение с помощью стандартной библиотеки
		draw.NearestNeighbor.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Src, nil)

		// Считаем что padding отсутствует, копируем картинку во framebuffer
		copy(pixels, dst.Pix) // единственный нормальный способ (copy) быстро вывести картинку, чтобы ее появление не было заметно

		w.Write([]byte("OK"))
	})

	fmt.Println("Starting http server on :8080")
	// Запускаем сервер на порту 8080
	if err := http.ListenAndServe(":8080", nil); err != nil {
		panic(err)
	}
}

Сборка приложения и удаление лишнего

# сборка приложения
go mod init fbapp
go mod tidy
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 GORISCV64=rva22u64 go build -ldflags='-w -s'

# копируем его на загрузочную sd-карту
cp fbapp /path/to/vendor_fs/fbapp

# удаляем всё остальное кроме boot
cd /path/to/vendor_fs
rm -rf bin etc home lib media mnt opt root sbin selinux srv usr var

Результат: получаем условно-полезное distroless-приложение работающее с сетью и выводом на видеоадаптер, где в userspace нет кода ни на C, ни на C++, готовое после включения игрушечного мини-ПК через 16 секунд (что весьма плохой показатель по скорости загрузки, но Ubuntu 24.04 без GUI грузится около минуты). Из этих 16 секунд 8 уходит на u-boot (две на чтение ядра с SD-карты) и 8 на запуск ядра и приложения (из них две секунды уходят на поднятие 1GE-линка). Можно пересобрать u-boot (отключив в нём кучу всего) и ядро (тоже исключив многое) и значительно ускорить загрузку

Существующие distroless-проекты

Существует ряд известных проектов, где в userspace полностью (или почти) избавились от традиционного подхода "обычных" дистрибутивов с libc, systemd и прочим. Например, Talos (дистрибутив для запуска нод kubernetes), где почти всё написано на Go (включая init). На текущий момент, в нём все же присутствует libc (musl) для ряда классических утилит для работы с блочными устройствами и файловой системой.

Также есть проект u-root, позволяющий создавать базовое окружение (а-ля busybox) на Go. Из исходных кодов Talos и u-root можно брать код (или черпать идеи) на тему того, как назначить ip-адрес (в условиях отсутствия ip/ifconfig), dhcp/ntp-клиент и т.д., т.е. те утилиты, которые нужны, но в угоду distroless-подхода удалены.

Еще один интересный проект firecracker-containerd - очень быстрый запуск контейнера на microvm, где vm обеспечивает дополнительный слой изоляции по сравнению с "обычными" контейнерами, когда они используют общее ядро.

Отдельно стоит отметить подход unikernel (например, unikraft), где не просто запускают ядро + приложение, а где их совмещают, тем самым убирая даже разделение на kernel space и user space. Данный подход интересен в первую очередь с точки зрения оптимизации ресурсов CPU, RAM и занимаемого места.

Почему не Rust и что еще сложного в distroless-подходе

К сожалению, приложения на Rust нельзя скомпилировать без зависимости от libc (точнее это можно сделать, отказавшись от стандартной библиотеки Rust и, тем самым, потеряв возможность использовать почти все библиотеки). Статическая линковка с musl libc создаст единый образ приложения, но не избавит от кода на C с бесконечными out-of-bounds и use-after-free. Существует несколько реализаций libc на Rust, которые находятся в стадии активной разработки, если эти проекты будут доведены до хорошего состояния, то можно будет спокойно использовать Rust для целей построения userspace без C-кода.

На текущий момент, Go обладает хорошей стандартной библиотекой и огромным количеством Pure Go библиотек, что делает его наиболее интересным с точки зрения distroless-строения. С другой стороны, нельзя забывать, что Go - это всё-таки управление памятью с помощью сборки мусора со всеми широкоизвестными проблемами такого подхода, особенно когда идёт речь о приложениях, где время отклика критично.

Новый язык Zig (как и Rust) можно использовать как замену C и C++ в приложениях, где время реакции критично, при этом можно собрать без libc (по-настоящему, а не статически слинковав с musl). Но пока не ясно, выдержит ли этот язык испытания временем, а его стандартная библиотека довольно скудная (по сравнению с Go).

Ещё одна проблема, с которой можно столкнуться в коммерческой разработке это сертификация подобного решения и бюрократические проблемы. Например, может быть предъявлено требование к использованию ОС прошедших те или иные сертификации. Также могут быть предъявлены требования, связанные с использованием криптографических алгоритмов, что усложнит применение distroless-подхода или сделает его вовсе невозможным.

Но самая большая проблема в distroless на pure Go/Zig это GUI и взаимодействие с различными устройствами и их первоначальная настройка (типа задание точки доступа wifi для wifi-адаптера). Большинство библиотек для взаимодействия с периферией написаны на C или C++. Даже настройка IP-адреса на distroless-хосте - это нетривиально, хотя ядро Linux и умеет статическую настройку и даже dhcp-клиент, но dhcp-клиент там не полноценный, не шлет периодические dhcp-request'ы (кстати, с IPv6 SLAAC с этим куда лучше). Однако, несмотря на это, есть большое количество возможных сценариев применения такого подхода - начиная от embedded (на SoC где запуск ядра Linux считается нормальным) и pet-проектов и заканчивая кровавым энтерпрайзом с microvm и системами с повышенными требованиями к безопасности.

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


  1. aakumykov
    21.09.2025 21:43

    Очень интересно, спасибо за статью.


  1. M_AJ
    21.09.2025 21:43

    Следующий шаг – отказаться от ядра и можно будет с чистой совестью сказать, что история сделала круг :)

    Хотя мне в общих чертах импонирует подобный подход, когда у нас есть какая-то минимальная система без всего лишнего (желательно "атомарная" как FedorаCore), а на ней все упаковано в контейнеры с самодостаточными приложениями, но на практике это не всегда рационально особенно, когда нужно взаимодействовать с каким-то сторонним ПО.


    1. svl87 Автор
      21.09.2025 21:43

      flatpak для этого можно использовать (и используют), а еще есть qubes os, где приложения друг от друга изолируются как VM


  1. diderevyagin
    21.09.2025 21:43

    Очень толковый материал, если стоит задача по максимуму облегчить и утилизировать ресурсы и не закапываться с кишки то очень интересный вариант ! Спасибо большое !


  1. Siemargl
    21.09.2025 21:43

    Если завязываться на достаточно старую libc, руки будут более развязаны с выбором языков.

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


    1. svl87 Автор
      21.09.2025 21:43

      в статье как раз про то как избавиться от всего что кладут в "традиционные" дистрибутивы, включая libc (не линкуясь с libc статически). runtime Go (или другого языка который умеет также) заменяет libc, выполняя те функции, которые заложены в libc, например открыть файл, записать в него, закрыть. runtime golang имплементирует это сам, делая syscall-ы в ядро, поэтому у runtime golang есть определенные требования к минимальной версии ядра Linux (и они очень мягкие, т.е. поддерживаются очень древние ядра): https://go.dev/wiki/MinimumRequirements . с точки зрения безопасности, это позволяет вам не следить за уязвимостями libc (и, возможно, других библиотек), а следить только за CVE в Go (или другого языка, умеющего тоже самое)


      1. Siemargl
        21.09.2025 21:43

        понятно что сделано, но зачем переписывать уже проверенное?


        1. svl87 Автор
          21.09.2025 21:43

          Традиционное user-space окружение дистрибутивов Linux это куча библиотек и программ, большая часть которых написано на C (и на, на языке на котором большинство уязвимостей возникают не из-за ошибок бизнес-логики, а из-за неправильной работы с памятью (потому что сам язык это никак не предотвращает, а программисты постоянно ошибаются). эти уязвимости там могут сидеть годами, а иногда десятилетиями. Сейчас дистростроители в принципе пытаются с этим что-то делать, например внедрение sudo-rs и rust-coreutils в ubuntu, но это долгий путь и неизвестно чем закончится

          Другой путь (используемый в этой статье) это минимизировать количество зависимостей. Кроме безопасности, минимизация числа зависимостей снимает и другие головные боли - упрощается сборка (особенно кросс-платформенная), отслеживание deprectated-функций во всех зависимостях и изменений API.


          1. Siemargl
            21.09.2025 21:43

            Слишком много смелых обобщений.

            минимизировать количество зависимостей - это и есть оставить только libc


            1. svl87 Автор
              21.09.2025 21:43

              но можно и не зависеть от libc совсем. каждый сам оценивает надо ему это или нет (и главное, какие плюсы и минусы от этого)


  1. jingvar
    21.09.2025 21:43

    Еще бы ы понять зачем все это? Вот конкретные примеры, а главное с обоснованием. И вишенка как это все дебажить без обвязки.


    1. svl87 Автор
      21.09.2025 21:43

      С точки зрения ИБ это сокращение трудозатрат на отслеживание и обновление "базового" образа. Допустим вы взяли какой-то дистр, добавили туда свое приложение, запустили. Потом (через какое-то время) сканер безопасности в очередной раз проверил образ и нашел там кучу уязвимостей, (как правило) большинство из которых к вам не применимы (в каких-то либах/бинарях которые вы вообще не используете или вы их используете, но не вызываете уязвимую функцию или не выполнены еще какие-то условия экспуатации). Вам надо тратить время на то чтобы проанализировать это всё или обновить базовый образ. В случае с таким подходом Go (из сторонних зависимостей) вам надо следить только за уязвимостями в его runtime (и сторонних библиотек на Go если используете) и не надо думать о том что там опять нашли в glibc/musl или вообще в busybox/shell, который вы и не используете. Называется это сокращение поверхности атаки.

      В целом, плюсы и минусы такого подхода довольно близки к distroless containers, например тут описаны https://docs.docker.com/dhi/core-concepts/distroless/

      Относительно дебага подходы могут быть разные - с локальной отладкой все более-менее понятно, ведь приложение без зависимостей можно запускать и на локальной машине/VM используя стандартные инструменты дебага. Отладка на железе - если с точки зрения самого приложения, то можно собирать образы с удаленной отладкой (, также (в случае go приложения), можно использовать u-root интегрируясь с ним и получить тот же ssh и прочие стандартные тулы типа ping, cat, cp и т.п.

      Если речь про то как отладить первоначальный запуск на железе, то обычно используете UART (serial) чтобы разобраться почему не грузится ядро, почему не монтируется ФС и что еще случается когда деплоишься на bare-metal.


      1. jingvar
        21.09.2025 21:43

        Откуда в образе ping tcpdump итд? Если их доставлять это же опять зависимости которые надо проверять с точки зрения ИБ нет?


        1. svl87 Автор
          21.09.2025 21:43

          собираете (руками/через CI) образ с такой же версией приложения как проблемная + добавляете актуальный инструментарий для отладки, чтобы блюсти ИБ. в этом нет особых сложностей, но в целом да, debug это одно из основных возражений для distroless. как это всегда бывает, это tradeoff между безопасностью (и рядом других преимуществ) и удобством