在Asp.NET Core中如何優雅的管理使用者機密資料

溪源More發表於2020-06-11

在Asp.NET Core中如何優雅的管理使用者機密資料

背景

回顧

在軟體開發過程中,使用配置檔案來管理某些對應用程式執行中需要使用的引數是常見的作法。在早期VB/VB.NET時代,經常使用.ini檔案來進行配置管理;而在.NET FX開發中,我們則傾向於使用web.config檔案,通過配置appsetting的配置節來處理;而在.NET Core開發中,我們有了新的基於json格式的appsetting.json檔案。

無論採用哪種方式,其實配置管理從來都是一件看起來簡單,但影響非常深遠的基礎性工作。尤其是配置的安全性,貫穿應用程式的始終,如果沒能做好安全性問題,極有可能會給系統帶來不可控的風向。

原始碼比配置檔案安全麼?

有人以為把配置存放在原始碼中,可能比存放在明文的配置檔案中似乎更安全,其實是“皇帝的新裝”。

在前不久,筆者的一位朋友就跟我說了一段故事:他說一位同事在離職後,直接將曾經寫過的一段程式碼上傳到github的公共倉庫,而這段程式碼中包含了某些涉及到原企業的機密資料,還好被github的安全機制提前發現而及時終止了該行為,否則後果不堪設想。

於是,筆者順手查了一下由於有意或無意洩露企業機密,造成企業損失的案例,發現還真不少。例如大疆前員工通過 Github 洩露公司原始碼,被罰 20 萬、獲刑半年 這起案件,也是一個典型的案例。

該員工離職後,將包含關鍵配置資訊的原始碼上傳到github的公共倉庫,被黑客利用,使得大量使用者私人資料被黑客獲取,該前員工最終被刑拘。 大疆前員工通過Github洩露公司原始碼,被罰20萬、獲刑半年

圖片來源: http://www.digitalmunition.com/WhyIWalkedFrom3k.pdf

大部分IT公司都會在入職前進行背景調查,而一旦有案底,可能就已經與許多IT公司無緣;即便是成為創業者,也可能面臨無法跟很多正規企業合作的問題。

小結

所以,安全性問題不容小覷,哪怕時間再忙,也不要急匆匆的就將資料庫連線字串或其他包含敏感資訊的內容輕易的記錄在原始碼或配置檔案中。在這個點上,一旦出現問題,往往都是非常嚴重的問題。

如何實現

在.NET FX時代,我們可以使用對web.config檔案的關鍵配置節進行加密的方式,來保護我們的敏感資訊,在.NET Core中,自然也有這些東西,接下來我將簡述在開發環境和生產環境下不同的配置加密手段,希望能夠給讀者帶來啟迪。

開發環境

在開發環境下,我們可以使用visual studio 工具提供的使用者機密管理器,只需0行程式碼,即可輕鬆完成關鍵配置節的處理。

機密管理器概述

根據微軟官方文件 的描述:

ASP.NET Core 機密管理器工具提供了開發過程中在原始碼外部儲存機密的另一種方法 。 若要使用機密管理器工具,請在專案檔案中安裝包 Microsoft.Extensions.Configuration.SecretManager 。 如果該依賴項存在並且已還原,則可以使用 dotnet user-secrets 命令來通過命令列設定機密的值。 這些機密將儲存在使用者配置檔案目錄中的 JSON 檔案中(詳細資訊隨作業系統而異),與原始碼無關。

機密管理器工具設定的機密是由使用機密的專案的 UserSecretsId 屬性組織的。 因此,必須確保在專案檔案中設定 UserSecretsId 屬性,如下面的程式碼片段所示。 預設值是 Visual Studio 分配的 GUID,但實際字串並不重要,只要它在計算機中是唯一的。

<PropertyGroup>
   <UserSecretsId>UniqueIdentifyingString</UserSecretsId>
</PropertyGroup> 

