Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for centralized allowlists #3355

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
562 changes: 562 additions & 0 deletions cmd/crowdsec-cli/cliallowlists/allowlists.go

Large diffs are not rendered by default.

30 changes: 21 additions & 9 deletions cmd/crowdsec-cli/clidecision/decisions.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@
return cmd
}

func (cli *cliDecisions) add(ctx context.Context, addIP, addRange, addDuration, addValue, addScope, addReason, addType string) error {
//nolint:revive // we'll reduce the number of args later
func (cli *cliDecisions) add(ctx context.Context, addIP, addRange, addDuration, addValue, addScope, addReason, addType string, bypassAllowlist bool) error {
alerts := models.AddAlertsRequest{}
origin := types.CscliOrigin
capacity := int32(0)
Expand Down Expand Up @@ -350,6 +351,15 @@
addReason = fmt.Sprintf("manual '%s' from '%s'", addType, cli.cfg().API.Client.Credentials.Login)
}

if !bypassAllowlist && (addScope == types.Ip || addScope == types.Range) {
resp, _, err := cli.client.Allowlists.CheckIfAllowlistedWithReason(ctx, addValue)
if err != nil {
log.Errorf("Cannot check if %s is in allowlist: %s", addValue, err)
} else if resp.Allowlisted {
return fmt.Errorf("%s is allowlisted by item %s, use --bypass-allowlist to add the decision anyway", addValue, resp.Reason)
}

Check warning on line 360 in cmd/crowdsec-cli/clidecision/decisions.go

View check run for this annotation

Codecov / codecov/patch

cmd/crowdsec-cli/clidecision/decisions.go#L359-L360

Added lines #L359 - L360 were not covered by tests
}

decision := models.Decision{
Duration: &addDuration,
Scope: &addScope,
Expand Down Expand Up @@ -398,13 +408,14 @@

func (cli *cliDecisions) newAddCmd() *cobra.Command {
var (
addIP string
addRange string
addDuration string
addValue string
addScope string
addReason string
addType string
addIP string
addRange string
addDuration string
addValue string
addScope string
addReason string
addType string
bypassAllowlist bool
)

cmd := &cobra.Command{
Expand All @@ -419,7 +430,7 @@
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType)
return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType, bypassAllowlist)
},
}

Expand All @@ -432,6 +443,7 @@
flags.StringVar(&addScope, "scope", types.Ip, "Decision scope (ie. ip,range,username)")
flags.StringVarP(&addReason, "reason", "R", "", "Decision reason (ie. scenario-name)")
flags.StringVarP(&addType, "type", "t", "ban", "Decision type (ie. ban,captcha,throttle)")
flags.BoolVarP(&bypassAllowlist, "bypass-allowlist", "B", false, "Add decision even if value is in allowlist")

return cmd
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/crowdsec-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/trace"

"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clialert"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cliallowlists"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clibouncer"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clicapi"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cliconsole"
Expand Down Expand Up @@ -163,6 +165,8 @@ func (cli *cliRoot) initialize() error {
}
}

csConfig.DbConfig.LogLevel = ptr.Of(cli.wantedLogLevel())

return nil
}

Expand Down Expand Up @@ -279,6 +283,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
cmd.AddCommand(cliitem.NewContext(cli.cfg).NewCommand())
cmd.AddCommand(cliitem.NewAppsecConfig(cli.cfg).NewCommand())
cmd.AddCommand(cliitem.NewAppsecRule(cli.cfg).NewCommand())
cmd.AddCommand(cliallowlists.New(cli.cfg).NewCommand())

cli.addSetup(cmd)

Expand Down
16 changes: 14 additions & 2 deletions cmd/crowdsec/crowdsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
"github.com/crowdsecurity/crowdsec/pkg/alertcontext"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
Expand All @@ -23,7 +24,7 @@ import (
)

// initCrowdsec prepares the log processor service
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, []acquisition.DataSource, error) {
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub, testMode bool) (*parser.Parsers, []acquisition.DataSource, error) {
var err error

if err = alertcontext.LoadConsoleContext(cConfig, hub); err != nil {
Expand Down Expand Up @@ -51,6 +52,17 @@ func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, []
return nil, nil, err
}

if !testMode {
err = apiclient.InitLAPIClient(
context.TODO(), cConfig.API.Client.Credentials.URL, cConfig.API.Client.Credentials.PapiURL,
cConfig.API.Client.Credentials.Login, cConfig.API.Client.Credentials.Password,
hub.GetInstalledListForAPI())

if err != nil {
return nil, nil, fmt.Errorf("while initializing LAPIClient: %w", err)
}
}

datasources, err := LoadAcquisition(cConfig)
if err != nil {
return nil, nil, fmt.Errorf("while loading acquisition config: %w", err)
Expand Down Expand Up @@ -116,7 +128,7 @@ func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.H
})
bucketWg.Wait()

apiClient, err := AuthenticatedLAPIClient(context.TODO(), *cConfig.API.Client.Credentials, hub)
apiClient, err := apiclient.GetLAPIClient()
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/crowdsec/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
return nil, err
}

csParsers, datasources, err := initCrowdsec(cConfig, hub)
csParsers, datasources, err := initCrowdsec(cConfig, hub, false)

Check warning on line 97 in cmd/crowdsec/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/crowdsec/serve.go#L97

Added line #L97 was not covered by tests
if err != nil {
return nil, fmt.Errorf("unable to init crowdsec: %w", err)
}
Expand Down Expand Up @@ -396,7 +396,7 @@
return err
}

csParsers, datasources, err := initCrowdsec(cConfig, hub)
csParsers, datasources, err := initCrowdsec(cConfig, hub, flags.TestMode)
if err != nil {
return fmt.Errorf("crowdsec init: %w", err)
}
Expand Down
116 changes: 104 additions & 12 deletions pkg/acquisition/modules/appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"github.com/crowdsecurity/go-cs-lib/trace"

"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/appsec"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/types"
Expand All @@ -31,6 +32,8 @@
)

var DefaultAuthCacheDuration = (1 * time.Minute)
var negativeAllowlistCacheDuration = (5 * time.Minute)
var positiveAllowlistCacheDuration = (5 * time.Minute)

// configuration structure of the acquis for the application security engine
type AppsecSourceConfig struct {
Expand All @@ -49,18 +52,20 @@

// runtime structure of AppsecSourceConfig
type AppsecSource struct {
metricsLevel int
config AppsecSourceConfig
logger *log.Entry
mux *http.ServeMux
server *http.Server
outChan chan types.Event
InChan chan appsec.ParsedRequest
AppsecRuntime *appsec.AppsecRuntimeConfig
AppsecConfigs map[string]appsec.AppsecConfig
lapiURL string
AuthCache AuthCache
AppsecRunners []AppsecRunner // one for each go-routine
metricsLevel int
config AppsecSourceConfig
logger *log.Entry
mux *http.ServeMux
server *http.Server
outChan chan types.Event
InChan chan appsec.ParsedRequest
AppsecRuntime *appsec.AppsecRuntimeConfig
AppsecConfigs map[string]appsec.AppsecConfig
lapiURL string
AuthCache AuthCache
AppsecRunners []AppsecRunner // one for each go-routine
allowlistCache allowlistCache
apiClient *apiclient.ApiClient
}

// Struct to handle cache of authentication
Expand All @@ -69,6 +74,17 @@
mu sync.RWMutex
}

// FIXME: auth and allowlist should probably be merged to a common structure
type allowlistCache struct {
mu sync.RWMutex
allowlist map[string]allowlistCacheEntry
}

type allowlistCacheEntry struct {
allowlisted bool
expiration time.Time
}

