.Net Core 5.0 — Sql Azure + Always Encrypted + Managed Identity

У меня есть база данных SQL Azure с зашифрованными столбцами (всегда зашифрованными с помощью Azure KeyVault). Я могу получить доступ к этой базе данных из SSMS и увидеть расшифрованные данные.

У меня также есть веб-приложение, созданное с помощью .Net Core 5.0, которое развернуто в службе приложений Azure. В службе приложений включена управляемая идентификация, а в Key Vault, имеющем ключи enc/dec для этой базы данных SQL, есть параметр политики доступа, позволяющий этой службе приложений расшифровывать данные.

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

Кроме того, строка подключения включает Column Encryption Setting=enabled;. Вот строка подключения:

Server=tcp:server.database.windows.net,1433;Database=somedb;Column Encryption Setting=enabled;

Проблема в том, что я не могу найти НИКАКИХ образцов с такой настройкой. Я нашел некоторые, и я понимаю, что мне нужно зарегистрироваться SqlColumnEncryptionAzureKeyVaultProvider. Вот мой код для получения SqlConnection:

    internal static class AzureSqlConnection
    {
        private static bool _isInitialized;

        private static void InitKeyVaultProvider(ILogger logger)
        {
            /*
             * from here - https://github.com/dotnet/SqlClient/blob/master/release-notes/add-ons/AzureKeyVaultProvider/1.2/1.2.0.md
             *      and  - https://github.com/dotnet/SqlClient/blob/master/doc/samples/AzureKeyVaultProviderExample.cs
             *
             */

            try
            {
                // Initialize AKV provider
                SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider =
                    new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback);

                // Register AKV provider
                SqlConnection.RegisterColumnEncryptionKeyStoreProviders(
                    new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>(1, StringComparer.OrdinalIgnoreCase)
                    {
                        {SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, sqlColumnEncryptionAzureKeyVaultProvider}
                    });

                _isInitialized = true;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Could not register SqlColumnEncryptionAzureKeyVaultProvider");
                throw;
            }
        }

        internal static async Task<SqlConnection> GetSqlConnection(string connectionString, ILogger logger)
        {
            if (!_isInitialized) InitKeyVaultProvider(logger);

            try
            {
                SqlConnection conn = new SqlConnection(connectionString);
                /*
                         * This is Managed Identity (not Always Encrypted)
                         *  https://learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi#modify-aspnet-core
                         *
                         */
#if !DEBUG
                conn.AccessToken = await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/");
                logger.LogInformation($"token: {conn.AccessToken}");
#endif
                await conn.OpenAsync();
                return conn;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Could not establish a connection to SQL Server");
                throw;
            }
        }

        private static async Task<string> AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope)
        {
            return await new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/");

            //AuthenticationContext? authContext = new AuthenticationContext(authority);
            //ClientCredential clientCred = new ClientCredential(s_clientId, s_clientSecret);
            //AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);
            //if (result == null)
            //{
            //    throw new InvalidOperationException($"Failed to retrieve an access token for {resource}");
            //}

            //return result.AccessToken;
        }
    }

Этот код не создает никаких исключений и работает для незашифрованных запросов. Но для зашифрованных запросов я получаю следующую ошибку:

Не удалось расшифровать ключ шифрования столбца. Недопустимое имя поставщика хранилища ключей: "AZURE_KEY_VAULT". Имя поставщика хранилища ключей должно обозначать либо поставщика системного хранилища ключей, либо зарегистрированного поставщика настраиваемого хранилища ключей. Допустимые имена поставщиков хранилища системных ключей: «MSSQL_CERTIFICATE_STORE», «MSSQL_CNG_STORE», «MSSQL_CSP_PROVIDER». Действительные (в настоящее время зарегистрированные) имена поставщиков пользовательских хранилищ ключей: . Проверьте информацию о поставщике хранилища ключей в определениях главного ключа столбца в базе данных и убедитесь, что все поставщики пользовательских хранилищ ключей, используемые в вашем приложении, правильно зарегистрированы. Не удалось расшифровать ключ шифрования столбца. Недопустимое имя поставщика хранилища ключей: "AZURE_KEY_VAULT". Имя поставщика хранилища ключей должно обозначать либо поставщика системного хранилища ключей, либо зарегистрированного поставщика настраиваемого хранилища ключей. Допустимые имена поставщиков хранилища системных ключей: «MSSQL_CERTIFICATE_STORE», «MSSQL_CNG_STORE», «MSSQL_CSP_PROVIDER». Действительные (в настоящее время зарегистрированные) имена поставщиков пользовательских хранилищ ключей: . Проверьте информацию о поставщике хранилища ключей в определениях главного ключа столбца в базе данных и убедитесь, что все поставщики пользовательских хранилищ ключей, используемые в вашем приложении, правильно зарегистрированы.

Похоже, поставщик хранилища ключей не зарегистрирован.

Что мне сделать, чтобы он работал для запроса зашифрованных данных?

используемые пакеты

    <PackageReference Include = "Microsoft.Azure.Services.AppAuthentication" Version = "1.6.0" />
    <PackageReference Include = "Microsoft.Data.SqlClient" Version = "2.1.0" />
    <PackageReference Include = "Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" Version = "1.2.0" />
    <PackageReference Include = "Microsoft.Extensions.Hosting" Version = "5.0.0" />

Это помогает? Похоже на ту же проблему, хотя принятый ответ, похоже, здесь не применяется stackoverflow.com/questions/51618582/…

Nick.McDermaid 11.12.2020 05:46

Я чувствую твою боль... это почти полезно, но нет. learn.microsoft.com/en-us/sql/relational-databases/security/‌​…

Nick.McDermaid 11.12.2020 05:48

Не могли бы вы предоставить политику доступа, которую вы настраиваете для MSI?

Jim Xu 14.12.2020 02:52

Как обычно: Клавиша: Get, List; Криптовалюта: развернуть, обернуть, проверить, подписать. Иначе он не работал бы нигде. Я считаю, что Key-List на самом деле не нужен. Но это все равно есть

AlexB 15.12.2020 09:36
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
В предыдущей статье мы завершили установку базы данных, для тех, кто не знает.
Как установить LAMP Stack 1/2 на Azure Linux VM
Как установить LAMP Stack 1/2 на Azure Linux VM
В дополнение к нашему предыдущему сообщению о намерении Azure прекратить поддержку Azure Database для MySQL в качестве единого сервера после 16...
4
4
1 936
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Оказывается, невозможно прочитать расшифрованные данные в .NET 5, когда используется MSI. В пакетах MS есть ошибка, и служба приложений никогда не авторизуется.

Вы должны использовать Service Principal. Это работает как шарм!

Обновлять

Я должен поблагодарить инженеров MS, которые предложили рабочее решение:

public static async Task<string> KeyVaultAuthenticationCallback(string authority, string resource, string scope)
{
     return await Task.Run(() => new ManagedIdentityCredential().GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
     /********************** Alternatively, to use User Assigned Managed Identity ****************/
     // var clientId = {clientId_of_UserAssigned_Identity};
     // return await Task.Run(() => new ManagedIdentityCredential(clientId).GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
}

Это не проблема с .NET 5. Вы взяли пример обратного вызова проверки подлинности для Azure Key Vault и изменили его, чтобы он относился к базе данных SQL Azure, а не к ресурсу AKV. Вам нужно настроить обратный вызов, чтобы получить действительный токен AKV. Это всего лишь один из способов получить токен с помощью библиотек Azure.Core и Azure.Identity:

    private static async Task<string> AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope)
    {
        return await Task.Run(() => new ManagedIdentityCredential().GetToken(new TokenRequestContext(new string [] {"https://vault.azure.net/.default"})).Token);
    }

Вот только это не работает. Я пробовал - github.com/dotnet/SqlClient/issues/847#issuecomment-75411123‌​7

AlexB 29.01.2021 22:21

Я в основном просто указал на твою ошибку. В вашем ответе было просто неверное общее заявление: «Невозможно прочитать расшифрованные данные в .NET 5, когда используется MSI. В пакетах MS есть ошибка, и служба приложений никогда не авторизуется». Я пытался указать вам на то, что решения есть, а вы смешиваете шифрование, MSI и .NET 5 вместе, тогда как эта группировка не имеет смысла. Все они являются независимыми функциями. Вам нужно реализовать правильный код, который заставляет их работать. В частности, код, который извлекает правильный маркер доступа для доступа к хранилищу ключей Azure.

David Engel 01.02.2021 20:07

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

AlexB 02.02.2021 00:05

Я смог использовать этот код, который использует TokenCredential для поставщика SqlColumnEncryption. DefaultAzureCredential возвращает управляемое удостоверение при развертывании в качестве службы приложений:

            SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new DefaultAzureCredential());
            Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>
            {
                { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider }
            };
            SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);

Вызовите его из вашего метода startup.Configure.

Ответ для Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider v3

SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new(new ManagedIdentityCredential());

Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers = new()
{
    [SqlColumnEncryptionAzureKeyVaultProvider.ProviderName] = azureKeyVaultProvider
};

SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);

Взято из microsoft/sql-server-samples после того, как я чуть не потерял рассудок.

Помимо этого требуется:

  • Назначьте роль управляемого удостоверения службы приложений Key Vault Crypto User на вкладке IAM в Key Vault.

  • Предоставьте пользователю базы данных управляемых удостоверений службы приложений разрешения VIEW ANY COLUMN MASTER KEY DEFINITION и VIEW ANY COLUMN ENCRYPTION KEY DEFINITION:

    GRANT VIEW ANY COLUMN MASTER KEY DEFINITION to [app-service-user]
    GRANT VIEW ANY COLUMN ENCRYPTION KEY DEFINITION to [app-service-user]
    
  • Разрешить доступ к службе приложений через брандмауэр Key Vault (т. е. добавить исходящие IP-адреса службы приложений в правила брандмауэра Key Vault).

Другие вопросы по теме