Secret Manager工具允許開發人員在開發ASP.NET Core應用程式期間儲存和檢索敏感資料。敏感資料儲存在與應用程式原始碼不同的位置。由於Secret Manager將祕密與原始碼分開儲存,因此敏感資料不會提交到原始碼儲存庫。但機密管理器不會對儲存的敏感資料進行加密,因此不應將其視為可信儲存。敏感資料作為鍵值對儲存在JSON檔案中。最好不要在開發和測試環境中使用生產機密。檢視引文

存放位置

在windows平臺下,機密資料的存放位置為:

%APPDATA%\Microsoft\UserSecrets\\secrets.json

而在Linux/MacOs平臺下,機密資料的存放位置為:

 ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json 

在前面的檔案路徑中, ``將替換UserSecretsId.csproj檔案中指定的值。

在Windows環境下使用機密管理器

在windows下,如果使用Visual Studio2019作為主力開發環境,只需在專案右鍵單擊,選擇選單【管理使用者機密】,即可新增使用者機密資料。

在管理使用者機密資料中,新增的配置資訊和傳統的配置資訊沒有任何區別。

{
"ConnectionStrings": {
"Default": "Server=xxx;Database=xxx;User ID=xxx;Password=xxx;"
}
}

我們同樣也可以使用IConfiguration的方式、IOptions的方式,進行配置的訪問。

在非Windows/非Visual Studio環境下使用機密管理器

完成安裝dotnet-cli後,在控制檯輸入

dotnet user-secrets init 

前面的命令將在UserSecretsId .csproj 檔案的PropertyGroup中新增 .csproj一個元素。 UserSecretsId是對專案是唯一的Guid值。

 <PropertyGroup>  
 	<TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>79a3edd0-2092-40a2-a04d-dcb46d5ca9ed</UserSecretsId> 
 </PropertyGroup> 

設定機密

 dotnet user-secrets set "Movies:ServiceApiKey" "12345" 

列出機密

 dotnet user-secrets list 

刪除機密

 dotnet user-secrets remove "Movies:ConnectionString" 

清除所有機密

 dotnet user-secrets clear 

生產環境

機密管理器為開發者在開發環境下提供了一種保留機密資料的方法,但在開發環境下是不建議使用的,如果想在生產環境下,對機密資料進行儲存該怎麼辦?

按照微軟官方文件的說法,推薦使用Azure Key Vault 來保護機密資料,但。。我不是貴雲的使用者(當然,買不起貴雲不是貴雲太貴,而是我個人的問題[手動狗頭])。

其次,與Azure Key Valut類似的套件,例如其他雲,差不多都有,所以都可以為我們所用。

但。。如果您如果跟我一樣,不想通過第三方依賴的形式來解決這個問題,那不如就用最簡單的辦法,例如AES加密。

使用AES加密配置節

該方法與平時使用AES對字串進行加密和解密的方法並無區別,此處從略。

使用資料保護Api(DataProtect Api實現)

在平時開發過程中,能夠動手擼AES加密是一種非常好的習慣,而微軟官方提供的資料保護API則將這個過程進一步簡化,只需調Api即可完成相應的資料加密操作。

關於資料保護api, Savorboard 大佬曾經寫過3篇部落格討論這個技術問題,大家可以參考下面的文章來獲取資訊。

ASP.NET Core 資料保護(Data Protection 叢集場景)【上】

ASP.NET Core 資料保護(Data Protection 叢集場景)【中】

ASP.NET Core 資料保護(Data Protection 叢集場景)【下】

