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

Cyberark refactor #96

Merged
merged 14 commits into from
Jan 29, 2025
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

# [v1.8.0] - 2025-01-29

### New Minor Release
- Added support for Cyberark-sourced secrets

# [v1.7.1] - 2023-05-30

### New bugfix release
Expand Down
53 changes: 43 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ You can specify named credentials in the config file under the top-level key `cr
Additionally, you can specify the following environment variables (or specify these key=value pairs in a file called `.env`):

```bash
# These will be loaded as the credential name `default`
AZURE_TENANT_ID=<tenant id>
AZURE_CLIENT_ID=<SPN name including http>
AZURE_CLIENT_SECRET=<SPN password>
```

They will be loaded as the credential name `default`.
# These will be loaded as the credential name `default_cyberark`
CYBERARK_LOGIN=
CYBERARK_API_KEY=
CYBERARK_ACCOUNT=
CYBERARK_APPLIANCE_URL=
```

# Config

Expand Down Expand Up @@ -46,28 +51,33 @@ workers:

## Credentials
The `credentials` section is a list of one or more named credentials used for fetching resources. Each
credential has a `tenantID`, `clientID`, `clientSecret`.
credential has either:

1. a `tenantID`, `clientID`, and `clientSecret`
1. a `login`, `apiKey`, `account`, and `applianceUrl`

The ENV vars (or .env file) will be injected
as a credential with the name `default` if you don't override `default` within your config file.
as a credential with the name `default` (or `default_cyberark`) if you don't override `default` (or `default_cyberark`) within your config file.

## Resources

The `resources` section is a list of one or more resources to fetch. Each resource has a `kind`, `vaultBaseURL`,
and optional `credential` field.

Valid kinds are: `cert`, `secret`, `all-secrets`, and `key`.
Valid kinds for KeyVault resources are: `cert`, `secret`, `all-secrets`, and `key`.

Note: The `all-secrets` fetches all of the secrets found in the vault, and cannot be used in conjunction with any specific secrets for the same vaultBaseURL
Valid kinds for Cyberark resources are: `cyberark-secret` and `all-cyberark-secrets`.

Unless a resource has a `kind` of `all-secrets`, there is also a required `name` field for the resource.
Note: The `all-secrets` and `all-cyberark-secrets` kinds fetch all of the secrets found in the vault, and cannot be used in conjunction with any specific secrets for the same vault

If you don't specify `credential`, a credential with the name `default` will be used (you can either
specify the `default` credential in the `credentials` array, or as ENV vars / .env file)
Unless a resource has a `kind` of `all-secrets` or `all-cyberark-secrets`, there is also a required `name` field for the resource.

If you don't specify `credential`, a credential with the name `default` will be used for AKV resources (you can either
specify the `default` credential in the `credentials` array, or as ENV vars / .env file). For Cyberark resources, the default credential is `default_cyberark`

### Aliases

A resource with a kind set to `cert`, `secret`, or `key` may specify an alias. This alias may be used to reference the resource in your specified `sink`:
A resource with a kind set to `cert`, `secret`, `cyberark-secret` or `key` may specify an alias. This alias may be used to reference the resource in your specified `sink`:

```yaml
workers:
Expand Down Expand Up @@ -294,6 +304,29 @@ workers:
template: "{{ .Secrets.thing1.Value }}{{ .Secrets.thing2.Value }}{{ .Secrets.thing3.Value }}"
```

### Example Cyberark Config

```yaml
credentials:
-
name: cyberark_test
login: D-AppA-POC-Workload
apiKey: abcde1234567890
account: conjur
applianceURL: https://example.cyberark.cloud/api
resources:
- kind: cyberark-secret
name: 'Operating System-SelfManaged-dummy-foo/password'
chrisjohnson marked this conversation as resolved.
Show resolved Hide resolved
credential: cyberark_test
safeName: D-AppA
version: 1
alias: password
```

Notes:
* the `login` attribute will have `host/data/` prepended to it, to simplify configuration
* secrets are identified by a path. The secret name provided in config will be interpolated with `"data/vault/%s/%s", safeName, secretName`, so the prefix need not be listed for every secret. Simliarly, this prefix and `safeName` will be stripped when retrieving all secrets

# Workers

Workers default to working in a loop, whose frequency is controlled by the `frequency` field in your config. Each iteration of the loop, the worker performs the following:
Expand Down
66 changes: 0 additions & 66 deletions certs/certs.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package certs

import (
"context"
"encoding/base64"
"fmt"
log "github.com/sirupsen/logrus"
"net/url"
"regexp"

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
)
Expand All @@ -16,64 +11,3 @@ type Cert keyvault.CertificateBundle
func (c Cert) String() string {
return base64.StdEncoding.EncodeToString(*c.Cer)
}

func GetCert(client keyvault.BaseClient, vaultBaseURL string, certName string, certVersion string) (Cert, error) {
cert, err := client.GetCertificate(context.Background(), vaultBaseURL, certName, certVersion)
if err != nil {
log.Printf("Error getting cert: %v", err.Error())
return Cert{}, err
}

return Cert(cert), nil
}

func GetCertByURL(client keyvault.BaseClient, certURL string) (Cert, error) {
u, err := url.Parse(certURL)
if err != nil {
log.Printf("Failed to parse URL for cert: %v", err.Error())
return Cert{}, err
}
vaultBaseURL := fmt.Sprintf("%v://%v", u.Scheme, u.Host)

regex := *regexp.MustCompile(`/certificates/(.*)(/.*)?`)
res := regex.FindAllStringSubmatch(u.Path, -1)
certName := res[0][1]

result, err := GetCert(client, vaultBaseURL, certName, "")
if err != nil {
log.Printf("Failed to get cert from parsed values %v and %v: %v", vaultBaseURL, certName, err.Error())
return Cert{}, err
}

return result, nil
}

