From eb235412de6a682b4f511522202f606f1f2d7f90 Mon Sep 17 00:00:00 2001 From: Drum Ogilvie Date: Thu, 21 Aug 2025 14:03:52 +0100 Subject: [PATCH 1/2] feat(azkv): Skipping key-version will get latest key This is a supported behaviour for Azure Key Vault, and is very helpful when it comes to doing rotation of those keys, as updated versions will automatically get fetched by sops for new files with no changes to the config in the repo. This brings AZKV in line with sops' AWS/GCP integrations, which do not require you to specify a key version. Thank you to @felixfontein for some RST syntax help. Co-authored-by: Felix Fontein Signed-off-by: Drum Ogilvie --- README.rst | 9 +++++++++ azkv/keysource.go | 45 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7df57afad0..7828ae88b9 100644 --- a/README.rst +++ b/README.rst @@ -378,6 +378,11 @@ a key. This has the following form:: https://${VAULT_URL}/keys/${KEY_NAME}/${KEY_VERSION} +You can omit the version, and have just a trailing slash, and this will use +whatever the latest version of the key is:: + + https://${VAULT_URL}/keys/${KEY_NAME}/ + To create a Key Vault and assign your service principal permissions on it from the commandline: @@ -401,6 +406,10 @@ Now you can encrypt a file using:: $ sops encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/some-string test.yaml > test.enc.yaml +or, without the version:: + + $ sops encrypt --azure-kv https://sops.vault.azure.net/keys/sops-key/ test.yaml > test.enc.yaml + And decrypt it using:: $ sops decrypt test.enc.yaml diff --git a/azkv/keysource.go b/azkv/keysource.go index 97761f5290..52a8c46c0b 100644 --- a/azkv/keysource.go +++ b/azkv/keysource.go @@ -79,12 +79,21 @@ func NewMasterKey(vaultURL string, keyName string, keyVersion string) *MasterKey // MasterKey. The URL format is {vaultUrl}/keys/{keyName}/{keyVersion}. func NewMasterKeyFromURL(url string) (*MasterKey, error) { url = strings.TrimSpace(url) - re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)/([^/]+)$") + re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)(/[^/]+)?$") parts := re.FindStringSubmatch(url) if len(parts) < 3 { - return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey", url) + return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey %v", url, parts) } - return NewMasterKey(parts[1], parts[2], parts[3]), nil + // Blank key versions are supported in Azure Key Vault, as they default to the latest + // version of the key. We need to put the actual version in the sops metadata block though + var key *MasterKey + if len(parts[3]) > 1 { + key = NewMasterKey(parts[1], parts[2], parts[3][1:]) + } else { + key = NewMasterKey(parts[1], parts[2], "") + } + err := key.ensureKeyHasVersion(context.Background()) + return key, err } // MasterKeysFromURLs takes a comma separated list of Azure Key Vault URLs, @@ -145,6 +154,36 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { return key.EncryptContext(context.Background(), dataKey) } +func (key *MasterKey) ensureKeyHasVersion(ctx context.Context) error { + if (key.Version != "") { + // Nothing to do + return nil + } + + token, err := key.getTokenCredential() + + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to get Azure token credential to retrieve key version: %w", err) + } + + c, err := azkeys.NewClient(key.VaultURL, token, key.clientOptions) + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to construct Azure Key Vault client to retrieve key version: %w", err) + } + + kdetail, err := c.GetKey(ctx, key.Name, key.Version, nil) + if err != nil { + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Encryption failed") + return fmt.Errorf("failed to fetch Azure Key to retrieve key version: %w", err) + } + key.Version = kdetail.Key.KID.Version() + + log.WithFields(logrus.Fields{"key": key.Name, "version": key.Version}).Info("Version fetch succeeded") + return nil +} + // EncryptContext takes a SOPS data key, encrypts it with Azure Key Vault, and stores // the result in the EncryptedKey field. func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error { From b9ee23844a29116371d9085e32cd244606e5b154 Mon Sep 17 00:00:00 2001 From: Drum Ogilvie Date: Tue, 9 Sep 2025 13:26:51 +0100 Subject: [PATCH 2/2] Update azkv/keysource.go Co-authored-by: Felix Fontein Signed-off-by: Drum Ogilvie --- azkv/keysource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azkv/keysource.go b/azkv/keysource.go index 52a8c46c0b..b0ee338145 100644 --- a/azkv/keysource.go +++ b/azkv/keysource.go @@ -79,7 +79,7 @@ func NewMasterKey(vaultURL string, keyName string, keyVersion string) *MasterKey // MasterKey. The URL format is {vaultUrl}/keys/{keyName}/{keyVersion}. func NewMasterKeyFromURL(url string) (*MasterKey, error) { url = strings.TrimSpace(url) - re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)(/[^/]+)?$") + re := regexp.MustCompile("^(https://[^/]+)/keys/([^/]+)(/[^/]*)?$") parts := re.FindStringSubmatch(url) if len(parts) < 3 { return nil, fmt.Errorf("could not parse %q into a valid Azure Key Vault MasterKey %v", url, parts)