Skip to content

Commit

Permalink
Added Keeper Secrets Manager provider (#203)
Browse files Browse the repository at this point in the history
Co-authored-by: Dotan J. Nahum <[email protected]>
  • Loading branch information
idimov-keeper and jondot authored Jan 21, 2024
1 parent 4aadf04 commit 01e94e1
Show file tree
Hide file tree
Showing 166 changed files with 9,069 additions and 1,009 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mocks:
mockgen -source pkg/providers/gopass.go -destination pkg/providers/mock_providers/gopass_mock.go
mockgen -source pkg/providers/github.go -destination pkg/providers/mock_providers/github_mock.go
mockgen -source pkg/providers/azure_keyvault.go -destination pkg/providers/mock_providers/azure_keyvault_mock.go
mockgen -source pkg/providers/keeper_secretsmanager.go -destination pkg/providers/mock_providers/keeper_secretsmanager_mock.go
readme:
yarn readme
lint:
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,37 @@ providers:
path: bar
```

## Keeper Secrets Manager

### Authentication

Configuration is environment based - you should populate `KSM_CONFIG=base64_config` or `KSM_CONFIG_FILE=ksm_config.json` in your environment with a valid [Secrets Manager Configuration](https://docs.keeper.io/secrets-manager/secrets-manager/about/secrets-manager-configuration#creating-a-secrets-manager-configuration). If both environment variables are set then `KSM_CONFIG` is used.

Note:
- _Secrets Manager CLI configuration file format (INI) is different from KSM SDK configuration format (JSON) but the CLI [command](https://docs.keeper.io/secrets-manager/secrets-manager/secrets-manager-command-line-interface/profile-command#export) `ksm profile export profile_name` can be used to export some of the individual profiles into JSON config file compatible with the provider._

### Features

- Sync - `yes`
- Mapping - `yes`
- Modes - `read`
- Key format
- `env_sync` - path is single record UID. Field labels (if empty -> field types) are used as keys, any duplicates will have a numeric suffix.
- `env` - any string, conforming to Keeper [Notation](https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation) _(`keeper://` prefix not required)_

### Example Config

```yaml
keeper_secretsmanager:
env_sync:
path: ABCDEFGHIJKLMNOPQRSTUV
env:
PGUSER:
path: ABCDEFGHIJKLMNOPQRSTUV/field/login
CERT:
path: ABCDEFGHIJKLMNOPQRSTUV/custom_field/ssl_cert
```

# Semantics

## Addressing
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/keeper-security/secrets-manager-go/core v1.6.2
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
Expand All @@ -140,7 +141,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sosedoff/ansible-vault-go v0.2.0 // indirect
github.com/sosedoff/ansible-vault-go v0.2.0
github.com/spf13/cobra v0.0.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/twpayne/go-pinentry v0.2.0 // indirect
Expand All @@ -160,7 +161,6 @@ require (
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/grpc v1.35.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
Expand Down
17 changes: 3 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keeper-security/secrets-manager-go/core v1.6.2 h1:bRZUJI/s5WwVbceSNlKyKqYuBNKkZCyNPH4lU2GYiF0=
github.com/keeper-security/secrets-manager-go/core v1.6.2/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand Down Expand Up @@ -935,8 +937,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -976,7 +976,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -1025,8 +1025,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
Expand Down Expand Up @@ -1138,14 +1136,10 @@ golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand All @@ -1157,8 +1151,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down Expand Up @@ -1227,10 +1219,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
234 changes: 234 additions & 0 deletions pkg/providers/keeper_secretsmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package providers

import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"

ksm "github.com/keeper-security/secrets-manager-go/core"
"github.com/spectralops/teller/pkg/core"
"github.com/spectralops/teller/pkg/logging"
)

type KsmClient interface {
GetSecret(p core.KeyPath) (*core.EnvEntry, error)
GetSecrets(p core.KeyPath) ([]core.EnvEntry, error)
}

type KeeperSecretsManager struct {
client KsmClient
logger logging.Logger
}

const keeperName = "keeper_secretsmanager"

//nolint
func init() {
metaInto := core.MetaInfo{
Description: "Keeper Secrets Manager",
Name: keeperName,
Authentication: "You should populate `KSM_CONFIG=Base64ConfigString or KSM_CONFIG_FILE=ksm_config.json` in your environment.",
ConfigTemplate: `
keeper_secretsmanager:
env_sync:
path: record_uid
env:
FOO_BAR:
path: UID/custom_field/foo_bar
# use Keeper Notation to pull data from typed records
`,
Ops: core.OpMatrix{Get: true, GetMapping: true},
}
RegisterProvider(metaInto, NewKeeperSecretsManager)
}

type SecretsManagerClient struct {
sm *ksm.SecretsManager
}

func (c SecretsManagerClient) GetSecret(p core.KeyPath) (*core.EnvEntry, error) {
nr, err := c.sm.GetNotationResults(p.Path)
if err != nil {
return nil, err
}

ent := core.EnvEntry{}
if len(nr) > 0 {
ent = p.Found(nr[0])
} else {
ent = p.Missing()
}

return &ent, nil
}

func (c SecretsManagerClient) GetSecrets(p core.KeyPath) ([]core.EnvEntry, error) {
// p.Path must be a record UID
// TODO: Add Path = folderUID... expect too many dulpicates, prefix key name with RUID?
type KeyValuePair struct {
key, value string
}

r := []core.EnvEntry{}

recs, err := c.sm.GetSecrets([]string{p.Path})
if err != nil {
return nil, err
}
if len(recs) < 1 {
return r, nil
}

entries := []KeyValuePair{}
rec := recs[0]
fields := rec.GetFieldsBySection(ksm.FieldSectionBoth)
for _, field := range fields {
fmap, ok := field.(map[string]interface{})
if !ok {
continue
}

iValues, ok := fmap["value"].([]interface{})
if !ok || len(iValues) < 1 {
continue
}

value := extractValue(iValues)
if value == "" {
continue
}

key := extractKey(fmap)
entries = append(entries, KeyValuePair{key: key, value: value})
}

// avoid duplicate key names
keymap := map[string]struct{}{}
for _, e := range entries {
key := e.key
if _, found := keymap[e.key]; found {
n := 1
for {
n++
mkey := key + "_" + strconv.Itoa(n)
if _, found := keymap[mkey]; !found {
key = mkey
break
}
}
}
keymap[key] = struct{}{}
ent := p.FoundWithKey(key, e.value)
r = append(r, ent)
}

return r, nil
}

func extractKey(fieldMap map[string]interface{}) string {
key := ""
if fLabel, ok := fieldMap["label"].(string); ok {
key = strings.TrimSpace(fLabel)
}
if key == "" {
if fType, ok := fieldMap["type"].(string); ok {
key = strings.TrimSpace(fType)
}
}
key = strings.ReplaceAll(key, " ", "_")
return key
}

func extractValue(iValues []interface{}) string {
value := ""
_, isArray := iValues[0].([]interface{})
_, isObject := iValues[0].(map[string]interface{})
isJSON := len(iValues) > 1 || isArray || isObject
if isJSON {
if len(iValues) == 1 {
if val, err := json.Marshal(iValues[0]); err == nil {
value = string(val)
}
} else if val, err := json.Marshal(iValues); err == nil {
value = string(val)
}
} else {
val := iValues[0]
// JavaScript number type, IEEE754 double precision float
if fval, ok := val.(float64); ok && fval == float64(int(fval)) {
val = int(fval) // convert to int
}
value = fmt.Sprintf("%v", val)
}
return value
}

func NewKsmClient() (KsmClient, error) {
config := os.Getenv("KSM_CONFIG")
configPath := os.Getenv("KSM_CONFIG_FILE")
if config == "" && configPath == "" {
return nil, fmt.Errorf("cannot find KSM_CONFIG or KSM_CONFIG_FILE for %s", keeperName)
}

// with both options present KSM_CONFIG overrides KSM_CONFIG_FILE
var options *ksm.ClientOptions = nil
if config != "" {
options = &ksm.ClientOptions{Config: ksm.NewMemoryKeyValueStorage(config)}
} else if stat, err := os.Stat(configPath); err == nil && stat.Size() > 2 {
options = &ksm.ClientOptions{Config: ksm.NewFileKeyValueStorage(configPath)}
}

if options == nil {
return nil, fmt.Errorf("failed to initialize KSM Client Options")
}

sm := ksm.NewSecretsManager(options)
if sm == nil {
return nil, fmt.Errorf("failed to initialize KSM Client")
}

return SecretsManagerClient{
sm: sm,
}, nil
}

func NewKeeperSecretsManager(logger logging.Logger) (core.Provider, error) {
ksmClient, err := NewKsmClient()
if err != nil {
return nil, err
}
return &KeeperSecretsManager{
client: ksmClient,
logger: logger,
}, nil
}

func (k *KeeperSecretsManager) Name() string {
return keeperName
}

func (k *KeeperSecretsManager) Get(p core.KeyPath) (*core.EnvEntry, error) {
return k.client.GetSecret(p)
}

func (k *KeeperSecretsManager) GetMapping(p core.KeyPath) ([]core.EnvEntry, error) {
return k.client.GetSecrets(p)
}

func (k *KeeperSecretsManager) Put(p core.KeyPath, val string) error {
return fmt.Errorf("provider %q does not implement write yet", k.Name())
}

func (k *KeeperSecretsManager) PutMapping(p core.KeyPath, m map[string]string) error {
return fmt.Errorf("provider %q does not implement write mapping yet", k.Name())
}

func (k *KeeperSecretsManager) Delete(kp core.KeyPath) error {
return fmt.Errorf("provider %s does not implement delete yet", k.Name())
}

func (k *KeeperSecretsManager) DeleteMapping(kp core.KeyPath) error {
return fmt.Errorf("provider %s does not implement delete mapping yet", k.Name())
}
Loading

0 comments on commit 01e94e1

Please sign in to comment.