Skip to content

Commit

Permalink
Merge pull request #50 from sgettys/feat/add_secret_id_resolving
Browse files Browse the repository at this point in the history
feat: Added ability to parse keyvalue as an azure keyvault ID
  • Loading branch information
carolynvs committed Dec 13, 2022
2 parents e361abc + 077dd80 commit 63869fc
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 2 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ The `azure.keyvault` plugin resolves credentials or parameters against secrets i
```
1. [Create a key vault][keyvault] and set the vault name in the config with name of the vault.

### Secret ID
The full secret FQDN can be used to resolve a secret that may not exist in the plugin configured vault. The plugin will attempt to parse a key value provided as a secret identifier and extract the keyvault name, secret name, and secret version from that value. If it is able to parse the key vault as a secret identifier then it will attempt to resolve the secret against that Azure Key Vault. If it is unable to find the parsed secret in the parsed Azure Key Vault then it will attempt to use the full key value as the secret name and attempt to resolve it in the configured Azure Key Vault.

An example CredentialSet that would resolve to both the configured Azure Key Vault as well as a separate Azure Key Vault based on the secret ID would look like this:

```yaml
name: example-credset
schemaVersion: 1.0.1
credentials:
- name: example-configured-secret
source:
secret: my-secret
- name: example-secret-id
source:
secret: https://my-vault.vault.azure.net/secrets/my-secret/secret-version1234
```

The version can be included or omitted in the secret ID. If the version is omitted then the latest version is fetched out.

This provides `porter` with the ability to fetch secrets out of multiple Azure Key Vaults without having the change the default vault configuration.

### Authentication

Authentication to Azure can use any of the following methods. Whichever mechanism is used, the principal that is used to access key vault needs to be granted at least [Get and List secret permissions][keyvaultacl] on the vault. However, if you authenticate using the Azure CLI and are logged in with the account that created the key vault in the portal then you will already have this permission.
Expand Down
60 changes: 58 additions & 2 deletions pkg/azure/keyvault/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keyvault
import (
"context"
"fmt"
"net/url"
"strings"

"get.porter.sh/plugin/azure/pkg/azure/azureconfig"
Expand All @@ -20,6 +21,12 @@ const (
SecretKeyName = "secret"
)

type secret struct {
vaultURL string
name string
version string
}

// Store implements the backing store for secrets in azure key vault.
type Store struct {
logger hclog.Logger
Expand Down Expand Up @@ -65,7 +72,22 @@ func (s *Store) Resolve(ctx context.Context, keyName string, keyValue string) (s
if err := s.Connect(ctx); err != nil {
return "", err
}

// Check if the keyValue is set to a full ID or just the secret name. The keyValue is only considered
// an ID if it includes at least the keyvault name and secret name. If version is not part of the ID then the version
// is set to "" which will fetch the latest version
secret := parseID(ctx, keyValue)
if secret != nil {
result, err := s.client.GetSecret(ctx, secret.vaultURL, secret.name, secret.version)
if err != nil {
// Instead of return error in this case instead log as a debug and attempt to fetch
// the secret from the configured secret store. Only return error if the secret is unable
// to be resolved in both ways
log.Debug(fmt.Sprintf("could not get secret %s by ID: %s", keyValue, err.Error()))
} else {
// If we were able to look it up based off of the parsed ID then return that immediately
return *result.Value, nil
}
}
secretVersion := ""
result, err := s.client.GetSecret(ctx, s.vaultUrl, keyValue, secretVersion)
if err != nil {
Expand Down Expand Up @@ -95,6 +117,40 @@ func (s *Store) Create(ctx context.Context, keyName string, keyValue string, val
if err != nil {
return log.Error(fmt.Errorf("failed to set secret for key %s in azure-keyvault: %w", keyValue, err))
}

return nil
}

// parseID will attempt to create a secret from an id. If the id is not valid then
// it will log a debug and return nil. This code was mainly copied from the azure keyvault internal library:
// https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/keyvault/internal/parse.go
func parseID(ctx context.Context, id string) *secret {
_, log := tracing.StartSpan(ctx, attribute.String("parsing secret as ID", id))
if id == "" {
log.Debug("unable to parse empty ID")
return nil
}
parsed, err := url.Parse(id)
if err != nil {
log.Debug(fmt.Sprintf("Unable to parse %s as secret ID: %s", id, err.Error()))
return nil
}
url := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
// Trim preceeding and trailing slashes
split := strings.Split(strings.TrimSuffix(strings.TrimPrefix(parsed.Path, "/"), "/"), "/")
if len(split) < 3 {
if len(split) == 2 {
return &secret{
vaultURL: url,
name: split[1],
version: "",
}
}
log.Debug(fmt.Sprintf("Unexpected ID format found for %s, unable to parse as secret ID", id))
return nil
}
return &secret{
vaultURL: url,
name: split[1],
version: split[2],
}
}
58 changes: 58 additions & 0 deletions pkg/azure/keyvault/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,61 @@ func TestResolve_NonSecret(t *testing.T) {
require.EqualError(t, err, "invalid value source: bogus")
})
}

func TestParseKeyValueAsSecretID(t *testing.T) {
tests := []struct {
name string
keyValue string
exp *secret
}{
{
name: "KeyValueValidSecretID",
keyValue: "https://myvaultname.vault.azure.net/secrets/my-secret/b86c2e6ad9054f4abf69cc185b99aa60",
exp: &secret{
vaultURL: "https://myvaultname.vault.azure.net",
name: "my-secret",
version: "b86c2e6ad9054f4abf69cc185b99aa60",
},
},
{
name: "KeyValueDoesNotIncludeVersion",
keyValue: "https://myvaultname.vault.azure.net/secrets/my-secret",
exp: &secret{
vaultURL: "https://myvaultname.vault.azure.net",
name: "my-secret",
version: "",
},
},
{
name: "KeyValueHasEmptyVersion",
keyValue: "https://myvaultname.vault.azure.net/secrets/my-secret/",
exp: &secret{
vaultURL: "https://myvaultname.vault.azure.net",
name: "my-secret",
version: "",
},
},
{
name: "KeyValueMissingSecret",
keyValue: "https://myvaultname.vault.azure.net/secrets/",
exp: nil,
},
{
name: "KeyValueIsInvalidURL",
keyValue: "test:/?not-keyvault",
exp: nil,
},
{
name: "KeyValueIsSecretNameOnly",
keyValue: "my-secret",
exp: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
got := parseID(ctx, test.keyValue)
require.Equal(t, test.exp, got)
})
}
}

0 comments on commit 63869fc

Please sign in to comment.