diff --git a/docs/backends.md b/docs/backends.md index d59e1d63..40678d5b 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -492,6 +492,141 @@ data: password-old: ``` +### Oracle Cloud Infrastruture Vault + +##### OCI Authentication - API Key Based +For API Key based Authentication, these are the required parameters: +``` +AVP_TYPE: ocivault +AVP_OCI_VAULT_ID: "ocid1.vault.oc1..." +AVP_OCI_VAULT_COMPARTMENT_ID: "ocid1.compartment.oc1..aaaaaaa" +AVP_OCI_TENANCY": "ocid1.tenancy.oc1..aaaaaaaa" +AVP_OCI_USER": "ocid1.user.oc1..aaaaaaaa" +AVP_OCI_REGION": "test-region" +AVP_OCI_FINGERPRINT": "test-fingerprint" +AVP_OCI_KEY_FILE": `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCyDz0+WvWGmcym +OEBQ0zhWO1Abs/UQ1v0A7kXQpTgwAFKO0SR56jJBII1VmBuctDUYkdO55FvAuhNv +-----END PRIVATE KEY-----` +AVP_OCI_KEY_PASSPHRASE: "" # This can be omitted if there is no key passphrase +``` + + +**Note**: API Key based authentication will be tried first. If its not available , it will be try with Instance principal based authentication + + +##### OCI Authentication - Instance Principal + + +Refer to the [Use Instance Principal authentication](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdk_authentication_methods.htm#sdk_authentication_methods_instance_principaldita) in the OCI SDK for Go. + +* create a dynamic group with the appropriate rules to include the compute instance where AVP will be running. +* Create a new policy that allows the dynamic group to use secret-family in the functions related compartment. e.g +``` +Allow dynamic-group to inspect secret-family in compartment +``` + +For OCI, `path` format is `ocivault` + +These are the parameters for OCI: +``` +AVP_TYPE: ocivault +AVP_OCI_VAULT_ID: "ocid1.vault.oc1..." +AVP_OCI_VAULT_COMPARTMENT_ID: "ocid1.compartment.oc1..aaaaaaa" +``` + +##### Examples + +Suppose the given OCI vault has 2 secrets with different number of password versions + +| Secret Name | Password Value and Versions | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| postgres-password | pg-password2 -- > version 2:latest
pg-password1 -- > version 1 | +| mysql-password | mysql-password3 -- > version 3:latest
mysql-password2 -- > version 2
mysql-password1 -- > version 1 | + + + +###### Path Annotation + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "ocivault" +type: Opaque +data: + POSTGRES_PASSWORD: ## This will evaluate to the b64encoded value of pg-password2 + MYSQL_PASSWORD: ## This will evaluate to the b64encoded value of mysql-password3 +``` + +###### Inline Path + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret +type: Opaque +data: + POSTGRES_PASSWORD: ## This will evaluate to the b64encoded value of pg-password2 + MYSQL_PASSWORD: ## This will evaluate to the b64encoded value of mysql-password3 +``` + + +###### Versioned secrets + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "ocivault" + avp.kubernetes.io/secret-version: "1" +type: Opaque +data: + POSTGRES_PASSWORD: ## This will evaluate to the b64encoded value of pg-password1 + MYSQL_PASSWORD: ## This will evaluate to the b64encoded value of mysql-password2 +``` + +###### Versioned secrets + +This is an edge case where if the annotated secret-version doesn't exist for a secret, it will return latest version for that secret. +In the below case version3 doesn't exists for postgres-password secret, so it will return the latest version for that secret. + + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "ocivault" + avp.kubernetes.io/secret-version: "3" +type: Opaque +data: + POSTGRES_PASSWORD: ## This will evaluate to the b64encoded value of pg-password2 + MYSQL_PASSWORD: ## This will evaluate to the b64encoded value of mysql-password2 +``` + +###### Latest Versioned secrets + +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: test-secret + annotations: + avp.kubernetes.io/path: "ocivault" + avp.kubernetes.io/secret-version: "latest" +type: Opaque +data: + POSTGRES_PASSWORD: ## This will evaluate to the b64encoded value of pg-password2 + MYSQL_PASSWORD: ## This will evaluate to the b64encoded value of mysql-password2 +``` + ### SOPS ##### SOPS Authentication Refer to the [SOPS project page](https://github.com/mozilla/sops) for authentication options/environment variables. diff --git a/docs/config.md b/docs/config.md index 64b64494..dc7f0d6d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -67,13 +67,13 @@ Make sure that these environment variables are available to the plugin when runn environment variables take precedence over configuration pulled from a Kubernetes Secret or a file. ### Full List of Supported Parameters -We support all the backend specific environment variables each backend's SDK will accept (e.g, `VAULT_NAMESPACE`, `AWS_REGION`, etc). Refer to the [specific backend's documentation](../backends) for details. +We support all the backend specific environment variables each backend's SDK will accept (e.g, `VAULT_NAMESPACE`, `AWS_REGION`, etc). Refer to the [specific backend's documentation](./backends.md) for details. We also support these AVP specific variables: | Name | Description | Notes | | -------------------------- |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AVP_TYPE | The type of Vault backend | Supported values: `vault`, `ibmsecretsmanager`, `awssecretsmanager`, `gcpsecretmanager`, `yandexcloudlockbox` and `1passwordconnect` | +| AVP_TYPE | The type of Vault backend | Supported values: `vault`, `ibmsecretsmanager`, `awssecretsmanager`, `gcpsecretmanager`, `yandexcloudlockbox` , `1passwordconnect` and `ocivault` | | AVP_KV_VERSION | The vault secret engine | Supported values: `1` and `2` (defaults to 2). KV_VERSION will be ignored if the `avp.kubernetes.io/kv-version` annotation is present in a YAML resource. | | AVP_AUTH_TYPE | The type of authentication | Supported values: vault: `approle, github, k8s, token`. Only honored for `AVP_TYPE` of `vault` | | AVP_GITHUB_TOKEN | Github token | Required with `AUTH_TYPE` of `github` | @@ -90,7 +90,14 @@ We also support these AVP specific variables: | AVP_YCL_KEY_ID | Yandex Cloud Lockbox service account Key ID | Required with `TYPE` of `yandexcloudlockbox` | | AVP_YCL_PRIVATE_KEY | Yandex Cloud Lockbox service account private key | Required with `TYPE` of `yandexcloudlockbox` | | AVP_PATH_VALIDATION | Regular Expression to validate the Vault path | Optional. Can be used for e.g. to prevent path traversals. | - +| AVP_OCI_TENANCY | OCI Vault tenancy id | Only valid with `TYPE` `ocivault` | +| AVP_OCI_USER | OCI Vault user id | Only valid with `TYPE` `ocivault` | +| AVP_OCI_REGION | OCI Vault region | Only valid with `TYPE` `ocivault` | +| AVP_OCI_FINGERPRINT | OCI Vault user fingerprint | Only valid with `TYPE` `ocivault` | +| AVP_OCI_KEY_FILE | OCI Vault api private key | Only valid with `TYPE` `ocivault` | +| AVP_OCI_KEY_PASSPHRASE | OCI Vault api private key passphrase | Only valid with `TYPE` `ocivault` | +| AVP_OCI_VAULT_ID | OCI Vault ocid | Only valid with `TYPE` `ocivault` | +| AVP_OCI_VAULT_COMPARTMENT_ID | OCI Vault compartment ocid | Only valid with `TYPE` `ocivault` | ### Full List of Supported Annotation We support several different annotations that can be used inside a kubernetes resource. These annotations will override any corresponding configuration set via Environment Variable or Configuration File. diff --git a/go.mod b/go.mod index 19c2472d..99a87940 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/hashicorp/vault/api v1.14.0 github.com/hashicorp/vault/sdk v0.13.0 github.com/keeper-security/secrets-manager-go/core v1.6.2 + github.com/oracle/oci-go-sdk/v65 v65.73.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.17.0 @@ -142,6 +143,7 @@ require ( github.com/go-playground/validator/v10 v10.13.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-test/deep v1.1.1 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect diff --git a/go.sum b/go.sum index a8c974f3..0e4f060d 100644 --- a/go.sum +++ b/go.sum @@ -493,6 +493,8 @@ github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -1153,6 +1155,8 @@ github.com/oracle/oci-go-sdk/v59 v59.0.0 h1:+zTvWfj9ZK0OwLRyXjUkZ8dPN3WvkQSRd3io github.com/oracle/oci-go-sdk/v59 v59.0.0/go.mod h1:PWyWRn+xkQxwwmLq/oO03X3tN1tk2vEIE2tFaJmldHM= github.com/oracle/oci-go-sdk/v60 v60.0.0 h1:EJAWjEi4SY5Raha6iUzq4LTQ0uM5YFw/wat/L1ehIEM= github.com/oracle/oci-go-sdk/v60 v60.0.0/go.mod h1:krz+2gkSzlSL/L4PvP0Z9pZpag9HYLNtsMd1PmxlA2w= +github.com/oracle/oci-go-sdk/v65 v65.73.0 h1:C7uel6CoKk4A1KPkdhFBAyvVyFRTHAmX8m0o64RmfPg= +github.com/oracle/oci-go-sdk/v65 v65.73.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= diff --git a/pkg/backends/ocivault.go b/pkg/backends/ocivault.go new file mode 100644 index 00000000..924450d6 --- /dev/null +++ b/pkg/backends/ocivault.go @@ -0,0 +1,180 @@ +package backends + +import ( + "context" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/example/helpers" + ocism "github.com/oracle/oci-go-sdk/v65/secrets" + ocivault "github.com/oracle/oci-go-sdk/v65/vault" +) + +var OCIPath, _ = regexp.Compile(`^ocivault(?:/(.+))?$`) +var OCISecretVersion, _ = regexp.Compile(`^\d+$`) + +type OCISecretIface interface { + GetSecretBundleByName(ctx context.Context, request ocism.GetSecretBundleByNameRequest) (response ocism.GetSecretBundleByNameResponse, err error) +} + +type OCIVaultIface interface { + ListSecrets(ctx context.Context, request ocivault.ListSecretsRequest) (response ocivault.ListSecretsResponse, err error) + ListSecretVersions(ctx context.Context, request ocivault.ListSecretVersionsRequest) (response ocivault.ListSecretVersionsResponse, err error) +} + +// OCIVault is a struct for working with a OCI Vault backend +type OCIVault struct { + secretClient OCISecretIface + vaultClient OCIVaultIface + vaultId string + compartmentId string +} + +// NewOCIVaultBackend initializes a new OCI Vault backend +func NewOCIVaultBackend(secret_client OCISecretIface,vault_client OCIVaultIface, vault_id string, compartment_id string ) *OCIVault { + return &OCIVault{ + secretClient: secret_client, + vaultClient: vault_client, + vaultId: vault_id, + compartmentId: compartment_id, + } +} + +// Login does nothing as a "login" is handled on the instantiation of the oci sdk +func (oci *OCIVault) Login() error { + return nil +} + + +// Iterate Secret Version +func (oci *OCIVault) CheckSecretVersion(secret_version_req ocivault.ListSecretVersionsRequest, version int64 ) (bool,error) { + + version_found := false + + listSecretVersionFunc := func(request ocivault.ListSecretVersionsRequest) (ocivault.ListSecretVersionsResponse, error) { + return oci.vaultClient.ListSecretVersions(context.Background(), request) + } + + for r, err := listSecretVersionFunc(secret_version_req); ; r, err = listSecretVersionFunc(secret_version_req) { + if err != nil{ + return version_found,err + } + + for _, secret_ver_summary := range r.Items { + if secret_ver_summary.VersionNumber != nil && *secret_ver_summary.VersionNumber == version { + version_found=true + break + } + } + + if r.OpcNextPage != nil && !version_found { + // if there are more items in next page, fetch items from next page + secret_version_req.Page = r.OpcNextPage + } else { + // no more result, break the loop + break + } + } + + return version_found,nil +} + +// GetSecrets gets secrets from OCI Vault and returns the formatted data +// For OCI Vault, the path is of format `ocivault/secretname` + +func (oci *OCIVault) GetSecrets(kvpath string, version string, annotations map[string]string) (map[string]interface{}, error) { + + var secretName *string = nil + matches := OCIPath.FindStringSubmatch(kvpath) + if len(matches) == 0 { + return nil, fmt.Errorf("path is not in the correct format (ocivault/) for OCI vault: %s", kvpath) + } + + if len(matches) > 1 && matches[1] != "" { + secretName = &matches[1] // Capture secret name if it exists + } + + list_secrets_req := ocivault.ListSecretsRequest{ + CompartmentId: common.String(oci.compartmentId), + SortBy: ocivault.ListSecretsSortByName, + VaultId: common.String(oci.vaultId), + Name: secretName, + Limit: common.Int(100), + LifecycleState: ocivault.SecretSummaryLifecycleStateActive, + SortOrder: ocivault.ListSecretsSortOrderDesc} + + listSecretsFunc := func(request ocivault.ListSecretsRequest) (ocivault.ListSecretsResponse, error) { + return oci.vaultClient.ListSecrets(context.Background(), request) + } + + data := make(map[string]interface{}) + + for r, err := listSecretsFunc(list_secrets_req); ; r, err = listSecretsFunc(list_secrets_req) { + helpers.FatalIfError(err) + + for _, secret := range r.Items { + req := ocism.GetSecretBundleByNameRequest{ + VaultId: common.String(oci.vaultId), + SecretName: common.String(*secret.SecretName)} + + if version != "" && !strings.EqualFold(version, "latest") { + isPositiveInteger := OCISecretVersion.MatchString(version) + if !isPositiveInteger { + return nil, fmt.Errorf("version string must contain only positive integers") + } + secret_version, err := strconv.ParseInt(version, 10, 64) + helpers.FatalIfError(err) + + list_secret_version_req := ocivault.ListSecretVersionsRequest{ + SecretId: common.String(*secret.Id), + SortBy: ocivault.ListSecretVersionsSortByVersionNumber, + SortOrder: ocivault.ListSecretVersionsSortOrderAsc, + Limit: common.Int(100)} + + version_found,err := oci.CheckSecretVersion(list_secret_version_req,secret_version) + helpers.FatalIfError(err) + if version_found { + req.VersionNumber = common.Int64(secret_version) + } else{ + req.Stage = ocism.GetSecretBundleByNameStageLatest + } + } else { + req.Stage = ocism.GetSecretBundleByNameStageLatest + } + + resp, err := oci.secretClient.GetSecretBundleByName(context.Background(), req) + helpers.FatalIfError(err) + + secretContent := resp.SecretBundle.SecretBundleContent.(ocism.Base64SecretBundleContentDetails) + encodedSecret := *secretContent.Content + + data[*secret.SecretName] = string(encodedSecret) + } + + if r.OpcNextPage != nil { + // if there are more items in next page, fetch items from next page + list_secrets_req.Page = r.OpcNextPage + } else { + // no more result, break the loop + break + } + } + + return data, nil +} + + +// GetIndividualSecret will get the specific secret (placeholder) from the SM backend +// For OCI Vault, the path is of format `ocivault/secretname` +// So, we use GetSecrets and extract the specific placeholder we want +func (oci *OCIVault) GetIndividualSecret(kvpath, secret, version string, annotations map[string]string) (interface{}, error) { + secretpath := kvpath + "/" + secret + data, err := oci.GetSecrets(secretpath, version, annotations) + if err != nil { + return nil, err + } + return data[secret], nil +} diff --git a/pkg/backends/ocivault_test.go b/pkg/backends/ocivault_test.go new file mode 100644 index 00000000..6ef1bdca --- /dev/null +++ b/pkg/backends/ocivault_test.go @@ -0,0 +1,248 @@ +package backends_test + +import ( + "context" + "reflect" + "strings" + "testing" + + "github.com/argoproj-labs/argocd-vault-plugin/pkg/backends" + "github.com/oracle/oci-go-sdk/v65/common" + ocism "github.com/oracle/oci-go-sdk/v65/secrets" + ocivault "github.com/oracle/oci-go-sdk/v65/vault" +) + +type mockOCISecretClient struct { + backends.OCISecretIface +} + +type mockOCIVaultClient struct { + backends.OCIVaultIface +} + +func (m *mockOCISecretClient) GetSecretBundleByName(ctx context.Context, request ocism.GetSecretBundleByNameRequest) (response ocism.GetSecretBundleByNameResponse, err error) { + data := ocism.GetSecretBundleByNameResponse{} + + switch *request.VaultId { + case "test_vaultid": + if request.VersionNumber == nil || request.Stage == ocism.GetSecretBundleByNameStageLatest { + content := "current-value" + secretBundle := ocism.Base64SecretBundleContentDetails{ + Content: &content, + } + data.SecretBundle.SecretBundleContent = secretBundle + } else { + content := "previous-value" + secretBundle := ocism.Base64SecretBundleContentDetails{ + Content: &content, + } + data.SecretBundle.SecretBundleContent = secretBundle + } + + } + + return data, nil +} + +func (m *mockOCIVaultClient) ListSecrets(ctx context.Context, request ocivault.ListSecretsRequest) (response ocivault.ListSecretsResponse, err error) { + data := ocivault.ListSecretsResponse{} + secretname := "secretname" + secretid := "secretid" + secretsummary := ocivault.SecretSummary{ + SecretName: &secretname, + Id: &secretid, + + } + data.Items = append(data.Items, secretsummary) + return data, nil +} + +func (m *mockOCIVaultClient) ListSecretVersions(ctx context.Context, request ocivault.ListSecretVersionsRequest) (response ocivault.ListSecretVersionsResponse, err error) { + data := ocivault.ListSecretVersionsResponse{} + version_number := int64(1) + secret_ver_summary := ocivault.SecretVersionSummary{ + VersionNumber: &version_number, + + } + data.Items = append(data.Items, secret_ver_summary) + + return data, nil +} + +func TestOCIVaultGetSecrets(t *testing.T) { + sm := backends.NewOCIVaultBackend(&mockOCISecretClient{},&mockOCIVaultClient{},"test_vaultid","test_compartmentid") + + t.Run("Get latest secrets", func(t *testing.T) { + data, err := sm.GetSecrets("ocivault/secretname", "latest", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "secretname": "current-value", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("Get secrets without version", func(t *testing.T) { + data, err := sm.GetSecrets("ocivault/secretname", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "secretname": "current-value", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("Get secrets with existing version", func(t *testing.T) { + data, err := sm.GetSecrets("ocivault/secretname", "1", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "secretname": "previous-value", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("Get secrets with non existing version", func(t *testing.T) { + data, err := sm.GetSecrets("ocivault/secretname", "5", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := map[string]interface{}{ + "secretname": "current-value", + } + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %s, got: %s.", expected, data) + } + }) + + t.Run("OCI latest GetIndividualSecret", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("ocivault/secretname", "secretname", "latest", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "current-value" + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + + t.Run("OCI GetIndividualSecret without version", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("ocivault/secretname", "secretname", "", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "current-value" + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + + t.Run("OCI GetIndividualSecret with existing version", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("ocivault/secretname", "secretname", "1", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "previous-value" + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + + t.Run("OCI GetIndividualSecret with non existing version", func(t *testing.T) { + secret, err := sm.GetIndividualSecret("ocivault/secretname", "secretname", "5", map[string]string{}) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := "current-value" + if !reflect.DeepEqual(expected, secret) { + t.Errorf("expected: %s, got: %s.", expected, secret) + } + }) + t.Run("OCI Invalid Path", func(t *testing.T) { + _, err := sm.GetSecrets("ibmcloud/arbitrary/secrets/groups/some-group", "", map[string]string{}) + if err == nil { + t.Fatalf("expected error") + } + + expectedErr := "path is not in the correct format" + if !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("Expected error to have %s but said %s", expectedErr, err) + } + }) + + t.Run("OCI Invalid Version Number", func(t *testing.T) { + _, err := sm.GetSecrets("ocivault/secretname", "eleven", map[string]string{}) + if err == nil { + t.Fatalf("expected error") + } + + expectedErr := "version string must contain only positive integers" + if !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("Expected error to have %s but said %s", expectedErr, err) + } + }) + + t.Run("OCI Negative Version Number", func(t *testing.T) { + _, err := sm.GetSecrets("ocivault/secretname", "-1", map[string]string{}) + if err == nil { + t.Fatalf("expected error") + } + + expectedErr := "version string must contain only positive integers" + if !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("Expected error to have %s but said %s", expectedErr, err) + } + }) + + t.Run("OCI Float Version Number", func(t *testing.T) { + _, err := sm.GetSecrets("ocivault/secretname", "1.0", map[string]string{}) + if err == nil { + t.Fatalf("expected error") + } + + expectedErr := "version string must contain only positive integers" + if !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("Expected error to have %s but said %s", expectedErr, err) + } + }) + + t.Run("OCI Check Version Number", func(t *testing.T) { + list_secret_version_req := ocivault.ListSecretVersionsRequest{ + SecretId: common.String("secretname"), + SortBy: ocivault.ListSecretVersionsSortByVersionNumber, + SortOrder: ocivault.ListSecretVersionsSortOrderAsc, + Limit: common.Int(100)} + data, err := sm.CheckSecretVersion(list_secret_version_req,int64(1)) + if err != nil { + t.Fatalf("expected 0 errors but got: %s", err) + } + + expected := true + + if !reflect.DeepEqual(expected, data) { + t.Errorf("expected: %v, got: %v.", expected, data) + } + }) + +} diff --git a/pkg/config/config.go b/pkg/config/config.go index bc896701..fb325597 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" @@ -25,6 +26,10 @@ import ( awssm "github.com/aws/aws-sdk-go-v2/service/secretsmanager" "github.com/hashicorp/vault/api" ksm "github.com/keeper-security/secrets-manager-go/core" + "github.com/oracle/oci-go-sdk/v65/common" + ociauth "github.com/oracle/oci-go-sdk/v65/common/auth" + ocism "github.com/oracle/oci-go-sdk/v65/secrets" + ocivault "github.com/oracle/oci-go-sdk/v65/vault" "github.com/spf13/viper" ycsdk "github.com/yandex-cloud/go-sdk" "github.com/yandex-cloud/go-sdk/iamkey" @@ -47,6 +52,7 @@ var backendPrefixes []string = []string{ "aws", "azure", "google", + "oci", "sops", "op_connect", "k8s_secret", @@ -179,6 +185,50 @@ func New(v *viper.Viper, co *Options) (*Config, error) { client := awssm.NewFromConfig(s) backend = backends.NewAWSSecretsManagerBackend(client) } + case types.OCIVaultbackend: + { + + if !v.IsSet(types.EnvOCIVaultId) || + !v.IsSet(types.EnvOCIVaultCompartmentId) { + return nil, fmt.Errorf( + "%s and %s are required for OCI Vault Service", + types.EnvOCIVaultId, + types.EnvOCIVaultCompartmentId, + ) + } + + var secret_client ocism.SecretsClient + var vault_client ocivault.VaultsClient + var err error + // Try using DefaultConfigProvider + + p1 := common.NewRawConfigurationProvider( + v.GetString(types.EnvOCITenancy), + v.GetString(types.EnvOCIUser), + v.GetString(types.EnvOCIRegion), + v.GetString(types.EnvOCIFingerprint), + v.GetString(types.EnvOCIKeyFile), + common.String(v.GetString(types.EnvOCIKeyPassphrase))) + secret_client, _ = ocism.NewSecretsClientWithConfigurationProvider(p1) + vault_client, err = ocivault.NewVaultsClientWithConfigurationProvider(p1) + if err == nil { + utils.VerboseToStdErr("Successfully created OCI Secrets client with DefaultConfigProvider\n") + } else { + // Fallback to InstancePrincipalConfigurationProvider if DefaultConfigProvider fails + utils.VerboseToStdErr("DefaultConfigProvider failed with error %v", err) + provider, err := ociauth.InstancePrincipalConfigurationProvider() + if err != nil { + utils.VerboseToStdErr("Error creating InstancePrincipalConfigurationProvider: %v", err) + } + secret_client, _ = ocism.NewSecretsClientWithConfigurationProvider(provider) + vault_client, err = ocivault.NewVaultsClientWithConfigurationProvider(provider) + if err != nil { + utils.VerboseToStdErr("Error creating Secrets client with InstancePrincipalConfigurationProviderr: %v", err) + } + } + backend = backends.NewOCIVaultBackend(secret_client,vault_client,v.GetString(types.EnvOCIVaultId),v.GetString(types.EnvOCIVaultCompartmentId)) + } + case types.GCPSecretManagerbackend: { ctx := context.Background() diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c7925b9d..d2c2d733 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -224,6 +224,46 @@ fDGt+yaf3RaZbVwHSVLzxiXGsu1WQJde3uJeNh5c6z+5 }, "*backends.KubernetesSecret", }, + { + map[string]interface{}{ + "AVP_TYPE": "ocivault", + "AVP_OCI_TENANCY": "ocid1.tenancy.oc1..aaaaaaaa", + "AVP_OCI_USER": "ocid1.user.oc1..aaaaaaaa", + "AVP_OCI_REGION": "test-region", + "AVP_OCI_FINGERPRINT": "test-fingerprint", + "AVP_OCI_VAULT_ID": "test-vault-id", + "AVP_OCI_VAULT_COMPARTMENT_ID": "test-vault-comp-id", + "AVP_OCI_KEY_FILE": `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCyDz0+WvWGmcym +OEBQ0zhWO1Abs/UQ1v0A7kXQpTgwAFKO0SR56jJBII1VmBuctDUYkdO55FvAuhNv +WTQcxi0WjVejZ7npNhyU1vcDVmJGmVc1vgb6Jo+pfDGq/EnkaNthpE50+jPeA3jD +l6/NHaHb4f9UUpX7o/YfG8RQ1kF7Dkq1YqVEPBrRfZOzenIE0CQjRjR6GnwdAePB +GtrmHP3ZOM/t5QlKYd68z2xQfbvl88/W99PnjKMLmhyObWzwtcpWsjnjLm8vg49X +ZOa7+y14/Ev/FR7HOAwsXpXFGtPiib5JcoFIddc+bdjlSaINx3nOq9lal/nNHv8Q +hcSMg+RxAgMBAAECggEAV3EUamLQ4GD3F0nYi9iueep21KPzXWm2pZZdwrDgfvIp +mOksOJLCSylpPveL19DHomE60LdMN8Epei0cYmUQD1sqBp0Rt21Ta+SFOaZabMEx +CrtfQcleE6Vh3s42m2zDD5hYzylv/z9FNwhu1RQQQKMjeI12CjXi0DQanHgbgAoZ +GN73kAAtwoi/wClpJsbMypshkShQGdMzBaXOh8rm5NGCOuzM0L2Ovm1yZdfn9in1 +T1ivStRXojAkWFYJ2PATpR0USEFboKahUDWHRMUUQhHXY5M07G9mxmyj1yeT3bPk +yD4cFW8PHLRhfiM4FAX5/0ATpeaS+9yjRaZgqRknYQKBgQD0ZA9TyOe/iYFIXeuZ +7fi/V08sJ7ULR+xRj65KYgOlowsRl4EzSURgaprMPpJQ4B6bWt4VHcxX3rpAnTie +46LgOqU1iK9LYTBgvTpgnhLxzvKYosgtE8qrPnoiAmtXR+cBEcHTXFlomdwFBO40 +tNZaLQi9WpcZvw87t5mm6t5E6wKBgQC6hIznWSj51fcO3+OobQCZql3lSftJ/6EV +BJ576uuaI4fbYgrw8O7q2WULVLeS34KERYS5mxQUhnkaC5hwQzYRVJ2lbsBFMqhv +pT9maKSEqMfDm5iJmSz0k+L4eJaU7xR/RMaZRtQLR7MfY9nyKGeP97e0Kk1rhYF7 +7oNS5biVEwKBgQDq3PY16M9+rSDHcSsoRSBWkguOPaKpcrdTMqem6Ebk+al7gIQz +y2eg2RJm0oM+ogQH/O2MkZR9pZiM3As79zviDboTloYQBRi+/1uI2qEOLXnK4jVJ +zMlqhKJO6NBLktgXmP8Spp9t/N8LG8/oaxnMk5bgkpy/q3NySmGpnfF5fQKBgGkC +69ntBvbyknCbeS+Af1AE7WyEpKha9jRBL4GRGCjmTD0mDAbvf3RWBV/FyL02feM+ +yKU/PKT5uQEC+kZqcOx8+W0E19ed19tT7EgaLlZKOH5XAiCmTvs8sBM4wX8ExEOL +U01E5WmcaqsHqtN+ECCsVY9oKcKZnfdKqEFp+OxlAoGAMrCGEePfP3z6uLQW5JZ6 +/F3yvIIbFf9qERwr56eyk6MPqZMN+QdZuVVY0qFxj0F1NRKFJN2IZU8LPnmNICuh +Ap0WFIpwv5OJ94OlooATeYakqTj5l2gdQkgxAI8bZlqO7XVtVEPSvXCoLrZKEVqA +Zfzm2fgQCYKEhd8HQDmkT/8= +-----END PRIVATE KEY-----`, + }, + "*backends.OCIVault", + }, } for _, tc := range testCases { for k, v := range tc.environment { diff --git a/pkg/types/constants.go b/pkg/types/constants.go index 49abbdef..d90e3cff 100644 --- a/pkg/types/constants.go +++ b/pkg/types/constants.go @@ -5,32 +5,40 @@ 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" + EnvOCITenancy = "AVP_OCI_TENANCY" + EnvOCIUser = "AVP_OCI_USER" + EnvOCIRegion = "AVP_OCI_REGION" + EnvOCIFingerprint = "AVP_OCI_FINGERPRINT" + EnvOCIKeyFile = "AVP_OCI_KEY_FILE" + EnvOCIKeyPassphrase = "AVP_OCI_KEY_PASSPHRASE" + EnvOCIVaultId = "AVP_OCI_VAULT_ID" + EnvOCIVaultCompartmentId = "AVP_OCI_VAULT_COMPARTMENT_ID" // Backend and Auth Constants VaultBackend = "vault" @@ -38,6 +46,7 @@ const ( AWSSecretsManagerbackend = "awssecretsmanager" GCPSecretManagerbackend = "gcpsecretmanager" AzureKeyVaultbackend = "azurekeyvault" + OCIVaultbackend = "ocivault" Sopsbackend = "sops" YandexCloudLockboxbackend = "yandexcloudlockbox" DelineaSecretServerbackend = "delineasecretserver"