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] - ?

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

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

### New bugfix release
Expand Down
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ 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=
CYBERARK_SAFE=
```

# Config

Expand Down Expand Up @@ -46,10 +52,13 @@ 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`, `applianceUrl`, and `safe`

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

Expand Down Expand Up @@ -294,6 +303,32 @@ 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
safe: D-AppA
-
resources:
- kind: secret
name: 'Operating System-SelfManaged-dummy-foo/password'
chrisjohnson marked this conversation as resolved.
Show resolved Hide resolved
vaultBaseURL: https://example.cyberark.cloud/api
credential: cyberark_test
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", c.Safe, secretName`, so the prefix and safe need not be listed for every secret. Simliarly, this prefix will be stripped when retrieving all secrets
* `vaultBaseURL` needs to be present and a valid URL, but is not used for cyberark secrets. It was left to maintain non-breaking config for AKV-sourced 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
}
27 changes: 11 additions & 16 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
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(vaultBaseURL string, certName string, certVersion string) (certs.Cert, error)
GetCerts(vaultBaseURL string) (results []certs.Cert, err error)
GetSecret(vaultBaseURL string, secretName string, secretVersion string) (secrets.Secret, error)
GetSecrets(vaultBaseURL string) (results map[string]secrets.Secret, err error)
GetKey(vaultBaseURL string, keyName string, keyVersion string) (keys.Key, error)
drew-valentine-cmm marked this conversation as resolved.
Show resolved Hide resolved
}

type Clients map[string]Client
109 changes: 109 additions & 0 deletions client/cyberarkclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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, Safe: cred.Safe}
}

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

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

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

secretPath := fmt.Sprintf("data/vault/%s/%s", c.Safe, 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(vaultBaseURL 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/", c.Safe), "", 1)
secretValueString := string(value)
result := secrets.Secret{
Value: &secretValueString,
ContentType: nil,
}

results[modResourceID] = result
}

return results, nil
}

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