From 1ccab95bc49cfdc425669373ee46e8c116718e22 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Fri, 21 Oct 2022 22:48:17 +0200 Subject: [PATCH] b2: Support file hiding instead of deleting them permanently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically fall back to hiding files if not authorized to permanently delete files. This allows using restic with an append-only application key with B2. Thus, an attacker cannot directly delete backups with the API key used by restic. To use this feature create an application key without the deleteFiles capability. It is recommended to restrict the key to just one bucket. For example using the b2 command line tool: b2 create-key --bucket listBuckets,readFiles,writeFiles,listFiles Suggested-by: Daniel Gröber --- changelog/unreleased/issue-2134 | 19 +++++++++++++++++++ internal/backend/b2/b2.go | 28 ++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 changelog/unreleased/issue-2134 diff --git a/changelog/unreleased/issue-2134 b/changelog/unreleased/issue-2134 new file mode 100644 index 000000000..1886967f8 --- /dev/null +++ b/changelog/unreleased/issue-2134 @@ -0,0 +1,19 @@ +Enhancement: Support B2 API keys restricted to hiding but not deleting files + +When the B2 backend does not have the necessary permissions to permanently +delete files, it now automatically falls back to hiding files. This allows +using restic with an application key which is not allowed to delete files. +This prevents an attacker to delete backups with the API key used by restic. + +To use this feature create an application key without the deleteFiles +capability. It is recommended to restrict the key to just one bucket. +For example using the b2 command line tool: + + b2 create-key --bucket listBuckets,readFiles,writeFiles,listFiles + +Alternatively, you can use the S3 backend to access B2, as described +in the documentation. In this mode, files are also only hidden instead +of being deleted permanently. + +https://github.com/restic/restic/issues/2134 +https://github.com/restic/restic/pull/2398 diff --git a/internal/backend/b2/b2.go b/internal/backend/b2/b2.go index b95506856..99379881e 100644 --- a/internal/backend/b2/b2.go +++ b/internal/backend/b2/b2.go @@ -18,6 +18,7 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/kurin/blazer/b2" + "github.com/kurin/blazer/base" ) // b2Backend is a backend which stores its data on Backblaze B2. @@ -28,6 +29,8 @@ type b2Backend struct { listMaxItems int layout.Layout sem sema.Semaphore + + canDelete bool } // Billing happens in 1000 item granlarity, but we are more interested in reducing the number of network round trips @@ -104,6 +107,7 @@ func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend }, listMaxItems: defaultListMaxItems, sem: sem, + canDelete: true, } return be, nil @@ -314,11 +318,27 @@ func (be *b2Backend) Remove(ctx context.Context, h restic.Handle) error { // the retry backend will also repeat the remove method up to 10 times for i := 0; i < 3; i++ { obj := be.bucket.Object(be.Filename(h)) - err := obj.Delete(ctx) - if err == nil { - // keep deleting until we are sure that no leftover file versions exist - continue + + var err error + if be.canDelete { + err = obj.Delete(ctx) + if err == nil { + // keep deleting until we are sure that no leftover file versions exist + continue + } + + code, _ := base.Code(err) + if code == 401 { // unauthorized + // fallback to hide if not allowed to delete files + be.canDelete = false + debug.Log("Removing %v failed, falling back to b2_hide_file.", h) + continue + } + } else { + // hide adds a new file version hiding all older ones, thus retries are not necessary + err = obj.Hide(ctx) } + // consider a file as removed if b2 informs us that it does not exist if b2.IsNotExist(err) { return nil