From fd2ade09cd525392472f70f5f3042b209199e7c4 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 9 Aug 2024 09:24:24 -0700 Subject: [PATCH 1/2] feat: add bitwarden secrets manager backend Signed-off-by: Richard Cox --- .goreleaser.yaml | 3 +- Dockerfile | 2 +- Makefile | 8 +- go.mod | 1 + go.sum | 2 + pkg/backends/bitwardensecretsmanager.go | 64 +++++++++++ pkg/backends/bitwardensecretsmanager_test.go | 98 +++++++++++++++++ pkg/config/config.go | 40 ++++++- pkg/types/constants.go | 108 ++++++++++--------- 9 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 pkg/backends/bitwardensecretsmanager.go create mode 100644 pkg/backends/bitwardensecretsmanager_test.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 632a0684..8dfe99bb 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,7 +9,8 @@ builds: -X "github.com/argoproj-labs/argocd-vault-plugin/version.BuildDate={{.Date}}" -X "github.com/argoproj-labs/argocd-vault-plugin/version.CommitSHA={{.Commit}}" env: - - "CGO_ENABLED=0" + - "CGO_ENABLED=1" + - "C=musl-gcc" goos: - darwin - linux diff --git a/Dockerfile b/Dockerfile index 8385546f..b1c4c3a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ADD go.mod go.mod ADD go.sum go.sum ENV GOPATH="" -RUN go mod download +RUN apt-get update && apt install musl-tools -y && go mod download VOLUME work WORKDIR work diff --git a/Makefile b/Makefile index 1197512f..646e1440 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,18 @@ BINARY=argocd-vault-plugin +LD_FLAGS=-ldflags '-linkmode external -extldflags "-static -Wl,-unresolved-symbols=ignore-all"' default: build quality: go vet github.com/argoproj-labs/argocd-vault-plugin - go test -race -v -coverprofile cover.out ./... + go test ${LD_FLAGS} -race -v -coverprofile cover.out ./... build: - go build -buildvcs=false -o ${BINARY} . + C=musl-gcc CGO_ENABLED=1 go build ${LD_FLAGS} -buildvcs=false -o ${BINARY} . +test: + C=musl-gcc CGO_ENABLED=1 go test ${LD_FLAGS} -buildvcs=false ./... + install: build e2e: install diff --git a/go.mod b/go.mod index f8e3e837..a25f7e42 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.27.1 github.com/aws/aws-sdk-go-v2/config v1.27.17 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.29.2 + github.com/bitwarden/sdk-go v0.1.1 github.com/googleapis/gax-go/v2 v2.12.0 github.com/hashicorp/go-hclog v1.6.2 github.com/hashicorp/vault v1.16.1 diff --git a/go.sum b/go.sum index 5ff5eb67..e2adda40 100644 --- a/go.sum +++ b/go.sum @@ -255,6 +255,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitwarden/sdk-go v0.1.1 h1:Fn7d0SuThIEwaIecg3SRBM6RUbUyQQ7x7Ex+qrcLbMA= +github.com/bitwarden/sdk-go v0.1.1/go.mod h1:Gp2ADXAL0XQ3GO3zxAv503xSlL6ORPf0VZg2J+yQ6jU= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= diff --git a/pkg/backends/bitwardensecretsmanager.go b/pkg/backends/bitwardensecretsmanager.go new file mode 100644 index 00000000..3bcb33e5 --- /dev/null +++ b/pkg/backends/bitwardensecretsmanager.go @@ -0,0 +1,64 @@ +package backends + +import ( + "fmt" + + bitwarden "github.com/bitwarden/sdk-go" +) + +type BitwardenSecretsClient interface { + List(string) (*bitwarden.SecretIdentifiersResponse, error) + Get(string) (*bitwarden.SecretResponse, error) +} + +// BitwardenSecretsManager is a struct for working with a Bitwarden Secrets Manager backend +type BitwardenSecretsManager struct { + Client BitwardenSecretsClient +} + +// NewBitwardenSecretsClient initializes a new Bitwarden Secrets Manager backend +func NewBitwardenSecretsClient(client BitwardenSecretsClient) *BitwardenSecretsManager { + return &BitwardenSecretsManager{ + Client: client, + } +} + +// Login does nothing as a "login" is handled on the instantiation of the Bitwarden SDK +func (bw *BitwardenSecretsManager) Login() error { + return nil +} + +// GetSecrets gets secrets from Bitwarden Secrets Manager and returns the formatted data +// The path is of format `organization-id +func (bw *BitwardenSecretsManager) GetSecrets(path string, _ string, _ map[string]string) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + secrets, err := bw.Client.List(path) + if err != nil { + return nil, err + } + + for _, secret := range secrets.Data { + value, err := bw.Client.Get(secret.ID) + if err != nil { + return nil, err + } + result[secret.ID] = value.Value + } + + return result, nil +} + +// GetIndividualSecret will get the specific secret (placeholder) from the SM backend +// The path is of format `organization-id/secret-id` +// organization id is ignored for indvidual secret fetching, but is included here to +// keep a standard path. +// Version is not supported and is ignored. +func (bw *BitwardenSecretsManager) GetIndividualSecret(_, secret, _ string, _ map[string]string) (interface{}, error) { + fmt.Println(secret) + value, err := bw.Client.Get(secret) + if err != nil { + return nil, err + } + return value.Value, nil +} diff --git a/pkg/backends/bitwardensecretsmanager_test.go b/pkg/backends/bitwardensecretsmanager_test.go new file mode 100644 index 00000000..d9938d07 --- /dev/null +++ b/pkg/backends/bitwardensecretsmanager_test.go @@ -0,0 +1,98 @@ +package backends_test + +import ( + "testing" + + "github.com/argoproj-labs/argocd-vault-plugin/pkg/backends" + bitwarden "github.com/bitwarden/sdk-go" +) + +type mockBitwardenSecretesClient struct{} + +func (bw mockBitwardenSecretesClient) List(organizationID string) (*bitwarden.SecretIdentifiersResponse, error) { + switch organizationID { + case "58293c58-5666-11ef-91a2-67fcd9d549c7": + return &bitwarden.SecretIdentifiersResponse{ + Data: []bitwarden.SecretIdentifierResponse{ + { + ID: "ce398fa2-5665-11ef-8916-97605d6da25b", + Key: "Human Readable Key", + OrganizationID: organizationID, + }, + { + ID: "98b6c8ee-5666-11ef-ac37-8742ac5fc78f", + Key: "Other Key", + OrganizationID: organizationID, + }, + }, + }, nil + default: + return nil, nil + } +} + +func (bw mockBitwardenSecretesClient) Get(secretID string) (*bitwarden.SecretResponse, error) { + switch secretID { + case "ce398fa2-5665-11ef-8916-97605d6da25b": + projectID := "ddb13dae-5665-11ef-8583-f73233caa8df" + return &bitwarden.SecretResponse{ + CreationDate: "2022-11-17T15:55:18.005669100Z", + ID: secretID, + Key: "Human Readable Key", + Note: "", + OrganizationID: "d4105690-5665-11ef-a058-c713a9374bb0", + ProjectID: &projectID, + RevisionDate: "2022-11-17T15:55:18.005669100Z", + Value: "my secret", + }, nil + case "98b6c8ee-5666-11ef-ac37-8742ac5fc78f": + projectID := "ddb13dae-5665-11ef-8583-f73233caa8df" + return &bitwarden.SecretResponse{ + CreationDate: "2019-05-11T15:55:18.005669100Z", + ID: secretID, + Key: "Other Key", + Note: "", + OrganizationID: "d4105690-5665-11ef-a058-c713a9374bb0", + ProjectID: &projectID, + RevisionDate: "2019-05-11T15:55:18.005669100Z", + Value: "my other secret", + }, nil + default: + return nil, nil + } + +} + +func TestBitwardenSecretsManager(t *testing.T) { + sm := backends.NewBitwardenSecretsClient(mockBitwardenSecretesClient{}) + + t.Run("Test Login", func(t *testing.T) { + err := sm.Login() + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + }) + + t.Run("GetIndividualSecret", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("58293c58-5666-11ef-91a2-67fcd9d549c7", "ce398fa2-5665-11ef-8916-97605d6da25b", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + if secret != "my secret" { + t.Fatalf("expected secret value 'my secret' but got: %s", secret) + } + }) + + t.Run("GetSecrets", func(t *testing.T) { + secrets, err := sm.GetSecrets("58293c58-5666-11ef-91a2-67fcd9d549c7", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + if secrets["ce398fa2-5665-11ef-8916-97605d6da25b"] != "my secret" { + t.Fatalf("expected 'my secret' but got: %s", secrets["ce398fa2-5665-11ef-8916-97605d6da25b"]) + } + if secrets["98b6c8ee-5666-11ef-ac37-8742ac5fc78f"] != "my other secret" { + t.Fatalf("expected 'my other secret' but got: %s", secrets["98b6c8ee-5666-11ef-ac37-8742ac5fc78f"]) + } + }) +} diff --git a/pkg/config/config.go b/pkg/config/config.go index bc896701..f4905a13 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "os" "strconv" "strings" "time" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" + gcpsm "cloud.google.com/go/secretmanager/apiv1" "github.com/1Password/connect-sdk-go/connect" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -23,6 +24,7 @@ import ( "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" "github.com/aws/aws-sdk-go-v2/config" awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + bitwarden "github.com/bitwarden/sdk-go" "github.com/hashicorp/vault/api" ksm "github.com/keeper-security/secrets-manager-go/core" "github.com/spf13/viper" @@ -50,6 +52,7 @@ var backendPrefixes []string = []string{ "sops", "op_connect", "k8s_secret", + "bitwarden", } // New returns a new Config struct @@ -283,6 +286,41 @@ func New(v *viper.Viper, co *Options) (*Config, error) { { backend = backends.NewKubernetesSecret() } + case types.BitwardenSecretsManagerBackend: + { + if !v.IsSet(types.EnvAvpBitwardenAPIURL) { + utils.VerboseToStdErr("warning: %s env var not set, using Bitwarden API URL %s", types.EnvAvpBitwardenAPIURL, types.BitwardenDefaultAPIURL) + v.Set(types.EnvAvpBitwardenAPIURL, types.BitwardenDefaultAPIURL) + } + + if !v.IsSet(types.EnvAvpBitwardenIdentityURL) { + utils.VerboseToStdErr("warning: %s env var not set, using Bitwarden Identity URL %s", types.EnvAvpBitwardenIdentityURL, types.BitwardenDefaultIdentityURL) + v.Set(types.EnvAvpBitwardenIdentityURL, types.BitwardenDefaultIdentityURL) + } + + if !v.IsSet(types.EnvAvpBitwardenToken) { + return nil, fmt.Errorf("%s is required for Bitwarden Secrets Manager", types.EnvAvpBitwardenToken) + } + + apiURL := v.GetString(types.EnvAvpBitwardenAPIURL) + identityURL := v.GetString(types.EnvAvpBitwardenIdentityURL) + + s, err := bitwarden.NewBitwardenClient( + &apiURL, + &identityURL, + ) + if err != nil { + return nil, err + } + + err = s.AccessTokenLogin(v.GetString(types.EnvAvpBitwardenToken), nil) + if err != nil { + return nil, err + } + + backend = backends.NewBitwardenSecretsClient(s.Secrets()) + + } default: return nil, fmt.Errorf("Must provide a supported Vault Type, received %s", v.GetString(types.EnvAvpType)) } diff --git a/pkg/types/constants.go b/pkg/types/constants.go index 49abbdef..8cc7813d 100644 --- a/pkg/types/constants.go +++ b/pkg/types/constants.go @@ -5,59 +5,65 @@ const ( EnvArgoCDPrefix = "ARGOCD_ENV" // Environment Variable Constants - EnvAvpType = "AVP_TYPE" - EnvAvpRoleID = "AVP_ROLE_ID" - EnvAvpSecretID = "AVP_SECRET_ID" - EnvAvpAuthType = "AVP_AUTH_TYPE" - EnvAvpGithubToken = "AVP_GITHUB_TOKEN" - EnvAvpK8sRole = "AVP_K8S_ROLE" - EnvAvpK8sMountPath = "AVP_K8S_MOUNT_PATH" - EnvAvpMountPath = "AVP_MOUNT_PATH" - EnvAvpK8sTokenPath = "AVP_K8S_TOKEN_PATH" - EnvAvpIBMAPIKey = "AVP_IBM_API_KEY" - EnvAvpIBMInstanceURL = "AVP_IBM_INSTANCE_URL" - EnvAvpKvVersion = "AVP_KV_VERSION" - EnvAvpPathPrefix = "AVP_PATH_PREFIX" - EnvAWSRegion = "AWS_REGION" - EnvVaultAddress = "VAULT_ADDR" - EnvYCLKeyID = "AVP_YCL_KEY_ID" - EnvYCLServiceAccountID = "AVP_YCL_SERVICE_ACCOUNT_ID" - EnvYCLPrivateKey = "AVP_YCL_PRIVATE_KEY" - EnvAvpUsername = "AVP_USERNAME" - EnvAvpPassword = "AVP_PASSWORD" - EnvPathValidation = "AVP_PATH_VALIDATION" - EnvAvpKSMConfigPath = "AVP_KEEPER_CONFIG_PATH" - EnvAvpDelineaURL = "AVP_DELINEA_URL" - EnvAvpDelineaUser = "AVP_DELINEA_USER" - EnvAvpDelineaPassword = "AVP_DELINEA_PASSWORD" - EnvAvpDelineaDomain = "AVP_DELINEA_DOMAIN" + EnvAvpType = "AVP_TYPE" + EnvAvpRoleID = "AVP_ROLE_ID" + EnvAvpSecretID = "AVP_SECRET_ID" + EnvAvpAuthType = "AVP_AUTH_TYPE" + EnvAvpGithubToken = "AVP_GITHUB_TOKEN" + EnvAvpK8sRole = "AVP_K8S_ROLE" + EnvAvpK8sMountPath = "AVP_K8S_MOUNT_PATH" + EnvAvpMountPath = "AVP_MOUNT_PATH" + EnvAvpK8sTokenPath = "AVP_K8S_TOKEN_PATH" + EnvAvpIBMAPIKey = "AVP_IBM_API_KEY" + EnvAvpIBMInstanceURL = "AVP_IBM_INSTANCE_URL" + EnvAvpKvVersion = "AVP_KV_VERSION" + EnvAvpPathPrefix = "AVP_PATH_PREFIX" + EnvAWSRegion = "AWS_REGION" + EnvVaultAddress = "VAULT_ADDR" + EnvYCLKeyID = "AVP_YCL_KEY_ID" + EnvYCLServiceAccountID = "AVP_YCL_SERVICE_ACCOUNT_ID" + EnvYCLPrivateKey = "AVP_YCL_PRIVATE_KEY" + EnvAvpUsername = "AVP_USERNAME" + EnvAvpPassword = "AVP_PASSWORD" + EnvPathValidation = "AVP_PATH_VALIDATION" + EnvAvpKSMConfigPath = "AVP_KEEPER_CONFIG_PATH" + EnvAvpDelineaURL = "AVP_DELINEA_URL" + EnvAvpDelineaUser = "AVP_DELINEA_USER" + EnvAvpDelineaPassword = "AVP_DELINEA_PASSWORD" + EnvAvpDelineaDomain = "AVP_DELINEA_DOMAIN" + EnvAvpBitwardenAPIURL = "AVP_BITWARDEN_API_URL" + EnvAvpBitwardenIdentityURL = "AVP_BITWARDEN_IDENTITY_URL" + EnvAvpBitwardenToken = "AVP_BITWARDEN_TOKEN" // Backend and Auth Constants - VaultBackend = "vault" - IBMSecretsManagerbackend = "ibmsecretsmanager" - AWSSecretsManagerbackend = "awssecretsmanager" - GCPSecretManagerbackend = "gcpsecretmanager" - AzureKeyVaultbackend = "azurekeyvault" - Sopsbackend = "sops" - YandexCloudLockboxbackend = "yandexcloudlockbox" - DelineaSecretServerbackend = "delineasecretserver" - OnePasswordConnect = "1passwordconnect" - KeeperSecretsManagerBackend = "keepersecretsmanager" - KubernetesSecretBackend = "kubernetessecret" - K8sAuth = "k8s" - ApproleAuth = "approle" - GithubAuth = "github" - TokenAuth = "token" - UserPass = "userpass" - IAMAuth = "iam" - AwsDefaultRegion = "us-east-2" - GCPCurrentSecretVersion = "latest" - IBMMaxRetries = 3 - IBMRetryIntervalSeconds = 20 - IBMMaxPerPage = 200 - IBMIAMCredentialsType = "iam_credentials" - IBMImportedCertType = "imported_cert" - IBMPublicCertType = "public_cert" + VaultBackend = "vault" + IBMSecretsManagerbackend = "ibmsecretsmanager" + AWSSecretsManagerbackend = "awssecretsmanager" + GCPSecretManagerbackend = "gcpsecretmanager" + AzureKeyVaultbackend = "azurekeyvault" + Sopsbackend = "sops" + YandexCloudLockboxbackend = "yandexcloudlockbox" + DelineaSecretServerbackend = "delineasecretserver" + OnePasswordConnect = "1passwordconnect" + KeeperSecretsManagerBackend = "keepersecretsmanager" + KubernetesSecretBackend = "kubernetessecret" + BitwardenSecretsManagerBackend = "bitwardensecretsmanager" + K8sAuth = "k8s" + ApproleAuth = "approle" + GithubAuth = "github" + TokenAuth = "token" + UserPass = "userpass" + IAMAuth = "iam" + AwsDefaultRegion = "us-east-2" + GCPCurrentSecretVersion = "latest" + IBMMaxRetries = 3 + IBMRetryIntervalSeconds = 20 + IBMMaxPerPage = 200 + IBMIAMCredentialsType = "iam_credentials" + IBMImportedCertType = "imported_cert" + IBMPublicCertType = "public_cert" + BitwardenDefaultAPIURL = "https://api.bitwarden.com" + BitwardenDefaultIdentityURL = "https://identity.bitwarden.com" // Supported annotations AVPPathAnnotation = "avp.kubernetes.io/path" From 07419642419ef76f741aaa6826740f2da264d91f Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 9 Aug 2024 09:40:05 -0700 Subject: [PATCH 2/2] docs: Adding Bitwarden Secrets Manager backend Signed-off-by: Richard Cox --- docs/backends.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/backends.md b/docs/backends.md index d59e1d63..0301acb7 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -807,4 +807,49 @@ metadata: type: Opaque data: password: -``` \ No newline at end of file +``` + +### Bitwarden Secrets Manager + +**Note**: The Bitwarden Secrets Manager backend does not support versioning. + +##### Authentication + +These are the parameters for Delinea: +``` +AVP_TYPE: bitwardensecretsmanager +AVP_BITWARDEN_TOKEN: Bitwarden machine Account Token + +Optional: +AVP_BITWARDEN_API_URL: API Endpoint URL (default: https://api.bitwarden.com) +AVP_BITWARDEN_IDENTITY_URL: Identity Endpoint URL (default: https://identity.bitwarden.com) +``` +##### Examples +Examples assume that the secrets are not saved base64 encoded in the Secret Server. + +###### Path Annotation + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "organization-id" +type: Opaque +stringData: + password: +``` + +###### Inline Path + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret +type: Opaque +data: + password: +``` +