Skip to content

Commit

Permalink
support file path for google secret manager service account
Browse files Browse the repository at this point in the history
  • Loading branch information
zemuldo committed Oct 24, 2023
1 parent 1d88d49 commit ec70a82
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 41 deletions.
1 change: 0 additions & 1 deletion lib/application.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
60 changes: 44 additions & 16 deletions lib/providers/azure_key_vault.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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"}
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
})

Expand Down
11 changes: 11 additions & 0 deletions lib/providers/azure_managed_identity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
93 changes: 75 additions & 18 deletions lib/providers/google_secret_manager.ex
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -55,29 +88,29 @@ 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"}
end
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}} <-
Expand All @@ -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}} <-
Expand All @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions test/providers/google_secret_manager_text.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit ec70a82

Please sign in to comment.