Skip to content

Commit 5515ec0

Browse files
azkv: enable specifying some auth methods directly, and add cachable user authentication methods
1 parent 7d4395f commit 5515ec0

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,14 @@ which tries several authentication methods, in this order:
336336
3. `Managed Identity credentials <https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential>`_
337337
4. `Azure CLI credentials <https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureCLICredential>`_
338338
339+
340+
If you want to force a specific method you can override this with the enviornment variable ``SOPS_AZURE_AUTH_METHOD``
341+
- ``default`` (same as not setting this variable)
342+
- ``msi``
343+
- ``azure-cli``
344+
- ``cached-device-code`` (device code authentication which caches the token in the os keyring)
345+
- ``cached-browser`` (interactive browser authentication which caches the token in the os keyring)
346+
339347
For example, you can use a Service Principal with the following environment variables:
340348
341349
.. code:: bash

azkv/keysource.go

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ import (
99
"context"
1010
"encoding/base64"
1111
"fmt"
12+
"path/filepath"
1213
"regexp"
1314
"strings"
1415
"time"
1516

17+
"encoding/json"
18+
"os"
19+
1620
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
21+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1722
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
1823
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
24+
azidentitycache "github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache"
1925
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys"
2026
"github.com/sirupsen/logrus"
2127

@@ -25,6 +31,11 @@ import (
2531
const (
2632
// KeyTypeIdentifier is the string used to identify an Azure Key Vault MasterKey.
2733
KeyTypeIdentifier = "azure_kv"
34+
35+
SopsAzureAuthMethodEnv = "SOPS_AZURE_AUTH_METHOD"
36+
37+
cachedBrowserAuthRecordFileName = "azure-auth-record-browser.json"
38+
cachedDeviceCodeAuthRecordFileName = "azure-auth-record-device-code.json"
2839
)
2940

3041
var (
@@ -230,7 +241,130 @@ func (key *MasterKey) TypeToIdentifier() string {
230241
// azidentity.NewDefaultAzureCredential.
231242
func (key *MasterKey) getTokenCredential() (azcore.TokenCredential, error) {
232243
if key.tokenCredential == nil {
233-
return azidentity.NewDefaultAzureCredential(nil)
244+
245+
authMethod := strings.ToLower(os.Getenv(SopsAzureAuthMethodEnv))
246+
switch authMethod {
247+
case "cached-browser":
248+
return cachedInteractiveBrowserCredentials()
249+
case "cached-device-code":
250+
return cachedDeviceCodeCredentials()
251+
case "azure-cli":
252+
return azidentity.NewAzureCLICredential(nil)
253+
case "msi":
254+
return azidentity.NewManagedIdentityCredential(nil)
255+
// If "DEFAULT" or not explicitly specified then use the default authentication chain.
256+
case "", "default":
257+
return azidentity.NewDefaultAzureCredential(nil)
258+
default:
259+
return nil, fmt.Errorf("Value `%s` is unsupported for environment variable `%s`, to resolve this either leave it unset or use one of `default`/`msi`/`azure-cli`/`cached-browser`/`cached-device-code`", authMethod, SopsAzureAuthMethodEnv)
260+
}
234261
}
235262
return key.tokenCredential, nil
236263
}
264+
265+
func sopsCacheDir() (string, error) {
266+
userCacheDir, err := os.UserCacheDir()
267+
if err != nil {
268+
return "", err
269+
}
270+
271+
cacheDir := filepath.Join(userCacheDir, "/sops")
272+
273+
if err = os.MkdirAll(cacheDir, 0o700); err != nil {
274+
return "", err
275+
}
276+
277+
return cacheDir, nil
278+
}
279+
280+
type CachableTokenCredential interface {
281+
Authenticate(ctx context.Context, opts *policy.TokenRequestOptions) (azidentity.AuthenticationRecord, error)
282+
GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error)
283+
}
284+
285+
func cacheStoreRecord(cachePath string, record azidentity.AuthenticationRecord) error {
286+
b, err := json.Marshal(record)
287+
if err != nil {
288+
return err
289+
}
290+
291+
return os.WriteFile(cachePath, b, 0600)
292+
}
293+
294+
func cacheLoadRecord(cachePath string) (azidentity.AuthenticationRecord, error) {
295+
var record azidentity.AuthenticationRecord
296+
297+
b, err := os.ReadFile(cachePath)
298+
if err != nil {
299+
return record, err
300+
}
301+
302+
err = json.Unmarshal(b, &record)
303+
if err != nil {
304+
return record, err
305+
}
306+
307+
return record, nil
308+
}
309+
310+
func cacheTokenCredential(cachePath string, tokenCredentialFn func(cache azidentity.Cache, record azidentity.AuthenticationRecord) (CachableTokenCredential, error)) (azcore.TokenCredential, error) {
311+
cache, err := azidentitycache.New(nil)
312+
// Errors if persistent caching is not supported by the current runtime
313+
if err != nil {
314+
return nil, err
315+
}
316+
317+
cachedRecord, cacheLoadErr := cacheLoadRecord(cachePath)
318+
319+
credential, err := tokenCredentialFn(cache, cachedRecord)
320+
if err != nil {
321+
return nil, err
322+
}
323+
324+
// If loading the authenticationRecord from the cachePath failed for any reason (validation, file doesn't exist, not encoded using json, etc.)
325+
if cacheLoadErr != nil {
326+
record, err := credential.Authenticate(context.Background(), nil)
327+
if err != nil {
328+
return nil, err
329+
}
330+
331+
if err = cacheStoreRecord(cachePath, record); err != nil {
332+
return nil, err
333+
}
334+
}
335+
336+
return credential, nil
337+
}
338+
339+
func cachedInteractiveBrowserCredentials() (azcore.TokenCredential, error) {
340+
cacheDir, err := sopsCacheDir()
341+
if err != nil {
342+
return nil, err
343+
}
344+
return cacheTokenCredential(
345+
filepath.Join(cacheDir, cachedBrowserAuthRecordFileName),
346+
func(cache azidentity.Cache, record azidentity.AuthenticationRecord) (CachableTokenCredential, error) {
347+
return azidentity.NewInteractiveBrowserCredential(&azidentity.InteractiveBrowserCredentialOptions{
348+
AuthenticationRecord: record,
349+
Cache: cache,
350+
})
351+
},
352+
)
353+
}
354+
355+
func cachedDeviceCodeCredentials() (azcore.TokenCredential, error) {
356+
cacheDir, err := sopsCacheDir()
357+
if err != nil {
358+
return nil, err
359+
}
360+
361+
return cacheTokenCredential(
362+
filepath.Join(cacheDir, cachedDeviceCodeAuthRecordFileName),
363+
func(cache azidentity.Cache, record azidentity.AuthenticationRecord) (CachableTokenCredential, error) {
364+
return azidentity.NewDeviceCodeCredential(&azidentity.DeviceCodeCredentialOptions{
365+
AuthenticationRecord: record,
366+
Cache: cache,
367+
})
368+
},
369+
)
370+
}

0 commit comments

Comments
 (0)