Allow auto registration of machines in LAPI (#3202)

Co-authored-by: marco <marco@crowdsec.net>
This commit is contained in:
blotus 2024-09-02 13:13:40 +02:00 committed by GitHub
parent 8c0c10cd7a
commit d2616766de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 548 additions and 173 deletions

View file

@ -95,7 +95,7 @@ func (cli *cliLapi) Status(out io.Writer, hub *cwhub.Hub) error {
return nil
}
func (cli *cliLapi) register(apiURL string, outputFile string, machine string) error {
func (cli *cliLapi) register(apiURL string, outputFile string, machine string, token string) error {
var err error
lapiUser := machine
@ -116,11 +116,12 @@ func (cli *cliLapi) register(apiURL string, outputFile string, machine string) e
}
_, err = apiclient.RegisterClient(&apiclient.Config{
MachineID: lapiUser,
Password: password,
UserAgent: cwversion.UserAgent(),
URL: apiurl,
VersionPrefix: LAPIURLPrefix,
MachineID: lapiUser,
Password: password,
UserAgent: cwversion.UserAgent(),
RegistrationToken: token,
URL: apiurl,
VersionPrefix: LAPIURLPrefix,
}, nil)
if err != nil {
return fmt.Errorf("api client register: %w", err)
@ -138,10 +139,12 @@ func (cli *cliLapi) register(apiURL string, outputFile string, machine string) e
dumpFile = ""
}
apiCfg := csconfig.ApiCredentialsCfg{
Login: lapiUser,
Password: password.String(),
URL: apiURL,
apiCfg := cfg.API.Client.Credentials
apiCfg.Login = lapiUser
apiCfg.Password = password.String()
if apiURL != "" {
apiCfg.URL = apiURL
}
apiConfigDump, err := yaml.Marshal(apiCfg)
@ -212,6 +215,7 @@ func (cli *cliLapi) newRegisterCmd() *cobra.Command {
apiURL string
outputFile string
machine string
token string
)
cmd := &cobra.Command{
@ -222,7 +226,7 @@ Keep in mind the machine needs to be validated by an administrator on LAPI side
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.register(apiURL, outputFile, machine)
return cli.register(apiURL, outputFile, machine, token)
},
}
@ -230,6 +234,7 @@ Keep in mind the machine needs to be validated by an administrator on LAPI side
flags.StringVarP(&apiURL, "url", "u", "", "URL of the API (ie. http://127.0.0.1)")
flags.StringVarP(&outputFile, "file", "f", "", "output file destination")
flags.StringVar(&machine, "machine", "", "Name of the machine to register with")
flags.StringVar(&token, "token", "", "Auto registration token to use")
return cmd
}

View file

@ -177,7 +177,7 @@ func RegisterClient(config *Config, client *http.Client) (*ApiClient, error) {
c.Alerts = (*AlertsService)(&c.common)
c.Auth = (*AuthService)(&c.common)
resp, err := c.Auth.RegisterWatcher(context.Background(), models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password})
resp, err := c.Auth.RegisterWatcher(context.Background(), models.WatcherRegistrationRequest{MachineID: &config.MachineID, Password: &config.Password, RegistrationToken: config.RegistrationToken})
/*if we have http status, return it*/
if err != nil {
if resp != nil && resp.Response != nil {

View file

@ -7,12 +7,13 @@ import (
)
type Config struct {
MachineID string
Password strfmt.Password
Scenarios []string
URL *url.URL
PapiURL *url.URL
VersionPrefix string
UserAgent string
UpdateScenario func() ([]string, error)
MachineID string
Password strfmt.Password
Scenarios []string
URL *url.URL
PapiURL *url.URL
VersionPrefix string
UserAgent string
RegistrationToken string
UpdateScenario func() ([]string, error)
}

View file

@ -71,7 +71,7 @@ func InitMachineTest(t *testing.T) (*gin.Engine, models.WatcherAuthResponse, csc
}
func LoginToTestAPI(t *testing.T, router *gin.Engine, config csconfig.Config) models.WatcherAuthResponse {
body := CreateTestMachine(t, router)
body := CreateTestMachine(t, router, "")
ValidateMachine(t, "test", config.API.Server.DbConfig)
w := httptest.NewRecorder()

View file

@ -21,7 +21,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers"
"github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/database"
@ -235,6 +235,7 @@ func NewServer(config *csconfig.LocalApiServerCfg) (*APIServer, error) {
Log: clog,
ConsoleConfig: config.ConsoleConfig,
DisableRemoteLapiRegistration: config.DisableRemoteLapiRegistration,
AutoRegisterCfg: config.AutoRegister,
}
var (

View file

@ -29,10 +29,15 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/types"
)
const (
validRegistrationToken = "igheethauCaeteSaiyee3LosohPhahze"
invalidRegistrationToken = "vohl1feibechieG5coh8musheish2auj"
)
var (
testMachineID = "test"
testPassword = strfmt.Password("test")
MachineTest = models.WatcherAuthRequest{
MachineTest = models.WatcherRegistrationRequest{
MachineID: &testMachineID,
Password: &testPassword,
}
@ -65,6 +70,14 @@ func LoadTestConfig(t *testing.T) csconfig.Config {
ShareTaintedScenarios: new(bool),
ShareCustomScenarios: new(bool),
},
AutoRegister: &csconfig.LocalAPIAutoRegisterCfg{
Enable: ptr.Of(true),
Token: validRegistrationToken,
AllowedRanges: []string{
"127.0.0.1/8",
"::1/128",
},
},
}
apiConfig := csconfig.APICfg{
@ -75,6 +88,9 @@ func LoadTestConfig(t *testing.T) csconfig.Config {
err := config.API.Server.LoadProfiles()
require.NoError(t, err)
err = config.API.Server.LoadAutoRegister()
require.NoError(t, err)
return config
}
@ -113,6 +129,9 @@ func LoadTestConfigForwardedFor(t *testing.T) csconfig.Config {
err := config.API.Server.LoadProfiles()
require.NoError(t, err)
err = config.API.Server.LoadAutoRegister()
require.NoError(t, err)
return config
}
@ -251,8 +270,10 @@ func readDecisionsStreamResp(t *testing.T, resp *httptest.ResponseRecorder) (map
return response, resp.Code
}
func CreateTestMachine(t *testing.T, router *gin.Engine) string {
b, err := json.Marshal(MachineTest)
func CreateTestMachine(t *testing.T, router *gin.Engine, token string) string {
regReq := MachineTest
regReq.RegistrationToken = token
b, err := json.Marshal(regReq)
require.NoError(t, err)
body := string(b)

View file

@ -29,6 +29,7 @@ type Controller struct {
ConsoleConfig *csconfig.ConsoleConfig
TrustedIPs []net.IPNet
HandlerV1 *v1.Controller
AutoRegisterCfg *csconfig.LocalAPIAutoRegisterCfg
DisableRemoteLapiRegistration bool
}
@ -89,6 +90,7 @@ func (c *Controller) NewV1() error {
PluginChannel: c.PluginChannel,
ConsoleConfig: *c.ConsoleConfig,
TrustedIPs: c.TrustedIPs,
AutoRegisterCfg: c.AutoRegisterCfg,
}
c.HandlerV1, err = v1.New(&v1Config)

View file

@ -23,9 +23,10 @@ type Controller struct {
AlertsAddChan chan []*models.Alert
DecisionDeleteChan chan []*models.Decision
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
TrustedIPs []net.IPNet
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
TrustedIPs []net.IPNet
AutoRegisterCfg *csconfig.LocalAPIAutoRegisterCfg
}
type ControllerV1Config struct {
@ -36,9 +37,10 @@ type ControllerV1Config struct {
AlertsAddChan chan []*models.Alert
DecisionDeleteChan chan []*models.Decision
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
TrustedIPs []net.IPNet
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
TrustedIPs []net.IPNet
AutoRegisterCfg *csconfig.LocalAPIAutoRegisterCfg
}
func New(cfg *ControllerV1Config) (*Controller, error) {
@ -59,9 +61,10 @@ func New(cfg *ControllerV1Config) (*Controller, error) {
PluginChannel: cfg.PluginChannel,
ConsoleConfig: cfg.ConsoleConfig,
TrustedIPs: cfg.TrustedIPs,
AutoRegisterCfg: cfg.AutoRegisterCfg,
}
v1.Middlewares, err = middlewares.NewMiddlewares(cfg.DbClient)
v1.Middlewares, err = middlewares.NewMiddlewares(cfg.DbClient)
if err != nil {
return v1, err
}

View file

@ -1,15 +1,50 @@
package v1
import (
"errors"
"net"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
func (c *Controller) shouldAutoRegister(token string, gctx *gin.Context) (bool, error) {
if !*c.AutoRegisterCfg.Enable {
return false, nil
}
clientIP := net.ParseIP(gctx.ClientIP())
// Can probaby happen if using unix socket ?
if clientIP == nil {
log.Warnf("Failed to parse client IP for watcher self registration: %s", gctx.ClientIP())
return false, nil
}
if token == "" || c.AutoRegisterCfg == nil {
return false, nil
}
// Check the token
if token != c.AutoRegisterCfg.Token {
return false, errors.New("invalid token for auto registration")
}
// Check the source IP
for _, ipRange := range c.AutoRegisterCfg.AllowedRangesParsed {
if ipRange.Contains(clientIP) {
return true, nil
}
}
return false, errors.New("IP not in allowed range for auto registration")
}
func (c *Controller) CreateMachine(gctx *gin.Context) {
var input models.WatcherRegistrationRequest
@ -19,14 +54,27 @@ func (c *Controller) CreateMachine(gctx *gin.Context) {
}
if err := input.Validate(strfmt.Default); err != nil {
gctx.JSON(http.StatusUnprocessableEntity, gin.H{"message": err.Error()})
return
}
autoRegister, err := c.shouldAutoRegister(input.RegistrationToken, gctx)
if err != nil {
log.WithFields(log.Fields{"ip": gctx.ClientIP(), "machine_id": *input.MachineID}).Errorf("Auto-register failed: %s", err)
gctx.JSON(http.StatusUnauthorized, gin.H{"message": err.Error()})
return
}
if _, err := c.DBClient.CreateMachine(input.MachineID, input.Password, gctx.ClientIP(), autoRegister, false, types.PasswordAuthType); err != nil {
c.HandleDBErrors(gctx, err)
return
}
if _, err := c.DBClient.CreateMachine(input.MachineID, input.Password, gctx.ClientIP(), false, false, types.PasswordAuthType); err != nil {
c.HandleDBErrors(gctx, err)
return
if autoRegister {
log.WithFields(log.Fields{"ip": gctx.ClientIP(), "machine_id": *input.MachineID}).Info("Auto-registered machine")
gctx.Status(http.StatusAccepted)
} else {
gctx.Status(http.StatusCreated)
}
gctx.Status(http.StatusCreated)
}

View file

@ -12,7 +12,7 @@ import (
func TestLogin(t *testing.T) {
router, config := NewAPITest(t)
body := CreateTestMachine(t, router)
body := CreateTestMachine(t, router, "")
// Login with machine not validated yet
w := httptest.NewRecorder()

View file

@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/go-cs-lib/ptr"
)
func TestCreateMachine(t *testing.T) {
@ -20,7 +22,7 @@ func TestCreateMachine(t *testing.T) {
req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req)
assert.Equal(t, 400, w.Code)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, `{"message":"invalid character 'e' in literal true (expecting 'r')"}`, w.Body.String())
// Create machine with invalid input
@ -29,7 +31,7 @@ func TestCreateMachine(t *testing.T) {
req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code)
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Equal(t, `{"message":"validation failure list:\nmachine_id in body is required\npassword in body is required"}`, w.Body.String())
// Create machine
@ -43,7 +45,7 @@ func TestCreateMachine(t *testing.T) {
req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "", w.Body.String())
}
@ -62,7 +64,7 @@ func TestCreateMachineWithForwardedFor(t *testing.T) {
req.Header.Add("X-Real-Ip", "1.1.1.1")
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "", w.Body.String())
ip := GetMachineIP(t, *MachineTest.MachineID, config.API.Server.DbConfig)
@ -85,7 +87,7 @@ func TestCreateMachineWithForwardedForNoConfig(t *testing.T) {
req.Header.Add("X-Real-IP", "1.1.1.1")
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "", w.Body.String())
ip := GetMachineIP(t, *MachineTest.MachineID, config.API.Server.DbConfig)
@ -109,7 +111,7 @@ func TestCreateMachineWithoutForwardedFor(t *testing.T) {
req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "", w.Body.String())
ip := GetMachineIP(t, *MachineTest.MachineID, config.API.Server.DbConfig)
@ -122,7 +124,7 @@ func TestCreateMachineWithoutForwardedFor(t *testing.T) {
func TestCreateMachineAlreadyExist(t *testing.T) {
router, _ := NewAPITest(t)
body := CreateTestMachine(t, router)
body := CreateTestMachine(t, router, "")
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
@ -134,6 +136,90 @@ func TestCreateMachineAlreadyExist(t *testing.T) {
req.Header.Add("User-Agent", UserAgent)
router.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Equal(t, `{"message":"user 'test': user already exist"}`, w.Body.String())
}
func TestAutoRegistration(t *testing.T) {
router, _ := NewAPITest(t)
//Invalid registration token / valid source IP
regReq := MachineTest
regReq.RegistrationToken = invalidRegistrationToken
b, err := json.Marshal(regReq)
require.NoError(t, err)
body := string(b)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "127.0.0.1:4242"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
//Invalid registration token / invalid source IP
regReq = MachineTest
regReq.RegistrationToken = invalidRegistrationToken
b, err = json.Marshal(regReq)
require.NoError(t, err)
body = string(b)
w = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "42.42.42.42:4242"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
//valid registration token / invalid source IP
regReq = MachineTest
regReq.RegistrationToken = validRegistrationToken
b, err = json.Marshal(regReq)
require.NoError(t, err)
body = string(b)
w = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "42.42.42.42:4242"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
//Valid registration token / valid source IP
regReq = MachineTest
regReq.RegistrationToken = validRegistrationToken
b, err = json.Marshal(regReq)
require.NoError(t, err)
body = string(b)
w = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "127.0.0.1:4242"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusAccepted, w.Code)
//No token / valid source IP
regReq = MachineTest
regReq.MachineID = ptr.Of("test2")
b, err = json.Marshal(regReq)
require.NoError(t, err)
body = string(b)
w = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/v1/watchers", strings.NewReader(body))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "127.0.0.1:4242"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}

View file

@ -236,32 +236,40 @@ type CapiWhitelist struct {
Cidrs []*net.IPNet `yaml:"cidrs,omitempty"`
}
type LocalAPIAutoRegisterCfg struct {
Enable *bool `yaml:"enabled"`
Token string `yaml:"token"`
AllowedRanges []string `yaml:"allowed_ranges,omitempty"`
AllowedRangesParsed []*net.IPNet `yaml:"-"`
}
/*local api service configuration*/
type LocalApiServerCfg struct {
Enable *bool `yaml:"enable"`
ListenURI string `yaml:"listen_uri,omitempty"` // 127.0.0.1:8080
ListenSocket string `yaml:"listen_socket,omitempty"`
TLS *TLSCfg `yaml:"tls"`
DbConfig *DatabaseCfg `yaml:"-"`
LogDir string `yaml:"-"`
LogMedia string `yaml:"-"`
OnlineClient *OnlineApiClientCfg `yaml:"online_client"`
ProfilesPath string `yaml:"profiles_path,omitempty"`
ConsoleConfigPath string `yaml:"console_path,omitempty"`
ConsoleConfig *ConsoleConfig `yaml:"-"`
Profiles []*ProfileCfg `yaml:"-"`
LogLevel *log.Level `yaml:"log_level"`
UseForwardedForHeaders bool `yaml:"use_forwarded_for_headers,omitempty"`
TrustedProxies *[]string `yaml:"trusted_proxies,omitempty"`
CompressLogs *bool `yaml:"-"`
LogMaxSize int `yaml:"-"`
LogMaxAge int `yaml:"-"`
LogMaxFiles int `yaml:"-"`
TrustedIPs []string `yaml:"trusted_ips,omitempty"`
PapiLogLevel *log.Level `yaml:"papi_log_level"`
DisableRemoteLapiRegistration bool `yaml:"disable_remote_lapi_registration,omitempty"`
CapiWhitelistsPath string `yaml:"capi_whitelists_path,omitempty"`
CapiWhitelists *CapiWhitelist `yaml:"-"`
Enable *bool `yaml:"enable"`
ListenURI string `yaml:"listen_uri,omitempty"` // 127.0.0.1:8080
ListenSocket string `yaml:"listen_socket,omitempty"`
TLS *TLSCfg `yaml:"tls"`
DbConfig *DatabaseCfg `yaml:"-"`
LogDir string `yaml:"-"`
LogMedia string `yaml:"-"`
OnlineClient *OnlineApiClientCfg `yaml:"online_client"`
ProfilesPath string `yaml:"profiles_path,omitempty"`
ConsoleConfigPath string `yaml:"console_path,omitempty"`
ConsoleConfig *ConsoleConfig `yaml:"-"`
Profiles []*ProfileCfg `yaml:"-"`
LogLevel *log.Level `yaml:"log_level"`
UseForwardedForHeaders bool `yaml:"use_forwarded_for_headers,omitempty"`
TrustedProxies *[]string `yaml:"trusted_proxies,omitempty"`
CompressLogs *bool `yaml:"-"`
LogMaxSize int `yaml:"-"`
LogMaxAge int `yaml:"-"`
LogMaxFiles int `yaml:"-"`
TrustedIPs []string `yaml:"trusted_ips,omitempty"`
PapiLogLevel *log.Level `yaml:"papi_log_level"`
DisableRemoteLapiRegistration bool `yaml:"disable_remote_lapi_registration,omitempty"`
CapiWhitelistsPath string `yaml:"capi_whitelists_path,omitempty"`
CapiWhitelists *CapiWhitelist `yaml:"-"`
AutoRegister *LocalAPIAutoRegisterCfg `yaml:"auto_registration,omitempty"`
}
func (c *LocalApiServerCfg) ClientURL() string {
@ -348,6 +356,14 @@ func (c *Config) LoadAPIServer(inCli bool) error {
log.Infof("loaded capi whitelist from %s: %d IPs, %d CIDRs", c.API.Server.CapiWhitelistsPath, len(c.API.Server.CapiWhitelists.Ips), len(c.API.Server.CapiWhitelists.Cidrs))
}
if err := c.API.Server.LoadAutoRegister(); err != nil {
return err
}
if c.API.Server.AutoRegister != nil && c.API.Server.AutoRegister.Enable != nil && *c.API.Server.AutoRegister.Enable && !inCli {
log.Infof("auto LAPI registration enabled for ranges %+v", c.API.Server.AutoRegister.AllowedRanges)
}
c.API.Server.LogDir = c.Common.LogDir
c.API.Server.LogMedia = c.Common.LogMedia
c.API.Server.CompressLogs = c.Common.CompressLogs
@ -455,3 +471,47 @@ func (c *Config) LoadAPIClient() error {
return c.API.Client.Load()
}
func (c *LocalApiServerCfg) LoadAutoRegister() error {
if c.AutoRegister == nil {
c.AutoRegister = &LocalAPIAutoRegisterCfg{
Enable: ptr.Of(false),
}
return nil
}
// Disable by default
if c.AutoRegister.Enable == nil {
c.AutoRegister.Enable = ptr.Of(false)
}
if !*c.AutoRegister.Enable {
return nil
}
if c.AutoRegister.Token == "" {
return errors.New("missing token value for api.server.auto_register")
}
if len(c.AutoRegister.Token) < 32 {
return errors.New("token value for api.server.auto_register is too short (min 32 characters)")
}
if c.AutoRegister.AllowedRanges == nil {
return errors.New("missing allowed_ranges value for api.server.auto_register")
}
c.AutoRegister.AllowedRangesParsed = make([]*net.IPNet, 0, len(c.AutoRegister.AllowedRanges))
for _, ipRange := range c.AutoRegister.AllowedRanges {
_, ipNet, err := net.ParseCIDR(ipRange)
if err != nil {
return fmt.Errorf("auto_register: failed to parse allowed range '%s': %w", ipRange, err)
}
c.AutoRegister.AllowedRangesParsed = append(c.AutoRegister.AllowedRangesParsed, ipNet)
}
return nil
}

View file

@ -217,6 +217,12 @@ func TestLoadAPIServer(t *testing.T) {
ProfilesPath: "./testdata/profiles.yaml",
UseForwardedForHeaders: false,
PapiLogLevel: &logLevel,
AutoRegister: &LocalAPIAutoRegisterCfg{
Enable: ptr.Of(false),
Token: "",
AllowedRanges: nil,
AllowedRangesParsed: nil,
},
},
},
{

View file

@ -312,6 +312,9 @@ paths:
'201':
description: Watcher Created
headers: {}
'202':
description: Watcher Validated
headers: {}
'400':
description: "400 response"
schema:
@ -726,6 +729,10 @@ definitions:
password:
type: string
format: password
registration_token:
type: string
minLength: 32
maxLength: 255
required:
- machine_id
- password

View file

@ -27,6 +27,11 @@ type WatcherRegistrationRequest struct {
// Required: true
// Format: password
Password *strfmt.Password `json:"password"`
// registration token
// Max Length: 255
// Min Length: 32
RegistrationToken string `json:"registration_token,omitempty"`
}
// Validate validates this watcher registration request
@ -41,6 +46,10 @@ func (m *WatcherRegistrationRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validateRegistrationToken(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@ -69,6 +78,22 @@ func (m *WatcherRegistrationRequest) validatePassword(formats strfmt.Registry) e
return nil
}
func (m *WatcherRegistrationRequest) validateRegistrationToken(formats strfmt.Registry) error {
if swag.IsZero(m.RegistrationToken) { // not required
return nil
}
if err := validate.MinLength("registration_token", "body", m.RegistrationToken, 32); err != nil {
return err
}
if err := validate.MaxLength("registration_token", "body", m.RegistrationToken, 255); err != nil {
return err
}
return nil
}
// ContextValidate validates this watcher registration request based on context it is used
func (m *WatcherRegistrationRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil

View file

@ -209,109 +209,6 @@ teardown() {
rm -rf -- "${backupdir:?}"
}
@test "cscli lapi status" {
rune -0 ./instance-crowdsec start
rune -0 cscli lapi status
assert_output --partial "Loaded credentials from"
assert_output --partial "Trying to authenticate with username"
assert_output --partial "You can successfully interact with Local API (LAPI)"
}
@test "cscli - missing LAPI credentials file" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
rm -f "$LOCAL_API_CREDENTIALS"
rune -1 cscli lapi status
assert_stderr --partial "loading api client: while reading yaml file: open ${LOCAL_API_CREDENTIALS}: no such file or directory"
rune -1 cscli alerts list
assert_stderr --partial "loading api client: while reading yaml file: open ${LOCAL_API_CREDENTIALS}: no such file or directory"
rune -1 cscli decisions list
assert_stderr --partial "loading api client: while reading yaml file: open ${LOCAL_API_CREDENTIALS}: no such file or directory"
}
@test "cscli - empty LAPI credentials file" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
: > "$LOCAL_API_CREDENTIALS"
rune -1 cscli lapi status
assert_stderr --partial "no credentials or URL found in api client configuration '${LOCAL_API_CREDENTIALS}'"
rune -1 cscli alerts list
assert_stderr --partial "no credentials or URL found in api client configuration '${LOCAL_API_CREDENTIALS}'"
rune -1 cscli decisions list
assert_stderr --partial "no credentials or URL found in api client configuration '${LOCAL_API_CREDENTIALS}'"
}
@test "cscli - LAPI credentials file can reference env variables" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
URL=$(config_get "$LOCAL_API_CREDENTIALS" '.url')
export URL
LOGIN=$(config_get "$LOCAL_API_CREDENTIALS" '.login')
export LOGIN
PASSWORD=$(config_get "$LOCAL_API_CREDENTIALS" '.password')
export PASSWORD
# shellcheck disable=SC2016
echo '{"url":"$URL","login":"$LOGIN","password":"$PASSWORD"}' > "$LOCAL_API_CREDENTIALS".local
config_set '.crowdsec_service.enable=false'
rune -0 ./instance-crowdsec start
rune -0 cscli lapi status
assert_output --partial "You can successfully interact with Local API (LAPI)"
rm "$LOCAL_API_CREDENTIALS".local
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.url="$URL"'
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.login="$LOGIN"'
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.password="$PASSWORD"'
rune -0 cscli lapi status
assert_output --partial "You can successfully interact with Local API (LAPI)"
# but if a variable is not defined, there is no specific error message
unset URL
rune -1 cscli lapi status
# shellcheck disable=SC2016
assert_stderr --partial 'BaseURL must have a trailing slash'
}
@test "cscli - missing LAPI client settings" {
config_set 'del(.api.client)'
rune -1 cscli lapi status
assert_stderr --partial "loading api client: no API client section in configuration"
rune -1 cscli alerts list
assert_stderr --partial "loading api client: no API client section in configuration"
rune -1 cscli decisions list
assert_stderr --partial "loading api client: no API client section in configuration"
}
@test "cscli - malformed LAPI url" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
config_set "$LOCAL_API_CREDENTIALS" '.url="http://127.0.0.1:-80"'
rune -1 cscli lapi status -o json
rune -0 jq -r '.msg' <(stderr)
assert_output 'failed to authenticate to Local API (LAPI): parse "http://127.0.0.1:-80/": invalid port ":-80" after host'
}
@test "cscli - bad LAPI password" {
rune -0 ./instance-crowdsec start
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
config_set "$LOCAL_API_CREDENTIALS" '.password="meh"'
rune -1 cscli lapi status -o json
rune -0 jq -r '.msg' <(stderr)
assert_output 'failed to authenticate to Local API (LAPI): API error: incorrect Username or Password'
}
@test "'cscli completion' with or without configuration file" {
rune -0 cscli completion bash
assert_output --partial "# bash completion for cscli"

View file

@ -0,0 +1,213 @@
#!/usr/bin/env bats
# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
set -u
setup_file() {
load "../lib/setup_file.sh"
}
teardown_file() {
load "../lib/teardown_file.sh"
}
setup() {
load "../lib/setup.sh"
load "../lib/bats-file/load.bash"
./instance-data load
# don't run crowdsec here, not all tests require a running instance
}
teardown() {
cd "$TEST_DIR" || exit 1
./instance-crowdsec stop
}
#----------
@test "cscli lapi status" {
rune -0 ./instance-crowdsec start
rune -0 cscli lapi status
assert_output --partial "Loaded credentials from"
assert_output --partial "Trying to authenticate with username"
assert_output --partial "You can successfully interact with Local API (LAPI)"
}
@test "cscli - missing LAPI credentials file" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
rm -f "$LOCAL_API_CREDENTIALS"
rune -1 cscli lapi status
assert_stderr --partial "loading api client: while reading yaml file: open $LOCAL_API_CREDENTIALS: no such file or directory"
rune -1 cscli alerts list
assert_stderr --partial "loading api client: while reading yaml file: open $LOCAL_API_CREDENTIALS: no such file or directory"
rune -1 cscli decisions list
assert_stderr --partial "loading api client: while reading yaml file: open $LOCAL_API_CREDENTIALS: no such file or directory"
}
@test "cscli - empty LAPI credentials file" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
: > "$LOCAL_API_CREDENTIALS"
rune -1 cscli lapi status
assert_stderr --partial "no credentials or URL found in api client configuration '$LOCAL_API_CREDENTIALS'"
rune -1 cscli alerts list
assert_stderr --partial "no credentials or URL found in api client configuration '$LOCAL_API_CREDENTIALS'"
rune -1 cscli decisions list
assert_stderr --partial "no credentials or URL found in api client configuration '$LOCAL_API_CREDENTIALS'"
}
@test "cscli - LAPI credentials file can reference env variables" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
URL=$(config_get "$LOCAL_API_CREDENTIALS" '.url')
export URL
LOGIN=$(config_get "$LOCAL_API_CREDENTIALS" '.login')
export LOGIN
PASSWORD=$(config_get "$LOCAL_API_CREDENTIALS" '.password')
export PASSWORD
# shellcheck disable=SC2016
echo '{"url":"$URL","login":"$LOGIN","password":"$PASSWORD"}' > "$LOCAL_API_CREDENTIALS".local
config_set '.crowdsec_service.enable=false'
rune -0 ./instance-crowdsec start
rune -0 cscli lapi status
assert_output --partial "You can successfully interact with Local API (LAPI)"
rm "$LOCAL_API_CREDENTIALS".local
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.url="$URL"'
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.login="$LOGIN"'
# shellcheck disable=SC2016
config_set "$LOCAL_API_CREDENTIALS" '.password="$PASSWORD"'
rune -0 cscli lapi status
assert_output --partial "You can successfully interact with Local API (LAPI)"
# but if a variable is not defined, there is no specific error message
unset URL
rune -1 cscli lapi status
# shellcheck disable=SC2016
assert_stderr --partial 'BaseURL must have a trailing slash'
}
@test "cscli - missing LAPI client settings" {
config_set 'del(.api.client)'
rune -1 cscli lapi status
assert_stderr --partial "loading api client: no API client section in configuration"
rune -1 cscli alerts list
assert_stderr --partial "loading api client: no API client section in configuration"
rune -1 cscli decisions list
assert_stderr --partial "loading api client: no API client section in configuration"
}
@test "cscli - malformed LAPI url" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
config_set "$LOCAL_API_CREDENTIALS" '.url="http://127.0.0.1:-80"'
rune -1 cscli lapi status -o json
rune -0 jq -r '.msg' <(stderr)
assert_output 'failed to authenticate to Local API (LAPI): parse "http://127.0.0.1:-80/": invalid port ":-80" after host'
}
@test "cscli - bad LAPI password" {
rune -0 ./instance-crowdsec start
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
config_set "$LOCAL_API_CREDENTIALS" '.password="meh"'
rune -1 cscli lapi status -o json
rune -0 jq -r '.msg' <(stderr)
assert_output 'failed to authenticate to Local API (LAPI): API error: incorrect Username or Password'
}
@test "cscli lapi register / machines validate" {
rune -1 cscli lapi register
assert_stderr --partial "connection refused"
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
rune -0 ./instance-crowdsec start
rune -0 cscli lapi register
assert_stderr --partial "Successfully registered to Local API"
assert_stderr --partial "Local API credentials written to '$LOCAL_API_CREDENTIALS'"
assert_stderr --partial "Run 'sudo systemctl reload crowdsec' for the new configuration to be effective."
LOGIN=$(config_get "$LOCAL_API_CREDENTIALS" '.login')
rune -0 cscli machines inspect "$LOGIN" -o json
rune -0 jq -r '.isValidated' <(output)
assert_output "null"
rune -0 cscli machines validate "$LOGIN"
rune -0 cscli machines inspect "$LOGIN" -o json
rune -0 jq -r '.isValidated' <(output)
assert_output "true"
}
@test "cscli lapi register --machine" {
rune -0 ./instance-crowdsec start
rune -0 cscli lapi register --machine newmachine
rune -0 cscli machines validate newmachine
rune -0 cscli machines inspect newmachine -o json
rune -0 jq -r '.isValidated' <(output)
assert_output "true"
}
@test "cscli lapi register --token (ignored)" {
# A token is ignored if the server is not configured with it
rune -1 cscli lapi register --machine newmachine --token meh
assert_stderr --partial "connection refused"
rune -0 ./instance-crowdsec start
rune -1 cscli lapi register --machine newmachine --token meh
assert_stderr --partial '422 Unprocessable Entity: API error: http code 422, invalid request:'
assert_stderr --partial 'registration_token in body should be at least 32 chars long'
rune -0 cscli lapi register --machine newmachine --token 12345678901234567890123456789012
assert_stderr --partial "Successfully registered to Local API"
rune -0 cscli machines inspect newmachine -o json
rune -0 jq -r '.isValidated' <(output)
assert_output "null"
}
@test "cscli lapi register --token" {
config_set '.api.server.auto_registration.enabled=true'
config_set '.api.server.auto_registration.token="12345678901234567890123456789012"'
config_set '.api.server.auto_registration.allowed_ranges=["127.0.0.1/32"]'
rune -0 ./instance-crowdsec start
rune -1 cscli lapi register --machine malicious --token 123456789012345678901234badtoken
assert_stderr --partial "401 Unauthorized: API error: invalid token for auto registration"
rune -1 cscli machines inspect malicious -o json
assert_stderr --partial "unable to read machine data 'malicious': user 'malicious': user doesn't exist"
rune -0 cscli lapi register --machine newmachine --token 12345678901234567890123456789012
assert_stderr --partial "Successfully registered to Local API"
rune -0 cscli machines inspect newmachine -o json
rune -0 jq -r '.isValidated' <(output)
assert_output "true"
}
@test "cscli lapi register --token (bad source ip)" {
config_set '.api.server.auto_registration.enabled=true'
config_set '.api.server.auto_registration.token="12345678901234567890123456789012"'
config_set '.api.server.auto_registration.allowed_ranges=["127.0.0.2/32"]'
rune -0 ./instance-crowdsec start
rune -1 cscli lapi register --machine outofrange --token 12345678901234567890123456789012
assert_stderr --partial "401 Unauthorized: API error: IP not in allowed range for auto registration"
rune -1 cscli machines inspect outofrange -o json
assert_stderr --partial "unable to read machine data 'outofrange': user 'outofrange': user doesn't exist"
}