mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-11 12:25:53 +02:00
appsec: support custom CA for lapi (#3503)
* apisever, appsec: refact listenAndServe..() * RemoveAll() -> Remove() * configure CA for tls auth request * ignore error from os.Remove(socket) when there's no file * appsec functional test * lint
This commit is contained in:
parent
9bb7ad8c3a
commit
a432a6352d
3 changed files with 354 additions and 88 deletions
|
@ -2,9 +2,12 @@ package appsecacquisition
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -64,6 +67,7 @@ type AppsecSource struct {
|
|||
AuthCache AuthCache
|
||||
AppsecRunners []AppsecRunner // one for each go-routine
|
||||
appsecAllowlistClient *allowlists.AppsecAllowlist
|
||||
lapiCACertPool *x509.CertPool
|
||||
}
|
||||
|
||||
// Struct to handle cache of authentication
|
||||
|
@ -158,6 +162,28 @@ func (w *AppsecSource) GetAggregMetrics() []prometheus.Collector {
|
|||
return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
|
||||
}
|
||||
|
||||
func loadCertPool(caCertPath string, logger log.FieldLogger) (*x509.CertPool, error) {
|
||||
caCertPool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
logger.Warnf("Error loading system CA certificates: %s", err)
|
||||
}
|
||||
|
||||
if caCertPool == nil {
|
||||
caCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
if caCertPath != "" {
|
||||
caCert, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while opening cert file: %w", err)
|
||||
}
|
||||
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
}
|
||||
|
||||
return caCertPool, nil
|
||||
}
|
||||
|
||||
func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLevel int) error {
|
||||
err := w.UnmarshalConfig(yamlConfig)
|
||||
if err != nil {
|
||||
|
@ -241,8 +267,7 @@ func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
|
|||
appsecAllowlistsClient: w.appsecAllowlistClient,
|
||||
}
|
||||
|
||||
err := runner.Init(appsecCfg.GetDataDir())
|
||||
if err != nil {
|
||||
if err = runner.Init(appsecCfg.GetDataDir()); err != nil {
|
||||
return fmt.Errorf("unable to initialize runner: %w", err)
|
||||
}
|
||||
|
||||
|
@ -254,6 +279,19 @@ func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
|
|||
// 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)
|
||||
|
||||
csConfig := csconfig.GetConfig()
|
||||
|
||||
caCertPath := ""
|
||||
|
||||
if csConfig.API.Server.TLS != nil {
|
||||
caCertPath = csConfig.API.Server.TLS.CACertPath
|
||||
}
|
||||
|
||||
w.lapiCACertPool, err = loadCertPool(caCertPath, w.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load LAPI CA cert pool: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -273,6 +311,103 @@ func (w *AppsecSource) OneShotAcquisition(_ context.Context, _ chan types.Event,
|
|||
return errors.New("AppSec datasource does not support command line acquisition")
|
||||
}
|
||||
|
||||
func (w *AppsecSource) listenAndServe(ctx context.Context, t *tomb.Tomb) error {
|
||||
defer trace.CatchPanic("crowdsec/acquis/appsec/listenAndServe")
|
||||
|
||||
w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
|
||||
|
||||
serverError := make(chan error, 2)
|
||||
|
||||
startServer := func(listener net.Listener, canTLS bool) {
|
||||
var err error
|
||||
|
||||
if canTLS && (w.config.CertFilePath != "" || w.config.KeyFilePath != "") {
|
||||
if w.config.KeyFilePath == "" {
|
||||
serverError <- errors.New("missing TLS key file")
|
||||
return
|
||||
}
|
||||
|
||||
if w.config.CertFilePath == "" {
|
||||
serverError <- errors.New("missing TLS cert file")
|
||||
return
|
||||
}
|
||||
|
||||
err = w.server.ServeTLS(listener, w.config.CertFilePath, w.config.KeyFilePath)
|
||||
} else {
|
||||
err = w.server.Serve(listener)
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, http.ErrServerClosed):
|
||||
break
|
||||
case err != nil:
|
||||
serverError <- err
|
||||
}
|
||||
}
|
||||
|
||||
// Starting Unix socket listener
|
||||
go func(socket string) {
|
||||
if socket == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(w.config.ListenSocket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
w.logger.Errorf("can't remove socket %s: %s", socket, err)
|
||||
}
|
||||
}
|
||||
|
||||
w.logger.Infof("creating unix socket %s", socket)
|
||||
|
||||
listener, err := net.Listen("unix", socket)
|
||||
if err != nil {
|
||||
serverError <- fmt.Errorf("appsec server failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.logger.Infof("Appsec listening on Unix socket %s", socket)
|
||||
startServer(listener, false)
|
||||
}(w.config.ListenSocket)
|
||||
|
||||
// Starting TCP listener
|
||||
go func(url string) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", url)
|
||||
if err != nil {
|
||||
serverError <- fmt.Errorf("listening on %s: %w", url, err)
|
||||
}
|
||||
|
||||
w.logger.Infof("Appsec listening on %s", url)
|
||||
startServer(listener, true)
|
||||
}(w.config.ListenAddr)
|
||||
|
||||
select {
|
||||
case err := <-serverError:
|
||||
return err
|
||||
case <-t.Dying():
|
||||
w.logger.Info("Shutting down Appsec server")
|
||||
// xx let's clean up the appsec runners :)
|
||||
appsec.AppsecRulesDetails = make(map[int]appsec.RulesDetails)
|
||||
|
||||
if err := w.server.Shutdown(ctx); err != nil {
|
||||
w.logger.Errorf("Error shutting down Appsec server: %s", err.Error())
|
||||
}
|
||||
|
||||
if w.config.ListenSocket != "" {
|
||||
if err := os.Remove(w.config.ListenSocket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
w.logger.Errorf("can't remove socket %s: %s", w.config.ListenSocket, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error {
|
||||
w.outChan = out
|
||||
|
||||
|
@ -285,13 +420,12 @@ func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch allowlists: %w", err)
|
||||
}
|
||||
|
||||
w.appsecAllowlistClient.StartRefresh(ctx, t)
|
||||
|
||||
t.Go(func() error {
|
||||
defer trace.CatchPanic("crowdsec/acquis/appsec/live")
|
||||
|
||||
w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
|
||||
|
||||
for _, runner := range w.AppsecRunners {
|
||||
runner.outChan = out
|
||||
|
||||
|
@ -301,60 +435,7 @@ func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.
|
|||
})
|
||||
}
|
||||
|
||||
t.Go(func() error {
|
||||
if w.config.ListenSocket != "" {
|
||||
w.logger.Infof("creating unix socket %s", w.config.ListenSocket)
|
||||
_ = os.RemoveAll(w.config.ListenSocket)
|
||||
|
||||
listener, err := net.Listen("unix", w.config.ListenSocket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("appsec server failed: %w", err)
|
||||
}
|
||||
|
||||
defer listener.Close()
|
||||
|
||||
if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
|
||||
err = w.server.ServeTLS(listener, w.config.CertFilePath, w.config.KeyFilePath)
|
||||
} else {
|
||||
err = w.server.Serve(listener)
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("appsec server failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
t.Go(func() error {
|
||||
var err error
|
||||
|
||||
if w.config.ListenAddr != "" {
|
||||
w.logger.Infof("creating TCP server on %s", w.config.ListenAddr)
|
||||
|
||||
if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
|
||||
err = w.server.ListenAndServeTLS(w.config.CertFilePath, w.config.KeyFilePath)
|
||||
} else {
|
||||
err = w.server.ListenAndServe()
|
||||
}
|
||||
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("appsec server failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
<-t.Dying()
|
||||
w.logger.Info("Shutting down Appsec server")
|
||||
// xx let's clean up the appsec runners :)
|
||||
appsec.AppsecRulesDetails = make(map[int]appsec.RulesDetails)
|
||||
|
||||
if err := w.server.Shutdown(ctx); err != nil {
|
||||
w.logger.Errorf("Error shutting down Appsec server: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
return w.listenAndServe(ctx, t)
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -373,21 +454,29 @@ func (w *AppsecSource) Dump() interface{} {
|
|||
}
|
||||
|
||||
func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
|
||||
client := &http.Client{
|
||||
Timeout: 200 * time.Millisecond,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, w.lapiURL, http.NoBody)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating request: %s", err)
|
||||
w.logger.Errorf("Error creating request: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Add("X-Api-Key", apiKey)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 200 * time.Millisecond,
|
||||
}
|
||||
|
||||
if w.lapiCACertPool != nil {
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: w.lapiCACertPool,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Errorf("Error performing request: %s", err)
|
||||
w.logger.Errorf("Error performing request: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -407,15 +408,11 @@ func (s *APIServer) Run(apiReady chan bool) error {
|
|||
// it also updates the URL field with the actual address the server is listening on
|
||||
// it's meant to be run in a separate goroutine
|
||||
func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
|
||||
var (
|
||||
tcpListener net.Listener
|
||||
unixListener net.Listener
|
||||
err error
|
||||
serverError = make(chan error, 2)
|
||||
listenerClosed = make(chan struct{})
|
||||
)
|
||||
serverError := make(chan error, 2)
|
||||
|
||||
startServer := func(listener net.Listener, canTLS bool) {
|
||||
var err error
|
||||
|
||||
if canTLS && s.TLS != nil && (s.TLS.CertFilePath != "" || s.TLS.KeyFilePath != "") {
|
||||
if s.TLS.KeyFilePath == "" {
|
||||
serverError <- errors.New("missing TLS key file")
|
||||
|
@ -441,38 +438,42 @@ func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
|
|||
}
|
||||
|
||||
// Starting TCP listener
|
||||
go func() {
|
||||
if s.URL == "" {
|
||||
go func(url string) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tcpListener, err = net.Listen("tcp", s.URL)
|
||||
listener, err := net.Listen("tcp", url)
|
||||
if err != nil {
|
||||
serverError <- fmt.Errorf("listening on %s: %w", s.URL, err)
|
||||
serverError <- fmt.Errorf("listening on %s: %w", url, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("CrowdSec Local API listening on %s", s.URL)
|
||||
startServer(tcpListener, true)
|
||||
}()
|
||||
log.Infof("CrowdSec Local API listening on %s", url)
|
||||
startServer(listener, true)
|
||||
}(s.URL)
|
||||
|
||||
// Starting Unix socket listener
|
||||
go func() {
|
||||
if s.UnixSocket == "" {
|
||||
go func(socket string) {
|
||||
if socket == "" {
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.RemoveAll(s.UnixSocket)
|
||||
if err := os.Remove(socket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
log.Errorf("can't remove socket %s: %s", socket, err)
|
||||
}
|
||||
}
|
||||
|
||||
unixListener, err = net.Listen("unix", s.UnixSocket)
|
||||
listener, err := net.Listen("unix", socket)
|
||||
if err != nil {
|
||||
serverError <- fmt.Errorf("while creating unix listener: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("CrowdSec Local API listening on Unix socket %s", s.UnixSocket)
|
||||
startServer(unixListener, false)
|
||||
}()
|
||||
log.Infof("CrowdSec Local API listening on Unix socket %s", socket)
|
||||
startServer(listener, false)
|
||||
}(s.UnixSocket)
|
||||
|
||||
apiReady <- true
|
||||
|
||||
|
@ -489,10 +490,12 @@ func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
|
|||
log.Errorf("while shutting down http server: %v", err)
|
||||
}
|
||||
|
||||
close(listenerClosed)
|
||||
case <-listenerClosed:
|
||||
if s.UnixSocket != "" {
|
||||
_ = os.RemoveAll(s.UnixSocket)
|
||||
if err := os.Remove(s.UnixSocket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
log.Errorf("can't remove socket %s: %s", s.UnixSocket, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
174
test/bats/appsec.bats
Normal file
174
test/bats/appsec.bats
Normal file
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
set -u
|
||||
|
||||
setup_file() {
|
||||
load "../lib/setup_file.sh"
|
||||
CONFIG_DIR=$(dirname "$CONFIG_YAML")
|
||||
export CONFIG_DIR
|
||||
|
||||
ACQUIS_DIR=$(config_get '.crowdsec_service.acquisition_dir')
|
||||
export ACQUIS_DIR
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load "../lib/teardown_file.sh"
|
||||
}
|
||||
|
||||
setup() {
|
||||
load "../lib/setup.sh"
|
||||
./instance-data load
|
||||
mkdir -p "$ACQUIS_DIR"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
./instance-crowdsec stop
|
||||
}
|
||||
|
||||
#----------
|
||||
|
||||
@test "invalid configuration" {
|
||||
config_set '.common.log_media="stdout"'
|
||||
|
||||
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
|
||||
source: appsec
|
||||
EOT
|
||||
|
||||
rune -1 wait-for "$CROWDSEC"
|
||||
assert_stderr --partial "crowdsec init: while loading acquisition config: missing labels in $ACQUIS_DIR/appsec.yaml (position 0)"
|
||||
|
||||
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
|
||||
source: appsec
|
||||
labels:
|
||||
type: appsec
|
||||
EOT
|
||||
|
||||
rune -1 wait-for "$CROWDSEC"
|
||||
assert_stderr --partial "crowdsec init: while loading acquisition config: while configuring datasource of type appsec from $ACQUIS_DIR/appsec.yaml (position 0): unable to parse appsec configuration: appsec_config or appsec_config_path must be set"
|
||||
}
|
||||
|
||||
@test "appsec allow and ban" {
|
||||
config_set '.common.log_media="stdout"'
|
||||
|
||||
rune -0 cscli collections install crowdsecurity/appsec-virtual-patching
|
||||
rune -0 cscli collections install crowdsecurity/appsec-generic-rules
|
||||
|
||||
socket="$BATS_TEST_TMPDIR"/sock
|
||||
|
||||
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
|
||||
source: appsec
|
||||
listen_socket: $socket
|
||||
labels:
|
||||
type: appsec
|
||||
appsec_config: crowdsecurity/appsec-default
|
||||
EOT
|
||||
|
||||
rune -0 wait-for \
|
||||
--err "Appsec Runner ready to process event" \
|
||||
"$CROWDSEC"
|
||||
|
||||
assert_stderr --partial "loading inband rule crowdsecurity/base-config"
|
||||
assert_stderr --partial "loading inband rule crowdsecurity/vpatch-*"
|
||||
assert_stderr --partial "loading inband rule crowdsecurity/generic-*"
|
||||
assert_stderr --partial "Created 1 appsec runners"
|
||||
assert_stderr --partial "Appsec Runner ready to process event"
|
||||
|
||||
./instance-crowdsec start
|
||||
|
||||
rune -0 cscli bouncers add appsecbouncer --key appkey
|
||||
|
||||
# appsec will perform a HEAD request to validate.
|
||||
# If it fails, check downstream with:
|
||||
#
|
||||
# lapisocket=$(config_get '.api.server.listen_socket')
|
||||
# rune -0 curl -sS --fail-with-body --unix-socket "$lapisocket" -H "X-Api-Key: appkey" "http://fakehost/v1/decisions/stream"
|
||||
# assert_json '{deleted:null,new:null}'
|
||||
|
||||
rune -0 curl -sS --fail-with-body --unix-socket "$socket" \
|
||||
-H "x-crowdsec-appsec-api-key: appkey" \
|
||||
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
|
||||
-H 'x-crowdsec-appsec-uri: /' \
|
||||
-H 'x-crowdsec-appsec-host: foo.com' \
|
||||
-H 'x-crowdsec-appsec-verb: GET' \
|
||||
'http://fakehost'
|
||||
|
||||
assert_json '{action:"allow",http_status:200}'
|
||||
|
||||
rune -22 curl -sS --fail-with-body --unix-socket "$socket" \
|
||||
-H "x-crowdsec-appsec-api-key: appkey" \
|
||||
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
|
||||
-H 'x-crowdsec-appsec-uri: /.env' \
|
||||
-H 'x-crowdsec-appsec-host: foo.com' \
|
||||
-H 'x-crowdsec-appsec-verb: GET' \
|
||||
'http://fakehost'
|
||||
|
||||
assert_json '{action:"ban",http_status:403}'
|
||||
}
|
||||
|
||||
@test "TLS connection to lapi, own CA" {
|
||||
tmpdir="$BATS_FILE_TMPDIR"
|
||||
|
||||
CFDIR="$BATS_TEST_DIRNAME/testdata/cfssl"
|
||||
|
||||
# Root CA
|
||||
cfssl gencert -loglevel 2 \
|
||||
--initca "$CFDIR/ca_root.json" \
|
||||
| cfssljson --bare "$tmpdir/root"
|
||||
|
||||
# Intermediate CA
|
||||
cfssl gencert -loglevel 2 \
|
||||
--initca "$CFDIR/ca_intermediate.json" \
|
||||
| cfssljson --bare "$tmpdir/inter"
|
||||
|
||||
cfssl sign -loglevel 2 \
|
||||
-ca "$tmpdir/root.pem" -ca-key "$tmpdir/root-key.pem" \
|
||||
-config "$CFDIR/profiles.json" -profile intermediate_ca "$tmpdir/inter.csr" \
|
||||
| cfssljson --bare "$tmpdir/inter"
|
||||
|
||||
# Server cert for crowdsec with the intermediate
|
||||
cfssl gencert -loglevel 2 \
|
||||
-ca "$tmpdir/inter.pem" -ca-key "$tmpdir/inter-key.pem" \
|
||||
-config "$CFDIR/profiles.json" -profile=server "$CFDIR/server.json" \
|
||||
| cfssljson --bare "$tmpdir/server"
|
||||
|
||||
cat "$tmpdir/root.pem" "$tmpdir/inter.pem" > "$tmpdir/bundle.pem"
|
||||
|
||||
export tmpdir
|
||||
config_set '
|
||||
.api.server.tls.cert_file=strenv(tmpdir) + "/server.pem" |
|
||||
.api.server.tls.key_file=strenv(tmpdir) + "/server-key.pem" |
|
||||
.api.server.tls.ca_cert_path=strenv(tmpdir) + "/inter.pem"
|
||||
'
|
||||
|
||||
rune -0 cscli collections install crowdsecurity/appsec-virtual-patching
|
||||
rune -0 cscli collections install crowdsecurity/appsec-generic-rules
|
||||
|
||||
socket="$BATS_TEST_TMPDIR"/sock
|
||||
|
||||
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
|
||||
source: appsec
|
||||
listen_socket: $socket
|
||||
labels:
|
||||
type: appsec
|
||||
appsec_config: crowdsecurity/appsec-default
|
||||
EOT
|
||||
|
||||
config_set "$CONFIG_DIR/local_api_credentials.yaml" '
|
||||
.url="https://127.0.0.1:8080" |
|
||||
.ca_cert_path=strenv(tmpdir) + "/bundle.pem"
|
||||
'
|
||||
|
||||
./instance-crowdsec start
|
||||
|
||||
rune -0 cscli bouncers add appsecbouncer --key appkey
|
||||
|
||||
rune -0 curl -sS --fail-with-body --unix-socket "$socket" \
|
||||
-H "x-crowdsec-appsec-api-key: appkey" \
|
||||
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
|
||||
-H 'x-crowdsec-appsec-uri: /' \
|
||||
-H 'x-crowdsec-appsec-host: foo.com' \
|
||||
-H 'x-crowdsec-appsec-verb: GET' \
|
||||
'http://fakehost'
|
||||
|
||||
assert_json '{action:"allow",http_status:200}'
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue