Більше ніяких паролів і секретів в Azure

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Мене звати Сергій Крам, я Solution Architect в компанії Intellias, також сертифікований Azure експерт, маю досвід в дизайні архітектури, розробці та впровадженні високо навантажених та масштабованих рішень. Останні кілька років відповідаю за розробку та впровадження хмарних рішень.

Мабуть, на кожному проєкті рано чи пізно починалась стадія, коли траплялись гарячі дискусії про те, де зберігати секрети, паролі до баз даних тощо. Часто це закінчується, скажімо, не дуже оптимальними рішеннями, залежно від того, в кого який досвід. Хороша новина полягає в тому, що з цим, в принципі, вже можна закінчити. У цій статті розглянемо, як взагалі можна працювати без секретів, без паролів до баз даних й інших ресурсів. А немає секретів — немає відповідно ніяких проблем.

Ми коротко пройдемося по еволюції керування секретами, розглянемо як працювали з секретами до сьогоднішнього дня і подивимося чи є кращий варіант. Також подивимось на .NET код і конфігурацію на Azure, щоб зрозуміти, як все працює в реальному середовищі.

Еволюція керування секретами

Local config

Перший етап еволюції — це local config.

  
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=MSSQLLocalDB;Initial Catalog=aspnet-FrameworkWebApplication..."
      providerName="System.Data.SqlClient" />
  </connectionStrings> 

Думаю, всі колись з цього починали. Всі, мабуть, згадують страшні webconfig-и і appconfig-и з подібними рядками підключення до баз даних. Тут навіть немає на чому зупинятись, адже це однозначно погана практика зберігати секрети у коді. Всі їх бачать, всім вони доступні, також доступна історія.

Сподіваюсь, більше ніхто такого не робить, навіть на своїх pet-проєктах. Так як є набагато простіші способи це вирішити, тому просто залишимо цей метод на сторінках історії і рухаємось далі.

Deployment pipeline

Наступний етап еволюції — ми не можемо зберігати секрети в коді. Одразу постає логічне питання: де їх нам зберігати? Відповідь — це deployment pipeline. Отже, у коді ми вже не маємо ніяких секретів, але у нас є певні шаблони: під dev, під stage, UAT середовища і, звісно, production. Також в нас є якісь build/deploy сервери чи сервіси (Jenkins, TeamCity, Azure DevOps тощо) і ми там можемо налаштувати секрети, які будуть прописуватися в конфігураційні файли вже на етапі, наприклад, публікації артефактів на сервер.

Проблема полягає в тому, що секрети зберігаються на CI\CD сервері, а до сервера має доступ багато людей без аудиту. І, насправді, це не є безпечне чи захищене сховище секретів. Це погана практика. Якщо ви використовуєте її на власних проєктах, вам варто серйозно поміркувати над альтернативами.

Environmental variables

Виникає питання: де ж тоді зберігати секрети? Якщо і в коді не можна, і на CI\CD серверах не можна. Хороше рішення — зберігати секрети там, де вони використовуються, бажано в якомусь захищеному сховищі. Якщо в нас, наприклад, є для простоти віртуальна машина, де працює наш код, то очевидно, що секрети мають бути на цій машині. Тому один із варіантів — просто покласти їх на цю машину.

Найпростіший спосіб це зробити — це так звані змінні середовища або environmental variables. До прикладу, саме так працює Azure App Services Configuration: всі налаштування доступні як змінні середовища для вашого додатку. Тобто в нас немає вже секретів ані в коді, ані на CI\CD серверах. Секрети є тільки на тій машині, де вони використовуються.

Проте змінні середовища — це теж не вельми безпечний спосіб, тому що вони неконтрольовано можуть потрапити в логи, в різні утиліти зі збирання статистики, швидкодії тощо. І випадково ці секрети можуть стати доступні широкому колу людей. Тому цей варіант кращий за попередні, але теж для реального використання не рекомендований.

Secrets Vault

Наступний етап еволюції — це так звані Secrets Vault або просто Vault. Наприклад, у нас є ресурси, база даних, BLOB-сховище. Якщо говоримо про Azure, ми створюємо Azure Key Vault — це захищений сервіс по зберіганню різних секретів, ключів, сертифікатів й іншого функціоналу.

У цей Key Vault ми зберігаємо секрети, паролі до бази даних, до сховища і до всіх інших сервісів, які нас цікавлять. Потім ми створюємо віртуальні машини, наприклад, де буде працювати наш код. На цих віртуальних машинах ми інсталюємо сертифікат.

Для чого це потрібно? З цим сертифікатом ми можемо авторизуватися на Key Vault. Тобто наш код при старті бере сертифікат локально, з цим сертифікатом підключається до Key Vault-а і динамічно отримує список секретів. А потім з цими паролями можна підключатися до баз даних тощо. Варіант чудовий, тому що секрети за межі середовища не виходять. В нас секрети не зберігаються в змінних середовищах, вони не можуть випадково попасти в логи, аналітику чи інші інструменти тощо.

Здавалося б, усе вже добре, але не ідеально. Ми ж програмісти любим, щоб все було ідеально, саме тому ми, мабуть, регулярно випускаєм новий JavaScript Framework). Так що ж тут не так? Проблема в тому, що тут у нас є людина. Тобто є якийсь адміністратор. Не важливо, це може бути програміст, тім лід, тех лід, архітектор, девопс, проєктний менеджер. Ця людина спочатку створює пароль до бази даних, робить Ctrl+C/Ctrl+V і записує його в Key Vault. Потім ця людина має створити сертифікат і встановити його на віртуальні машини. Якщо у нас є кілька цих середовищ, то це треба повторити для кожного з них. На кожне середовище, наприклад, Stage, UAT і т.д., дублюється робота. І ця людина візуально бачить пароль до бази даних, вона його копіює, вставляє. А, ледь не забув, секрети потрібно регулярно оновлювати! Тобто, якщо забрати звідси людину, то все буде ОК. Тому цей варіант однозначно можна і треба використовувати. Він рекомендований, але давайте подумаємо, що можна зробити краще.

Чи є кращий варіант?

Давайте подивимось концептуально. Ми маємо Azure Subscription. По суті підписка, аккаунт, в якому працюють наші сервіси і якісь ресурси. В нас є віртуальні машини, в нас є база даних і наш код на віртуальних машинах.

Сам Azure знає, що це код, який працює на віртуальній машині № 1. Віртуальна машина № 1 належить вашій підписці або вашій компанії. І Azure знає, що база даних, яку ви створили, це теж ваша база даних. Також він знає, що ваша віртуальна машина хоче підключитись до вашої бази даних. І вже з цього можна робити якісь висновки, що це може бути дозволено. Так само інша віртуальна машина може підключатися до сховища, наприклад, до Blob Storage.

Єдине, чого тут не вистачає, це певних правил. Наприклад, я не хочу, щоб віртуальна машина № 2 теж мала доступ до бази даних. Я хочу вказувати, тобто створити якісь правила, хто куди може чи не може підключатись. Тобто нам потрібен ще третій компонент. Ним може бути Azure Active Directory, де по суті записуються усі правила, хто куди має доступ.

Як бачимо на цій схемі, ми взагалі не згадували про паролі чи секрети. Ми говорили просто про якийсь контекст середовища, в якому працює наш код. І, відповідно, з цього середовища, з контексту, де він працює, можна зробити якісь висновки, куди йому можна підключатись, куди не можна.

Те, що я розказав, це і є такий функціонал в Azure і називається він Managed identities for Azure resources.

Managed identities for Azure resources

Розглянемо трохи детальніше як він працює. Можливо, схема виглядає заплутаною, але насправді все просто. Наприклад, ми створили віртуальну машину. У нас є код, який виконується на цій віртуальній машині. Ця віртуальна машина працює на якомусь сервері в Azure. На цьому сервері в Azure є спеціальний сервіс, який працює паралельно з нашим кодом. Називається він Azure Instance Metadata Service (IMDS) і доступний через HTTP API: 169.254.169.254/metadata. Відповідно, наш код, який працює на віртуальній машині може робити виклики на Instance Metadata Service і отримувати інформацію про те, де працює цей код, інформацію про свою identity тощо. А, зробивши виклик на http://169.254.169.254/metadata/identity/oauth2/token, можна отримати токен доступу, з яким можна робити виклики до інших сервісів.

Як це практично використовувати? Спочатку створюємо пул віртуальних машин. Далі для них ми створюємо identity в Active Directory і називаємо його, наприклад, My virtual machines. Прописуємо правило, що цей My virtual machines має доступ до SQL сервера. Тепер наш код при старті робить запит на Instance Metadata Service, отримує інформацію про себе, тобто йому приходить відповідь — ти працюєш від імені My Virtual Machines і ось твій токен доступу. Далі можна з ним робити виклик на інші ресурси, зокрема на базу даних. Оскільки в Active Directory уже прописані правила чи є доступ до бази даних, чи немає, ваш виклик пройде або успішно, або ні. В даній схемі у нас взагалі немає ніяких статичних секретів, не треба їх кудись записувати, робити Copy-Paste, думати, як їх регулярно оновлювати.

Можна перевірити вручну як це працює. Для цього потрібно зайти на свою віртуальну машину і зробити запит:

curl 'http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.azure.com/&api-version=2018-02-01' -H Metadata:true​

Відповідь буде виглядати десь так:

{​
    "access_token": "eyJ0eXAi…",​
    "expires_on": "1586984735",​
    "resource": "https://management.azure.com",​
    "token_type": "Bearer",​
    "client_id": "5E29463D-71DA-4FE0-8E69-999B57DB23B0“​
}​

Загалом ми отримуємо стандартну для OAuth 2.0 протоколу відповідь, в тому числі JWT токен доступу.

Як це працює в .NET

Поки все виглядає страшно, і не зрозуміло, як це спрощує життя розробнику, який пише код. Тому давайте подивимось, як це працює з точки зору коду:

Install-Package Azure.Identity​

var credential = new DefaultAzureCredential();​
var blobClient = new BlobServiceClient(​
       new Uri("https://myaccount.blob.core.windows.net/"), ​
       credential);

У .NET це працює, умовно кажучи, в один рядок коду. Нам потрібно встановити nuget пакет Azure Identity. В цьому пакеті є клас, який називається DefaultAzureCredential. Він ховає в собі всю магію. Наприклад, ми хочемо підключитися до сховища даних і прочитати файл. Для цього беремо клас BlobServiceClient і замість секретів, паролів тощо передаємо екземпляр класу DefaultAzureCredential. На цьому все. Бібліотека Azure Identity сама сходить на цей IMDS, про який я говорив, візьме там токен, потім BlobServiceClient з цим токеном буде підключатись до нашого сховища даних. Коли час дії токену закінчиться, воно там само оновить його. Все працює прозоро і не потрібні ніякі конфігурації і секрети.

Локальна розробка

Поки що ми говорили про те, як це працюватиме в Azure. Хороша новина — все буде працювати під час локальної розробки автоматично, прозоро, без змін в коді чи налаштуваннях. Для цього потрібно увійти у Visual Studio під акаунтом, який має доступ до потрібних ресурсів і все. Якщо потрібно запустити код з консолі — достатньо виконати попередньо az login.

Демо

Давайте розглянемо практичну задачу і покрокові скріншоти налаштування. Візьмемо простий сценарій, коли в нас .NET сервіс працює на Azure App Service і нам потрібно отримувати список файлів з Azure Storage.

Спочатку додаємо nuget пакети Azure.Identity і Azure.Storage.Blobs:

І в коді використовуємо клас DefaultAzureCredential (рядок 19):

Власне, це все. Повністю проєкт можна подивитися тут.

Тепер налаштування на стороні Azure. Відкриваємо наш App Service, в моєму випадку він називається demo-mi-app, і відкриваємо секцію Identity:

Вмикаємо System assigned identity:

Поки що в нас немає ніяких ролей:

Відкриваємо тепер сховище, в моєму випадку demomistorage, і відкриваємо секцію Access Control (AIM)\Roles assignments:

І даємо доступ нашому сервісу:

Перевіряємо:

Все, тепер наш сервіс має доступ до файлів.

Підтримувані сервіси

Наш код може працювати як на віртуальних машинах, так і на App Service, Functions, Kubernetes, в Logic Apps тощо. Можна підключатись до Key Vault, до Azure Storage, до баз даних Azure SQL, до Cosmos DB і так далі.

Що ще варто знати?

Є стара версія бібліотеки і є нова. Те, що я вам показав, це нова версія бібліотеки. Вона набагато простіша у використанні. Є стара версія, з нею трошки складніше працювати. Зараз всі сервіси переходять на нову версію, але є кілька сервісів, мабуть, Service Bus один із них, де потрібно ще використовувати стару, бо вони ще не оновили собі SDK.

SDK доступні не тільки для .NET, а й для інших популярних мов програмування. Ну й у крайньому разі можна робити вручну HTTP запити і отримувати токен доступу до ресурсів. Більше можна прочитати — тут.

Ну і насамкінець, варто сказати, що це не якась абстрактна річ, про яку розповідають на конференціях. Наприклад, такий підхід реалізований на одному проєкті, через який проходить 1 мільярд євро на рік.

Часте питання — чи працює ця технологія при роботі з іншими сервісами? Якщо у вас є веб сервіси, доступ до яких контролюється через ту ж саму Azure Active Directory — то, думаю, все буде працювати.

