Skip to content

Add support for Bitwarden Secret Manager EU vaults #1606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/kamal/cli/secrets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ class Kamal::Cli::Secrets < Kamal::Cli::Base
option :account, type: :string, required: false, desc: "The account identifier or username"
option :from, type: :string, required: false, desc: "A vault or folder to fetch the secrets from"
option :inline, type: :boolean, required: false, hidden: true
option :server_url, type: :string, aliases: "-u", required: false, desc: "Override the default server-url"
def fetch(*secrets)
adapter = initialize_adapter(options[:adapter])

if adapter.requires_account? && options[:account].blank?
return puts "No value provided for required options '--account'"
end

results = adapter.fetch(secrets, **options.slice(:account, :from).symbolize_keys)
results = adapter.fetch(secrets, **options.slice(:account, :from, :server_url).symbolize_keys)

return_or_puts JSON.dump(results).shellescape, inline: options[:inline]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/aws_secrets_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ def requires_account?
end

private
def login(_account)
def login(_account, **)
nil
end

def fetch_secrets(secrets, from:, account: nil, session:)
def fetch_secrets(secrets, from:, account: nil, **)
{}.tap do |results|
get_from_secrets_manager(prefixed_secrets(secrets, from: from), account: account).each do |secret|
secret_name = secret["Name"]
Expand Down
6 changes: 3 additions & 3 deletions lib/kamal/secrets/adapters/base.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
class Kamal::Secrets::Adapters::Base
delegate :optionize, to: Kamal::Utils

def fetch(secrets, account: nil, from: nil)
def fetch(secrets, account: nil, from: nil, server_url: nil)
raise RuntimeError, "Missing required option '--account'" if requires_account? && account.blank?

check_dependencies!

session = login(account)
fetch_secrets(secrets, from: from, account: account, session: session)
session = login(account, server_url: server_url)
fetch_secrets(secrets, from: from, account: account, session: session, server_url: server_url)
end

def requires_account?
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/bitwarden.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Kamal::Secrets::Adapters::Bitwarden < Kamal::Secrets::Adapters::Base
private
def login(account)
def login(account, **)
status = run_command("status")

if status["status"] == "unauthenticated"
Expand All @@ -21,7 +21,7 @@ def login(account)
session
end

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, session:, **)
{}.tap do |results|
items_fields(prefixed_secrets(secrets, from: from)).each do |item, fields|
item_json = run_command("get item #{item.shellescape}", session: session, raw: true)
Expand Down
17 changes: 9 additions & 8 deletions lib/kamal/secrets/adapters/bitwarden_secrets_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def requires_account?
LIST_COMMAND = "secret list"
GET_COMMAND = "secret get"

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, server_url:, **)
raise RuntimeError, "You must specify what to retrieve from Bitwarden Secrets Manager" if secrets.length == 0

secrets = prefixed_secrets(secrets, from: from)
Expand All @@ -18,13 +18,13 @@ def fetch_secrets(secrets, from:, account:, session:)
{}.tap do |results|
if command.nil?
secrets.each do |secret_uuid|
item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}")
item_json = run_command("#{GET_COMMAND} #{secret_uuid.shellescape}", server_url: server_url)
raise RuntimeError, "Could not read #{secret_uuid} from Bitwarden Secrets Manager" unless $?.success?
item_json = JSON.parse(item_json)
results[item_json["key"]] = item_json["value"]
end
else
items_json = run_command(command)
items_json = run_command(command, server_url: server_url)
raise RuntimeError, "Could not read secrets from Bitwarden Secrets Manager" unless $?.success?

JSON.parse(items_json).each do |item_json|
Expand All @@ -45,13 +45,14 @@ def extract_command_and_project(secrets)
end
end

def run_command(command, session: nil)
full_command = [ "bws", command ].join(" ")
`#{full_command}`
def run_command(command, server_url: nil)
full_command = [ "bws", command ]
full_command << "--server-url=#{server_url}" if server_url
`#{full_command.join(" ")}`
end