(接下來我要貼程式碼了,如果沒興趣,請出門左拐,程式碼不能完整執行,檢視程式碼

首先,注入配置項

 public static IServiceCollection AddProtectedConfiguration(this IServiceCollection services, string directory)
        {
            services
                .AddDataProtection()
                .PersistKeysToFileSystem(new DirectoryInfo(directory))
                .UseCustomCryptographicAlgorithms(new ManagedAuthenticatedEncryptorConfiguration
                {
                    EncryptionAlgorithmType = typeof(Aes),
                    EncryptionAlgorithmKeySize = 256,
                    ValidationAlgorithmType = typeof(HMACSHA256)
                });
            ;

            return services;
        }

其次,實現對配置節的加/解密。(使用AES演算法的資料保護機制)


public class ProtectedConfigurationSection : IConfigurationSection
    {
        private readonly IDataProtectionProvider _dataProtectionProvider;
        private readonly IConfigurationSection _section;
        private readonly Lazy<IDataProtector> _protector;

        public ProtectedConfigurationSection(
            IDataProtectionProvider dataProtectionProvider,
            IConfigurationSection section)
        {
            _dataProtectionProvider = dataProtectionProvider;
            _section = section;

            _protector = new Lazy<IDataProtector>(() => dataProtectionProvider.CreateProtector(section.Path));
        }

        public IConfigurationSection GetSection(string key)
        {
            return new ProtectedConfigurationSection(_dataProtectionProvider, _section.GetSection(key));
        }

        public IEnumerable<IConfigurationSection> GetChildren()
        {
            return _section.GetChildren()
                .Select(x => new ProtectedConfigurationSection(_dataProtectionProvider, x));
        }

        public IChangeToken GetReloadToken()
        {
            return _section.GetReloadToken();
        }

        public string this[string key]
        {
            get => GetProtectedValue(_section[key]);
            set => _section[key] = _protector.Value.Protect(value);
        }

        public string Key => _section.Key;
        public string Path => _section.Path;

        public string Value
        {
            get => GetProtectedValue(_section.Value);
            set => _section.Value = _protector.Value.Protect(value);
        }

        private string GetProtectedValue(string value)
        {
            if (value == null)
                return null;

            return _protector.Value.Unprotect(value);
        }
    }

再次,在使用前,先將待加密的字串轉換成BASE64純文字,然後再使用資料保護API對資料進行處理,得到處理後的字串。

private readonly IDataProtectionProvider _dataProtectorTokenProvider;
public TokenAuthController( IDataProtectionProvider dataProtectorTokenProvider)
{
}
[Route("encrypt"), HttpGet, HttpPost]
public string Encrypt(string section, string value)
{
     var protector = _dataProtectorTokenProvider.CreateProtector(section);
     return protector.Protect(value);
}

再替換配置檔案中的對應內容。

{
  "ConnectionStrings": {
    "Default": "此處是加密後的字串"
  }
}

然後我們就可以按照平時獲取IOptions的方式來獲取了。

問題

公眾號【DotNET騷操作】號主【周杰】同學提出以下觀點:

1、在生產環境下,使用AES加密,其實依然是一種不夠安全的行為,充其量也就能忽悠下產品經理,畢竟幾條簡單的語句,就能把機密資料dump出來。

也許在這種情況下,我們應該優先考慮accessKeyId/accessSecret,儘量通過設定多級子賬號,通過授權Api的機制來管理機密資料,而不是直接暴露類似於資料庫連線字串這樣的關鍵配置資訊。另外,應該定期更換資料庫的密碼,儘量將類似的問題可能造成的風險降到最低。資料保護api也提供的類似的機制,使得開發者能夠輕鬆的管理機密資料的時效性問題。

2、配置檔案放到CI/CD中,釋出的時候在CI/CD中進行組裝,然後運維只是負責管理CI/CD的賬戶資訊,而最高機密資料,則由其他人負責配置。

嗯,我完全同意他的第二種做法,另外考慮到由於運維同樣有可能會有意無意洩露機密資料,所以如果再給運維配備一本《刑法》,並讓他日常補習【侵犯商業祕密罪】相關條款,這個流程就更加閉環了。

結語

本文簡述了在.NET Core中,如何在開發環境下使用使用者機密管理器、在生產環境下使用AES+IDataProvider的方式來保護我們的使用者敏感資料。由於時間倉促,如有考慮不周之處,還請各位大佬批評指正。

相關文章