Skip to content
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

Feat: Added Oracle Cloud Infrastructure(OCI) Vault service as a backend with vaultid #670

Open
wants to merge 13 commits 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
135 changes: 135 additions & 0 deletions docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,141 @@ data:
password-old: <path:keyvault#password#33740fc26214497f8904d93f20f7db6d>
```

### 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: "<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 <dynamic-group-name> to inspect secret-family in compartment <compartment-name>
```

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 <br>pg-password1 -- > version 1 |
| mysql-password | mysql-password3 -- > version 3:latest <br>mysql-password2 -- > version 2 <br>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: <postgres-password> ## This will evaluate to the b64encoded value of pg-password2
MYSQL_PASSWORD: <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: <path:ocivault#postgres-password> ## This will evaluate to the b64encoded value of pg-password2
MYSQL_PASSWORD: <path:ocivault#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: <path:ocivault#postgres-password> ## This will evaluate to the b64encoded value of pg-password1
MYSQL_PASSWORD: <path:ocivault#mysql-password#2> ## 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: <path:ocivault#postgres-password> ## This will evaluate to the b64encoded value of pg-password2
MYSQL_PASSWORD: <path:ocivault#mysql-password#2> ## 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: <path:ocivault#postgres-password> ## This will evaluate to the b64encoded value of pg-password2
MYSQL_PASSWORD: <path:ocivault#mysql-password#2> ## 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.
Expand Down
13 changes: 10 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
180 changes: 180 additions & 0 deletions pkg/backends/ocivault.go
Original file line number Diff line number Diff line change
@@ -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 aws 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
}
Loading