From ec70a82825d990d6dcc5735179c146792768fcc8 Mon Sep 17 00:00:00 2001 From: Zemuldo Date: Tue, 24 Oct 2023 11:19:54 +0300 Subject: [PATCH] support file path for google secret manager service account --- lib/application.ex | 1 - lib/providers/azure_key_vault.ex | 60 ++++++++---- lib/providers/azure_managed_identity.ex | 11 +++ lib/providers/google_secret_manager.ex | 93 +++++++++++++++---- mix.exs | 9 +- test/providers/google_secret_manager_text.exs | 4 +- 6 files changed, 137 insertions(+), 41 deletions(-) diff --git a/lib/application.ex b/lib/application.ex index bdd8c02..4d5e3c4 100644 --- a/lib/application.ex +++ b/lib/application.ex @@ -1,5 +1,4 @@ defmodule ExSecrets.Application do - @moduledoc """ Application for adding providers and cache to the supervision tree. All are besically GenServers that do a bunch of API calls and file access like .env reader diff --git a/lib/providers/azure_key_vault.ex b/lib/providers/azure_key_vault.ex index ea7f921..de0469f 100644 --- a/lib/providers/azure_key_vault.ex +++ b/lib/providers/azure_key_vault.ex @@ -11,15 +11,15 @@ defmodule ExSecrets.Providers.AzureKeyVault do ## Configuration You can configure this provider as shown below. Either `client_secret` or `client_cert_path` is required to implement authentication. - Client `client_certificate` is recommended for security. If you provide both, `client_certificate` is used. All other config options are mandatory. + Client `client_certificate_?` is recommended for security. If you provide both, `client_certificate_?` is used. All other config options are mandatory. ``` - Azure KeyVault configuration: config :ex_secrets, :providers, %{ azure_key_vault: %{ tenant_id: "tenant-id", client_id: "client-id", client_secret: "client-secret", - client_certificate: "/path/cert.key", + client_certificate_string: "base 64 encoded contents of cert.key", + client_certificate_path: "/path/cert.key", client_certificate_x5t: "x5t", key_vault_name: "key-vault-name" } @@ -75,8 +75,10 @@ defmodule ExSecrets.Providers.AzureKeyVault do Finally generate the `x5t` JWT header required by Entra using the command below. See https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials#assertion-format. ``` - openssl x509 -in mycert.crt -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64 + openssl x509 -in mycert.crt -fingerprint -noout | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64 ``` + + Save the value as `client_certificate_x5t` in the config. """ @headers %{"Content-Type" => "application/x-www-form-urlencoded"} @@ -165,11 +167,20 @@ defmodule ExSecrets.Providers.AzureKeyVault do defp get_access_token() do client = http_adpater() client_secret = Config.provider_config_value(:azure_key_vault, :client_secret) - client_certificate = Config.provider_config_value(:azure_key_vault, :client_certificate) + + client_certificate_str = + Config.provider_config_value(:azure_key_vault, :client_certificate_string) + + client_certificate_path = + Config.provider_config_value(:azure_key_vault, :client_certificate_path) + tenant_id = Config.provider_config_value(:azure_key_vault, :tenant_id) with req_body when is_binary(req_body) <- - build_claims_body(%{"secret" => client_secret, "cert" => client_certificate}), + build_claims_body(%{ + "secret" => client_secret, + "cert" => client_certificate_str || client_certificate_path + }), {:ok, %{body: body, status_code: 200}} <- tenant_id |> token_uri() @@ -185,16 +196,33 @@ defmodule ExSecrets.Providers.AzureKeyVault do end end + defp get_cert() do + string = Config.provider_config_value(:azure_key_vault, :client_certificate_string) + path = Config.provider_config_value(:azure_key_vault, :client_certificate_path) + + cond do + is_binary(string) -> Base.decode64(string) + is_binary(path) -> File.read(path) + true -> {:error, :no_cert} + end + end + defp build_claims_body(%{"cert" => cert}) when is_binary(cert) do client_id = Config.provider_config_value(:azure_key_vault, :client_id) - URI.encode_query(%{ - "client_id" => client_id, - "scope" => @scope, - "grant_type" => "client_credentials", - "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - "client_assertion" => jwt() - }) + case get_cert() |> IO.inspect() do + {:ok, cert} -> + URI.encode_query(%{ + "client_id" => client_id, + "scope" => @scope, + "grant_type" => "client_credentials", + "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion" => jwt(cert) + }) + + err -> + err + end end defp build_claims_body(%{"secret" => secret}) when is_binary(secret) do @@ -210,9 +238,9 @@ defmodule ExSecrets.Providers.AzureKeyVault do defp build_claims_body(_), do: {:error, :no_auth} - defp jwt() do + defp jwt(cert) do + File.write("base64.crt", Base.encode64(cert)) client_id = Config.provider_config_value(:azure_key_vault, :client_id) - client_certificate = Config.provider_config_value(:azure_key_vault, :client_certificate) client_certificate_x5t = Config.provider_config_value(:azure_key_vault, :client_certificate_x5t) @@ -221,7 +249,7 @@ defmodule ExSecrets.Providers.AzureKeyVault do t = DateTime.to_unix(DateTime.utc_now()) signer = - Joken.Signer.create("RS256", %{"pem" => File.read!(client_certificate)}, %{ + Joken.Signer.create("RS256", %{"pem" => cert}, %{ "x5t" => client_certificate_x5t }) diff --git a/lib/providers/azure_managed_identity.ex b/lib/providers/azure_managed_identity.ex index 7d0f34a..887ca00 100644 --- a/lib/providers/azure_managed_identity.ex +++ b/lib/providers/azure_managed_identity.ex @@ -5,6 +5,17 @@ defmodule ExSecrets.Providers.AzureManagedIdentity do @moduledoc """ Azure Key Vault provider provides secrets from an Azure Key Vault through a rest API. + + Only the keyvault name is required here once the managed identity has been given access to the keyvault. + + ``` + config :ex_secrets, :providers, %{ + azure_managed_identity: %{ + key_vault_name: "key-vault-name" + } + ``` + + The provider will handle token renewals and secret fetch. """ @headers %{"Content-Type" => "application/x-www-form-urlencoded", "Metadata" => "true"} diff --git a/lib/providers/google_secret_manager.ex b/lib/providers/google_secret_manager.ex index b3ba07c..75d3482 100644 --- a/lib/providers/google_secret_manager.ex +++ b/lib/providers/google_secret_manager.ex @@ -1,7 +1,40 @@ defmodule ExSecrets.Providers.GoogleSecretManager do @moduledoc """ Google Secret Manager provider provides secrets from an Google Secret Manager through a rest API. - To create GCP secretb + + ### Configuration + + Using the Service Account Credentials File + ``` + Application.put_env(:ex_secrets, :providers, %{ + google_secret_manager: %{ + service_account_credentials_path: ".temp/cred.json" + } + }) + + ``` + + Using the json file contents + + ``` + Application.put_env(:ex_secrets, :providers, %{ + google_secret_manager: %{ + service_account_credentials: %{ + "type" => "service_account", + "project_id" => "project-id", + "private_key_id" => "keyid", + "private_key" => "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n", + "client_email" => "secretaccess@project-id.iam.gserviceaccount.com", + "client_id" => "client-id", + "auth_uri" => "https://accounts.google.com/o/oauth2/auth", + "token_uri" => "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url" => "https://www.googleapis.com/robot/v1/metadata/x509/secretaccess%40project-id.iam.gserviceaccount.com", + "universe_domain" => "googleapis.com" + } + } + }) + ``` """ use ExSecrets.Providers.Base @@ -15,10 +48,10 @@ defmodule ExSecrets.Providers.GoogleSecretManager do @secrets_base_uri "https://secretmanager.googleapis.com/v1/projects/PROJECT_NAME/secrets/SECRET_NAME/versions/latest:access" def init(_) do - case get_access_token() do - {:ok, data} -> - {:ok, data |> Map.put("issued_at", get_current_epoch())} - + with {:ok, cred} <- get_service_account_credentials(), + {:ok, data} <- get_access_token(cred) do + {:ok, data |> Map.put("issued_at", get_current_epoch())} + else _ -> {:ok, %{}} end @@ -55,7 +88,7 @@ defmodule ExSecrets.Providers.GoogleSecretManager do current_time ) when issued_at + expires_in - current_time > 5 do - with {:ok, value} <- get_secret_call(name, access_token) do + with {:ok, value} <- get_secret_call(name, access_token, state.cred) do {:ok, value, state} else _ -> {:error, "Failed to get secret"} @@ -63,21 +96,21 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end defp get_secret(name, state, _) do - with {:ok, %{"access_token" => access_token} = new_state} <- get_access_token(), - {:ok, value} <- get_secret_call(name, access_token) do + with {:ok, cred} <- get_service_account_credentials(), + {:ok, %{"access_token" => access_token} = new_state} <- get_access_token(cred), + {:ok, value} <- get_secret_call(name, access_token, cred) do {:ok, value, state |> Map.merge(new_state)} else _ -> {:error, "Failed to get secret"} end end - defp get_secret_call(name, access_token) do + defp get_secret_call(name, access_token, cred) do client = http_adpater() - service_account_info = Config.provider_config_value(:google_secret_manager, :service_account) url = @secrets_base_uri - |> String.replace("PROJECT_NAME", service_account_info["project_id"]) + |> String.replace("PROJECT_NAME", cred["project_id"]) |> String.replace("SECRET_NAME", name) with {:ok, %{body: body, status_code: 200}} <- @@ -89,12 +122,12 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end - defp get_access_token() do + defp get_access_token(cred) do client = http_adpater() token_req_body = %{ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: jwt() + assertion: jwt(cred) } with {:ok, %{body: body, status_code: 200}} <- @@ -107,15 +140,39 @@ defmodule ExSecrets.Providers.GoogleSecretManager do end end - defp jwt do - service_account_info = Config.provider_config_value(:google_secret_manager, :service_account) + defp get_service_account_credentials() do + path = Config.provider_config_value(:google_secret_manager, :service_account_credentials_path) + cred = Config.provider_config_value(:google_secret_manager, :service_account_credentials) + + cond do + is_map(cred) -> + {:ok, cred} + + is_binary(path) -> + get_cred_from_path(path) + + true -> + {:error, :no_auth} + end + end + + defp get_cred_from_path(path) do + with {:ok, s} <- File.read(path), + {:ok, cred} <- Poison.decode(s) do + {:ok, cred} + else + _ -> {:error, :no_auth} + end + end + + defp jwt(cred) do t = DateTime.to_unix(DateTime.utc_now()) - signer = Joken.Signer.create("RS256", %{"pem" => service_account_info["private_key"]}) + signer = Joken.Signer.create("RS256", %{"pem" => cred["private_key"]}) claims = %{ - "iss" => service_account_info["client_email"], - "sub" => service_account_info["client_email"], + "iss" => cred["client_email"], + "sub" => cred["client_email"], "aud" => "https://oauth2.googleapis.com/token", "exp" => t + 1200, "iat" => t, diff --git a/mix.exs b/mix.exs index dd5c5f7..0550a4a 100644 --- a/mix.exs +++ b/mix.exs @@ -12,10 +12,11 @@ defmodule ExSecrets.MixProject do package: package(), deps: deps(), docs: [ - main: "readme", # The main page in the docs - logo: "logo.png", - extras: ["README.md", "GUIDES.md", "CHANGELOG.md", "LICENSE"] - ] + # The main page in the docs + main: "readme", + logo: "logo.png", + extras: ["README.md", "GUIDES.md", "CHANGELOG.md", "LICENSE"] + ] ] end diff --git a/test/providers/google_secret_manager_text.exs b/test/providers/google_secret_manager_text.exs index 6fa01d4..74f08aa 100644 --- a/test/providers/google_secret_manager_text.exs +++ b/test/providers/google_secret_manager_text.exs @@ -11,12 +11,12 @@ defmodule ExSecrets.Providers.GoogleSecretManagerTest do setup do Application.put_env(:ex_secrets, :providers, %{ google_secret_manager: %{ - service_account: %{ + service_account_credentials: %{ "type" => "service_account", "project_id" => "test", "private_key_id" => "test123", "private_key" => - "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDpqU2PXKm08Png\nhgYHu6VN/GAmvrBCoXmCO68WPSBx5la3JI0lSyf3uW9AXzzfe6ApP08xYS9WVGUN\n8xgFLJnVMjA+MhUaZ0AGmiE2bp1qWwkOxKaWf+Ynh0xwS7p7tIcE9CHU1KcArgfE\nAjwQxs/DbgXYUdNTOAbTeP4CLpSxh8SiZpqQ2egCPjNVHWePCPlkOp9s1pDjSktb\nH4O6CiZZ6bS1/7mQ2erzfzzG9w7l1aPLcfDQLHtXcFzXkSvOVhXwIaS3fS4+lf/4\ndXFzinbahbyitfL6uaItBrluFvotAA5nv1Z3Pc0o+4t8PThmLCKP6T5cBvWOc81K\nlQ23LPd/AgMBAAECggEACmzPVRIhUD1gKLBSHI42teAIujHP02k47qKTET7w76QD\nQnCTC5Lq2ZagbBLTuHTflHeKpP1dC1EAoTqzW6e9xVFT7bJ2VpM8vA6sZK1SwKgH\nI22KsTRLpH/Y3TnDvDk1vPbXe5NxUApztj8TRvxX0LRb9mbQMupRA6ZmTtqdL751\nJYdhBbrqiNE97r66BPx8fEmP+797dtSaRRSilX29TqtX2C7xnrk0LyO8tnwd9anV\n2/zBRNu44qQufeVojrFKdAL+Thfgt664zZgzP7NeuTL4X94BOnogH7IcGUEa9sB7\nGxZ5xhIBrYrYNXAmoZjLezi023q6cqq9pXrGQeQ3YQKBgQD8nGlaJuSnxiIO3Qn9\n4hccAWTA3vHn5AIaInl9Z0sZGm/83ct2uE371XDr26hwVVmqVWaanG8EXtF+zwia\npnCudrUXsV2P8QbSBhWBGCsa1NykbVg9q+dU2n0W7JhFbhwZFbd4g+3TjNCd1Gor\nkQ2GtbOugXt2mikaD0ls/1j1ZQKBgQDsy88mwHbNvbSXVmX+ibRzNMd8u3d3EMlS\nV+mkx6+/d3OLaPHqGRZcWfKprT+Fiz/H7C9MQU+ypjqcA0oEA6nE3XTAwlfBXKrN\nnNY8AoqSmrMc3OfxgEHKL6v5D+xNGPEaet3EVnCQ8CvTXYynHraA4pkDZ7bAq7H7\ntG34j7AtEwKBgGBP1k8cAxQAk92s4vFccUkpMtviZMLwCOkj+cQZTOWuUcJMYhXK\noVkCAQK8BhWGRSCPXQZX3HADIsbBcttb2Bx8gAEfi7ekwt/yl+JXb5/URqeeVQV2\ndEXC4+yImmnmWGosAH6/dj6xMpzqbuxbapfQ0UgYcBVBI6ie6XTYSneNAoGAT7MB\nU/+vfOv+3nj790IN9ECta/QE75Q8znQ8dXOoWX8w6pk14x7ygb7ch/OBz8bgfr+l\n47qPwodkbqJExTkeaN5Ir6A5vSEdc/r3uFb6oQFki7BmeMg8XHrTHQ8Y75IXhFwa\nTDzzwjSz634vGwihUJvz+EtuHUcsrpU59lEWcPUCgYEAkUhT9yfA47Wfc9Pv3vS5\n5dYm2QHfEU0RhOozY6DKMJs60zsysp6XFBg0vpKJXa8/Q7gFXz6wd+kYJsGUc6lo\ncfVt3fXFLNqnOxYKZOU+T41qhASOXdSM/OR5OJ7m8kYepEhKIuXxlVNxO8reNnm5\nWJ9lbCurW3dM5ol4DgCKaak=\n-----END PRIVATE KEY-----\n", + "-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----\n", "client_email" => "test@test.iam.gserviceaccount.com", "client_id" => "test", "auth_uri" => "https://accounts.google.com/o/oauth2/auth",