mirror of
https://github.com/crowdsecurity/cs-firewall-bouncer.git
synced 2024-08-19 01:18:49 +03:00
add "dry-run" backend mode, added connection tests with api key and tls (#297)
This commit is contained in:
17
cmd/root.go
17
cmd/root.go
@@ -158,18 +158,13 @@ func Execute() error {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
log.Infof("crowdsec-firewall-bouncer %s", version.String())
|
||||
log.Infof("Starting crowdsec-firewall-bouncer %s", version.String())
|
||||
|
||||
backend, err := backend.NewBackend(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *testConfig {
|
||||
log.Info("config is valid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = backend.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,11 +174,17 @@ func Execute() error {
|
||||
bouncer := &csbouncer.StreamBouncer{}
|
||||
err = bouncer.ConfigReader(bytes.NewReader(configBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to configure bouncer: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
bouncer.UserAgent = fmt.Sprintf("%s/%s", name, version.String())
|
||||
if err := bouncer.Init(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unable to configure bouncer: %w", err)
|
||||
}
|
||||
|
||||
if *testConfig {
|
||||
log.Info("config is valid")
|
||||
return nil
|
||||
}
|
||||
|
||||
if bouncer.InsecureSkipVerify != nil {
|
||||
|
||||
4
go.mod
4
go.mod
@@ -4,12 +4,12 @@ go 1.20
|
||||
|
||||
require (
|
||||
github.com/crowdsecurity/crowdsec v1.5.2
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.5
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.7
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.2
|
||||
github.com/google/nftables v0.0.0-20220808154552-2eca00135732
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/sirupsen/logrus v1.9.2
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sys v0.8.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
|
||||
8
go.sum
8
go.sum
@@ -15,8 +15,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/crowdsecurity/crowdsec v1.5.2 h1:2wl5ULsZlD8Du9PGe415x1fYRcOfVx95KI2Si0Qeb98=
|
||||
github.com/crowdsecurity/crowdsec v1.5.2/go.mod h1:R1wnz8wqV4r1teYt9Yc5PVTaBb37ug2yqCffIvXEuRw=
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.5 h1:vZ989qKUDTavycjGLjqm2M6UzXJpmLaq35UoaiF9474=
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.5/go.mod h1:ShrcSSYmzBTKnpqON9/UFvorDMhhn5mbeQC2HXCv7kE=
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.7 h1:uA2iTwiqZ6hDWONqjQI4uF5r2daGGCMEPtuW1DQGqug=
|
||||
github.com/crowdsecurity/go-cs-bouncer v0.0.7/go.mod h1:ShrcSSYmzBTKnpqON9/UFvorDMhhn5mbeQC2HXCv7kE=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.2 h1:+Tjmf/IclOXNzU9sxKVQvUl9CkMfbM60xQ0zA05NWps=
|
||||
github.com/crowdsecurity/go-cs-lib v0.0.2/go.mod h1:iznTJ19qLTYdZBcRb5RVDlcUdSlayBCivBkWsXlOY3g=
|
||||
github.com/crowdsecurity/grokky v0.2.1 h1:t4VYnDlAd0RjDM2SlILalbwfCrQxtJSMGdQOR0zwkE4=
|
||||
@@ -222,8 +222,8 @@ golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaE
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
|
||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/dryrun"
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/iptables"
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/nftables"
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/pf"
|
||||
@@ -89,6 +90,11 @@ func NewBackend(config *cfg.BouncerConfig) (*BackendCTX, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "dry-run":
|
||||
b.firewall, err = dryrun.NewDryRun(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return b, fmt.Errorf("firewall '%s' is not supported", config.Mode)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
IptablesMode = "iptables"
|
||||
NftablesMode = "nftables"
|
||||
PfMode = "pf"
|
||||
DryRunMode = "dry-run"
|
||||
)
|
||||
|
||||
type BouncerConfig struct {
|
||||
@@ -139,6 +140,8 @@ func NewConfig(reader io.Reader) (*BouncerConfig, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case DryRunMode:
|
||||
// nothing specific to do
|
||||
default:
|
||||
log.Warningf("unexpected %s mode", config.Mode)
|
||||
}
|
||||
|
||||
46
pkg/dryrun/dryrun.go
Normal file
46
pkg/dryrun/dryrun.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dryrun
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/cfg"
|
||||
"github.com/crowdsecurity/cs-firewall-bouncer/pkg/types"
|
||||
)
|
||||
|
||||
type dryRun struct {
|
||||
}
|
||||
|
||||
func NewDryRun(config *cfg.BouncerConfig) (types.Backend, error) {
|
||||
return &dryRun{}, nil
|
||||
}
|
||||
|
||||
func (d *dryRun) Init() error {
|
||||
log.Infof("backend.Init() called")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dryRun) Commit() error {
|
||||
log.Infof("backend.Commit() called")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dryRun) Add(decision *models.Decision) error {
|
||||
log.Infof("backend.Add() called with %s", *decision.Value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dryRun) CollectMetrics() {
|
||||
log.Infof("backend.CollectMetrics() called")
|
||||
}
|
||||
|
||||
func (d *dryRun) Delete(decision *models.Decision) error {
|
||||
log.Infof("backend.Delete() called with %s", *decision.Value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dryRun) ShutDown() error {
|
||||
log.Infof("backend.ShutDown() called")
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
|
||||
def test_partial_config(crowdsec, bouncer, fw_cfg_factory):
|
||||
import json
|
||||
|
||||
def test_backend_mode(bouncer, fw_cfg_factory):
|
||||
cfg = fw_cfg_factory()
|
||||
|
||||
del cfg['mode']
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
# XXX: improve this message
|
||||
"*unable to load configuration: config does not contain 'mode'*",
|
||||
])
|
||||
fw.proc.wait(timeout=0.2)
|
||||
@@ -19,5 +22,84 @@ def test_partial_config(crowdsec, bouncer, fw_cfg_factory):
|
||||
fw.proc.wait(timeout=0.2)
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
# cfg['mode'] = 'pf'
|
||||
cfg['api_key'] = ''
|
||||
cfg['mode'] = 'dry-run'
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*Starting crowdsec-firewall-bouncer*",
|
||||
"*backend type : dry-run*",
|
||||
"*backend.Init() called*",
|
||||
"*unable to configure bouncer: config does not contain LAPI url*",
|
||||
])
|
||||
fw.proc.wait(timeout=0.2)
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
|
||||
def test_api_url(crowdsec, bouncer, fw_cfg_factory):
|
||||
cfg = fw_cfg_factory()
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*unable to configure bouncer: config does not contain LAPI url*",
|
||||
])
|
||||
fw.proc.wait()
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
cfg['api_url'] = ''
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*unable to configure bouncer: config does not contain LAPI url*",
|
||||
])
|
||||
fw.proc.wait()
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
|
||||
def test_api_key(crowdsec, bouncer, fw_cfg_factory, api_key_factory, bouncer_under_test):
|
||||
api_key = api_key_factory()
|
||||
env = {
|
||||
'BOUNCER_KEY_firewall': api_key
|
||||
}
|
||||
|
||||
with crowdsec(environment=env) as lapi:
|
||||
lapi.wait_for_http(8080, '/health')
|
||||
port = lapi.probe.get_bound_port('8080')
|
||||
|
||||
cfg = fw_cfg_factory()
|
||||
cfg['api_url'] = f'http://localhost:{port}'
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*unable to configure bouncer: config does not contain LAPI key or certificate*",
|
||||
])
|
||||
fw.proc.wait()
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
cfg['api_key'] = 'badkey'
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*Using API key auth*",
|
||||
"*API error: access forbidden*",
|
||||
"*process terminated with error: bouncer stream halted*",
|
||||
])
|
||||
fw.proc.wait()
|
||||
assert not fw.proc.is_running()
|
||||
|
||||
cfg['api_key'] = api_key
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*Using API key auth*",
|
||||
"*Processing new and deleted decisions*",
|
||||
])
|
||||
assert fw.proc.is_running()
|
||||
|
||||
# check that the bouncer is registered
|
||||
res = lapi.cont.exec_run('cscli bouncers list -o json')
|
||||
assert res.exit_code == 0
|
||||
bouncers = json.loads(res.output)
|
||||
assert len(bouncers) == 1
|
||||
assert bouncers[0]['name'] == 'firewall'
|
||||
assert bouncers[0]['auth_type'] == 'api-key'
|
||||
assert bouncers[0]['type'] == bouncer_under_test
|
||||
|
||||
110
test/bouncer/test_tls.py
Normal file
110
test/bouncer/test_tls.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import json
|
||||
|
||||
def test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factory):
|
||||
"""TLS with server-only certificate"""
|
||||
|
||||
api_key = api_key_factory()
|
||||
|
||||
lapi_env = {
|
||||
'CACERT_FILE': '/etc/ssl/crowdsec/ca.crt',
|
||||
'LAPI_CERT_FILE': '/etc/ssl/crowdsec/lapi.crt',
|
||||
'LAPI_KEY_FILE': '/etc/ssl/crowdsec/lapi.key',
|
||||
'USE_TLS': 'true',
|
||||
'LOCAL_API_URL': 'https://localhost:8080',
|
||||
'BOUNCER_KEY_custom': api_key,
|
||||
}
|
||||
|
||||
certs = certs_dir(lapi_hostname='lapi')
|
||||
|
||||
volumes = {
|
||||
certs: {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
|
||||
}
|
||||
|
||||
with crowdsec(environment=lapi_env, volumes=volumes) as cs:
|
||||
cs.wait_for_log("*CrowdSec Local API listening*")
|
||||
# TODO: wait_for_https
|
||||
cs.wait_for_http(8080, '/health', want_status=None)
|
||||
|
||||
port = cs.probe.get_bound_port('8080')
|
||||
cfg = fw_cfg_factory()
|
||||
cfg['api_url'] = f'https://localhost:{port}'
|
||||
cfg['api_key'] = api_key
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*Using API key auth*",
|
||||
"*auth-api: auth with api key failed*",
|
||||
"*tls: failed to verify certificate: x509: certificate signed by unknown authority*",
|
||||
])
|
||||
|
||||
cfg['ca_cert_path'] = (certs / 'ca.crt').as_posix()
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*Using CA cert *ca.crt*",
|
||||
"*Using API key auth*",
|
||||
"*Processing new and deleted decisions*",
|
||||
])
|
||||
|
||||
|
||||
def test_tls_mutual(crowdsec, certs_dir, api_key_factory, bouncer, fw_cfg_factory, bouncer_under_test):
|
||||
"""TLS with two-way bouncer/lapi authentication"""
|
||||
|
||||
lapi_env = {
|
||||
'CACERT_FILE': '/etc/ssl/crowdsec/ca.crt',
|
||||
'LAPI_CERT_FILE': '/etc/ssl/crowdsec/lapi.crt',
|
||||
'LAPI_KEY_FILE': '/etc/ssl/crowdsec/lapi.key',
|
||||
'USE_TLS': 'true',
|
||||
'LOCAL_API_URL': 'https://localhost:8080',
|
||||
}
|
||||
|
||||
certs = certs_dir(lapi_hostname='lapi')
|
||||
|
||||
volumes = {
|
||||
certs: {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'},
|
||||
}
|
||||
|
||||
with crowdsec(environment=lapi_env, volumes=volumes) as cs:
|
||||
cs.wait_for_log("*CrowdSec Local API listening*")
|
||||
# TODO: wait_for_https
|
||||
cs.wait_for_http(8080, '/health', want_status=None)
|
||||
|
||||
port = cs.probe.get_bound_port('8080')
|
||||
cfg = fw_cfg_factory()
|
||||
cfg['api_url'] = f'https://localhost:{port}'
|
||||
cfg['ca_cert_path'] = (certs / 'ca.crt').as_posix()
|
||||
|
||||
cfg['cert_path'] = (certs / 'agent.crt').as_posix()
|
||||
cfg['key_path'] = (certs / 'agent.key').as_posix()
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*Starting crowdsec-firewall-bouncer*",
|
||||
"*Using CA cert*",
|
||||
"*Using cert auth with cert * and key *",
|
||||
"*API error: access forbidden*",
|
||||
])
|
||||
|
||||
cs.wait_for_log("*client certificate OU (?agent-ou?) doesn't match expected OU (?bouncer-ou?)*")
|
||||
|
||||
cfg['cert_path'] = (certs / 'bouncer.crt').as_posix()
|
||||
cfg['key_path'] = (certs / 'bouncer.key').as_posix()
|
||||
|
||||
with bouncer(cfg) as cb:
|
||||
cb.wait_for_lines_fnmatch([
|
||||
"*backend type : dry-run*",
|
||||
"*Using CA cert*",
|
||||
"*Using cert auth with cert * and key *",
|
||||
"*Processing new and deleted decisions . . .*",
|
||||
])
|
||||
|
||||
# check that the bouncer is registered
|
||||
res = cs.cont.exec_run('cscli bouncers list -o json')
|
||||
assert res.exit_code == 0
|
||||
bouncers = json.loads(res.output)
|
||||
assert len(bouncers) == 1
|
||||
assert bouncers[0]['name'].startswith('@')
|
||||
assert bouncers[0]['auth_type'] == 'tls'
|
||||
assert bouncers[0]['type'] == bouncer_under_test
|
||||
@@ -2,6 +2,8 @@
|
||||
def test_yaml_local(bouncer, fw_cfg_factory):
|
||||
cfg = fw_cfg_factory()
|
||||
|
||||
cfg.pop('mode')
|
||||
|
||||
with bouncer(cfg) as fw:
|
||||
fw.wait_for_lines_fnmatch([
|
||||
"*unable to load configuration: config does not contain 'mode'*",
|
||||
|
||||
@@ -46,6 +46,7 @@ def bouncer_with_lapi(bouncer, crowdsec, fw_cfg_factory, api_key_factory, tmp_pa
|
||||
|
||||
|
||||
_default_config = {
|
||||
'mode': 'dry-run',
|
||||
'log_level': 'info',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user