From 64a7ec5341569ae95abaabc4ac25ccfb52cb5e6c Mon Sep 17 00:00:00 2001 From: Roger Gammans Date: Sat, 5 Mar 2022 18:16:13 +0000 Subject: [PATCH] azure: add SAS authentication option --- changelog/unreleased/issue-2295 | 14 ++++++++++++++ cmd/restic/global.go | 4 ++++ doc/030_preparing_a_new_repo.rst | 11 +++++++++++ doc/040_backup.rst | 1 + internal/backend/azure/azure.go | 28 ++++++++++++++++++++++++---- internal/backend/azure/config.go | 1 + 6 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/issue-2295 diff --git a/changelog/unreleased/issue-2295 b/changelog/unreleased/issue-2295 new file mode 100644 index 000000000..b2cc9e475 --- /dev/null +++ b/changelog/unreleased/issue-2295 @@ -0,0 +1,14 @@ +Enhancement: Allow use of SAS token to authenticate to Azure + +Previously restic only supported AccountKeys to authenticate to Azure +storage accounts, which necessitates giving a significant amount of +access. + +We added support for Azure SAS tokens which are a more fine-grained +and time-limited manner of granting access. Set the `AZURE_ACCOUNT_NAME` +and `AZURE_ACCOUNT_SAS` environment variables to use a SAS token for +authentication. Note that if `AZURE_ACCOUNT_KEY` is set, it will take +preference. + +https://github.com/restic/restic/issues/2295 +https://github.com/restic/restic/pull/3661 diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 41f6bfb0e..7410b4097 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -596,6 +596,10 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro cfg.AccountKey = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_KEY")) } + if cfg.AccountSAS.String() == "" { + cfg.AccountSAS = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_SAS")) + } + if err := opts.Apply(loc.Scheme, &cfg); err != nil { return nil, err } diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst index fd079ac21..0ae1d4815 100644 --- a/doc/030_preparing_a_new_repo.rst +++ b/doc/030_preparing_a_new_repo.rst @@ -499,6 +499,17 @@ account name and key as follows: $ export AZURE_ACCOUNT_NAME= $ export AZURE_ACCOUNT_KEY= +or + +.. code-block:: console + + $ export AZURE_ACCOUNT_NAME= + $ export AZURE_ACCOUNT_SAS= + +With the later form, ensure your ``SAS_TOKEN`` does not start with a leading +``?``. If the generated token starts with a leading ``?`` it is safe to just +delete the first character (the ``?``) before use. + Afterwards you can initialize a repository in a container called ``foo`` in the root path like this: diff --git a/doc/040_backup.rst b/doc/040_backup.rst index 18af542b0..fa535f403 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -593,6 +593,7 @@ environment variables. The following lists these environment variables: AZURE_ACCOUNT_NAME Account name for Azure AZURE_ACCOUNT_KEY Account key for Azure + AZURE_ACCOUNT_SAS Shared access signatures (SAS) for Azure GOOGLE_PROJECT_ID Project ID for Google Cloud Storage GOOGLE_APPLICATION_CREDENTIALS Application Credentials for Google Cloud Storage (e.g. $HOME/.config/gs-secret-restic-key.json) diff --git a/internal/backend/azure/azure.go b/internal/backend/azure/azure.go index 447db5c9a..9f2f0b163 100644 --- a/internal/backend/azure/azure.go +++ b/internal/backend/azure/azure.go @@ -4,6 +4,7 @@ import ( "context" "crypto/md5" "encoding/base64" + "fmt" "hash" "io" "net/http" @@ -39,10 +40,29 @@ var _ restic.Backend = &Backend{} func open(cfg Config, rt http.RoundTripper) (*Backend, error) { debug.Log("open, config %#v", cfg) - - client, err := storage.NewBasicClient(cfg.AccountName, cfg.AccountKey.Unwrap()) - if err != nil { - return nil, errors.Wrap(err, "NewBasicClient") + var client storage.Client + var err error + if cfg.AccountKey.String() != "" { + // We have an account key value, find the BlobServiceClient + // from with a BasicClient + debug.Log(" - using account key") + client, err = storage.NewBasicClient(cfg.AccountName, cfg.AccountKey.Unwrap()) + if err != nil { + return nil, errors.Wrap(err, "NewBasicClient") + } + } else if cfg.AccountSAS.String() != "" { + // Get the client using the SAS Token as authentication, this + // is longer winded than above because the SDK wants a URL for the Account + // if your using a SAS token, and not just the account name + // we (as per the SDK ) assume the default Azure portal. + url := fmt.Sprintf("https://%s.blob.core.windows.net/", cfg.AccountName) + debug.Log(" - using sas token") + client, err = storage.NewAccountSASClientFromEndpointToken(url, cfg.AccountSAS.Unwrap()) + if err != nil { + return nil, errors.Wrap(err, "NewAccountSASClientFromEndpointToken") + } + } else { + return nil, errors.New("no azure authentication information found") } client.HTTPClient = &http.Client{Transport: rt} diff --git a/internal/backend/azure/config.go b/internal/backend/azure/config.go index 0a6079797..cc5169e3e 100644 --- a/internal/backend/azure/config.go +++ b/internal/backend/azure/config.go @@ -12,6 +12,7 @@ import ( // server. type Config struct { AccountName string + AccountSAS options.SecretString AccountKey options.SecretString Container string Prefix string