🐳 Монтируем безопасные секреты во временя сборки с помощью Docker и Docker Compose |

🐳 Монтируем безопасные секреты во временя сборки с помощью Docker и Docker Compose

Мануал

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

Это может быть частное git-репозиторий или частный репозиторий пакетов, например, самостоятельная версия PyPI, RubyGems и т. д..

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

Основная идея заключается в том, что вам нужен только секретный API-ключ или токен для получения доступа к частному репозиторию.

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

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

Использование ARG небезопасно

Давайте быстро рассмотрим, как использование ARG для сборки является небезопасным способом решения этой проблемы:

FROM debian:stable-slim

ARG ACCESS_TOKEN
RUN echo ./private-install-script --access-token "${ACCESS_TOKEN}"

./private-install-script – это место для команды, которую вы запустите для установки ваших пакетов.

Он не существует.

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

Затем при сборке образа вы можете сделать что-то вроде этого:

export ACCESS_TOKEN="supersecretvalue123"
docker image build --build-arg ACCESS_TOKEN="${ACCESS_TOKEN}" . -t myimage

При желании можно использовать Docker Compose или настроить чтение токена из файла .env, чтобы не экспортировать его вручную каждый раз при сборке образа.

Кажется разумным, верно?

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

Проблема в том, что слой RUN содержит ваш ACCESS_TOKEN в виде простого текста.

Если бы вы собирали образ таким образом, то смогли бы увидеть секретное значение в выходных данных:

docker image build --build-arg ACCESS_TOKEN="${ACCESS_TOKEN}" . -t myimage
[+] Building 1.2s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 146B
=> [internal] load metadata for docker.io/library/debian:stable-slim
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [1/2] FROM docker.io/library/debian:stable-slim
=> [2/2] RUN echo ./private-install-script --access-token "supersecretvalue123"
=> exporting to image
=> => exporting layers
=> => writing image sha256:672eead902c28f4bb3a346d56adc21620ab14be34f3d8a153cebdaf88949f1d6
=> => naming to docker.io/library/myimage

Вы также можете запустить docker image history myimage и найти его в результатах:

docker image history myimage
IMAGE          CREATED          CREATED BY
672eead902c2   39 seconds ago   RUN |1 ACCESS_TOKEN=supersecretvalue123 /bin...
<missing>      39 seconds ago   ARG ACCESS_TOKEN
<missing>      2 months ago     /bin/sh -c #(nop)  CMD ["bash"]
<missing>      2 months ago     /bin/sh -c #(nop) ADD file:d43b1d5d6f0054ace...

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

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

Что, если вы добавите инструмент для сканирования уязвимостей в системе безопасности, и он просканирует ваше образ?

Теперь у них есть доступ к вашему секрету.

Бездумное копирование секретов облегчает случайную утечку информации.

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

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

Кроме того, я использовал его только для токенов доступа к приватным репо, которые достаточно легко переделать.

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

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

Прежде всего, стоит отметить, что у вас должен быть включен BuildKit.

Если вы используете Docker v23.0+ (февраль 2023 года) на любой платформе, то он включен по умолчанию.

Если вы используете более старую версию Docker, вы можете включить его несколькими способами, об этом говорится в документации Docker.

Немного измененный Dockerfile из приведенного выше примера:

FROM debian:stable-slim

RUN --mount=type=secret,id=ACCESS_TOKEN \
    echo "./private-install-script --access-token $(cat /run/secrets/ACCESS_TOKEN)"
  • Вместо ARG мы используем –mount в нашей инструкции RUN.
  • Этот секрет будет доступен только для этой конкретной инструкции RUN
  • type=secret говорит, что мы хотим, чтобы это был секрет, другие типы указаны в документации Docker
  • id=ACCESS_TOKEN позволяет нам назвать наш секрет как угодно (мы будем ссылаться на него позже)
  • $(cat /run/secrets/ACCESS_TOKEN) позволяет нам получить доступ к секрету по его id
  • Смонтированные секреты попадают в /run/secrets, catпозволяет нам получить их значение

Тогда мы сможем создать наш образ:

