Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
id
auth
Commits
92aea0b7
Commit
92aea0b7
authored
Nov 17, 2017
by
ale
Browse files
Add configurable rate limiting / blacklisting support
parent
6a2216a2
Changes
3
Hide whitespace changes
Inline
Side-by-side
server/authserver.go
View file @
92aea0b7
...
...
@@ -71,16 +71,122 @@ type U2FShortTermStorage interface {
GetUserChallenge
(
string
)
(
*
u2f
.
Challenge
,
bool
)
}
type
ratelimitKeyFunc
func
(
*
User
,
*
auth
.
Request
)
string
func
usernameKey
(
user
*
User
,
_
*
auth
.
Request
)
string
{
return
user
.
Name
}
func
ipAddrKey
(
_
*
User
,
req
*
auth
.
Request
)
string
{
if
req
.
DeviceInfo
!=
nil
{
return
req
.
DeviceInfo
.
RemoteAddr
}
return
""
}
type
authRatelimiterConfig
struct
{
Limit
int
`yaml:"limit"`
Period
int
`yaml:"period"`
BlacklistTime
int
`yaml:"blacklist_for"`
OnFailure
bool
`yaml:"on_failure"`
Keys
[]
string
`yaml:"keys"`
keyFuncs
[]
ratelimitKeyFunc
}
func
(
r
*
authRatelimiterConfig
)
compile
()
error
{
for
_
,
k
:=
range
r
.
Keys
{
var
f
ratelimitKeyFunc
switch
k
{
case
"ip"
:
f
=
ipAddrKey
case
"user"
:
f
=
usernameKey
default
:
return
fmt
.
Errorf
(
"unknown key %s"
,
k
)
}
r
.
keyFuncs
=
append
(
r
.
keyFuncs
,
f
)
}
return
nil
}
const
rlKeySep
=
";"
func
(
r
*
authRatelimiterConfig
)
key
(
user
*
User
,
req
*
auth
.
Request
)
string
{
if
len
(
r
.
keyFuncs
)
==
1
{
return
r
.
keyFuncs
[
0
](
user
,
req
)
}
var
parts
[]
string
for
_
,
f
:=
range
r
.
keyFuncs
{
parts
=
append
(
parts
,
f
(
user
,
req
))
}
return
strings
.
Join
(
parts
,
rlKeySep
)
}
type
authRatelimiter
struct
{
*
authRatelimiterConfig
rl
*
Ratelimiter
}
func
(
r
*
authRatelimiter
)
AllowIncr
(
user
*
User
,
req
*
auth
.
Request
)
bool
{
return
r
.
rl
.
AllowIncr
(
r
.
key
(
user
,
req
))
}
type
authBlacklist
struct
{
*
authRatelimiterConfig
bl
*
Blacklist
}
func
(
b
*
authBlacklist
)
Allow
(
user
*
User
,
req
*
auth
.
Request
)
bool
{
return
b
.
bl
.
Allow
(
b
.
key
(
user
,
req
))
}
func
(
b
*
authBlacklist
)
Incr
(
user
*
User
,
req
*
auth
.
Request
,
resp
*
auth
.
Response
)
{
if
b
.
OnFailure
&&
resp
.
Status
==
auth
.
StatusOK
{
return
}
b
.
bl
.
Incr
(
b
.
key
(
user
,
req
))
}
type
requestFilterFunc
func
(
*
User
,
*
auth
.
Request
,
*
auth
.
Response
)
*
auth
.
Response
// BackendSpec specifies backend-specific configuration for a service.
type
BackendSpec
struct
{
LDAPSpec
*
LDAPServiceConfig
`yaml:"ldap"`
FileSpec
string
`yaml:"file"`
}
// ServiceConfig
configures
authentication backends for a service.
// ServiceConfig
defines the
authentication backends for a service.
type
ServiceConfig
struct
{
BackendSpecs
[]
*
BackendSpec
`yaml:"backends"`
ChallengeResponse
bool
`yaml:"challenge_response"`
Ratelimits
[]
string
`yaml:"rate_limits"`
rl
[]
*
authRatelimiter
bl
[]
*
authBlacklist
filters
[]
requestFilterFunc
}
func
(
c
*
ServiceConfig
)
checkRateLimits
(
user
*
User
,
req
*
auth
.
Request
)
bool
{
for
_
,
rl
:=
range
c
.
rl
{
if
!
rl
.
AllowIncr
(
user
,
req
)
{
return
false
}
}
for
_
,
bl
:=
range
c
.
bl
{
if
!
bl
.
Allow
(
user
,
req
)
{
return
false
}
}
return
true
}
func
(
c
*
ServiceConfig
)
notifyBlacklists
(
user
*
User
,
req
*
auth
.
Request
,
resp
*
auth
.
Response
)
{
for
_
,
bl
:=
range
c
.
bl
{
bl
.
Incr
(
user
,
req
,
resp
)
}
}
// Config for the authentication server.
...
...
@@ -94,6 +200,14 @@ type Config struct {
// Service-specific configuration.
Services
map
[
string
]
*
ServiceConfig
`yaml:"services"`
// Named rate limiter configurations.
RateLimiters
map
[
string
]
*
authRatelimiterConfig
`yaml:"rate_limits"`
// Runtime versions of the above. These objects are shared by
// all services, as they contain the actual map data.
rl
map
[
string
]
*
Ratelimiter
bl
map
[
string
]
*
Blacklist
path
string
}
...
...
@@ -115,6 +229,39 @@ func (c *Config) relativePath(path string) string {
return
filepath
.
Join
(
filepath
.
Dir
(
c
.
path
),
path
)
}
func
(
c
*
Config
)
compile
()
error
{
c
.
rl
=
make
(
map
[
string
]
*
Ratelimiter
)
c
.
bl
=
make
(
map
[
string
]
*
Blacklist
)
for
name
,
params
:=
range
c
.
RateLimiters
{
if
err
:=
params
.
compile
();
err
!=
nil
{
return
err
}
if
params
.
BlacklistTime
>
0
{
c
.
bl
[
name
]
=
newBlacklist
(
params
.
Limit
,
params
.
Period
,
params
.
BlacklistTime
)
}
else
{
c
.
rl
[
name
]
=
newRatelimiter
(
params
.
Limit
,
params
.
Period
)
}
}
for
_
,
sc
:=
range
c
.
Services
{
for
_
,
name
:=
range
sc
.
Ratelimits
{
config
,
ok
:=
c
.
RateLimiters
[
name
]
if
!
ok
{
return
fmt
.
Errorf
(
"unknown rate limiter %s"
,
name
)
}
if
rl
,
ok
:=
c
.
rl
[
name
];
ok
{
sc
.
rl
=
append
(
sc
.
rl
,
&
authRatelimiter
{
config
,
rl
})
}
else
if
bl
,
ok
:=
c
.
bl
[
name
];
ok
{
sc
.
bl
=
append
(
sc
.
bl
,
&
authBlacklist
{
config
,
bl
})
}
else
{
panic
(
"can't find rl/bl"
)
}
}
}
return
nil
}
// LoadConfig loads the configuration from a YAML-encoded file.
func
LoadConfig
(
path
string
)
(
*
Config
,
error
)
{
data
,
err
:=
ioutil
.
ReadFile
(
path
)
...
...
@@ -125,6 +272,10 @@ func LoadConfig(path string) (*Config, error) {
if
err
:=
yaml
.
Unmarshal
(
data
,
&
config
);
err
!=
nil
{
return
nil
,
err
}
log
.
Printf
(
"configuration: %+v"
,
config
)
if
err
:=
config
.
compile
();
err
!=
nil
{
return
nil
,
err
}
return
&
config
,
nil
}
...
...
@@ -183,6 +334,17 @@ func (s *Server) getServiceConfig(service string) (*ServiceConfig, bool) {
return
c
,
ok
}
func
(
s
*
Server
)
getUser
(
ctx
context
.
Context
,
serviceConfig
*
ServiceConfig
,
username
string
)
(
*
User
,
bool
)
{
for
_
,
spec
:=
range
serviceConfig
.
BackendSpecs
{
for
_
,
b
:=
range
s
.
backends
{
if
user
,
ok
:=
b
.
GetUser
(
ctx
,
spec
,
username
);
ok
{
return
user
,
true
}
}
}
return
nil
,
false
}
// Authenticate a user with the parameters specified in the incoming AuthRequest.
func
(
s
*
Server
)
Authenticate
(
ctx
context
.
Context
,
req
*
auth
.
Request
)
*
auth
.
Response
{
serviceConfig
,
ok
:=
s
.
getServiceConfig
(
req
.
Service
)
...
...
@@ -191,22 +353,28 @@ func (s *Server) Authenticate(ctx context.Context, req *auth.Request) *auth.Resp
return
newError
()
}
var
user
*
User
outer
:
for
_
,
spec
:=
range
serviceConfig
.
BackendSpecs
{
for
_
,
b
:=
range
s
.
backends
{
if
user
,
ok
=
b
.
GetUser
(
ctx
,
spec
,
req
.
Username
);
ok
{
break
outer
}
}
}
user
,
ok
:=
s
.
getUser
(
ctx
,
serviceConfig
,
req
.
Username
)
if
!
ok
{
// User is unknown to all backends.
log
.
Printf
(
"unknown user %s"
,
req
.
Username
)
return
newError
()
}
// Apply rate limiting and blacklisting _before_ invoking the
// authentication handlers, as they may be CPU intensive.
if
allowed
:=
serviceConfig
.
checkRateLimits
(
user
,
req
);
!
allowed
{
return
newError
()
}
resp
:=
s
.
authenticateUser
(
req
,
serviceConfig
,
user
)
// Notify blacklists of the result.
serviceConfig
.
notifyBlacklists
(
user
,
req
,
resp
)
return
resp
}
func
(
s
*
Server
)
authenticateUser
(
req
*
auth
.
Request
,
serviceConfig
*
ServiceConfig
,
user
*
User
)
*
auth
.
Response
{
// Verify different credentials depending on whether the user
// has 2FA enabled or not, and on whether the service itself
// supports challenge-response authentication.
...
...
@@ -218,7 +386,16 @@ outer:
resp
=
s
.
authenticateUserWithASP
(
user
,
req
)
}
}
else
{
resp
=
s
.
authenticateUser
(
user
,
req
)
resp
=
s
.
authenticateUserWithPassword
(
user
,
req
)
}
// Process the response through filters (device info checks,
// etc) that may or may not change the response itself.
for
_
,
f
:=
range
serviceConfig
.
filters
{
if
resp
.
Status
==
auth
.
StatusError
{
break
}
resp
=
f
(
user
,
req
,
resp
)
}
// If the response is successful, augment it with user information.
...
...
@@ -229,7 +406,7 @@ outer:
return
resp
}
func
(
s
*
Server
)
authenticateUser
(
user
*
User
,
req
*
auth
.
Request
)
*
auth
.
Response
{
func
(
s
*
Server
)
authenticateUser
WithPassword
(
user
*
User
,
req
*
auth
.
Request
)
*
auth
.
Response
{
// Ok we only need to check the password here.
if
checkPassword
(
req
.
Password
,
user
.
EncryptedPassword
)
{
return
newOK
()
...
...
server/authserver_test.go
View file @
92aea0b7
...
...
@@ -89,6 +89,24 @@ services:
backends:
- { file: users.yml }
`
testConfigStrWithRatelimit
=
`---
enabled_backends:
- file
services:
test:
backends:
- { file: users.yml }
rate_limits:
- failed_login_bl
rate_limits:
failed_login_bl:
limit: 10
period: 300
blacklist_for: 3600
on_failure: true
keys: [user]
`
)
func
runAuthenticationTest
(
t
*
testing
.
T
,
client
client
.
Client
)
{
...
...
@@ -160,3 +178,56 @@ func TestAuthServer(t *testing.T) {
defer
s
.
Close
()
runAuthenticationTest
(
t
,
&
clientAdapter
{
s
.
srv
})
}
func
TestAuthServer_Blacklist
(
t
*
testing
.
T
)
{
s
:=
createTestServer
(
t
,
map
[
string
]
string
{
"users.yml"
:
testUsersFileStr
,
"config.yml"
:
testConfigStrWithRatelimit
,
})
defer
s
.
Close
()
c
:=
&
clientAdapter
{
s
.
srv
}
// Trigger the failed login blacklist, then verify that the
// user is blacklisted even when trying with the right password.
for
i
:=
0
;
i
<
100
;
i
++
{
c
.
Authenticate
(
context
.
Background
(),
&
auth
.
Request
{
Service
:
"test"
,
Username
:
"testuser"
,
Password
:
[]
byte
(
"bad_password"
),
})
}
resp
,
_
:=
c
.
Authenticate
(
context
.
Background
(),
&
auth
.
Request
{
Service
:
"test"
,
Username
:
"testuser"
,
Password
:
[]
byte
(
"password"
),
})
if
resp
.
Status
!=
auth
.
StatusError
{
t
.
Fatalf
(
"user was not blacklisted: %v"
,
resp
)
}
}
func
TestAuthServer_Blacklist_BelowLimit
(
t
*
testing
.
T
)
{
s
:=
createTestServer
(
t
,
map
[
string
]
string
{
"users.yml"
:
testUsersFileStr
,
"config.yml"
:
testConfigStrWithRatelimit
,
})
defer
s
.
Close
()
c
:=
&
clientAdapter
{
s
.
srv
}
// A small number of failures should not trigger the blacklist.
for
i
:=
0
;
i
<
8
;
i
++
{
c
.
Authenticate
(
context
.
Background
(),
&
auth
.
Request
{
Service
:
"test"
,
Username
:
"testuser"
,
Password
:
[]
byte
(
"bad_password"
),
})
}
resp
,
_
:=
c
.
Authenticate
(
context
.
Background
(),
&
auth
.
Request
{
Service
:
"test"
,
Username
:
"testuser"
,
Password
:
[]
byte
(
"password"
),
})
if
resp
.
Status
!=
auth
.
StatusOK
{
t
.
Fatalf
(
"user was incorrectly blacklisted: %+v"
,
s
.
srv
.
config
.
Services
[
"test"
])
}
}
server/ratelimit.go
0 → 100644
View file @
92aea0b7
package
server
import
(
"sync"
"time"
)
// Try to use as little memory as possible for each entry: use a UNIX
// timestamp instead of a time.Time, and use an int32 as a saturating
// counter.
type
ratelimitDatum
struct
{
stamp
int64
counter
int32
}
func
(
d
ratelimitDatum
)
age
(
now
int64
)
int64
{
return
now
-
d
.
stamp
}
// Ratelimiter is a simple counter-based rate limiter, allowing the
// first N requests over each period of time T.
type
Ratelimiter
struct
{
limit
int32
period
int64
mx
sync
.
Mutex
c
map
[
string
]
ratelimitDatum
}
func
newRatelimiter
(
limit
,
period
int
)
*
Ratelimiter
{
r
:=
&
Ratelimiter
{
limit
:
int32
(
limit
),
period
:
int64
(
period
),
c
:
make
(
map
[
string
]
ratelimitDatum
),
}
go
r
.
expungeThread
()
return
r
}
// AllowIncr performs a check and an increment at the same time, while
// holding a mutex, so it is robust in face of concurrent requests.
func
(
r
*
Ratelimiter
)
AllowIncr
(
key
string
)
bool
{
if
key
==
""
{
return
true
}
r
.
mx
.
Lock
()
d
:=
r
.
get
(
key
)
var
allowed
bool
if
d
.
counter
<=
r
.
limit
{
allowed
=
true
d
.
counter
++
r
.
set
(
key
,
d
)
}
r
.
mx
.
Unlock
()
return
allowed
}
func
(
r
*
Ratelimiter
)
get
(
key
string
)
ratelimitDatum
{
now
:=
time
.
Now
()
.
Unix
()
d
,
ok
:=
r
.
c
[
key
]
if
!
ok
||
d
.
age
(
now
)
>
r
.
period
{
d
=
ratelimitDatum
{
stamp
:
now
}
r
.
c
[
key
]
=
d
}
return
d
}
func
(
r
*
Ratelimiter
)
set
(
key
string
,
d
ratelimitDatum
)
{
r
.
c
[
key
]
=
d
}
func
(
r
*
Ratelimiter
)
expunge
()
{
cutoff
:=
time
.
Now
()
.
Unix
()
-
2
*
r
.
period
r
.
mx
.
Lock
()
for
k
,
d
:=
range
r
.
c
{
if
d
.
stamp
<
cutoff
{
delete
(
r
.
c
,
k
)
}
}
r
.
mx
.
Unlock
()
}
var
ratelimitExpungePeriod
=
300
*
time
.
Second
func
(
r
*
Ratelimiter
)
expungeThread
()
{
for
range
time
.
NewTicker
(
ratelimitExpungePeriod
)
.
C
{
r
.
expunge
()
}
}
// Blacklist can blacklist keys whose request rate is above a
// specified threshold.
type
Blacklist
struct
{
r
*
Ratelimiter
bl
map
[
string
]
int64
blTime
int64
}
func
newBlacklist
(
limit
,
period
,
blacklistTime
int
)
*
Blacklist
{
return
&
Blacklist
{
r
:
newRatelimiter
(
limit
,
period
),
bl
:
make
(
map
[
string
]
int64
),
blTime
:
int64
(
blacklistTime
),
}
}
// Allow returns true if this request (identified by the given key)
// should be allowed.
func
(
b
*
Blacklist
)
Allow
(
key
string
)
bool
{
if
key
==
""
{
return
true
}
b
.
r
.
mx
.
Lock
()
deadline
,
ok
:=
b
.
bl
[
key
]
if
ok
&&
deadline
<
time
.
Now
()
.
Unix
()
{
delete
(
b
.
bl
,
key
)
ok
=
false
}
b
.
r
.
mx
.
Unlock
()
return
!
ok
}
// Incr increments the counter for the given key for the current time
// period.
func
(
b
*
Blacklist
)
Incr
(
key
string
)
{
if
key
==
""
{
return
}
// Count one higher than limit, and trigger the blacklist when
// we reach that.
limitp1
:=
b
.
r
.
limit
+
1
b
.
r
.
mx
.
Lock
()
d
:=
b
.
r
.
get
(
key
)
if
d
.
counter
<
limitp1
{
d
.
counter
++
b
.
r
.
set
(
key
,
d
)
}
else
if
d
.
counter
==
limitp1
{
b
.
bl
[
key
]
=
time
.
Now
()
.
Unix
()
+
b
.
blTime
}
b
.
r
.
mx
.
Unlock
()
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment