- PVSM.RU - https://www.pvsm.ru -
Как правильно хранить секреты? В репозитории, в системе деплоя или в системе управления конфигурациями? На личном компьютере, на серверах, а может в коробке под кроватью? А как управлять секретами, чтобы не допускать утечек?
Сергей Носков (Albibek [1]) — руководитель группы информационной безопасности платформы из Авито, знает ответ на эти вопросы и поделится с нами. В Авито два года активно используют HashiCorp Vault, за это время набили шишки, и прокачали опыт до уровня «Мастер».
В статье всесторонне поговорим про Vault: что это такое, где и как используется в компании, как в Авито управляют секретами с помощью HashiCorp Vault, как используют Puppet и Kubernetes, варианты использования с Puppet и другими SCM, какие возникают проблемы, что болит у безопасников и разработчиков, и, конечно, поделимся идеями, как все исправить.
Любая конфиденциальная информация:
Всю информацию, которую мы хотим сохранить в тайне, мы называем секретом. Это создает проблему с хранением: в репозитории хранить плохо, в зашифрованном виде — нужно где-то держать ключи шифрования.
HashiCorp Vault — одно из неплохих решений проблемы.
На мой взгляд, это достаточно удобный инструмент.
Инструмент развивается и за последнее время в нем появилось много интересных фич: CORS-заголовки для GUI без посредников; встроенный GUI; нативная интеграция с Kubernetes; плагины для logical- и auth-бэкендов и фреймворк.
Большинство изменений, которые понравились лично мне, — это возможность не писать расширения и дополнения, которые будут стоять снаружи относительно инструмента.
Например, есть Vault, вы хотите его расширить — написать дополнительную логику или свой UI для автоматизации, который будет что-то автоматизировать. До изменений приходилось поднимать дополнительный сервис, который стоит перед Vault, и проксирует все запросы: сначала запросы идут в сервис, потом — в Vault. Это плохо тем, что в промежуточном сервисе может быть пониженный уровень безопасности, а через него идут все секреты. Риски безопасности намного выше, когда секрет проходит через несколько точек сразу!
Когда вы поднимаете вопрос хранения конфиденциальной информации и решаете шифровать, то, как только вы что-то зашифровали, ваш секрет смещается из места шифровки туда, где хранится ключ. Это происходит постоянно: как только вы сохранили куда-то секрет или изменили существующий, у вас появляется еще один и начинается заколдованный круг — где хранить секрет для доступа к секрету.
Секрет доступа к секрету — это та часть безопасности, которая называется аутентификацией. У безопасности есть еще одна часть — авторизация. В процессе авторизации проверяется, можно ли пользователю получить доступ именно туда, куда он запрашивает. В случае с Vault есть доверенная третья сторона, которая решает, выдать секрет или нет. Авторизация лишь частично решает проблему.
В Авито HashiCorp установлен в единственной большой инсталляции на всю сеть. У HashiCorp Vault много разных бэкендов. Мы используем бэкенд на базе Consul тоже от HashiCorp, потому что Vault умеет поддерживать собственную отказоустойчивость только через Consul.ь.
Unseal — это способ не держать в одном месте мастер-ключ. Когда запускается Vault, он все шифрует на каком-то ключе, и снова появляется проблема курицы и яйца: где хранить секрет, которым будут шифроваться все остальные секреты. Чтобы избежать этой проблемы, в Vault предусмотрен составной ключ, для получения которого требуется несколько частей ключа, которые мы раздаём нескольким сотрудникам. В Авито мы настроили в опциях Unseal на 3 человека из 7. Если мы запускаем Vault, то чтобы он начал работать, обязательно должны прийти как минимум 3 человека и ввести свою часть ключа. Ключ поделен на 7 частей и можно принести любые из них.
Мы собрали маленький тестовый Vault — песочницу для разработчиков, где они могут играть. Он есть в виде Docker-контейнера и создает простые секретики, чтобы люди могли потрогать инструмент руками, освоиться. В песочнице нет Consul и кластеризации, это просто файловая система, на которой Vault держит зашифрованные секреты, и маленький скрипт для инициализации.
Вот что мы сейчас храним в Vault:
Внутри себя Vault хранит всё только в JSON, это не всегда удобно и требует дополнительных действий от разработчика, поэтому в основном мы выкладываем секреты в виде файла.
Мы стараемся доставлять секреты в виде файлов.
Мы не говорим разработчику: «Иди в Vault, бери секрет!», а выкладываем на диск файл и сообщаем: «Разработчик, у тебя на диске появится файл, забирай секрет из него, а мы уже разберемся, как достать его из Vault и принести тебе».
Мы приняли простое соглашение для полей JSON, в котором указываем, с какими правами выкладывать файл. Это метаданные для файловой системы, а поле data — это закодированная строка c самим секретом, которая станет содержимым файла.
Практически во всей инфраструктуре Авито используется Puppet, им раскатываются все серверы.
У Puppet есть удобный инструмент организации иерархии — Hiera. Vault очень хорошо интегрируется с Hiera через дополнительный модуль, потому что в эту библиотеку идёт запрос ключ-значение, а Vault сам база ключ-значение, но со всеми функциями безопасности — с прозрачным шифрованием и возможностью выбора доступа к ключам.
Поэтому первое, что мы внедрили — это Vault в Puppet, но с одним дополнением — у нас есть промежуточный слой, который называется Router backend. Router backend — отдельный модуль Hiera, просто файлы на диске, в которых написано, куда Hiera должна идти за ключом — в Vault или в другое место.
Он нужен, чтобы Hiera не ходила в Vault постоянно, потому что она идет всегда по всей иерархии. Это не проблема Vault или нагрузки на него, а особенность работы самой Hiera. Поэтому если оставить только модуль для Vault без Router backend, Puppet-мастер будет очень долго собирать конфигурацию для Puppet-агента, так как будет проверять каждый ключ в Vault.
Для Puppet проблема курицы и яйца решается за счёт того, что авторизующая сторона у нас — Puppet-мастер. Именно он выдает секрет для доступа к секрету. Puppet-мастер имеет доступ ко всем секретам сразу, но каждому хосту разрешено получать только тот, который ему предназначен. Хост на Puppet-мастере уже авторизован своим сертификатом, который генерируется локально и пределов хоста не покидает. В принципе, секрет для доступа к секрету остается, но это уже не так критично.
Наш процесс выкладки нового секрета в Puppet состоит из следующих этапов.
Puppet-агенты прогоняются у нас каждые 30 мин, поэтому приходится немного подождать, пока выкатится секрет. Проблем это не вызывает — мы выкладываем секреты не каждый день. Пока в дело не включается Kubernetes, накладных расходов немного и мы готовы выкладывать секреты в Vault руками с минимальной автоматизацией.
Дополнительным плюсом мы получаем «фишку» Hiera — секрет можно выкладывать сразу для группы хостов или в зависимости от роли хоста, которую мы выставляем в переменной role.
Единственная опасность: если у вас Puppet, и вы используете Hiera, не подставляйте в пути в шаблоны переменных что попало, потому что многие факты и переменные собираются на клиентской стороне. Если злоумышленник подменит факт на клиенте, Puppet-мастер отдаст ему чужие секреты. Обязательно проверяйте переменные: используйте только те, что Puppet-мастер запрещает определять на клиентской стороне.
Если вдруг у вас не Puppet, то, скорее всего, Ansible. Для Chef и других централизованных SCM свои решения — это плагин, который умеет обращаться к Vault. Предлагаю несколько вариантов, которые можно реализовать с Ansible.
Локально для сервера генерируете токен, который фактически является паролем для доступа к Vault. Токен действует постоянно. Вы можете обновлять его или автоматизировать. С этим токеном в обращаетесь в Vault и забираете свои секреты.
Идея в том, что у вас на сервере, куда надо доставить секреты, крутится агент, который приходит в Vault, смотрит все секреты и выкладывает их в виде файлов. Мы используем агента на нескольких отдельных серверах, где нет Puppet.
Минусы:
Vault имеет функцию transit encryption, суть которой в том, что Vault выступает сервером шифрования. Вы просто приносите ему открытый текст, а он на своем закрытом ключе, который есть только у него, шифрует и выдает закрытый текст. Дальше вы выбираете, кто этот закрытый текст сможет расшифровать.
У Ansible есть сущность, которая тоже называется Vault. Это — не HashiCorp Vault, а Ansible Vault. Здесь нужно не перепутать, а секреты можно хранить как в первом, так и во втором. В Ansible есть уже готовый плагин для доcтавки секретов из Hashicorp Vault. Если вам дать личный доступ в Vault, то вы сможете расшифровывать секреты. Когда вы накатываете Ansible, он от вашего имени идёт в Vault, расшифровывает секреты, которые в зашифрованном виде находятся в репозитории, и выкатывает в продакшен.
Здесь тоже есть недостаток — каждый администратор получает доступ к секретам. Но есть аудит: Vault умеет вести журнал активности о том, какой пользователь приходил, какой секрет читал, какой получил доступ. Вы всегда знаете, кто, когда и что делал с секретом. Этот вариант мне кажется неплохим.
Самый большой недостаток, который вызывает самую большую боль у нас — то, что в Vault нельзя никому делегировать полное управление какой-то частью данных. В Vault доступ к секрету осуществляется по путям, похожим на пути в UNIX — имена принято разделять слешами, и в результате получается «директория». Когда у вас есть такой путь, иногда вы хотите взять часть пути и отдать её на управление кому-то еще.
Например, вы завели сертификаты, назвали /certs, и хотите отдать отдельным безопасникам, которые занимаются PKI. В Vault не получится этого сделать. Вы не можете дать право выдавать права внутри этого префикса — чтобы безопасники сами могли раздавать права на сертификаты еще кому-то.
В Vault нет возможности выборочно выдавать права на выдачу прав. Как только вы дали право на выдачу прав, то выдали и возможность получить полный доступ ко всем секретам. Другими словами, вы не можете дать доступ на часть Vault.
Это одна из самых больших проблем. У меня есть идея, как ее решить, позже про нее расскажу.
На РИТ++ я рассказывал [2] про отдельную систему, которую мы реализовали для Kubernetes: она служит третьей стороной, ходит в API, проверяет доступ и потом запрашивает секрет в Vault.
Сейчас наша система потеряла актуальность, потому что в Vault 0.9 появилась нативная поддержка Kubernetes. Теперь Vault сам умеет ходить в Kubernetes и убеждаться, что доступ к секрету разрешён. Делает он это при помощи Service Account Token. Например, когда у вас выкатился pod, там лежит специальный, подписанный и авторизованный для него JWT, предназначенный для запросов к API Kubernetes. С токеном можно также авторизоваться в Vault и получить секреты именно для вашего namespace.
Все делается на уровне самого Vault. Правда, на каждый namespace надо будет заводить роль, то есть сообщать Vault, что есть такой namespace, в нём будет авторизация, и прописывать, куда ходить в Kubernetes. Это делается один раз, а дальше Vault будет сам ходить в API, подтверждать валидность JWT и выдавать свой собственный токен для доступа.
В плане имени сервисов и дополнительных метаданных мы доверяем разработчикам. Есть маленькая вероятность, что разработчики могут случайно или намеренно получить секреты других сервисов, которые крутятся в одном namespace, поэтому мы ввели правило: один сервис — один namespace.
Новый микросервис? Заводите новый namespace со своими секретами. Перейти через границу в соседний нельзя — там свои Service Account Token. Граница безопасности в Kubernetes на данный момент — это namespace. Если в двух разных namespace нужен один секрет — копируем.
В Kubernetes есть kubernetes secrets. Они хранятся в etcd в Kubernetes в незашифрованном виде и могут «засветиться» в dashboard или при запуске kubectl get pods. Если в вашем кластере отключена аутентификация в etcd, или если вы дали кому-то полный read-only-доступ, то ему видны все секреты. Именно поэтому мы ввели два правила: запрещено использовать kubernetes secrets и запрещено указывать секреты в переменные окружения в манифестах. Если вы в deployment.yaml прописываете секрет в environment — это плохо, потому что сам манифест могут посмотреть все кому не лень.
Как я уже сказал, мы должны как-то положить файл в Kubernetes. У нас есть какой-то секрет: сущность, пароль, который в JSON записан в Vault. Как его теперь превратить в файл внутри контейнера в Kubernetes?
Первый вариант доставки.
Разработчику всего лишь надо запомнить путь, в котором лежит его секрет.
Мы используем примерно такой префикс:
/k8s/<cluster>/<namespace>/<service>/some_secret
В имя префикса заложены имя кластера, namespace и имя сервиса. У каждого сервиса свой секрет, в каждом namespace свой секрет.
Второй вариант — это свой собственный entrypoint. К нему мы сейчас переходим в Авито, потому что с init-container у разработчиков естьпроблемы. На схеме этот вариант справа.
Собственный entrypoint не все могут себе позволить. Мы — можем, поэтому в каждом контейнере мы форсим наш специальный entrypoint.
Наш entrypoint делает то же самое, что init-container: идет в Vault с Service Account Token, берет секреты и выкладывает их. Кроме как в файлы, он кладет их еще в environment. Вы получаете возможность запускать приложение как это рекомендуется концепцией Twelve-Factor App: приложение берет все настройки, в том числе, секреты, из переменных окружения.
Переменные окружения не видно в манифестах и дашборде, поскольку их устанавливает PID 1(основной процесс контейнера) при запуске. Это не переменные окружения из deployment.yaml, а переменные окружения, которые выставил entrypoint в процессе работы. Они не видны в dashboard, их не видно, даже если сделать kubectl exec в контейнер, потому что в этом случае запускается другой процесс, параллельный PID1.
С организационной точки зрения, процесс идет следующим образом. Разработчик узнаёт от security champion или из документации, что ему нельзя хранить секреты в репозитории, а только в Vault. Тогда он приходит к нам и спрашивает, куда класть секреты — подаёт заявку в security на заведение префикса. В будущем можно создавать префикс без заявки, сразу при создании сервиса.
Разработчик ждет, и это плохо, т.к. для него главное — time-to-market. Дальше он читает инструкции, разбирается с длинными файлами — «вставь туда ту строку, вставь сюда эту строку». Никогда раньше разработчик не заводил init-container, но он вынужден разобраться и прописывать его в deployment.yaml (helm chart).
Commit -> deploy -> feel pain -> fix -> repeat
Коммитит, ждет, пока TeamCity выкатит, видит ошибки в TeamCity, начинает испытывать боль, пытается что-то поправить, снова испытывает боль. Дополнительно накладывается то, что каждая выкатка в TeamCity ещё может встать в очередь. Иногда разработчик не может разобраться сам, приходит к нам, и мы разбираемся вместе.
В основном разработчик страдает из-за собственных ошибок: неправильно задал init-container или не дочитал документацию.
Со стороны Security тоже есть проблемы. Безопасник получает заявку, в которой всегда мало информации, и мы все равно выясняем недостающие вопросы: выясняем имена кластеров, namespace сервиса, так как разработчик не указывает их в заявке и даже не всегда знает, что это такое. Когда всё выясним, создаём политики и роли в Vault, прописываем политики группам и вместе с разработчиком начинаем выяснять, где и почему он ошибся, и вместе читаем логи.
Проблему помогает решить юнит «Архитектура» сокрытием от разработчика deployment.yaml. Они разрабатывают штуку, которая все генерирует за разработчика, в том числе entrypoint. Благодаря тому, что мы подставляем свой entrypoint, мы можем использовать его не только для доставки секретов, но и для других вещей, которые может понадобиться делать при старте.
Мы говорим: «Зачем вам в dev-кластере секреты production? Заведите тестовый секрет, ходите с ним!» В итоге появляются копи и секретов, которыми сложно управлять. Если секрет поменялся, надо про это не забыть, пойти и поменять везде, и пока нет возможности определить, что это один и тот же секрет, кроме как по имени сервиса.
В новых версиях Kubernetes появилась подсистема KMS — Key Management Service — новая возможность Kubernetes по шифрованию секретов. В v1.11 она была с состоянии alpha, в v1.12 её перевели в beta.
Картинка с сайта проекта провайдера KMS для Vault, и на ней есть ошибка. Если найдете — напишите в комментариях.
Смысл KMS в том, чтобы устранить один-единственный недостаток — нешифрованное хранение данных в etcd.
KMS, как Ansible, умеет вот что.
Разработчики написали специальный сервис, который это делает, используя transit encryption. Идея выглядит рабочей, но важно помнить, что секреты перестают находиться только под контролем Vault и уходят куда-то ещё, в зону ответственности администраторов Kubernetes.
Минусы KMS.
Плюсы KMS.
В TeamCity все просто, потому что JetBrains написали плагин, который сам умеет прописать у себя секреты для доступа к секрету, зашифровать их средствами TeamCity, а потом там же в качестве параметра где-нибудь в шаблоне подставить через проценты. В этот момент агент TeamCity сам сходит в Vault, заберёт секрет и принесёт в билд в виде параметра.
Некоторые секреты нужны при деплое, например, миграции БД или оповещения в Slack. AppRole заводится на каждый проект — настройки тоже содержат секрет (данные для AppRole), но он вводится в режиме write-only — прочитать его потом TeamCity не позволяет.
TeamCity сам заботится о том, чтобы при попадании секрета в логи билда, он автоматически маскировался. В результате секрет либо не «проезжает» через диск совсем, либо вычищается с диска средствами TeamCity. В результате вся безопасность секрета хорошо обеспечена самим TeamCity и плагином, и дополнительных танцев с бубном не требуется
Вот основные вопросы, над которыми надо подумать, если в качестве CI вы используете другую систему (не TeamCity).
В результате, скорее всего, вы напишете для своей CI/CD что-то очень похожее на плагин TeamCity. Авторизующей стороной здесь будет выступать, скорее всего, CI/CD, и именно она будет решать, можно ли этому билду иметь доступ к этому секрету, и по результатам отдавать или не отдавать сам секрет.
Важно не забывать чистить результаты билда по окончании сборки, если они выложились на диск, либо обеспечить, чтобы они находились только в памяти.
С сертификатами ничего особенного — мы используем Vault в основном для их хранения.
Vault имеет специальный PKI backend для выпуска сертификатов, в котором можно заводить Certificate Authority и подписывать новые сертификаты. У нас единый внутренний PKI… Корневой CA и CA второго уровня существуют отдельно, а CA третьего уровня мы уже управляем через Vault. Для хранения выпущенных сертификатов любого уровня, включая сертификаты подписанные внешними CA, мы используем отдельный префикс, и кладём туда почти все действующие сертификаты в целях учёта и мониторинга. Формат хранения сертификатов собственный, подходящий для хранения отдельно закрытого ключа и собственно сертификата.
Слишком много ручной работы для безопасника, слишком большой порог вхождения для разработчика и нет встроенных средств для делегирования, хотя очень хочется…
Как быть? Дальше начинаются мечты.
Как можно избавиться от кучи копий секрета?
У нас есть master-секрет и специальный демон, который ходит, смотрит секрет и его метаданные, выкладывает куда надо, получается slave-секрет. По пути, куда демон выложил slave, ничего нельзя менять руками, потому что демон придет и заново выложит master-секрет поверх slave.
Сначала мы хотели сделать механизм симлинков, чтобы просто указать: «Вот этот секрет ищи там!», как в Linux. Оказалось, что возникают проблемы с правами доступа: неизвестно, как права доступа проверять — как в Linux или нет, с родительскими путями, с переходами между точками монтирования. Слишком много неоднозначных моментов и шансов ошибиться, поэтому от симлинков мы отказались.
Второе, что мы хотим сделать — определить владельца для каждого секрета. По умолчанию секрет принадлежит тому, кто его создал. При необходимости можно расширить зону ответственности до юнита, выдав группу-владельца.
Когда мы научимся делегировать, мы выдадим владельцу права на секрет, и он сможет делать с секретом, что хочет.
Сейчас за все секреты и безопасность отвечаем мы, а хотим переложить ответственность на создателя. Безопасность не пострадает, потому что человек, который пришел к нам с запросом хранения секрета, понимает, что ему нужно безопасно хранить секрет и осознает свою ответственность.
Поскольку это владелец секрета, то для варианта доставки master-slave он мог бы выбирать, куда и в каком формате ему доставлять секрет. Получается, что владелец сам всем управляет, не надо подавать заявки, нужный префикс можно занять самостоятельно, секреты тоже можно создавать и удалять самому.
Политика доступа Access Control List в Vault поделена на две части:
Сейчас только администратор Vault может менять ACL. Получив доступ на такую ACL, можно внутрь прописать все, что хочется, например, path “*” { capabilities = [sudo, ...] }
, и получить полный доступ. Это суть Большого недостатка № 1 — невозможно запретить менять со держимое ACL.
Мы хотим задавать ACL готовым шаблоном, который содержит путь и плейсхолдеры, на которых разрешено генерировать новые ACL по этому шаблону.
Ниже желтым шрифтом записан путь готовой стандартной ACL от Vault и дальше разрешённые действия на этот путь. Мы ее рассматриваем, как ACL на разрешение менять другую ACL внизу, которая задана в виде шаблона.
Мы хотим делегировать доступ на /k8s, разрешаем генерировать только такие шаблоны. Например, давать доступ только на чтение в конкретный кластер, namespace, сервис, но не менять поле capabilities.
Дополнительно мы хотим давать разрешение привязывать эти ACL и выдавать разные права.
Мы применили шаблон для выдачи прав разработчику. При шаблонизации он запустил команду $ vault write policy-mgr/create/k8s-microservice ...
. И в результате получили ACL, в которой указано cluster=prod, namespace=..., service=… и т.д. Права проставились автоматически, создалась политика с именем /k8s/some-srv
— это просто имя ACL, которое можно генерировать по шаблону.
В результате разработчик по своему усмотрению через наш плагин присваивает эту ACL кому захочет, а сам становится владельцем, может ей управлять, как секретом: удалять, отдавать и отнимать у пользователей и групп. Теперь человек сам ответственен за свой префикс: он управляет всеми секретами, генерирует по шаблону ACL, может присваивать ACL тем, кому захочет. Естественно, мы тоже можем его ограничить.
Вся магия работает при помощи новой сущности Vault — плагинов. Они представляют собой отдельный сервис, очень похожий на тот, о котором я говорил в начале, и работают почти точно также. Единственное важное отличие — они не прокси. Плагины запускаются «сбоку» от Vault, причём запускает их основной процесс Vault. За счёт этого все запросы идут не через сервис, а в Vault, который уже сам взаимодействует с плагином, отправляя ему проверенный и очищенный запрос.
Про плагины, как они устроены и как их писать, можно почитать на сайте Vault [3]. Писать их лучше всего на Go, что достаточно просто, т.к. для Go есть фреймворк. Vault общается с плагином по grpc, запускает его как сервис, но не надо пугаться, вы его не касаетесь — во фреймворке уже все заложено. Вы просто пишете, более-менее стандартное REST приложение, в котором указываете endpoints, к ним даете готовые функции, хэндлеры, на которых будет логика.
Не бойтесь, что вы что-то сломаете в основном Vault. Плагин — это отдельный сервис. Даже если плагин у вас запаниковал и упал, работу Vault это никак не нарушит. Vault просто перезапустит плагин и будет работать дальше.
Кроме того, есть дополнительные настройки для самого плагина: он обязательно проверяет хэш-суммы, чтобы никто не подменил бинарник. Безопасность запуска плагинов обеспечена.
● https://qithub.com/isok/hiera-vault [4]
● https://www.owasp.org/index.php/Security [5]
● https://bloq.ietbrains.com/teamcity/2017/09/vault/ [6]
● https://qithub.com/oracle/kubernetes-vault-kms-pluqin/ [7]
Про DevOps и безопасность, CI/CD, k8s, Puppet и всё в таком духе будем говорить на HighLoad++ [8] (ближайший в Питере в апреле) и на DevOpsConf [9]. Приходите поделиться своим опытом или посмотреть [10] на других. Чтобы не забыть, подпишитесь на блог и рассылку [11], в которой мы будем напоминать о дедлайнах и собирать полезные материалы.
Автор: olegbunin
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/informatsionnaya-bezopasnost/308016
Ссылки в тексте:
[1] Albibek: https://habr.com/ru/users/albibek/
[2] рассказывал: https://www.youtube.com/watch?v=IulNdGlQR3A
[3] на сайте Vault: https://www.vaultproject.io/
[4] https://qithub.com/isok/hiera-vault: https://qithub.com/isok/hiera-vault
[5] https://www.owasp.org/index.php/Security : https://www.owasp.org/index.php/Security
[6] https://bloq.ietbrains.com/teamcity/2017/09/vault/ : https://bloq.ietbrains.com/teamcity/2017/09/vault/
[7] https://qithub.com/oracle/kubernetes-vault-kms-pluqin/ : https://qithub.com/oracle/kubernetes-vault-kms-pluqin/
[8] HighLoad++: https://www.highload.ru/
[9] DevOpsConf: https://devopsconf.io/
[10] посмотреть: http://youtube.com/c/DevOpsChannel/
[11] рассылку: http://eepurl.com/bN_0E1
[12] Источник: https://habr.com/ru/post/438740/?utm_campaign=438740
Нажмите здесь для печати.