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 139 azure go review #5

Open
wants to merge 27 commits into
base: metron-develop-main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7ef8f3d
Azure Go-lang Key Vault
ayusha-metron Feb 7, 2025
38167c5
Added encrytion and decryptiop code
ayusha-metron Feb 10, 2025
25741ce
Add Azure key Value storgae
ayusha-metron Feb 11, 2025
01acdc0
Refactor changes
ayusha-metron Feb 13, 2025
b0a3eac
Update Key storage functions
ayusha-metron Feb 17, 2025
2fbd1c4
Remove extra code
ayusha-metron Feb 18, 2025
8d05063
Added comments suggestion
ayusha-metron Feb 18, 2025
42365ee
Change marshalindent to marsha
ayusha-metron Feb 18, 2025
dbfea4a
Update code
ayusha-metron Feb 18, 2025
aed817f
Removed extra code
ayusha-metron Feb 18, 2025
d5c4827
Function rename
ayusha-metron Feb 18, 2025
40465b1
Update DeleteALL storage function
ayusha-metron Feb 18, 2025
bb2f6d8
Update looger
ayusha-metron Feb 20, 2025
7531c8f
Update changeKey function
ayusha-metron Feb 20, 2025
0ef1c52
Modify asymmetric encryption and decryption code
ayusha-metron Feb 27, 2025
82b15a6
Update log in storage functions
ayusha-metron Feb 27, 2025
cf937d3
Added more comments
ayusha-metron Feb 27, 2025
f12298a
Added check case
ayusha-metron Feb 28, 2025
4f46080
Merge pull request #2 from metron-labs/KEEP-139-azure-go-develop
ayusha-metron Feb 28, 2025
2118b48
Update save config function
ayusha-metron Mar 3, 2025
cf65a69
modified mod file
ayusha-metron Mar 3, 2025
5ac3808
Update mod file
ayusha-metron Mar 3, 2025
dd0d6fe
Update dependency
ayusha-metron Mar 3, 2025
b574cfa
Logs update
ayusha-metron Mar 3, 2025
25c3d14
Mod file update
ayusha-metron Mar 3, 2025
c6bdfa7
New mod file inside azure
ayusha-metron Mar 3, 2025
182e2b2
Merge pull request #7 from metron-labs/keep-139-change-save-config
ayusha-metron Mar 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=
333 changes: 333 additions & 0 deletions integrations/azure/AzureKeyValueStorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
// -*- coding: utf-8 -*-
// _ __
// | |/ /___ ___ _ __ ___ _ _ (R)
// | ' </ -_) -_) '_ \/ -_) '_|
// |_|\_\___\___| .__/\___|_|
// |_|

// Keeper Secrets Manager
// Copyright 2025 Keeper Security Inc.
// Contact: [email protected]
package azurekv

import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
"github.com/keeper-security/secrets-manager-go/core"
"github.com/keeper-security/secrets-manager-go/integrations/azurekv/logger"
)

type AzureConfig struct {
TenantID string
ClientID string
ClientSecret string
KeyURL string
}

type AzureKeyValueStorage struct {
configFileLocation string
config map[core.ConfigKey]interface{}
lastSavedConfigHash string
cryptoClient *azkeys.Client
keyName string
keyVersion string
azureConfig *AzureConfig
}

// Creates a new instance of AzureKeyValueStorage.
func NewAzureKeyValueStorage(configFileLocation string, azSessionConfig *AzureConfig) *AzureKeyValueStorage {
if configFileLocation == "" {
if envConfigFileLocation, ok := os.LookupEnv("KSM_CONFIG_FILE"); ok {
configFileLocation = envConfigFileLocation
} else {
configFileLocation = core.DEFAULT_CONFIG_PATH
}
}

credential, err := fetchCredentials(azSessionConfig)
if err != nil {
return nil
}

baseURL, keyName, keyVersion, err := fetchKeyDetails(azSessionConfig.KeyURL)
if err != nil {
return nil
}

// Create a new Azure Key Vault client.
client, err := azkeys.NewClient(baseURL, credential, nil)
if err != nil {
logger.Errorf("Failed to create Azure Key Vault client: %v", err)
return nil
}

azureDetails := &AzureKeyValueStorage{
configFileLocation: configFileLocation,
config: make(map[core.ConfigKey]interface{}),
lastSavedConfigHash: "",
cryptoClient: client,
keyName: keyName,
keyVersion: keyVersion,
azureConfig: azSessionConfig,
}

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

return azureDetails
}

// Loads the decrypted configuration from the config file if encrypted config is present, else encrypts the config.
func (s *AzureKeyValueStorage) loadConfig() error {
var config map[core.ConfigKey]interface{}
var jsonError error
var decryptionError bool

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

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

if len(contents) == 0 {
logger.Errorf("Config file is empty %s", s.configFileLocation)
contents = []byte("{}")
}

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

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

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

if jsonError != nil {
configJson, err := decryptBuffer(s.cryptoClient, s.keyName, s.keyVersion, 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", s.configFileLocation)
}

if err := json.Unmarshal(configJson, &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", s.configFileLocation)
}

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

s.lastSavedConfigHash = s.createHash(configJsonBytes)
}

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", s.configFileLocation)
}

return nil
}

// Saves the encrypted updated configuration to the config file and updates the hash of the config.
func (s *AzureKeyValueStorage) saveConfig(updatedConfig map[core.ConfigKey]interface{}, force bool) error {
config := s.config
if config == nil {
config = make(map[core.ConfigKey]interface{})
}

configJson, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal current config: %w", err)
}
configHash := s.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 := s.createHash(updatedConfigJson)
if updatedConfigHash != configHash {
configHash = updatedConfigHash
s.config = make(map[core.ConfigKey]interface{})
for k, v := range updatedConfig {
s.config[k] = fmt.Sprintf("%v", v)
}
}
}

if !force && configHash == s.lastSavedConfigHash {
logger.Info("Skipped config JSON save. No changes detected.")
return nil
}

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

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

s.lastSavedConfigHash = configHash
return nil
}

