package csconfig import ( "bytes" "crypto/tls" "crypto/x509" "errors" "fmt" "io" "net" "os" "strings" "time" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" "github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/yamlpatch" "github.com/crowdsecurity/crowdsec/pkg/apiclient" ) type APICfg struct { Client *LocalApiClientCfg `yaml:"client"` Server *LocalApiServerCfg `yaml:"server"` CTI *CTICfg `yaml:"cti"` } type ApiCredentialsCfg struct { PapiURL string `yaml:"papi_url,omitempty" json:"papi_url,omitempty"` URL string `yaml:"url,omitempty" json:"url,omitempty"` Login string `yaml:"login,omitempty" json:"login,omitempty"` Password string `yaml:"password,omitempty" json:"-"` CACertPath string `yaml:"ca_cert_path,omitempty"` KeyPath string `yaml:"key_path,omitempty"` CertPath string `yaml:"cert_path,omitempty"` } /*global api config (for lapi->oapi)*/ type OnlineApiClientCfg struct { CredentialsFilePath string `yaml:"credentials_path,omitempty"` // credz will be edited by software, store in diff file Credentials *ApiCredentialsCfg `yaml:"-"` } /*local api config (for crowdsec/cscli->lapi)*/ type LocalApiClientCfg struct { CredentialsFilePath string `yaml:"credentials_path,omitempty"` // credz will be edited by software, store in diff file Credentials *ApiCredentialsCfg `yaml:"-"` InsecureSkipVerify *bool `yaml:"insecure_skip_verify"` // check if api certificate is bad or not } type CTICfg struct { Key *string `yaml:"key,omitempty"` CacheTimeout *time.Duration `yaml:"cache_timeout,omitempty"` CacheSize *int `yaml:"cache_size,omitempty"` Enabled *bool `yaml:"enabled,omitempty"` LogLevel *log.Level `yaml:"log_level,omitempty"` } func (a *CTICfg) Load() error { if a.Key == nil { *a.Enabled = false } if a.Key != nil && *a.Key == "" { return errors.New("empty cti key") } if a.Enabled == nil { a.Enabled = new(bool) *a.Enabled = true } if a.CacheTimeout == nil { a.CacheTimeout = new(time.Duration) *a.CacheTimeout = 10 * time.Minute } if a.CacheSize == nil { a.CacheSize = new(int) *a.CacheSize = 100 } return nil } func (o *OnlineApiClientCfg) Load() error { o.Credentials = new(ApiCredentialsCfg) fcontent, err := os.ReadFile(o.CredentialsFilePath) if err != nil { return err } dec := yaml.NewDecoder(bytes.NewReader(fcontent)) dec.KnownFields(true) err = dec.Decode(o.Credentials) if err != nil { if !errors.Is(err, io.EOF) { return fmt.Errorf("failed unmarshaling api server credentials configuration file '%s': %w", o.CredentialsFilePath, err) } } switch { case o.Credentials.Login == "": log.Warningf("can't load CAPI credentials from '%s' (missing login field)", o.CredentialsFilePath) o.Credentials = nil case o.Credentials.Password == "": log.Warningf("can't load CAPI credentials from '%s' (missing password field)", o.CredentialsFilePath) o.Credentials = nil case o.Credentials.URL == "": log.Warningf("can't load CAPI credentials from '%s' (missing url field)", o.CredentialsFilePath) o.Credentials = nil } return nil } func (l *LocalApiClientCfg) Load() error { patcher := yamlpatch.NewPatcher(l.CredentialsFilePath, ".local") fcontent, err := patcher.MergedPatchContent() if err != nil { return err } dec := yaml.NewDecoder(bytes.NewReader(fcontent)) dec.KnownFields(true) err = dec.Decode(&l.Credentials) if err != nil { if !errors.Is(err, io.EOF) { return fmt.Errorf("failed unmarshaling api client credential configuration file '%s': %w", l.CredentialsFilePath, err) } } if l.Credentials == nil || l.Credentials.URL == "" { return fmt.Errorf("no credentials or URL found in api client configuration '%s'", l.CredentialsFilePath) } if l.Credentials != nil && l.Credentials.URL != "" { // don't append a trailing slash if the URL is a unix socket if strings.HasPrefix(l.Credentials.URL, "http") && !strings.HasSuffix(l.Credentials.URL, "/") { l.Credentials.URL += "/" } } // is the configuration asking for client authentication via TLS? credTLSClientAuth := l.Credentials.CertPath != "" || l.Credentials.KeyPath != "" // is the configuration asking for TLS encryption and server authentication? credTLS := credTLSClientAuth || l.Credentials.CACertPath != "" credSocket := strings.HasPrefix(l.Credentials.URL, "/") if credTLS && credSocket { return errors.New("cannot use TLS with a unix socket") } if credTLSClientAuth && l.Credentials.Login != "" { return errors.New("user/password authentication and TLS authentication are mutually exclusive") } if l.InsecureSkipVerify == nil { apiclient.InsecureSkipVerify = false } else { apiclient.InsecureSkipVerify = *l.InsecureSkipVerify } if l.Credentials.CACertPath != "" { caCert, err := os.ReadFile(l.Credentials.CACertPath) if err != nil { return fmt.Errorf("failed to load cacert: %w", err) } caCertPool, err := x509.SystemCertPool() if err != nil { log.Warningf("Error loading system CA certificates: %s", err) } if caCertPool == nil { caCertPool = x509.NewCertPool() } caCertPool.AppendCertsFromPEM(caCert) apiclient.CaCertPool = caCertPool } if l.Credentials.CertPath != "" && l.Credentials.KeyPath != "" { cert, err := tls.LoadX509KeyPair(l.Credentials.CertPath, l.Credentials.KeyPath) if err != nil { return fmt.Errorf("failed to load api client certificate: %w", err) } apiclient.Cert = &cert } return nil } func (c *LocalApiServerCfg) GetTrustedIPs() ([]net.IPNet, error) { trustedIPs := make([]net.IPNet, 0) for _, ip := range c.TrustedIPs { cidr := toValidCIDR(ip) _, ipNet, err := net.ParseCIDR(cidr) if err != nil { return nil, err } trustedIPs = append(trustedIPs, *ipNet) } return trustedIPs, nil } func toValidCIDR(ip string) string { if strings.Contains(ip, "/") { return ip } if strings.Contains(ip, ":") { return ip + "/128" } return ip + "/32" } type CapiWhitelist struct { Ips []net.IP `yaml:"ips,omitempty"` Cidrs []*net.IPNet `yaml:"cidrs,omitempty"` } /*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:"-"` } func (c *LocalApiServerCfg) ClientURL() string { if c == nil { return "" } if c.ListenSocket != "" { return c.ListenSocket } if c.ListenURI != "" { return "http://" + c.ListenURI } return "" } func (c *Config) LoadAPIServer(inCli bool) error { if c.DisableAPI { log.Warning("crowdsec local API is disabled from flag") } if c.API.Server == nil { log.Warning("crowdsec local API is disabled") c.DisableAPI = true return nil } if c.API.Server.Enable == nil { // if the option is not present, it is enabled by default c.API.Server.Enable = ptr.Of(true) } if !*c.API.Server.Enable { log.Warning("crowdsec local API is disabled because 'enable' is set to false") c.DisableAPI = true } if c.DisableAPI { return nil } if c.API.Server.ListenURI == "" && c.API.Server.ListenSocket == "" { return errors.New("no listen_uri or listen_socket specified") } // inherit log level from common, then api->server var logLevel log.Level if c.API.Server.LogLevel != nil { logLevel = *c.API.Server.LogLevel } else if c.Common.LogLevel != nil { logLevel = *c.Common.LogLevel } else { logLevel = log.InfoLevel } if c.API.Server.PapiLogLevel == nil { c.API.Server.PapiLogLevel = &logLevel } if c.API.Server.OnlineClient != nil && c.API.Server.OnlineClient.CredentialsFilePath != "" { if err := c.API.Server.OnlineClient.Load(); err != nil { return fmt.Errorf("loading online client credentials: %w", err) } } if (c.API.Server.OnlineClient == nil || c.API.Server.OnlineClient.Credentials == nil) && !inCli { log.Printf("push and pull to Central API disabled") } if err := c.LoadDBConfig(inCli); err != nil { return err } if err := c.API.Server.LoadCapiWhitelists(); err != nil { return err } if c.API.Server.CapiWhitelistsPath != "" && !inCli { 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)) } c.API.Server.LogDir = c.Common.LogDir c.API.Server.LogMedia = c.Common.LogMedia c.API.Server.CompressLogs = c.Common.CompressLogs c.API.Server.LogMaxSize = c.Common.LogMaxSize c.API.Server.LogMaxAge = c.Common.LogMaxAge c.API.Server.LogMaxFiles = c.Common.LogMaxFiles if c.API.Server.UseForwardedForHeaders && c.API.Server.TrustedProxies == nil { c.API.Server.TrustedProxies = &[]string{"0.0.0.0/0"} } if c.API.Server.TrustedProxies != nil { c.API.Server.UseForwardedForHeaders = true } if err := c.API.Server.LoadProfiles(); err != nil { return fmt.Errorf("while loading profiles for LAPI: %w", err) } if c.API.Server.ConsoleConfigPath == "" { c.API.Server.ConsoleConfigPath = DefaultConsoleConfigFilePath } if err := c.API.Server.LoadConsoleConfig(); err != nil { return fmt.Errorf("while loading console options: %w", err) } if c.API.CTI != nil { if err := c.API.CTI.Load(); err != nil { return fmt.Errorf("loading CTI configuration: %w", err) } } return nil } // we cannot unmarshal to type net.IPNet, so we need to do it manually type capiWhitelists struct { Ips []string `yaml:"ips"` Cidrs []string `yaml:"cidrs"` } func parseCapiWhitelists(fd io.Reader) (*CapiWhitelist, error) { fromCfg := capiWhitelists{} decoder := yaml.NewDecoder(fd) if err := decoder.Decode(&fromCfg); err != nil { if errors.Is(err, io.EOF) { return nil, errors.New("empty file") } return nil, err } ret := &CapiWhitelist{ Ips: make([]net.IP, len(fromCfg.Ips)), Cidrs: make([]*net.IPNet, len(fromCfg.Cidrs)), } for idx, v := range fromCfg.Ips { ip := net.ParseIP(v) if ip == nil { return nil, fmt.Errorf("invalid IP address: %s", v) } ret.Ips[idx] = ip } for idx, v := range fromCfg.Cidrs { _, tnet, err := net.ParseCIDR(v) if err != nil { return nil, err } ret.Cidrs[idx] = tnet } return ret, nil } func (c *LocalApiServerCfg) LoadCapiWhitelists() error { if c.CapiWhitelistsPath == "" { return nil } fd, err := os.Open(c.CapiWhitelistsPath) if err != nil { return fmt.Errorf("while opening capi whitelist file: %w", err) } defer fd.Close() c.CapiWhitelists, err = parseCapiWhitelists(fd) if err != nil { return fmt.Errorf("while parsing capi whitelist file '%s': %w", c.CapiWhitelistsPath, err) } return nil } func (c *Config) LoadAPIClient() error { if c.API == nil || c.API.Client == nil || c.API.Client.CredentialsFilePath == "" || c.DisableAgent { return errors.New("no API client section in configuration") } if err := c.API.Client.Load(); err != nil { return err } return nil }