mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
cmd/cli: rate-limit PIN brute-force on control socket
Currently there is no limit on PIN attempts, allowing unlimited brute force if an attacker gains socket access. While the socket is root-only by default, rate limiting is cheap defense-in-depth.
This commit is contained in:
1 parent
a61677b6e4
commit
01490434a6
2 files changed
+44
-4
No files matched your search
+22
-1
@@ -650,6 +650,19 @@ const defaultDeactivationPin = -1
|
|||||||
// cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run.
|
// cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run.
|
||||||
var cdDeactivationPin atomic.Int64
|
var cdDeactivationPin atomic.Int64
|
||||||
|
|
||||||
|
// Brute-force protection for the deactivation PIN endpoint on the control socket.
|
||||||
|
// After deactivationMaxFailedAttempts consecutive wrong PINs, further attempts are
|
||||||
|
// rejected for deactivationLockoutSeconds. Counter resets on a correct PIN.
|
||||||
|
const (
|
||||||
|
deactivationMaxFailedAttempts = 5
|
||||||
|
deactivationLockoutSeconds = 60
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
deactivationFailedAttempts atomic.Int64
|
||||||
|
deactivationLockedUntil atomic.Int64
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cdDeactivationPin.Store(defaultDeactivationPin)
|
cdDeactivationPin.Store(defaultDeactivationPin)
|
||||||
}
|
}
|
||||||
@@ -1788,6 +1801,9 @@ var errInvalidDeactivationPin = errors.New("deactivation pin is invalid")
|
|||||||
// errRequiredDeactivationPin indicates that the deactivation pin is required but not provided by users.
|
// errRequiredDeactivationPin indicates that the deactivation pin is required but not provided by users.
|
||||||
var errRequiredDeactivationPin = errors.New("deactivation pin is required to stop or uninstall the service")
|
var errRequiredDeactivationPin = errors.New("deactivation pin is required to stop or uninstall the service")
|
||||||
|
|
||||||
|
// errTooManyDeactivationPin represents an error indicating excessive deactivation PIN request attempts.
|
||||||
|
var errTooManyDeactivationPin = errors.New("too many request attempts")
|
||||||
|
|
||||||
// checkDeactivationPin validates if the deactivation pin matches one in ControlD config.
|
// checkDeactivationPin validates if the deactivation pin matches one in ControlD config.
|
||||||
func checkDeactivationPin(s service.Service, stopCh chan struct{}) error {
|
func checkDeactivationPin(s service.Service, stopCh chan struct{}) error {
|
||||||
mainLog.Load().Debug().Msg("Checking deactivation pin")
|
mainLog.Load().Debug().Msg("Checking deactivation pin")
|
||||||
@@ -1816,6 +1832,9 @@ func checkDeactivationPin(s service.Service, stopCh chan struct{}) error {
|
|||||||
case http.StatusBadRequest:
|
case http.StatusBadRequest:
|
||||||
mainLog.Load().Error().Msg(errRequiredDeactivationPin.Error())
|
mainLog.Load().Error().Msg(errRequiredDeactivationPin.Error())
|
||||||
return errRequiredDeactivationPin // pin is required
|
return errRequiredDeactivationPin // pin is required
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
mainLog.Load().Error().Msg(errTooManyDeactivationPin.Error())
|
||||||
|
return errTooManyDeactivationPin
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
return nil // valid pin
|
return nil // valid pin
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
@@ -1828,7 +1847,9 @@ func checkDeactivationPin(s service.Service, stopCh chan struct{}) error {
|
|||||||
|
|
||||||
// isCheckDeactivationPinErr reports whether there is an error during check deactivation pin process.
|
// isCheckDeactivationPinErr reports whether there is an error during check deactivation pin process.
|
||||||
func isCheckDeactivationPinErr(err error) bool {
|
func isCheckDeactivationPinErr(err error) bool {
|
||||||
return errors.Is(err, errInvalidDeactivationPin) || errors.Is(err, errRequiredDeactivationPin)
|
return errors.Is(err, errInvalidDeactivationPin) ||
|
||||||
|
errors.Is(err, errRequiredDeactivationPin) ||
|
||||||
|
errors.Is(err, errTooManyDeactivationPin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureUninstall ensures that s.Uninstall will remove ctrld service from system completely.
|
// ensureUninstall ensures that s.Uninstall will remove ctrld service from system completely.
|
||||||
|
|||||||
@@ -61,12 +61,18 @@ func newControlServer(addr string) (*controlServer, error) {
|
|||||||
func (s *controlServer) start() error {
|
func (s *controlServer) start() error {
|
||||||
_ = os.Remove(s.addr)
|
_ = os.Remove(s.addr)
|
||||||
unixListener, err := net.Listen("unix", s.addr)
|
unixListener, err := net.Listen("unix", s.addr)
|
||||||
if l, ok := unixListener.(*net.UnixListener); ok {
|
|
||||||
l.SetUnlinkOnClose(true)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Restrict socket permissions to owner-only (0600) so that only the
|
||||||
|
// process owner (typically root) can connect. Defense-in-depth since
|
||||||
|
// the control server endpoints carry no authentication of their own.
|
||||||
|
if err := os.Chmod(s.addr, 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if l, ok := unixListener.(*net.UnixListener); ok {
|
||||||
|
l.SetUnlinkOnClose(true)
|
||||||
|
}
|
||||||
go s.server.Serve(unixListener)
|
go s.server.Serve(unixListener)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -222,6 +228,13 @@ func (p *prog) registerControlServerHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loggerCtx := ctrld.LoggerCtx(context.Background(), p.logger.Load())
|
loggerCtx := ctrld.LoggerCtx(context.Background(), p.logger.Load())
|
||||||
|
|
||||||
|
// Reject further attempts while locked out due to repeated wrong PINs.
|
||||||
|
if now := time.Now().Unix(); now < deactivationLockedUntil.Load() {
|
||||||
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Re-fetch pin code from API.
|
// Re-fetch pin code from API.
|
||||||
rcReq := &controld.ResolverConfigRequest{
|
rcReq := &controld.ResolverConfigRequest{
|
||||||
RawUID: cdUID,
|
RawUID: cdUID,
|
||||||
@@ -255,6 +268,7 @@ func (p *prog) registerControlServerHandler() {
|
|||||||
switch req.Pin {
|
switch req.Pin {
|
||||||
case cdDeactivationPin.Load():
|
case cdDeactivationPin.Load():
|
||||||
code = http.StatusOK
|
code = http.StatusOK
|
||||||
|
deactivationFailedAttempts.Store(0)
|
||||||
select {
|
select {
|
||||||
case p.pinCodeValidCh <- struct{}{}:
|
case p.pinCodeValidCh <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -262,6 +276,11 @@ func (p *prog) registerControlServerHandler() {
|
|||||||
case defaultDeactivationPin:
|
case defaultDeactivationPin:
|
||||||
// If the pin code was set, but users do not provide --pin, return proper code to client.
|
// If the pin code was set, but users do not provide --pin, return proper code to client.
|
||||||
code = http.StatusBadRequest
|
code = http.StatusBadRequest
|
||||||
|
default:
|
||||||
|
if deactivationFailedAttempts.Add(1) >= deactivationMaxFailedAttempts {
|
||||||
|
deactivationLockedUntil.Store(time.Now().Unix() + deactivationLockoutSeconds)
|
||||||
|
deactivationFailedAttempts.Store(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in new issue
Block a user