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

Keep-142 AWS GO SDK Develop #3

Open
wants to merge 13 commits into
base: KEEP-142-aws-go-review
Choose a base branch
from
4 changes: 2 additions & 2 deletions example/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/keeper-security/secrets-manager-go/core v1.6.3 h1:XEHZ8fQ2DFBISK80jWdHmzT56PFqEkXSkakqZxTD8zI=
github.com/keeper-security/secrets-manager-go/core v1.6.3/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
github.com/keeper-security/secrets-manager-go/core v1.6.4 h1:ly2XvAgDxHoHVvFXOIYlxzxBF0yoQir1KfNHUNG4eRA=
github.com/keeper-security/secrets-manager-go/core v1.6.4/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
294 changes: 294 additions & 0 deletions integrations/aws/AWSKeyValueStorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
package awskv

import (
"awskv/aws/logger"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/kms/types"
"github.com/keeper-security/secrets-manager-go/core"
)

type AWSConfig struct {
ClientID string
ClientSecret string
Region string
}

type AWSKeyVaultStorage struct {
configFileLocation string
config map[core.ConfigKey]interface{}
lastSavedConfigHash string
kmsClient *kms.Client
keyARN string
awsConfig *AWSConfig
}

func NewAWSKeyValueStorage(configFileLocation string, KeyARN string, awsSessionConfig *AWSConfig) *AWSKeyVaultStorage {
if configFileLocation == "" {
if envConfigFileLocation, ok := os.LookupEnv("KSM_CONFIG_FILE"); ok {
configFileLocation = envConfigFileLocation
} else {
configFileLocation = core.DEFAULT_CONFIG_PATH
}
}

cfg, err := getConfig(awsSessionConfig)
if err != nil {
return nil
}

if KeyARN == "" {
logger.Errorf("Failed to create client secret credential: %v", err)
return nil
}

client := kms.NewFromConfig(*cfg)
awsDetails := &AWSKeyVaultStorage{
configFileLocation: configFileLocation,
config: make(map[core.ConfigKey]interface{}),
lastSavedConfigHash: "",
kmsClient: client,
keyARN: KeyARN,
awsConfig: awsSessionConfig,
}

keyData, err := awsDetails.getKeyDetails()
if err != nil && keyData.KeyMetadata.KeyUsage != types.KeyUsageTypeEncryptDecrypt {
logger.Errorf("Failed to create client secret credential: %v", err)
return nil
}

err = awsDetails.loadConfig()
if err != nil {
return nil
}
return awsDetails
}

func (a *AWSKeyVaultStorage) loadConfig() error {
var config map[core.ConfigKey]interface{}
var jsonError error
var decryptionError bool
var decryptData []byte

if err := a.createConfigFileIfMissing(); err != nil {
return err
}

contents, err := os.ReadFile(a.configFileLocation)
if err != nil {
logger.Errorf("Failed to load config file %s: %s", a.configFileLocation, err.Error())
return fmt.Errorf("failed to load config file %s", a.configFileLocation)
}

if len(contents) == 0 {
logger.Errorf("Empty config file %s", a.configFileLocation)
contents = []byte("{}")
}

if err := json.Unmarshal(contents, &config); err == nil {
a.config = config
if err := a.saveConfig(config); err != nil {
return err
}

configJson, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

a.lastSavedConfigHash = a.createHash(configJson)
} else {
jsonError = err
}

if jsonError != nil {
keydata, err := a.getKeyDetails()
if err != nil {
}

if keydata.KeyMetadata.KeySpec == types.KeySpecSymmetricDefault {
decryptData, err = decryptSymmetric(a.kmsClient, a.keyARN, contents)
if err != nil {
decryptionError = true
logger.Errorf("Failed to decrypt config file: %s", err.Error())
return fmt.Errorf("failed to decrypt config file %s", a.configFileLocation)
}
} else {
decryptData, err = decryptAsymmetric(a.kmsClient, a.keyARN, contents)
if err != nil {
decryptionError = true
logger.Errorf("Failed to decrypt config file: %s", err.Error())
return fmt.Errorf("failed to decrypt config file %s", a.configFileLocation)
}
}

if err := json.Unmarshal(decryptData, &config); err != nil {
decryptionError = true
logger.Errorf("Failed to parse decrypted config file: %s", err.Error())
return fmt.Errorf("failed to parse decrypted config file %s", a.configFileLocation)
}

a.config = config
a.lastSavedConfigHash = a.createHash(decryptData)
}

if jsonError != nil && decryptionError {
logger.Errorf("Config file is not a valid JSON file: %s", jsonError.Error())
return fmt.Errorf("%s may contain JSON format problems", a.configFileLocation)
}

return nil
}

func (a *AWSKeyVaultStorage) saveConfig(updatedConfig map[core.ConfigKey]interface{}) error {
configJson, err := json.Marshal(a.config)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the comment in code wherever needed like what the method do see here
https://github.com/Keeper-Security/secrets-manager-go/blob/master/core/core.go

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the comments.

if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}

configHash := a.createHash(configJson)
if len(updatedConfig) > 0 {
updatedConfigJson, err := json.Marshal(updatedConfig)
if err != nil {
return fmt.Errorf("failed to marshal updated config: %w", err)
}

updatedConfigHash := a.createHash(updatedConfigJson)
if updatedConfigHash != configHash {
configHash = updatedConfigHash
a.config = make(map[core.ConfigKey]interface{})
for k, v := range updatedConfig {
a.config[k] = fmt.Sprintf("%v", v)
}
}
}

if configHash == a.lastSavedConfigHash {
fmt.Println("Skipped config JSON save. No changes detected.")
return nil
}

if err := a.createConfigFileIfMissing(); err != nil {
return err
}

