b2: Support file hiding instead of deleting them permanently

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 <bucketName> <keyName> listBuckets,readFiles,writeFiles,listFiles

Suggested-by: Daniel Gröber <dxld@darkboxed.org>
This commit is contained in:
Michael Eischer 2022-10-21 22:48:17 +02:00
parent 24a2e5cab9
commit 1ccab95bc4
2 changed files with 43 additions and 4 deletions

View file

@ -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 <bucketName> <keyName> 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

View file

@ -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