func NewAuthCache() AuthCache {
return AuthCache{
APIKeys: make(map[string]time.Time, 0),
Expand All @@ -90,6 +106,30 @@
return expiration, exists
}

func NewAllowlistCache() allowlistCache {
return allowlistCache{
allowlist: make(map[string]allowlistCacheEntry, 0),
mu: sync.RWMutex{},
}

Check warning on line 113 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L109-L113

Added lines #L109 - L113 were not covered by tests
}

func (ac *allowlistCache) Set(value string, allowlisted bool, expiration time.Time) {
ac.mu.Lock()
ac.allowlist[value] = allowlistCacheEntry{
allowlisted: allowlisted,
expiration: expiration,
}
ac.mu.Unlock()

Check warning on line 122 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L116-L122

Added lines #L116 - L122 were not covered by tests
}

func (ac *allowlistCache) Get(value string) (bool, time.Time, bool) {
ac.mu.RLock()
entry, exists := ac.allowlist[value]
ac.mu.RUnlock()

return entry.allowlisted, entry.expiration, exists

Check warning on line 130 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L125-L130

Added lines #L125 - L130 were not covered by tests
}

// @tko + @sbl : we might want to get rid of that or improve it
type BodyResponse struct {
Action string `json:"action"`
Expand Down Expand Up @@ -246,6 +286,12 @@
// We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
w.mux.HandleFunc(w.config.Path, w.appsecHandler)

w.apiClient, err = apiclient.GetLAPIClient()
if err != nil {
return fmt.Errorf("unable to get authenticated LAPI client: %w", err)
}
w.allowlistCache = NewAllowlistCache()

Check warning on line 294 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L289-L294

Added lines #L289 - L294 were not covered by tests
return nil
}

Expand Down Expand Up @@ -364,6 +410,33 @@
return resp.StatusCode == http.StatusOK
}

func (w *AppsecSource) isAllowlisted(ctx context.Context, value string, query bool) bool {
var err error

allowlisted, expiration, exists := w.allowlistCache.Get(value)
if exists && !time.Now().After(expiration) {
return allowlisted
}

Check warning on line 419 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L413-L419

Added lines #L413 - L419 were not covered by tests

if !query {
return false
}

Check warning on line 423 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L421-L423

Added lines #L421 - L423 were not covered by tests

allowlisted, _, err = w.apiClient.Allowlists.CheckIfAllowlisted(ctx, value)
if err != nil {
w.logger.Errorf("unable to check if %s is allowlisted: %s", value, err)
return false
}

Check warning on line 429 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L425-L429

Added lines #L425 - L429 were not covered by tests

if allowlisted {
w.allowlistCache.Set(value, allowlisted, time.Now().Add(positiveAllowlistCacheDuration))
} else {
w.allowlistCache.Set(value, allowlisted, time.Now().Add(negativeAllowlistCacheDuration))
}

Check warning on line 435 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L431-L435

Added lines #L431 - L435 were not covered by tests

return allowlisted

Check warning on line 437 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L437

Added line #L437 was not covered by tests
}

// should this be in the runner ?
func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
w.logger.Debugf("Received request from '%s' on %s", r.RemoteAddr, r.URL.Path)
Expand All @@ -389,6 +462,25 @@
w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration))
}

// check if the client IP is allowlisted
if w.isAllowlisted(r.Context(), clientIP, false) {
w.logger.Infof("%s is allowlisted by LAPI, not processing", clientIP)
statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(appsec.AppsecTempResponse{
InBandInterrupt: false,
OutOfBandInterrupt: false,
Action: appsec.AllowRemediation,
}, w.logger)
body, err := json.Marshal(appsecResponse)
if err != nil {
w.logger.Errorf("unable to serialize response: %s", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(statusCode)
rw.Write(body)
return

Check warning on line 481 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L466-L481

Added lines #L466 - L481 were not covered by tests
}

// parse the request only once
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func (lc *LokiClient) getURLFor(endpoint string, params map[string]string) strin
func (lc *LokiClient) Ready(ctx context.Context) error {
tick := time.NewTicker(500 * time.Millisecond)
url := lc.getURLFor("ready", nil)
lc.Logger.Debugf("Using url: %s for ready check", url)
for {
select {
case <-ctx.Done():
Expand Down
Loading
Loading