export ACCESS_TOKEN="supersecretvalue123"
docker image build --secret "id=ACCESS_TOKEN" . -t myimage
[+] Building 1.0s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 306B
=> [internal] load metadata for docker.io/library/debian:stable-slim
=> [internal] load .dockerignore
=> => transferring context: 2B
=> CACHED [stage-0 1/2] FROM docker.io/library/debian:stable-slim
=> [stage-0 2/2] RUN --mount=type=secret,id=ACCESS_TOKEN     echo "./private-install-script --access-token $(cat /run/secrets/ACCESS_TOKEN)"
=> exporting to image
=> => exporting layers
=> => writing image sha256:b3e758370af54e7cc1612b33bb349bd8a5905a85276e70998a955899e27e4cdc
=> => naming to docker.io/library/myimage
  • –secret «id=ACCESS_TOKEN» должен совпадать с id в Dockerfile
  • Опционально мы можем задать id=x,env=ACCESS_TOKEN, чтобы иметь идентификатор, отличный от имени env var, в этом случае вы будете ссылаться на id=x в вашем Dockerfile
  • Лично я стараюсь не задавать env=, поскольку использование одного и того же имени интуитивно понятно.

Обратите внимание, что само секретное значение нигде не выводится

А вот и история:

docker image history myimage
IMAGE          CREATED          CREATED BY
cc8bd26ab409   20 seconds ago   RUN /bin/sh -c echo "./private-install-scrip...
<missing>      2 months ago     /bin/sh -c #(nop)  CMD ["bash"]
<missing>      2 months ago     /bin/sh -c #(nop) ADD file:d43b1d5d6f0054ace...

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

Вы можете запустить приведенную выше команду с параметром –no-trunc, чтобы избежать обрезания вывода, это будет выглядеть некрасиво, но вот первая строка вывода без обрезания, которая включает нашу приватную команду RUN /bin/sh -c echo «./private-install-script –access-token $(cat /run/secrets/ACCESS_TOKEN)».

Действительно ли это работает?

Учитывая, что мы ничего не делаем в нашей инструкции RUN (это фиктивная команда echo), как мы можем убедиться, что все действительно работает?

Я не советую использовать этот Dockerfile для чего-либо, кроме локального тестирования, но вот быстрый способ убедиться, что все действительно работает.

Это полностью уничтожит цель использования секретного монтирования:

FROM debian:stable-slim

RUN --mount=type=secret,id=ACCESS_TOKEN \
    cat /run/secrets/ACCESS_TOKEN > /tmp/dontdothisnormally

CMD ["cat", "/tmp/dontdothisnormally"]

Пересоберите с помощью docker image build –secret «id=ACCESS_TOKEN» . -t myimage и затем:

docker container run --rm myimage
supersecretvalue123

Да, он действительно устанавливается!

В качестве эксперимента вы можете попробовать просмотреть /run/secrets/ACCESS_TOKEN в CMD, и вы будете рады узнать, что он недоступен.

Вы также не можете использовать –mount с CMD, что вполне логично, поскольку монтирование доступно для RUN.

Что, если мой инструмент ожидает установки переменной окружения?

В нашем примере мы выполнили ./x –access-token $(cat /run/secrets/ACCESS_TOKEN), поэтому ее установка в качестве переменной окружения ничем не отличается от того, как вы обычно устанавливаете ее, например ACCESS_TOKEN=$(cat /run/secrets/ACCESS_TOKEN) ./x.

Вы также можете сделать его более красивым, отформатировав его следующим образом:

RUN --mount=type=secret,id=ACCESS_TOKEN \
  ACCESS_TOKEN=$(cat /run/secrets/ACCESS_TOKEN) \
    ./x

А как насчет нескольких секретов?!

Нет проблем, Docker позаботится об этом:

RUN \
    --mount=type=secret,id=ACCESS_KEY \
    --mount=type=secret,id=ACCESS_TOKEN \
        ACCESS_KEY=$(cat /run/secrets/ACCESS_KEY) \
        ACCESS_TOKEN=$(cat /run/secrets/ACCESS_TOKEN) \
          ./x

Тогда вы сможете собрать его с помощью:

export ACCESS_KEY="somethingsecure"
export ACCESS_TOKEN="supersecretvalue123"
docker image build --secret "id=ACCESS_KEY" --secret "id=ACCESS_TOKEN" . -t myimage

Как насчет монтирования файлов, а не переменных окружения?

Это может быть удобно, если у вас есть что-то вроде файла ~/.pypirc или какого-то конфигурационного файла, содержащего чувствительные API-ключи или токены, которые используются для команды:

