diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 596c88f..a903d24 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -650,6 +650,19 @@ const defaultDeactivationPin = -1 // cdDeactivationPin is used in cd mode to decide whether stop and uninstall commands can be run. 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() { 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. 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. func checkDeactivationPin(s service.Service, stopCh chan struct{}) error { mainLog.Load().Debug().Msg("Checking deactivation pin") @@ -1816,6 +1832,9 @@ func checkDeactivationPin(s service.Service, stopCh chan struct{}) error { case http.StatusBadRequest: mainLog.Load().Error().Msg(errRequiredDeactivationPin.Error()) return errRequiredDeactivationPin // pin is required + case http.StatusTooManyRequests: + mainLog.Load().Error().Msg(errTooManyDeactivationPin.Error()) + return errTooManyDeactivationPin case http.StatusOK: return nil // valid pin 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. 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. diff --git a/cmd/cli/control_server.go b/cmd/cli/control_server.go index cd5d4b0..b716686 100644 --- a/cmd/cli/control_server.go +++ b/cmd/cli/control_server.go @@ -61,12 +61,18 @@ func newControlServer(addr string) (*controlServer, error) { func (s *controlServer) start() error { _ = os.Remove(s.addr) unixListener, err := net.Listen("unix", s.addr) - if l, ok := unixListener.(*net.UnixListener); ok { - l.SetUnlinkOnClose(true) - } if err != nil { 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) return nil } @@ -222,6 +228,13 @@ func (p *prog) registerControlServerHandler() { } 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. rcReq := &controld.ResolverConfigRequest{ RawUID: cdUID, @@ -255,6 +268,7 @@ func (p *prog) registerControlServerHandler() { switch req.Pin { case cdDeactivationPin.Load(): code = http.StatusOK + deactivationFailedAttempts.Store(0) select { case p.pinCodeValidCh <- struct{}{}: default: @@ -262,6 +276,11 @@ func (p *prog) registerControlServerHandler() { case defaultDeactivationPin: // If the pin code was set, but users do not provide --pin, return proper code to client. code = http.StatusBadRequest + default: + if deactivationFailedAttempts.Add(1) >= deactivationMaxFailedAttempts { + deactivationLockedUntil.Store(time.Now().Unix() + deactivationLockoutSeconds) + deactivationFailedAttempts.Store(0) + } } w.WriteHeader(code) }))