func GetCerts(client keyvault.BaseClient, vaultBaseURL string) (results []Cert, err error) {
max := int32(25)
pages, err := client.GetCertificates(context.Background(), vaultBaseURL, &max)
if err != nil {
log.Printf("Error getting cert: %v", err.Error())
return nil, err
}

for {
for _, value := range pages.Values() {
certURL := *value.ID
cert, err := GetCertByURL(client, certURL)
if err != nil {
log.Printf("Error loading cert contents: %v", err.Error())
return nil, err
}

results = append(results, cert)
}

if pages.NotDone() {
pages.Next()
} else {
break
}
}

return results, nil
}
28 changes: 12 additions & 16 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
package client

import (
"fmt"

"github.com/covermymeds/azure-key-vault-agent/config"
"github.com/covermymeds/azure-key-vault-agent/iam"

"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
"github.com/covermymeds/azure-key-vault-agent/certs"
"github.com/covermymeds/azure-key-vault-agent/keys"
"github.com/covermymeds/azure-key-vault-agent/secrets"
)

type Clients map[string]keyvault.BaseClient

func NewClient(cred config.CredentialConfig) keyvault.BaseClient {
client := keyvault.New()
authorizer, err := iam.GetKeyvaultAuthorizer(cred.TenantID, cred.ClientID, cred.ClientSecret)
if err != nil {
panic(fmt.Sprintf("Error authorizing: %v", err.Error()))
}
client.Authorizer = authorizer
return client
type Client interface {
GetCert(vault string, certName string, certVersion string) (certs.Cert, error)
GetCerts(vault string) (results []certs.Cert, err error)
GetSecret(vault string, secretName string, secretVersion string) (secrets.Secret, error)
GetSecrets(vault string) (results map[string]secrets.Secret, err error)
GetKey(vault string, keyName string, keyVersion string) (keys.Key, error)
GetKeys(vault string) (results []keys.Key, err error)
}

type Clients map[string]Client
113 changes: 113 additions & 0 deletions client/cyberarkclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package client

import (
"fmt"
"strconv"
"strings"

"github.com/covermymeds/azure-key-vault-agent/certs"
"github.com/covermymeds/azure-key-vault-agent/config"
"github.com/covermymeds/azure-key-vault-agent/keys"
"github.com/covermymeds/azure-key-vault-agent/secrets"
log "github.com/sirupsen/logrus"

"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/cyberark/conjur-api-go/conjurapi/authn"
)

type CyberarkClient struct {
Client *conjurapi.Client
Safe string
}

func NewCyberarkClient(cred config.CyberarkCredentialConfig) CyberarkClient {
cyberarkConfig := conjurapi.Config{
Account: cred.Account,
ApplianceURL: cred.ApplianceURL,
}

cyberarkClient, err := conjurapi.NewClientFromKey(cyberarkConfig,
authn.LoginPair{
Login: fmt.Sprintf("host/data/%s", cred.Login),
APIKey: cred.ApiKey,
},
)
if err != nil {
panic(fmt.Sprintf("Error creating Cyberark client: %v", err.Error()))
}
return CyberarkClient{Client: cyberarkClient}
}

func (c CyberarkClient) GetCert(safeName string, certName string, certVersion string) (certs.Cert, error) {
panic("cyberark doesn't have Cert type resources. use regular Secrets instead")
}

func (c CyberarkClient) GetCerts(safeName string) (results []certs.Cert, err error) {
panic("cyberark doesn't have Cert type resources. use regular Secrets instead")
}

func (c CyberarkClient) GetSecret(safeName string, secretName string, secretVersion string) (secrets.Secret, error) {
var secretValue []byte
var err error

secretPath := fmt.Sprintf("data/vault/%s/%s", safeName, secretName)

if secretVersion == "" {
secretValue, err = c.Client.RetrieveSecret(secretPath)
} else {
secretVersionInt, convErr := strconv.Atoi(secretVersion)
if convErr != nil {
return secrets.Secret{}, fmt.Errorf("failed to convert secret version to integer: %s", secretVersion)
}
secretValue, err = c.Client.RetrieveSecretWithVersion(secretPath, secretVersionInt)
}
if err != nil {
log.Printf("Error getting secret: %v", err.Error())
return secrets.Secret{}, err
}

secretValueString := string(secretValue)
result := secrets.Secret{
Value: &secretValueString,
ContentType: nil,
}

return result, nil
}

func (c CyberarkClient) GetSecrets(safeName string) (results map[string]secrets.Secret, err error) {
resources, err := c.Client.ResourceIDs(&conjurapi.ResourceFilter{Kind: "variable"})
if err != nil {
log.Printf("Error getting secrets: %v", err.Error())
return map[string]secrets.Secret{}, err
}

secretValues, err := c.Client.RetrieveBatchSecrets(resources)
if err != nil {
log.Printf("Error getting secrets: %v", err.Error())
return map[string]secrets.Secret{}, err
}

results = make(map[string]secrets.Secret)

for resourceID, value := range secretValues {
modResourceID := strings.Replace(resourceID, fmt.Sprintf("conjur:variable:data/vault/%s/", safeName), "", 1)
secretValueString := string(value)
result := secrets.Secret{
Value: &secretValueString,
ContentType: nil,
}

results[modResourceID] = result
}

return results, nil
}

func (c CyberarkClient) GetKey(safeName string, keyName string, keyVersion string) (keys.Key, error) {
panic("cyberark does not have a Key secret type. use regular Secrets instead")
}

func (c CyberarkClient) GetKeys(safeName string) ([]keys.Key, error) {
panic("cyberark does not have a Key secret type. use regular Secrets instead")
}
Loading