RUN --mount=type=secret,id=pypirc,target=/root/.pypirc \
  echo twine upload dist/*
  • target= позволяет нам определить, где этот файл будет находиться в нашем изображении.
  • Эта строка echo демонстрирует загрузку пакета Python, здесь она не важна.

Затем мы можем собрать его с помощью:

docker image build --secret "id=pypirc,src=${HOME}/.pypirc" . -t myimage
[+] Building 1.0s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 600B
=> [internal] load metadata for docker.io/library/debian:stable-slim
=> [internal] load .dockerignore
=> => transferring context: 2B
=> CACHED [stage-0 1/2] FROM docker.io/library/debian:stable-slim
=> [stage-0 2/2] RUN --mount=type=secret,id=pypirc,target=/root/.pypirc   echo twine upload dist/*
=> exporting to image
=> => exporting layers
=> => writing image sha256:78d52c314c41ce45593e30d9c502c7a8105f6190e6ca84653277b94551bdc397
=> => naming to docker.io/library/myimage
  • src= – это место, где файл лежит на вашем хосте Docker при выполнении команды сборки.

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

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

Просто задайте каждому из них уникальный идентификатор, а также src и target, и все готово.

🐳 Docker secret – как использовать в Docker Swarm и Docker Compose

Docker Compose

В большом проекте у вас может быть несколько секретов, и вы используете Docker Compose.

Вот несколько примеров, которые преобразуют вышеописанное в формат Docker Compose.

Доступ к переменным окружения

Во-первых, вот пример Dockerfile.

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

Не делайте этого в реальном проекте!

Кстати, в этом файле нет ничего специфичного для Docker Compose:

FROM debian:stable-slim

RUN --mount=type=secret,id=ACCESS_KEY \
    --mount=type=secret,id=ACCESS_TOKEN \
    echo "$(cat /run/secrets/ACCESS_KEY) | $(cat /run/secrets/ACCESS_TOKEN)" \
      > /tmp/dontdothisnormally

CMD ["cat", "/tmp/dontdothisnormally"]

Затем мы настраиваем docker-compose.yml, чтобы установить наши секреты.

Обратите внимание на отступ свойства services.myservice.build.secrets, важно, что это свойство сборки:

services:
  myservice:
    build:
      context: "."
      secrets:
        - "ACCESS_KEY"
        - "ACCESS_TOKEN"

secrets:
  ACCESS_KEY:
    environment: "ACCESS_KEY"
  ACCESS_TOKEN:
    environment: "ACCESS_TOKEN"

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

Свойство окружения – это имя переменной env, которая будет считываться с вашего хоста Docker.

Имя ключа словаря – это идентификатор секрета.

Наконец, по желанию создайте файл .env, и Docker Compose будет автоматически считывать данные из него.

Если вы не выполните этот шаг, то вам нужно будет экспортировать эти env-вары в текущую оболочку, иначе ваши секреты будут пустыми:

export ACCESS_KEY=somethingsecure
export ACCESS_TOKEN=supersecretvalue123

Теперь вы можете увидеть, как все это работает:

docker compose build && docker compose run myservice
somethingsecure | supersecretvalue123

Доступ к файлам

Это тот же Dockerfile, что и в нашем примере с файлами:

FROM debian:stable-slim

RUN --mount=type=secret,id=pypirc,target=/root/.pypirc \
  echo twine upload dist/*

Затем мы настраиваем docker-compose.yml, чтобы установить наш секрет:

services:
  myservice:
    build:
      context: "."
      secrets:
        - "pypirc"

secrets:
  pypirc:
    file: "${HOME}/.pypirc"

Единственное реальное отличие от другого примера Docker Compose заключается в том, что мы выполняем поиск по файлу в нижней части вышеуказанного файла.

Как и в команде Docker, этот путь ищется на вашем хосте Docker.

Теперь вы можете увидеть, как все это работает:

$ docker compose build && docker compose run myservice
root@e491336de8f2:/#

Технически здесь нечего смотреть, и вы попадёте в сеанс командной строки, так как именно это делает образ Debian по умолчанию.

Вы всегда можете отменить CMD и вывести файл, чтобы убедиться, что он работает (он работает).

Здесь я рассмотрел только несколько распространённых случаев использования.

Не стесняйтесь проверить документацию для других вариантов.

Вы даже можете подключить SSH-ключ.

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

см. также:

 

Пожалуйста, не спамьте и никого не оскорбляйте. Это поле для комментариев, а не спамбокс. Рекламные ссылки не индексируются!
Добавить комментарий