Лучшие практики при написании безопасного Dockerfile

    В данной статье мы рассмотрим небезопасные варианты написания собственного Dockerfile, а также лучшие практики, включая работу с секретами и встраивание инструментов статического анализа. Тем не менее для написания безопасного Dockerfile наличия документа с лучшими практиками мало. В первую очередь требуется организовать культуру написания кода. К ней, например, относятся формализация и контроль процесса использования сторонних компонентов, организация собственных Software Bill-of-Materials (SBOM), выстраивание принципов при написании собственных базовых образов, согласованное использование безопасных функций, и так далее. В данном случае отправной точкой для организации процессов может служить модель оценки зрелости BSIMM. Однако в этой статьей пойдет речь именно о технических аспектах.

    Безопасное написание Dockerfile

    Задавать LABEL и не использовать тег latest

    Использовать тег latest для базовых образов крайне нежелательно, так как это создает неопределенное поведение по мере того, как базовый образ будет обновляться. К тому же это не гарантирует, что последние версии базовых образов не будут уязвимы. Наиболее предпочтительный вариант можно увидеть на примере ниже:

    FROM redis@sha256:3479bbcab384fa343b52743b933661335448f816
    LABEL version 1.0
    LABEL description "Test image for labels"

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

    LABEL securitytxt="https://www.example.com/.well-known/security.txt"

    Ссылка в ярлыке security.txt позволяет предоставить контактную информацию для исследователей, которые могли обнаружить проблему безопасности в образе. Особенно это актуально, если образ является публично доступным . Больше деталей по этому ярлыку можно найти тут.

    Не использовать автоматическое обновление компонентов

    Использование apt-get upgrade, yum update может привести к тому, что внутри вашего контейнера будет произведена установка неизвестного вам ранее ПО, либо ПО уязвимой версии. Чтобы избежать этого, устанавливайте пакеты, четко указывая версию для каждого из них. Каждая версия должна проверяться на наличие в ней уязвимостей до того, как компонент окажется внутри вашего контейнера. Версия компонента может быть проверена с помощью инструментов класса Software Composition Analysis (SCA).

    Пример установки компонента с точностью до версии:

    RUN apt-get install cowsay=3.03+dfsg1-6

    Если cowsay=3.03+dfsg1-6 зависит от компонента libcowsay, то его версию тоже надо фиксировать.

    Производить скачивание пакетов безопасным образом

    Использование curl и wget без мер предосторожности позволяет злоумышленнику выполнять скачивание нежелательных компонентов с неизвестных ресурсов (атака "человек-посередине", при которой злоумышленник может перехватить незащищенный трафик и подменить скачиваемый нами пакет на зловредный). Это полностью разрушает концепцию Zero trust, согласно которой необходимо проверять любое подключение или действие до предоставления доступа (или в данном случае установки компонента с неизвестного ресурса). Соответственно следующий сценарий скачивания будет являться грубейшей ошибкой, так как происходит выполнение скрипта, полученного из недоверенного источника по небезопасному каналу без должных проверок:

    RUN wget http://somesite.com/some-package/install.sh | sh

    Чтобы убедиться, что скачанный компонент будет являться действительно тем, что мы ожидаем, в качестве решения может подойти использование GNU Privacy Guard (GPG). Разберемся, как это работает.

    В большинстве случаев вендоры вместе с библиотекой или ПО предоставляют также хеш-сумму, которую мы можем проверить при скачивании. Данная хеш-сумма подписывается вендорами с помощью закрытого ключа в рамках GPG, а открытые ключи помещаются в репозитории. Следующий пример демонстрирует, как может выглядеть безопасное скачивание компонентов Node.js:

    RUN gpg --keyserver pool.sks-keyservers.net \
    --recv-keys 7937DFD2AB06298B2293C3187D33FF9D0246406D \
                114F43EE0176B71C7BC219DD50A3051F888C628D
    
    ENV NODE_VERSION 0.10.38
    ENV NPM_VERSION 2.10.0
    RUN curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/node-v \
    $NODE_VERSION-linux-x64.tar.gz" \
    && curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/\SHASUMS256.txt.asc" \
    && gpg --verify SHASUMS256.txt.asc \
    && grep " node-v$NODE_VERSION-linux-x64.tar.gz$" SHASUMS256.txt.asc | sha256sum -c -

    Давайте разберемся, что здесь происходит:

    1. Получение открытых GPG-ключей

    2. Скачивание Node.js пакета

    3. Скачивание хеш-суммы Node.js пакета на базе алгоритма SHA256

    4. Использование GPG-клиента для проверки, что хеш-сумма подписана тем, кто владеет закрытыми ключами

    5. Проверяем, что вычисленная хеш-сумма от пакета совпадает со скачанной с помощью sha256sum

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

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

    Пример безопасного добавления GPG-ключей вместе с источником пакетов.

    RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 \
    --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
    RUN echo "deb http://nginx.org/packages/mainline/debian/\
    jessie nginx" >> /etc/apt/sources.list

    В случае, если для пакетов цифровая подпись в явном виде не предоставляется вендором, ее необходимо предварительно создать и сравнить в рамках сборки.

    Пример для SHA256:

    RUN curl -sSL -o redis.tar.gz \
    http://download.redis.io/releases/redis-3.0.1.tar.gz \
    && echo "0e21be5d7c5e6ab6adcbed257619897db59be9e1ded7ef6fd1582d0cdb5e5bb7 \
    *redis.tar.gz" | sha256sum -c -

    Не использовать ADD

    Инструкция ADD, получив в качестве параметра путь к архиву, автоматически распакует этот архив при своем выполнении. Это может привести, в свою очередь, к появлению zip-бомбы внутри контейнера. При распаковке zip-бомба может вызвать отказ в обслуживании (DoS) приложения, путем заполнения всего выделенного свободного места.

    Еще одна небольшая особенность ADD команды заключается в том, что вы можете передать ей URL в качестве параметра, и она будет извлекать контент во время сборки, что также может привести к атаке "человек-посередине":

    ADD https://cloudberry.engineering/absolutely-trust-me.tar.gz

    Аналогично предыдущей рекомендации стоит добавлять компоненты в образ инструкцией COPY, так как она работает с локальными данными, предварительно проверяя их при помощи SCA инструментов.

    Задавать USER в конце Dockerfile

    В случае, если злоумышленнику удастся заполучить shell внутри вашего контейнера, он может оказаться root'ом, что сильно упростит дальнейшую атаку с выходом за пределы контейнера. Чтобы избежать этого, указывайте пользователя в явном виде через инструкцию USER. Однако это работает только в том случае, если вашему приложению не требуется привилегии root после сборки.

    RUN groupadd -r user_grp && 
    useradd -r -g user_grp user
    USER user

    Использовать gosu вместо sudo в процессе инициализации

    Утилита gosu будет полезна, когда необходимо предоставлять root доступ после сборки Dockerfile во время инициализации, но при этом приложение должно запускаться в непривилегированном режиме.

    В примере ниже выполняется команда chown в рамках entrypoint-скрипта, требующая права root, после чего приложение продолжает выполняться из-под пользователя redis.

    #!/bin/bash
    set -e
    if [ "$1" = 'redis-server' ];
      then
        chown -R redis . 
        exec gosu redis "$@"
      fi
    exec "$@"

    Основная цель инструмента - запуск процессов от определенного пользователя, но в отличие от sudo и su, gosu не делает fork процессов, как показано ниже:

    $ docker run -it --rm ubuntu:trusty su -c 'exec ps aux'
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0  46636  2688 ?        Ss+  02:22   0:00 su -c exec ps a
    root         6  0.0  0.0  15576  2220 ?        Rs   02:22   0:00 ps aux
    $ docker run -it --rm ubuntu:trusty sudo ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  3.0  0.0  46020  3144 ?        Ss+  02:22   0:00 sudo ps aux
    root         7  0.0  0.0  15576  2172 ?        R+   02:22   0:00 ps aux
    $ docker run -it --rm -v $PWD/gosu-amd64:/usr/local/bin/gosu:ro ubuntu:trusty gosu root ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0   7140   768 ?        Rs+  02:22   0:00 ps aux

    Это сохраняет концепцию, согласно которой контейнер ассоциируется с единственным процессом. Тем не менее, из слов разработчиков gosu не является заменой sudo, так как в рамках данного fork'а происходит взаимодействие с Linux PAM через функции pam_open_session() и  pam_close_session(). Использование gosu вместо sudo за пределами процесса инициализации может привести к некорректной работе приложения.

    Distroless images и минимальные образы

    Можно сильно сократить поверхность атаки злоумышленника отказавшись от базовых образов Linux-дистрибутивов (Ubuntu, Debian, Alpine) и перейдя на Disroless-образы. Это образы, которые содержат только само приложение и необходимые для него зависимости без использования лишних системных компонентов (например, bash). Одним из явных преимуществ, помимо сокращения поверхности атаки и возможностей злоумышленника, является снижение размера образа. Это в свою очередь уменьшает "шум" в результатах сканирования такими инструментами как Trivy, Clair и так далее.

    Подобная стратегия имеет много преимуществ с точки зрения безопасности, но сильно затрудняет работу разработчиков, которым необходимо заниматься отладкой внутри контейнера. Альтернативным вариантом может являться использование минимального образа, например, на базе UNIX-сборки Alpine. При ответственном соблюдении данного подхода, образ разработчика будет содержать минимум зависимостей, что позволит сканерам образов сформировать меньший объем результатов, среди которых будет проще отсеять ложные срабатывания.

    Цикл статей по созданию минимального образа:

    Одним из вариантов, как можно эффективно сократить размер образа является multi-stage сборка, которая ко всему прочему поможет безопасно работать с секретами. Об этом будет в следующем подразделе.

    Полезным инструментом также является Docker-slim, позволяющий уменьшить размер написанного образа.

    Безопасная работа с секретами

    Секреты могут по ошибке помещаться в качестве параметра инструкции ENV или передаваться внутрь образа как текстовый файл. Также секреты могут скачиваться через wget. Подобные сценарии недопустимы, так как злоумышленник может с легкостью получить доступ к секретам. Это можно сделать, например, получив доступ к API Docker:

    # docker inspect ubuntu -f {{json .Config.Env}}
    ["SECRET=mypassword", ...]

    Также злоумышленник может получить доступ к секретам через логи, директорию /proc или при утечки файлов исходных кодов. В данном случае лучше всего может подойти использование решений класса Vault, например, HashiCorp Vault или Conjur, однако рассмотрим и другие методы.

    Соблюдать принцип многоэтапных сборок

    Многоэтапная (multi-stage) сборка поможет не только сократить размер вашего образа, но еще и организовать эффективное управление секретами. Основной принцип - извлекать и управлять секретами на промежуточном этапе сборки образа, который позже удалится. В итоге конфиденциальные данные не попадут в сборку конечного образа.

    #builder
    FROM ubuntu as intermediate
    
    WORKDIR /app
    COPY secret/key /tmp/
    RUN scp -i /tmp/key build@acme/files .
    
    #runner
    FROM ubuntu
    WORKDIR /app
    COPY --from=intermediate /app .

    Один из минусов этого сценария - сложное кэширование, что ведет к замедлению сборки.

    Работа с секретами через BuildKit

    С версии Docker 18.09 появляется экспериментальная интеграция со сборщиком BuildKit, которая позволит кроме увеличения производительности, эффективно работать с секретами.

    Пример синтаксиса:

    # syntax = docker/dockerfile:1.0-experimental
    FROM alpine
    
    # shows secret from default secret location
    RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecre
    
    # shows secret from custom secret location
    RUN --mount=type=secret,id=mysecret,dst=/foobar cat /foobar

    После выполнения сборки с помощью buildkit с указанием ключа --secret секретная информация не сохранится в конечном образе.

    Пример:

    $ docker build --no-cache --progress=plain --secret id=mysecret,src=mysecret.txt .

    Подробнее можно прочитать в официальной документации Docker.

    Остерегаться рекурсивного копирования

    Команда из примера ниже может привести к тому, что файл секрета случайно окажется внутри вашего образа, если находится внутри той же директории. Если вы имеете подобные файлы, не забывайте использовать .dockerignore. Среди файлов внутри образа могут оказаться .git, .aws, .env.

    COPY . .

    Один из примеров подобных ошибок - кейс с утечкой исходных кодов Twitter Vine. Так в 2016 году специалист по информационной безопасности обнаружил на DockerHub образ vinewww, внутри которого обнаружились исходные коды сервиса Vine, ключи API и секреты сторонних сервисов.

    Анализаторы Dockerfile

    Используйте анализаторы Dockerfile, которые можно встроить в ваш пайплайн сборки. Это могут быть:

    Hadolint - простой линтер Dockerfile. Большая часть проверок не относится к security и основана на официальных рекомендациях Docker (ссылка). Однако для наиболее распространенных ошибок таких проверок будет достаточно.

    Conftest - анализатор файлов конфигураций, в том числе Dockerfile. Для проверки Dockerfile требуется предварительно написать правила на языке Rego, который, помимо этого, используется в быстро набирающей технологии Open Policy Agent для защиты облачных сред в процессе эксплуатации. Это позволит сохранить вариативность, возможность кастомизации и не потребует изучения ранее неизвестных языков. Conftest не содержит встроенный набор правил, поэтому подразумевается, что вы будете писать их самостоятельно. В качестве отправной точки можно использовать эту статью.

    Для автоматизации процесса проверки можно встроить эти инструменты в пайплайн разработки. Пример встраивания:

    Способы и примеры внедрения утилит для проверки безопасности Docker.

    Практика

    Вы можете попробовать проэксплуатировать распространенные уязвимости при написании Dockerfile с помощью образа Pentest-in-Docker. Пошаговое описание можно найти в репозитории. Одна из главных ошибок - использование debian:wheazy, старого образа Debian, на котором поддерживается не требующийся для работы приложения Bash, содержащий в себе уязвимость удаленного выполнения кода (RCE). Таким образом злоумышленник может получить доступ к сервисной учетной записи www-data, отправив запрос на подключение к reverse-shell. Вторая ошибка - использование sudo, что позволяет злоумышленнику повысить привилегии от www-data до root внутри контейнера. И наконец отсутствие USER в конце Dockerfile из-за чего злоумышленник может выполнять действия из-под root в случае, если получит доступ к API Docker.

    Дополнительные ресурсы

    Swordfish Security
    Информационная безопасность, DevSecOps, SSDL

    Комментарии 20

      +9

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


      RUN apt-get install cowsay=3.03+dfsg1-6

      А если пакетов 10, 20, 100? Выглядит так, что на самом деле нужна некая обвязка, которая по условному докерфайлу, который идет с незафиксированными зависимостями и мы его принимаем как "есть", будет генерировать уже полноценный Dockerfile со всеми прописанными зависимостями. Но ни коммерческих решений, ни опенсурсных для этого я не видел. А разработчик… а разработчик чокнется прописывать эти 10, 20, 100 зависимостей на каждый чих… Это, на мой взгляд, вполне автоматизируется и должно быть автоматизировано.

        +1
        Хотелось бы видить в статье опрос по этому поводу. Кто так делает? Кто понимает что так надо, но всё равно не делает, т.к это постоянно нужно обновлять.
        +1
        Хорошо тем кто на Go пишет — cобирай FROM scratch и все)

        А с node хоть сколько в apt версии пакетов указывай, при yarn install приедет два чемодана node_modules и что там в них никто не знает
          +7
          На opennet как раз сегодня новость:
          Попытки зафиксировать версии пакетов в дистрибутиве приводят лишь на нарастанию устаревших версий в репозитории, не обновляемых годами. Прекращение сопровождения пакета негативно отражается на множестве других пакетов и приводит к ещё большим проблемам. Кроме того, перекрёстные зависимости приводят к тому, что многие библиотеки Node.js становится невозможно деинсталлировать из системы, что, в свою очередь, не позволяет деинсталлировать и другие программы на Node.j
            0

            Версии фиксятся в апп а не в либах, если не фиксить версии в апп, то она в самый неудобный момент может сдохнуть.

            0

            scratch запускает процессы под root'ом, нужно об этом понить

            –2
            Некоторые моменты спорны
            Использование apt-get upgrade, yum update может привести к тому, что внутри вашего контейнера будет произведена установка неизвестного вам ранее ПО, либо ПО уязвимой версии.
            Обычно это рекомендуется делать, чтобы обновить «системные компоненты» Docker образа: kernel, glibc (если он есть) etc. Потому что со времени выпуска образа контейнера могут выйти обновления фиксирующие баги или улучшающие функционал. С другой стороны: установка неизвестного вам ранее ПО — это вообще из ряда вон выходящее. Откуда оно у вас там взялось? Вот пример Dockerfile:
            RUN dnf update -y; \
                dnf install -y \
                wget \
                yarn \
                unzip; \
                dnf clean all;
            

            Как сюда может затесаться неизвестное вам ранее ПО и тем более не безопасное? Обновление выполняется с официального репозитория.

            Использование curl и wget без мер предосторожности позволяет злоумышленнику выполнять скачивание нежелательных компонентов с неизвестных ресурсов (атака «человек-посередине», при которой злоумышленник может перехватить незащищенный трафик и подменить скачиваемый нами пакет на зловредный).
            Тут что-то всего до кучи: MITM и скачивание нежелательных компонентов с неизвестных ресурсов. Чтобы выполнить атаку «Человек посередине» надо хорошо постараться. Как-то оказаться посередине, произвести подмену во время скачивания. Хлопотно это. Куда чаще мелькают новости, что в известных репозиториях обнаруживается подмена оригинальных пакетов на сторонние, либо у известных пакетов обнаруживается странные незаявленные возможности. Это всё куда клоню? Доверяй, но проверяй. И тестируй собранные образы.

            Теперь немного не по теме.
            Секреты могут по ошибке помещаться в качестве параметра инструкции ENV или передаваться внутрь образа как текстовый файл.
            С этим уже надо постараться, чтобы оно оказалось по ошибке в Dockerfile.

            В случае, если злоумышленнику удастся заполучить shell внутри вашего контейнера, он может оказаться root'ом, что сильно упростит дальнейшую атаку с выходом за пределы контейнера.
            В большинстве случаев не требуется выход за пределы окружения контейнера (чтобы не светиться) и повышение привилегий тоже. Если основное приложение работает от непривилегированного пользователя, то и неосновное тоже будет работать.
              +1
              RUN dnf update -y; \
              dnf install -y \
              wget \
              yarn \
              unzip; \
              dnf clean all;

              легко. Вы не контролируете, что там в манифесте НОВОЙ ВЕРСИИ пакета, который придет. Там запросто может быть новая зависимость (новый пакет). И я уже писал, что новая версия пакетов wget/yarn/unzip в общем случае может притянуть что-то еще.


              Обычно это рекомендуется делать, чтобы обновить «системные компоненты» Docker образа: kernel, glibc (если он есть) etc

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


              Чтобы выполнить атаку «Человек посередине» надо хорошо постараться

              не нужно стараться. Любой мобильный провайдер делает перехват HTTP. Я был очень удивлен, когда на Мегафоне вместо скачивания пакета с дебиан оф репо подсовывается что угодно, кроме исходного файла.


              Доверяй, но проверяй. И тестируй собранные образы.

              это никуда не девается )


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

              Злоумышленнику же хочется запустить там Майнер. Или в соседние поды (если речь про кубер) сходить. Все равно будет щупать на предмет «сходить и повысить себе привилегии», а не тихонько сидеть у себя в уголке )

                +1
                легко. Вы не контролируете, что там в манифесте НОВОЙ ВЕРСИИ пакета, который придет. Там запросто может быть новая зависимость (новый пакет). И я уже писал, что новая версия пакетов wget/yarn/unzip в общем случае может притянуть что-то еще.
                точно так же как и не контролируете что в самом wget. С таким подходом можно дойти до абсурда. Или вы серьезно считаете, что ментейнеры дистрибутива (Debian/Ubuntu/RHEL) не заметят какой то левой зависимости?

                не нужно стараться. Любой мобильный провайдер делает перехват HTTP.
                что мешает использовать https?
                  –1
                  что мешает использовать https?

                  никто не мешает ) пользуйтесь — я разрешаю. А вообще смысла особо для репозиториев RPM/DEB в https нет — там все подписано ключами GPG. Проблема в первую очередь с curl | sh, ну, и потенциальной доступностью репы в целом.


                  точно так же как и не контролируете что в самом wget. С таким подходом можно дойти до абсурда.

                  нет, но мне это и не нужно. Мне нужна конкретная версия wget.


                  Или вы серьезно считаете, что ментейнеры дистрибутива (Debian/Ubuntu/RHEL) не заметят какой то левой зависимости?

                  не "левой", а нежелательной для меня.

                    +1
                    Проблема в первую очередь с curl | sh, ну, и потенциальной доступностью репы в целом.
                    ну если вы запускаете curl | sh, то ССЗБ. О какой безопасности мы тогда вообще говорим ))

                    не «левой», а нежелательной для меня.
                    есть конкретные примеры? Когда при обновлении прилетали «нежелательные» зависимости и именно из-за них возникали уязвимости/риски?
                      0
                      ну если вы запускаете curl | sh, то ССЗБ. О какой безопасности мы тогда вообще говорим ))

                      По первому в статье есть солюшен, как можно сделать, без организации своего «доверенного» репо:


                      RUN curl -sSL -o redis.tar.gz \
                      http://download.redis.io/releases/redis-3.0.1.tar.gz \
                      && echo "0e21be5d7c5e6ab6adcbed257619897db59be9e1ded7ef6fd1582d0cdb5e5bb7 \
                      *redis.tar.gz" | sha256sum -c -```
                  0
                  Любой мобильный провайдер делает перехват HTTP. Я был очень удивлен, когда на Мегафоне вместо скачивания пакета с дебиан оф репо подсовывается что угодно, кроме исходного файла.

                  Чтоа? А вы не разбирались с Мегафоном почему оно так?
                    0

                    А чего разбираться? Оно и так понятно — подпихивают свою рекламу, куда могут. Насколько это законно я не вдавался в детали.

                0

                Спасибо огромное за статью. Было очень интересно!

                  –6
                  Лучшая практика — это не использовать Dockerfile, я как-то n лет назад повелся на уговоры смузи-хипстеров и пробовал все перевести с LXC на докер, так замучился писать эти Dockerfile, там целые косяки подводных камней плавают, каждый раз выясняются какие-то нюансы, о которых к тому же с документации не узнать, это еще не говоря о баганутости самого докера, в итоге быстро послал его, сижу на LXC уже наверное более пяти лет и горя не знаю
                    +1
                    LXC слишком новое и хипстерское. Нужно OpenVZ.
                    0
                    yum update может привести к тому, что внутри вашего контейнера будет произведена установка неизвестного вам ранее ПО
                    можно примеры в студию в контексте RHEL? У меня за 15 лет использования, что то ничего неизвестного не устанавливалось при yum update

                    либо ПО уязвимой версии.
                    как раз отсутствие yum update скорее приведет к ситуации с дырявым ПО. Или вы предлагаете смотреть changelog на все установленные пакеты?
                    $ docker run -it --rm centos:7 rpm -qa | wc -l
                    147
                    
                      0
                      Или вы предлагаете смотреть changelog на все установленные пакеты?

                      честно — да, это трудоемко. Но оголтелый update (смотрим в общем, не только на RHEL, а вообще) может сделать по очевидным соображениям хуже, чем есть сейчас.
                      Я уж не говорю о том, что RHEL (именно RHEL) внутри контейнеров очень редко используют — все больше alpine/ubuntu/centos...

                        0
                        UBI не исправил эту практику?

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

                    Самое читаемое