def login(account)
run_command("project list")
def login(_account, server_url: nil)
run_command("project list", server_url: server_url)
raise RuntimeError, "Could not authenticate to Bitwarden Secrets Manager. Did you set a valid access token?" unless $?.success?
end

Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/enpass.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def requires_account?
end

private
def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, **)
secrets_titles = fetch_secret_titles(secrets)

result = `enpass-cli -json -vault #{from.shellescape} show #{secrets_titles.map(&:shellescape).join(" ")}`.strip
Expand All @@ -31,7 +31,7 @@ def cli_installed?
$?.success?
end

def login(account)
def login(_account, **)
nil
end

Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/gcp_secret_manager.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Kamal::Secrets::Adapters::GcpSecretManager < Kamal::Secrets::Adapters::Base
private
def login(account)
def login(_account, **)
# Since only the account option is passed from the cli, we'll use it for both account and service account
# impersonation.
#
Expand All @@ -26,7 +26,7 @@ def login(account)
nil
end

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, account:, **)
user, service_account = parse_account(account)

{}.tap do |results|
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/last_pass.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Kamal::Secrets::Adapters::LastPass < Kamal::Secrets::Adapters::Base
private
def login(account)
def login(account, **)
unless loggedin?(account)
`lpass login #{account.shellescape}`
raise RuntimeError, "Failed to login to LastPass" unless $?.success?
Expand All @@ -11,7 +11,7 @@ def loggedin?(account)
`lpass status --color never`.strip == "Logged in as #{account}."
end

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, **)
secrets = prefixed_secrets(secrets, from: from)
items = `lpass show #{secrets.map(&:shellescape).join(" ")} --json`
raise RuntimeError, "Could not read #{secrets} from LastPass" unless $?.success?
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/one_password.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class Kamal::Secrets::Adapters::OnePassword < Kamal::Secrets::Adapters::Base
delegate :optionize, to: Kamal::Utils

private
def login(account)
def login(account, **)
unless loggedin?(account)
`op signin #{to_options(account: account, force: true, raw: true)}`.tap do
raise RuntimeError, "Failed to login to 1Password" unless $?.success?
Expand All @@ -15,7 +15,7 @@ def loggedin?(account)
$?.success?
end

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, account:, session:, **)
if secrets.blank?
fetch_all_secrets(from: from, account: account, session: session)
else
Expand Down
4 changes: 2 additions & 2 deletions lib/kamal/secrets/adapters/test.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
class Kamal::Secrets::Adapters::Test < Kamal::Secrets::Adapters::Base
private
def login(account)
def login(_account, **)
true
end

def fetch_secrets(secrets, from:, account:, session:)
def fetch_secrets(secrets, from:, **)
prefixed_secrets(secrets, from: from).to_h { |secret| [ secret, secret.reverse ] }
end

Expand Down
21 changes: 21 additions & 0 deletions test/secrets/bitwarden_secrets_manager_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,27 @@ class BitwardenSecretsManagerAdapterTest < SecretAdapterTestCase
assert_equal "Bitwarden Secrets Manager CLI is not installed", error.message
end

test "fetch with different server-url" do
stub_ticks.with("bws --version 2> /dev/null")
stub_ticks.with("bws project list --server-url=https://example.com").returns("OK")
stub_ticks
.with("bws secret get 82aeb5bd-6958-4a89-8197-eacab758acce --server-url=https://example.com")
.returns(<<~JSON)
{
"key": "KAMAL_REGISTRY_PASSWORD",
"value": "some_password"
}
JSON

json = JSON.parse(shellunescape(run_command("fetch", "82aeb5bd-6958-4a89-8197-eacab758acce",
"--server-url", "https://example.com")))
expected_json = {
"KAMAL_REGISTRY_PASSWORD"=>"some_password"
}

assert_equal expected_json, json
end

private
def stub_login
stub_ticks.with("bws project list").returns("OK")
Expand Down