add "dry-run" backend mode, added connection tests with api key and tls (#297)

This commit is contained in:
mmetc
2023-06-12 15:57:09 +02:00
committed by GitHub
parent e427ca2849
commit 1fcde2def0
10 changed files with 269 additions and 18 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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
View 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
}

View File

@@ -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
View 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

View File

@@ -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'*",

View File

@@ -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',
}