diff --git a/changelog/unreleased/enhancement-brute-force-protection-links.md b/changelog/unreleased/enhancement-brute-force-protection-links.md new file mode 100644 index 00000000000..af48db5ea8b --- /dev/null +++ b/changelog/unreleased/enhancement-brute-force-protection-links.md @@ -0,0 +1,15 @@ +Enhancement: Implement brute force protection for public links + +Public links will be protected by default, allowing up to 5 wrong password +attempts per hour. If such rate is exceeded, the link will be blocked for +all the users until the failure rate goes below the configured threshold +(5 failures per hour by default, as said). + +The failure rate is configurable, so it can be 10 failures each 2 hours +or 3 failures per minute. + +Note that the protection will apply per service replica, so one replica +might be blocked while another replica is fully functional. + +https://github.com/owncloud/ocis/pull/11864 +https://github.com/owncloud/reva/pull/460 diff --git a/ocis-pkg/service/grpc/handler/metadata/metadata.go b/ocis-pkg/service/grpc/handler/metadata/metadata.go new file mode 100644 index 00000000000..4b41de6c137 --- /dev/null +++ b/ocis-pkg/service/grpc/handler/metadata/metadata.go @@ -0,0 +1,66 @@ +package metadata + +import ( + "context" + "strings" + + "github.com/owncloud/reva/v2/pkg/rgrpc" + "go-micro.dev/v4/server" + "google.golang.org/grpc/metadata" +) + +const ( + // Prefix used to auto propagate GRPC metadata keys across servers. + // This is used in the NewHandlerWrapper and NewSubscriberWrapper, and + // it's expected to work in go-micro services. + // It needs to match the prefix used in reva for the same purpose. + AutoPropPrefix = rgrpc.AutoPropPrefix +) + +// NewHandlerWrapper propagates the grpc metadata. +func NewHandlerWrapper() server.HandlerWrapper { + return func(h server.HandlerFunc) server.HandlerFunc { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.MD{} + } + + pairs := make([]string, 0, md.Len()*2) + for key, values := range md { + if strings.HasPrefix(key, AutoPropPrefix) { + for _, value := range values { + pairs = append(pairs, key, value) + } + } + } + + newctx := metadata.AppendToOutgoingContext(ctx, pairs...) + return h(newctx, req, rsp) + } + } +} + +// NewSubscriberWrapper propagates the grpc metadata +func NewSubscriberWrapper() server.SubscriberWrapper { + return func(next server.SubscriberFunc) server.SubscriberFunc { + return func(ctx context.Context, msg server.Message) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + md = metadata.MD{} + } + + pairs := make([]string, 0, md.Len()*2) + for key, values := range md { + if strings.HasPrefix(key, AutoPropPrefix) { + for _, value := range values { + pairs = append(pairs, key, value) + } + } + } + + newctx := metadata.AppendToOutgoingContext(ctx, pairs...) + return next(newctx, msg) + } + } +} diff --git a/ocis-pkg/service/grpc/service.go b/ocis-pkg/service/grpc/service.go index f9d53612b08..077a7aa742c 100644 --- a/ocis-pkg/service/grpc/service.go +++ b/ocis-pkg/service/grpc/service.go @@ -21,6 +21,7 @@ import ( ociscrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/registry" + ocismetadata "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc/handler/metadata" ) // Service simply wraps the go-micro grpc service. @@ -64,6 +65,7 @@ func NewServiceWithClient(client client.Client, opts ...Option) (Service, error) mtracer.NewHandlerWrapper( mtracer.WithTraceProvider(sopts.TraceProvider), ), + ocismetadata.NewHandlerWrapper(), } if sopts.Logger.GetLevel() == zerolog.DebugLevel { handlerWrappers = append(handlerWrappers, LogHandler(&sopts.Logger)) @@ -87,9 +89,10 @@ func NewServiceWithClient(client client.Client, opts ...Option) (Service, error) mtracer.WithTraceProvider(sopts.TraceProvider), )), micro.WrapHandler(handlerWrappers...), - micro.WrapSubscriber(mtracer.NewSubscriberWrapper( - mtracer.WithTraceProvider(sopts.TraceProvider), - )), + micro.WrapSubscriber( + mtracer.NewSubscriberWrapper(mtracer.WithTraceProvider(sopts.TraceProvider)), + ocismetadata.NewSubscriberWrapper(), + ), } return Service{micro.NewService(mopts...)}, nil diff --git a/services/proxy/pkg/middleware/public_share_auth.go b/services/proxy/pkg/middleware/public_share_auth.go index 2380b8a8c2f..bc69e36ef19 100644 --- a/services/proxy/pkg/middleware/public_share_auth.go +++ b/services/proxy/pkg/middleware/public_share_auth.go @@ -6,6 +6,7 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/reva/v2/pkg/auth/manager/publicshares" "github.com/owncloud/reva/v2/pkg/rgrpc/todo/pool" ) @@ -102,7 +103,10 @@ func (a PublicShareAuthenticator) Authenticate(r *http.Request) (*http.Request, return nil, false } - authResp, err := client.Authenticate(r.Context(), &gateway.AuthenticateRequest{ + // we just need the reva access token, so we want to skip the brute force + // protection in this case + ctx := publicshares.MarkSkipAttemptContext(r.Context(), shareToken) + authResp, err := client.Authenticate(ctx, &gateway.AuthenticateRequest{ Type: authenticationType, ClientId: shareToken, ClientSecret: sharePassword, diff --git a/services/storage-publiclink/README.md b/services/storage-publiclink/README.md new file mode 100644 index 00000000000..1396a0cfb74 --- /dev/null +++ b/services/storage-publiclink/README.md @@ -0,0 +1,29 @@ +# storage-publiclink + +## Brute Force Protection + +The brute force protection will prevent access to public links if wrong +passwords are entered. The implementation is very similar to a rate limiter, +but taking into account only wrong password attempts. + +By default, you're allowed a maximum of 5 failed attempts +(`STORAGE_PUBLICLINK_BRUTEFORCE_MAXATTEMPTS=5`) in 1 hour +(`STORAGE_PUBLICLINK_BRUTEFORCE_TIMEGAP=1h`). You can adjust those values to +your liking in order to define the failure rate threshold (5 failures per +hour, by default). + +If the failure rate threshold is exceeded, the public link will be blocked +until such rate goes below the threshold. This means that it will remain +blocked for an undefined time: a couple of seconds in the best case, or up to +`STORAGE_PUBLICLINK_BRUTEFORCE_TIME` in the worst case. + +If the public link is blocked by the brute force protection, it will be +blocked for all the users. + +In case of multiple service replicas, the brute force protection won't share +any data among the replicas and the failure rate will apply per replica. This +means that a replica might be blocked due to high failure rate while the rest +work fine. + +As said, this feature is enabled by default, with a 5 failures per hour rate. +If you want to disable this feature, set the related configuration values to 0. diff --git a/services/storage-publiclink/pkg/config/config.go b/services/storage-publiclink/pkg/config/config.go index c6cab3d609e..23d8382786f 100644 --- a/services/storage-publiclink/pkg/config/config.go +++ b/services/storage-publiclink/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "context" + "time" "github.com/owncloud/ocis/v2/ocis-pkg/shared" ) @@ -17,6 +18,7 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` Reva *shared.Reva `yaml:"reva"` + BruteForce BruteForce `yaml:"brute_force"` SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"STORAGE_PUBLICLINK_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token." introductionVersion:"pre5.0"` @@ -53,3 +55,8 @@ type GRPCConfig struct { type StorageProvider struct { MountID string `yaml:"mount_id" env:"STORAGE_PUBLICLINK_STORAGE_PROVIDER_MOUNT_ID" desc:"Mount ID of this storage. Admins can set the ID for the storage in this config option manually which is then used to reference the storage. Any reasonable long string is possible, preferably this would be an UUIDv4 format." introductionVersion:"pre5.0"` } + +type BruteForce struct { + TimeGap time.Duration `yaml:"time_gap" env:"STORAGE_PUBLICLINK_BRUTEFORCE_TIMEGAP" desc:"The duration of the time gap computed for the brute force protection." introductionVersion:"Curie"` + MaxAttempts int `yaml:"max_attempts" env:"STORAGE_PUBLICLINK_BRUTEFORCE_MAXATTEMPTS" desc:"The maximum number of failed attempts allowed in the time gap defined in STORAGE_PUBLICLINK_BRUTEFORCE_TIMEGAP." introductionVersion:"Curie"` +} diff --git a/services/storage-publiclink/pkg/config/defaults/defaultconfig.go b/services/storage-publiclink/pkg/config/defaults/defaultconfig.go index c69e97616e0..36031d4f034 100644 --- a/services/storage-publiclink/pkg/config/defaults/defaultconfig.go +++ b/services/storage-publiclink/pkg/config/defaults/defaultconfig.go @@ -1,6 +1,8 @@ package defaults import ( + "time" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" "github.com/owncloud/ocis/v2/ocis-pkg/structs" "github.com/owncloud/ocis/v2/services/storage-publiclink/pkg/config" @@ -35,6 +37,10 @@ func DefaultConfig() *config.Config { StorageProvider: config.StorageProvider{ MountID: "7993447f-687f-490d-875c-ac95e89a62a4", }, + BruteForce: config.BruteForce{ + TimeGap: 1 * time.Hour, + MaxAttempts: 5, + }, } } diff --git a/services/storage-publiclink/pkg/revaconfig/config.go b/services/storage-publiclink/pkg/revaconfig/config.go index 2b63e5d9e6e..553ea165a3b 100644 --- a/services/storage-publiclink/pkg/revaconfig/config.go +++ b/services/storage-publiclink/pkg/revaconfig/config.go @@ -37,7 +37,9 @@ func StoragePublicLinkConfigFromStruct(cfg *config.Config) map[string]interface{ "auth_manager": "publicshares", "auth_managers": map[string]interface{}{ "publicshares": map[string]interface{}{ - "gateway_addr": cfg.Reva.Address, + "gateway_addr": cfg.Reva.Address, + "brute_force_time_gap": cfg.BruteForce.TimeGap, + "brute_force_max_attempts": cfg.BruteForce.MaxAttempts, }, }, },