👍НравитсяПонравилось11
В избранноеВ избранном4
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Небольшой лайфхак для ресурсов, которые не поддерживают секьюрные динамические(!) параметры из коробки. Например, Automation Account даже при задании в привязанном к джобе командлете типа параметра securestring всё равно обрабатывает его как обычную строку. В таком случае можно сгенерировать ключ для 256-битного AES шифрования, положить его в KV, и использовать для шифрования/дешифрования с обеих сторон. В Powershell уже есть удобный модуль для этого.

Имхо, все же последний вариант — так себе, так как вендорлок, уменьшает переносимость и по сути плюсов к безопасности не даёт.

Если использовать Azure KeyVault/Hashicorp vault, как например бекенд для хранения секретов для VM/пода, это норм, если использовать как вы показали — лезть через API колл в хранилище, то это уже оверкилл и реального профита не даст. Скомпроментировавши вмку, без труда хакер подключится к хранилищу секретов и вытянет все что захочет и по сути периметр уже прорван.

На этом этапе эффективнее уже на уровне container/network policy, типа белого списка куда может обращаться софтина с вм/пода. Ну и сетевой мониторинг. И про принцип least privileges не забываем. А managed identity никак не защитит, если к кредам доступ есть.

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

По-суті це воно і є — індивідуальний скоуп/RBAC для кожного (мікро)сервісу/аплікейшина, ротація секретів кожну годину для усіх живих інстансів, автоматично.
Єдиний мінус — не потрібен хашікорп волт... ну хіба-що секрети для 3rd-party інтеграцій👌.

А managed identity никак не защитит, если к кредам доступ есть.

Если есть ключ к keyvault ситуация ещё хуже.. least privileges — в managed identity есть rbac уровня ресурса, которого не будет в key vault(из коробки) из-за возможности видеть все сикреты. однозначный плюс managed identity — убирает значительную часть девопс/дев работы связанную с менеджментом и использованием секретов. По поводу вендор лока — практически в любой paas сервис в любом Клауде адрессующую какую-то функциональность будет делать вендор лок. Cloud agnostic будет требовать отказаться от каких-то готовых решений/доп телодвижений дев команды и кодинга devops тимы, как в случае с тем же keyvault.

лезть через API колл в хранилище, то это уже оверкилл

Не усі проекти в Кубернетесі, тому іноді треба явно іти за секретами у волт. В таких випадках в нас окремий Key Vault для кожного севісу, щоб не можна було дістати «чужі» секрети.

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

Якщо скомпрометована ВМ — то уже нема різниці, як було доставлено секрети, хакер має доступ до всього, до чого має сам сервіс.

container/network policy, типа белого списка куда может обращаться софтина с вм/пода.

Абсолютно, Managed Identities не відміняє layered approach to security — є network policy, реверс-проксі/проксі, VPN і т.д.

Іронія в тому, що мені більше імпонує Канбан)

Ніхто не заважає змінити ім’я на Костянтин Анбан ;)

Я делал ровно то же самое 2 года назад. Никаких дополнительных библиотек не использовал. Инфраструктуру в CI/CD мы разворачивали через ARM-темплейты — в них есть возможность создать identity для разворачиваемого сервиса или функции.
а дальше просто в конфигурацию при деплое записывается указатель на запись в key vault и все работает.
Единственная проблема, которая возникла с таким подходом — добавлять identity в keyvault. ARM-темплейты довольно лимитированные по функционалу, поэтому само добавление identity в keyvault делалось просто шеллскриптом при деплое.

+1, статье не хватает IaC. При назначении этого добра руками вместо выделенного service connection’а для деплоя тоже не очень секьюрно выходит, мягко говоря.

Единственная проблема, которая возникла с таким подходом — добавлять identity в keyvault. ARM-темплейты довольно лимитированные по функционалу, поэтому само добавление identity в keyvault делалось просто шеллскриптом при деплое.

Хм, а можно подробнее про проблему? Если KV тоже деплоится через ARM, всё можно сделать в темплейте через Access Policies. Или у Вас был один «централизованный» KV и много отдельных темплейтов под сервисы, которые в него ходят, и деплоятся тоже отдельно? Мы в основном делаем один KV под отдельный компонент, и соответственно в пределах компонента всё в одном темплейте.

а колесо в чтому?
майкрософт так i рекомендує
навіть в єкзамені AZ-204 це є

Більше ніяких паролів і секретів в Azure

заголовок звучить ніби хакнули когось

заголовок звучить ніби хакнули когось

👀

Подписаться на комментарии