// Creates the config file if does not exist and encrypts it.
func (s *AzureKeyValueStorage) createConfigFileIfMissing() error {
if _, err := os.Stat(s.configFileLocation); !os.IsNotExist(err) {
logger.Infof("Config file already exists at: %s", s.configFileLocation)
return nil
}

dir := filepath.Dir(s.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 := s.encryptConfig([]byte("{}")); err != nil {
return err
}

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

// creates an MD5 hash of the provided config data.
func (s *AzureKeyValueStorage) createHash(data []byte) string {
hash := md5.Sum(data)
return hex.EncodeToString(hash[:])
}

func fetchCredentials(azSessionConfig *AzureConfig) (azcore.TokenCredential, error) {
var secretCredentials azcore.TokenCredential
var err error
if azSessionConfig != nil && azSessionConfig.TenantID != "" && azSessionConfig.ClientID != "" && azSessionConfig.ClientSecret != "" {
secretCredentials, err = azidentity.NewClientSecretCredential(azSessionConfig.TenantID, azSessionConfig.ClientID, azSessionConfig.ClientSecret, nil)
if err != nil {
return nil, fmt.Errorf("failed to create client secret credential: %w", err)
}
} else {
secretCredentials, err = azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, fmt.Errorf("failed to create default Azure credential: %w", err)
}
}
return secretCredentials, nil
}

func (s *AzureKeyValueStorage) encryptConfig(config []byte) error {
var blob []byte
var err error

if config == nil {
blob, err = encryptBuffer(s.cryptoClient, s.keyName, s.keyVersion, []byte("{}"))
if err != nil {
return fmt.Errorf("failed to encrypt empty configuration: %w", err)
}
} else {
blob, err = encryptBuffer(s.cryptoClient, s.keyName, s.keyVersion, config)
if err != nil {
return fmt.Errorf("failed to encrypt configuration: %w", err)
}
}

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

func fetchKeyDetails(keyURL string) (string, string, string, error) {
if keyURL == "" {
return "", "", "", fmt.Errorf("key URL is empty")
}

parsedURL, err := url.Parse(keyURL)
if err != nil {
return "", "", "", fmt.Errorf("failed to parse key URL: %v", err)
}
pathSegments := strings.Split(strings.Trim(parsedURL.Path, "/"), "/")
if len(pathSegments) < 3 {
return "", "", "", fmt.Errorf("invalid key URL format")
}
vaultURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
keyName := pathSegments[1]
keyVersion := pathSegments[2]
return vaultURL, keyName, keyVersion, nil
}

// Changes the key used to encrypt/decrypt the configuration.
func (s *AzureKeyValueStorage) ChangeKey(newKeyURL string) (bool, error) {
oldState := struct {
vaultURL, keyName, keyVersion string
cryptoClient *azkeys.Client
}{
s.azureConfig.KeyURL, s.keyName, s.keyVersion, s.cryptoClient,
}

// Extract the key details like vaultURL, keyname and keyversion from the new key URL `https://<vault-name>.vault.azure.net/keys/<key-name>/<version>`
vaultURL, keyName, keyVersion, err := fetchKeyDetails(newKeyURL)
if err != nil {
logger.Errorf("Failed to extract key details from URL '%s': %v", newKeyURL, err)
return false, fmt.Errorf("failed to extract key details from URL '%s': %w", newKeyURL, err)
}

s.azureConfig.KeyURL = newKeyURL
s.keyName = keyName
s.keyVersion = keyVersion

cred, err := fetchCredentials(s.azureConfig)
if err != nil {
return false, err
}

client, err := azkeys.NewClient(vaultURL, cred, nil)
if err != nil {
return false, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
}

s.cryptoClient = client
if err := s.saveConfig(s.config, true); err != nil {
s.azureConfig.KeyURL = oldState.vaultURL
s.keyName = oldState.keyName
s.keyVersion = oldState.keyVersion
s.cryptoClient = oldState.cryptoClient
logger.Errorf("Failed to change the key to '%s' for config '%s': %v", newKeyURL, s.configFileLocation, err)
return false, fmt.Errorf("failed to change the key for %s: %w", s.configFileLocation, err)
}

return true, nil
}
59 changes: 59 additions & 0 deletions integrations/azure/ReadME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
**Azure Key Vault**

Protect Secrets Manager connection details with Azure Key Vault

Keeper Secrets Manager integrates with Azure Key Vault 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 Azure Key Vault
* 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 Azure packages: azure-identity and azure-keyvault-client.
* Works with just RSA key types with `WrapKey` and `UnWrapKey` permissions.

Setup
1. Install Secret-Manager-Go Package

The Secrets Manager azure 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 Azure Connection

configuration variables can be provided as

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

clientOptions := &ksm.ClientOptions{
Token: "[One Time Access Token]",
Config: azurekv.NewAzureKeyValueStorage("ksm-config.json", &azurekv.AzureConfig{
TenantID: "<Some Tenant ID>",
ClientID: "<Some Client ID>",
ClientSecret: "<Some Client Secret>",
KeyURL: "<Key URL>",
}),
}
```
The storage will require an Azure Key URL, as well Secrets Manager configuration which will be encrypted by Azure Key Vault.

Provide `TenantID` , `ClientID` , `ClientSecret` and `KeyURL` variables.

KeyURL must be like this `https://<vault-name>.vault.azure.net/keys/<key-name>/<version>`

For more information about URL see the Azure Documentation
https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-identifiers

You will need an Azure App directory App to use the Azure Key Vault integration.

For more information on Azure App Directory App registration and Permissions see the Azure documentation: https://learn.microsoft.com/en-us/azure/key-vault/general/authentication

You're ready to use the KSM integration 👍

Using the Azure Key Vault 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