Skip to content

Commit

Permalink
Add brute force protection feature and update config params
Browse files Browse the repository at this point in the history
Implemented brute force protection with configurable history limits. Renamed `MaxConnections` to `MaxConcurrentRequests` and added `MaxPasswordHistoryEntries` configuration. Integrated corresponding validation and updated relevant documentation.

Signed-off-by: Christian Roessner <c@roessner.co>
  • Loading branch information
Christian Roessner committed Sep 29, 2024
1 parent efe9337 commit 7020c9d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 41 deletions.
20 changes: 15 additions & 5 deletions server/config/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -1479,14 +1479,23 @@ func (f *File) validateHTTPRequestHeaders() error {
return nil
}

// validateMaxConnections ensures that the MaxConnections parameter is set to a valid value.
// validateMaxConnections ensures that the MaxConcurrentRequests parameter is set to a valid value.
func (f *File) validateMaxConnections() error {
if f.Server.MaxConnections == 0 {
f.Server.MaxConnections = global.MaxConnections
if f.Server.MaxConcurrentRequests == 0 {
f.Server.MaxConcurrentRequests = global.MaxConcurrentRequests
}

if f.Server.MaxConnections < 0 {
f.Server.MaxConnections = global.MaxConnections
if f.Server.MaxConcurrentRequests < 0 {
f.Server.MaxConcurrentRequests = global.MaxConcurrentRequests
}

return nil
}

// validateMaxPasswordHistoryEntries sets MaxPasswordHistoryEntries to a default value if non-positive and returns an error if any.
func (f *File) validateMaxPasswordHistoryEntries() error {
if f.Server.MaxPasswordHistoryEntries <= 0 {
f.Server.MaxPasswordHistoryEntries = global.MaxPasswordHistoryEntries
}

return nil
Expand Down Expand Up @@ -1521,6 +1530,7 @@ func (f *File) validate() (err error) {
f.validateMasterUserDelimiter,
f.validateHTTPRequestHeaders,
f.validateMaxConnections,
f.validateMaxPasswordHistoryEntries,
}

for _, validator := range validators {
Expand Down
39 changes: 20 additions & 19 deletions server/config/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,26 @@ import (
// ServerSection represents the configuration for a server, including network settings, TLS, logging, backends, features,
// protocol handling, and integrations with other systems such as Redis and Prometheus.
type ServerSection struct {
Address string `mapstructure:"address"`
MaxConnections int32 `mapstructure:"max_connections"`
HTTP3 bool `mapstructure:"http3"`
HAproxyV2 bool `mapstructure:"haproxy_v2"`
TLS TLS `mapstructure:"tls"`
BasicAuth BasicAuth `mapstructure:"basic_auth"`
InstanceName string `mapstructure:"instance_name"`
Log Log `maptostructure:"log"`
Backends []*Backend `mapstructure:"backends"`
Features []*Feature `mapstructure:"features"`
BruteForceProtocols []*Protocol `mapstructure:"brute_force_protocols"`
HydraAdminUrl string `mapstructure:"ory_hydra_admin_url"`
DNS DNS `mapstructure:"dns"`
Insights Insights `mapstructure:"insights"`
Redis Redis `mapstructure:"redis"`
MasterUser MasterUser `mapstructure:"master_user"`
Frontend Frontend `mapstructure:"frontend"`
PrometheusTimer PrometheusTimer `mapstructure:"prometheus_timer"`
DefaultHTTPRequestHeader DefaultHTTPRequestHeader `mapstructure:"default_http_request_header"`
Address string `mapstructure:"address"`
MaxConcurrentRequests int32 `mapstructure:"max_concurrent_requests"`
MaxPasswordHistoryEntries int32 `mapstructure:"max_password_history_entries"`
HTTP3 bool `mapstructure:"http3"`
HAproxyV2 bool `mapstructure:"haproxy_v2"`
TLS TLS `mapstructure:"tls"`
BasicAuth BasicAuth `mapstructure:"basic_auth"`
InstanceName string `mapstructure:"instance_name"`
Log Log `maptostructure:"log"`
Backends []*Backend `mapstructure:"backends"`
Features []*Feature `mapstructure:"features"`
BruteForceProtocols []*Protocol `mapstructure:"brute_force_protocols"`
HydraAdminUrl string `mapstructure:"ory_hydra_admin_url"`
DNS DNS `mapstructure:"dns"`
Insights Insights `mapstructure:"insights"`
Redis Redis `mapstructure:"redis"`
MasterUser MasterUser `mapstructure:"master_user"`
Frontend Frontend `mapstructure:"frontend"`
PrometheusTimer PrometheusTimer `mapstructure:"prometheus_timer"`
DefaultHTTPRequestHeader DefaultHTTPRequestHeader `mapstructure:"default_http_request_header"`
}

// TLS represents the configuration for enabling TLS and managing certificates.
Expand Down
2 changes: 1 addition & 1 deletion server/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (f *Feature) String() string {
func (f *Feature) Set(value string) error {
switch value {
case "":
case global.FeatureTLSEncryption, global.FeatureRBL, global.FeatureRelayDomains, global.FeatureLua, global.FeatureBackendServersMonitoring:
case global.FeatureTLSEncryption, global.FeatureRBL, global.FeatureRelayDomains, global.FeatureLua, global.FeatureBackendServersMonitoring, global.FeatureBruteForce:
f.name = value
default:
return errors.ErrWrongFeature
Expand Down
60 changes: 47 additions & 13 deletions server/core/bruteforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,29 @@ func (a *AuthState) getBruteForceBucketRedisKey(rule *config.BruteForceRule) (ke
return
}

// checkTooManyPasswordHashes checks if the number of password hashes for a given Redis key exceeds the configured limit.
func (a *AuthState) checkTooManyPasswordHashes(key string) bool {
if length, err := rediscli.ReadHandle.HLen(context.Background(), key).Result(); err != nil {
if !stderrors.Is(err, redis.Nil) {
level.Error(log.Logger).Log(global.LogKeyGUID, a.GUID, global.LogKeyError, err)
} else {
stats.RedisReadCounter.Inc()
}

return true
} else {
if length > int64(config.LoadableConfig.Server.MaxPasswordHistoryEntries) {
level.Error(log.Logger).Log(global.LogKeyGUID, a.GUID, global.LogKeyError, fmt.Sprintf("too many entries in Redis hash key %s", key))

stats.RedisReadCounter.Inc()

return true
}
}

return false
}

// loadBruteForcePasswordHistoryFromRedis loads password history related to brute force attacks from Redis for a given key.
// The function will fetch all associated passwords in the form of a hash along with a counter.
// The Redis key is created for each unique user presented by the variable `key` which is a GUID,
Expand All @@ -324,6 +347,10 @@ func (a *AuthState) loadBruteForcePasswordHistoryFromRedis(key string) {

util.DebugModule(global.DbgBf, global.LogKeyGUID, a.GUID, "load_key", key)

if a.checkTooManyPasswordHashes(key) {
return
}

if passwordHistory, err := rediscli.ReadHandle.HGetAll(context.Background(), key).Result(); err != nil {
if !stderrors.Is(err, redis.Nil) {
level.Error(log.Logger).Log(global.LogKeyGUID, a.GUID, global.LogKeyError, err)
Expand Down Expand Up @@ -363,6 +390,10 @@ func (a *AuthState) loadBruteForcePasswordHistoryFromRedis(key string) {
// This overall history is then used to compute the total number of seen passwords.
// Each of these phases are independent and are executed if the Redis hash key retrieval and the password history fetch operations are successful.
func (a *AuthState) getAllPasswordHistories() {
if !config.LoadableConfig.HasFeature(global.FeatureBruteForce) {
return
}

// Get password history for the current used username
if key := a.getBruteForcePasswordHistoryRedisHashKey(true); key != "" {
a.loadBruteForcePasswordHistoryFromRedis(key)
Expand Down Expand Up @@ -401,12 +432,20 @@ func (a *AuthState) getAllPasswordHistories() {
//
// The function concludes by logging that the process has finished.
func (a *AuthState) saveBruteForcePasswordToRedis() {
if !config.LoadableConfig.HasFeature(global.FeatureBruteForce) {
return
}

var keys []string

keys = append(keys, a.getBruteForcePasswordHistoryRedisHashKey(true))
keys = append(keys, a.getBruteForcePasswordHistoryRedisHashKey(false))

for index := range keys {
if a.checkTooManyPasswordHashes(keys[index]) {
continue
}

util.DebugModule(global.DbgBf, global.LogKeyGUID, a.GUID, "incr_key", keys[index])

// We can increment a key/value, even it never existed before.
Expand Down Expand Up @@ -434,20 +473,7 @@ func (a *AuthState) saveBruteForcePasswordToRedis() {
} else {
stats.RedisWriteCounter.Inc()
}

util.DebugModule(
global.DbgBf,
global.LogKeyGUID, a.GUID,
"key", keys[index],
global.LogKeyMsg, "Set expire",
)
}

util.DebugModule(
global.DbgBf,
global.LogKeyGUID, a.GUID,
global.LogKeyMsg, "Finished",
)
}

// loadBruteForceBucketCounterFromRedis is a method on the AuthState struct that loads the brute force
Expand All @@ -457,6 +483,10 @@ func (a *AuthState) saveBruteForcePasswordToRedis() {
// If the BruteForceCounter is not initialized, it creates a new map.
// Finally, it updates the BruteForceCounter map with the counter value retrieved from Redis using the rule name as the key.
func (a *AuthState) loadBruteForceBucketCounterFromRedis(rule *config.BruteForceRule) {
if !config.LoadableConfig.HasFeature(global.FeatureBruteForce) {
return
}

cache := new(backend.BruteForceBucketCache)

if key := a.getBruteForceBucketRedisKey(rule); key != "" {
Expand Down Expand Up @@ -608,6 +638,10 @@ func (a *AuthState) checkBruteForce() (blockClientIP bool) {
network *net.IPNet
)

if !config.LoadableConfig.HasFeature(global.FeatureBruteForce) {
return
}

stopTimer := stats.PrometheusTimer(global.PromBruteForce, "brute_force_check_request_total")

defer stopTimer()
Expand Down
2 changes: 1 addition & 1 deletion server/core/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ func HTTPApp(ctx context.Context) {
pprof.Register(router)
}

limitCounter := NewLimitCounter(config.LoadableConfig.Server.MaxConnections)
limitCounter := NewLimitCounter(config.LoadableConfig.Server.MaxConcurrentRequests)

router.Use(limitCounter.Middleware())

Expand Down
10 changes: 8 additions & 2 deletions server/global/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,11 @@ const (
// MaxActionWorkers is the maximum number of action workers
MaxActionWorkers = 10

// MaxConnections represents the maximum number of simultaneous connections allowed.
MaxConnections = 3000
// MaxConcurrentRequests represents the maximum number of simultaneous connections allowed.
MaxConcurrentRequests = 3000

// MaxPasswordHistoryEntries defines the maximum number of previous passwords to store for history and validation purposes.
MaxPasswordHistoryEntries = 100
)

// Log level.
Expand Down Expand Up @@ -362,6 +365,9 @@ const (

// FeatureBackendServersMonitoring enables a custom backend list with fail-state monitoring
FeatureBackendServersMonitoring = "backend_server_monitoring"

// FeatureBruteForce enables the brute force protection system
FeatureBruteForce = "brute_force"
)

// Statistics label for the loin counter.
Expand Down

0 comments on commit 7020c9d

Please sign in to comment.