if err := a.encryptConfig(configJson); err != nil {
return err
}

a.lastSavedConfigHash = configHash
return nil
}

func (a *AWSKeyVaultStorage) createConfigFileIfMissing() error {
if _, err := os.Stat(a.configFileLocation); !os.IsNotExist(err) {
logger.Infof("Config file already exists at: %s", a.configFileLocation)
return nil
}

dir := filepath.Dir(a.configFileLocation)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}

if err := a.encryptConfig([]byte("{}")); err != nil {
return err
}

logger.Infof("Config file created at: %s", a.configFileLocation)
return nil
}

func (a *AWSKeyVaultStorage) getKeyDetails() (*kms.DescribeKeyOutput, error) {
keyDetails, err := a.kmsClient.DescribeKey(context.Background(), &kms.DescribeKeyInput{
KeyId: &a.keyARN,
})

if err != nil {
logger.Errorf("Failed to get key details: %v", err)
return nil, fmt.Errorf("failed to get key details: %w", err)
}

return keyDetails, nil
}

func (a *AWSKeyVaultStorage) createHash(config []byte) string {
hash := md5.Sum(config)
return hex.EncodeToString(hash[:])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we do return hex.EncodeToString(hash)?

}

func getConfig(awsSessionConfig *AWSConfig) (*aws.Config, error) {
if awsSessionConfig.ClientID != "" && awsSessionConfig.ClientSecret != "" && awsSessionConfig.Region != "" {
return &aws.Config{
Credentials: credentials.NewStaticCredentialsProvider(awsSessionConfig.ClientID, awsSessionConfig.ClientSecret, ""),
Region: awsSessionConfig.Region,
}, nil
} else {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to load default config: %w", err)
}
return &cfg, nil
}
}

func (a *AWSKeyVaultStorage) encryptConfig(config []byte) error {
keydata, err := a.getKeyDetails()
if err != nil {
return err
}

var blob []byte
if keydata.KeyMetadata.KeySpec == types.KeySpecSymmetricDefault {
blob, err = encryptSymmetric(a.kmsClient, a.keyARN, config)
if err != nil {
return fmt.Errorf("failed to encrypt config: %w", err)
}
} else {
blob, err = encryptAsymmetric(a.kmsClient, a.keyARN, config)
if err != nil {
return fmt.Errorf("failed to encrypt config: %w", err)
}
}

if err := os.WriteFile(a.configFileLocation, blob, 0644); err != nil {
return fmt.Errorf("failed to write config file %s: %w", a.configFileLocation, err)
}

logger.Debug("Config file created at: ", a.configFileLocation)
return nil
}

func (a *AWSKeyVaultStorage) ChangeKey(newKeyARN string) (bool, error) {
oldKeyARN := a.keyARN
oldKMSClient := a.kmsClient
config, err := getConfig(a.awsConfig)
if err != nil {
return false, fmt.Errorf("failed to get config: %w", err)
}

client := kms.NewFromConfig(*config)
a.kmsClient = client
a.keyARN = newKeyARN
if err := a.saveConfig(a.config); err != nil {
a.kmsClient = oldKMSClient
a.keyARN = oldKeyARN
logger.Errorf("Failed to change the key to '%s' for config '%s': %v", newKeyARN, a.configFileLocation, err)
return false, fmt.Errorf("failed to change the key for %s: %w", a.configFileLocation, err)
}

return true, nil
}
53 changes: 53 additions & 0 deletions integrations/aws/ReadME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
**AWS Key Management**

Protect Secrets Manager connection details with AWS Key Management

Keeper Secrets Manager integrates with AWS Key Management in order to provide protection for Keeper Secrets Manager configuration files. With this integration, you can protect connection details on your machine while taking advantage of Keeper's zero-knowledge encryption of all your secret credentials.
Features

* Encrypt and Decrypt your Keeper Secrets Manager configuration files with AWS Key Management
* Protect against unauthorized access to your Secrets Manager connections
* Requires only minor changes to code for immediate protection. Works with all Keeper Secrets Manager Go-Lang SDK functionality

Prerequisites

* Supports the Go-Lang Secrets Manager SDK.
* Requires AWS packages: aws, config, credentials, kms, kms-types
* Works with just AES/RSA key types with `Encrypt` and `Decrypt` permissions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Work with both Symmetric and Asymmetric(RSA) right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Works with both of the types.


Setup
1. Install Secret-Manager-Go Package

The Secrets Manager AWS package are located in the Keeper Secrets Manager storage package which can be installed using

> `go get github.com/keeper-security/secrets-manager-go/core`
Configure AWS Connection

configuration variables can be provided as

```
import awskv "github.com/keeper-security/secrets-manager-go/awskv"

clientOptions := &ksm.ClientOptions{
Token: "[One Time Access Token]",
Config: awskv.NewAWSKeyValueStorage(<config-file-path-with-its-name>, <key-arn>, &awskv.AWSConfig{
ClientID: "<Some Client ID>",
ClientSecret: "<Some Client Secret>",
Region: "<Cloud Region>",
}),
}
```
The storage will require an AWS credentials if not present it will fetch from environment, as well Secrets Manager configuration which will be encrypted by AWS Key Management.

Provide `ClientID` , `ClientSecret` and `Region` variables.

KeyURL must be like this `arn:<partition>:kms:<region>:<account-id>:key/<key-id>`

For more information about URL see the AWS Key Management Documentation
https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html

You're ready to use the KSM integration 👍

Using the AWS Key Management Integration

Review the SDK usage. Refer to the SDK (documentation) [https://docs.keeper.io/en/privileged-access-manager/secrets-manager/developer-sdk-library/golang-sdk#retrieve-secrets].
Loading