如何连接到 Microsoft Azure Key Vault 使用 Ruby 在 Rails 上获取令牌并从 Vault 中读取值

How to connect to Microsoft Azure Key Vault get a token and read a value from the Vault with Ruby On Rails

我的应用程序是使用 Ruby On Rails 构建的。我被要求使用 Microsoft Azure Key Vault 来存储我们的秘密字符串。

我知道 Microsoft Teams 提供了可用的 Gems:

https://rubygems.org/gems/azure_mgmt_key_vault

https://rubygems.org/gems/azure_key_vault

如何“提取”或“引用”密钥并将其传递给我的应用程序?

经过大量的工作和汗水,我想通了。更重要的是,我能够使用证书连接到 Microsoft Azure Key Vault。所以在下面我把我所有的代码都用两种方法来获取令牌。一个带有 client secret id,另一个带有 certificate.

我找到了如何生成 self-sign 证书(用于调试目的)并获得 编码 thumbprint:

上传到 Azure 的证书是通过以下方式生成的: openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes -days 3650

获取证书的x5t编码base64指纹: echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64

我建了一个GEM。

我有一个配置文件lib\azurekeyvault\configuration.rb:

module AzureKeyVault
    class Configuration
      attr_accessor :azure_tenant_id, :azure_client_id, :azure_client_secret, :azure_subscription_id, :vault_base_url, :api_version, :resource, :azure_certificate_thumbprint, :azure_certificate_private_key_file
  
      def initialize

        @azure_tenant_id = nil
        @azure_client_id = nil
        @azure_client_secret = nil
        @azure_subscription_id = nil
        @vault_base_url = nil
        @api_version = nil
        @resource = nil
        @azure_certificate_thumbprint = nil
        @azure_certificate_private_key_file = nil        

      end
    end
  end

这是奇迹发生的文件lib\azurekeyvault\extraction.rb:

module AzureKeyVault
    require 'singleton'

    class Extraction
        include Singleton

        def initialize
            @configuration = AzureKeyVault.configuration
        end

        def get_value(secret_name, secret_version = nil)
            get_secret(secret_name, secret_version)
        end

        private
        ### Get a Secret value from Microsoft Azure Vault
        ## secret_name: Name of the Key which contain the value
        ## secret_version (optional): Version of the key value we need, by omitting version the system to use the latest available version
        def get_secret(secret_name, secret_version = nil)
            # GET {vaultBaseUrl}/secrets/{secret-name}/{secret-version}?api-version=7.1
            vault_base_url  = @configuration.vault_base_url
            api_version     = @configuration.api_version
            azure_certificate_thumbprint = @configuration.azure_certificate_thumbprint

            auth_token = nil
            if azure_certificate_thumbprint.nil?
                auth_token = get_auth_token()
            else
                auth_token = get_auth_certificate_token()
            end
            return nil if auth_token.nil?

            url = "#{vault_base_url}/secrets/#{secret_name}/#{secret_version}?api-version=#{api_version}"
            headers = { 'Authorization' => "Bearer " + auth_token }

            begin
                response = HTTParty.get(url, {headers: headers})
                return response.parsed_response['value']
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end
        
        def get_auth_token
            #Microsoft identity platform and the OAuth 2.0 client credentials flow
            # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
            # https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow#request-an-access-token
            
            azure_tenant_id = @configuration.azure_tenant_id
            azure_client_id = @configuration.azure_client_id
            azure_client_secret = @configuration.azure_client_secret
            resource = @configuration.resource

            authUrl = "https://login.microsoftonline.com/#{azure_tenant_id}/oauth2/token"

            data = {
                'grant_type': 'client_credentials',
                'client_id': azure_client_id,
                'client_secret': azure_client_secret,
                'resource': resource
            }

            begin

                response= HTTParty.post(authUrl, body: data)
                token = nil

                if response
                    #puts response.to_json
                    token = response.parsed_response['access_token']
                end
                return token
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end
        def get_auth_certificate_token

            begin
                # Microsoft identity platform and the OAuth 2.0 client credentials flow
                #
                # Certificat that was upload to Azure was generated with: 
                # openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes -days 3650
                #
                # To obtain the x5t encode base64 thumbprint of the certificate: 
                # echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64
        
                # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
                # https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/v1-oauth2-client-creds-grant-flow#request-an-access-token
                
                azure_tenant_id = @configuration.azure_tenant_id
                azure_client_id = @configuration.azure_client_id
                resource        = @configuration.resource
                azure_certificate_thumbprint        = @configuration.azure_certificate_thumbprint
                azure_certificate_private_key_file  = @configuration.azure_certificate_private_key_file

                authUrl = "https://login.microsoftonline.com/#{azure_tenant_id}/oauth2/token"
                exp = Time.now.to_i + 4 * 3600
                nbf = Time.now.to_i - 3600
                jti = SecureRandom.uuid

                #//x5t THUMBPRINT of Cert
                header = {
                    "alg": "RS256",
                    "typ": "JWT",
                    "x5t": azure_certificate_thumbprint
                }
                #Claim (payload)
                payload = {
                    "aud": authUrl,
                    "exp": exp,
                    "iss": azure_client_id,
                    "jti": jti,
                    "nbf": nbf,
                    "sub": azure_client_id
                }
                            
                token = "#{Base64.strict_encode64(header.to_json)}.#{Base64.strict_encode64(payload.to_json)}"

                # Get the private key, from the file
                azure_certificate_private_key = OpenSSL::PKey.read(File.read(azure_certificate_private_key_file))
                # The hash algorithm, I assume SHA256 is being used
                base64_signature = Base64.strict_encode64(azure_certificate_private_key.sign(OpenSSL::Digest::SHA256.new, token))

                jwt_client_assertion = "#{token}.#{base64_signature}"

                data = {
                    'grant_type': 'client_credentials',
                    'client_id': azure_client_id,
                    'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
                    'client_assertion': jwt_client_assertion,
                    'resource': resource
                }

                response = HTTParty.post(authUrl, body: data)
                token = nil

                if response
                    token = response.parsed_response['access_token']
                end
                return token
            rescue HTTParty::Error => e
                puts "HTTParty ERROR: #{e.message}"
                raise e
            rescue Exception => e
                puts "ERROR: #{e.message}"
                raise e               
            end
        end        
    end
end

我还有一个 Initialiser,我在其中为我的配置变量赋值

AzureKeyVault.configure do |config|

    config.azure_tenant_id = ENV["AZURE_VAULT_TENANT_ID"]
    config.azure_client_id = ENV["AZURE_VAULT_CLIENT_ID"]
    config.azure_client_secret = ENV["AZURE_VAULT_CLIENT_SECRET"]
    config.azure_subscription_id = ENV["AZURE_VAULT_SUBSCRIPTION_ID"]
    config.vault_base_url = ENV["AZURE_VAULT_BASE_URL"]
    config.api_version = ENV["AZURE_VAULT_API_VERSION"]
    config.resource = ENV["AZURE_VAULT_RESOURCE"]
    # To obtain the x5t encode base64 thumbprint of the certificate: 
    # echo $(openssl x509 -in public_certificate.pem -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64
    config.azure_certificate_thumbprint = ENV["AZURE_CERTIFICATE_THUMBPRINT"]
    #Certificat that was upload to Azure was generated with: 
    # openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_certificate.pem -nodes    
    config.azure_certificate_private_key_file = ENV["AZURE_CERTIFICATE_PRIVATE_KEY_FILE"]

end

注意:post 和回答 (@Jason Johnston) 对我理解发生了什么有很大帮助: