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 "dry-run" backend mode, added connection tests with api key and tls #297

Merged
merged 4 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
6 changes: 6 additions & 0 deletions pkg/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
IptablesMode = "iptables"
NftablesMode = "nftables"
PfMode = "pf"
DryRunMode = "dry-run"
)

type BouncerConfig struct {
Expand Down Expand Up @@ -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)
}
Expand Down
46 changes: 46 additions & 0 deletions pkg/dryrun/dryrun.go
Original file line number Diff line number Diff line change
@@ -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
}
90 changes: 86 additions & 4 deletions test/bouncer/test_firewall_bouncer.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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 changes: 110 additions & 0 deletions test/bouncer/test_tls.py
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions test/bouncer/test_yaml_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'*",
Expand Down
1 change: 1 addition & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def closure(config_lapi=None, config_bouncer=None, api_key=None):


_default_config = {
'mode': 'dry-run',
'log_level': 'info',
}

Expand Down