mirror of
https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin.git
synced 2026-07-21 11:38:59 +02:00
51 feature support tls connections to crowdsec not signed by a public ca (#53)
* ✨ Add support for insecure tls connections to LAPI * 📝 Add documentation for the TLS insecure parameter * 🚧 Add tls authority certificate and checks for params * 📝 Add example for tls communication in readme and folder * 📝 Update documentation and example for tls * 🚨 Fix easy lint errors * 🦺 logic to fetch certificates * 🚨 Fix lint on readme * ♻️ Refactor validate to fix lint and clean * 🚧 Add doc, cert gen for crowdsec example * 🚧 Progress on setting up Crowdsec with tls * 🚧 Update certs validation for example * ♻️ Add load variable from file or value and get client cert * ♻️ Refactor getting variables * 🚨 Fix lint, no new line on new files * 🐛 Fix bug on condition check lapi key cert * ♻️ Update after review * ♻️ Update after review * 🍱 fix mathieu code * ♻️ Refactor logic of loading tls certificates * 🍱 clean code * 🍱 last fix * 🍱 fix lint * ♻️ Add documentation in readme, fix lint, remove unfinished tests * 🐛 Fix conditions logics * 🚨 Fix Lint * ♻️ simplify code on getVariable Co-authored-by: Max Lerebourg <maxlerebourg@gmail.com>
This commit is contained in:
co-authored by
Max Lerebourg
parent
b2ed600d5e
commit
a2fe60c621
+203
-54
@@ -5,11 +5,17 @@ package crowdsec_bouncer_traefik_plugin //nolint:revive,stylecheck
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
@@ -37,37 +43,46 @@ var (
|
||||
|
||||
// Config the plugin configuration.
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
LogLevel string `json:"logLevel,omitempty"`
|
||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
||||
RedisCacheHost string `json:"redisCacheHost,omitempty"`
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
LogLevel string `json:"logLevel,omitempty"`
|
||||
CrowdsecMode string `json:"crowdsecMode,omitempty"`
|
||||
CrowdsecLapiScheme string `json:"crowdsecLapiScheme,omitempty"`
|
||||
CrowdsecLapiHost string `json:"crowdsecLapiHost,omitempty"`
|
||||
CrowdsecLapiKey string `json:"crowdsecLapiKey,omitempty"`
|
||||
CrowdsecLapiKeyFile string `json:"crowdsecLapiKeyFile,omitempty"`
|
||||
CrowdsecLapiTLSInsecureVerify bool `json:"crowdsecLapiTlsInsecureVerify,omitempty"`
|
||||
CrowdsecLapiTLSCertificateAuthority string `json:"crowdsecLapiTlsCertificateAuthority,omitempty"`
|
||||
CrowdsecLapiTLSCertificateAuthorityFile string `json:"crowdsecLapiTlsCertificateAuthorityFile,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncer string `json:"crowdsecLapiTlsCertificateBouncer,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncerFile string `json:"crowdsecLapiTlsCertificateBouncerFile,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncerKey string `json:"crowdsecLapiTlsCertificateBouncerKey,omitempty"`
|
||||
CrowdsecLapiTLSCertificateBouncerKeyFile string `json:"crowdsecLapiTlsCertificateBouncerKeyFile,omitempty"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds,omitempty"`
|
||||
DefaultDecisionSeconds int64 `json:"defaultDecisionSeconds,omitempty"`
|
||||
ForwardedHeadersCustomName string `json:"forwardedheaderscustomheader,omitempty"`
|
||||
ForwardedHeadersTrustedIPs []string `json:"forwardedHeadersTrustedIps,omitempty"`
|
||||
ClientTrustedIPs []string `json:"clientTrustedIps,omitempty"`
|
||||
RedisCacheEnabled bool `json:"redisCacheEnabled,omitempty"`
|
||||
RedisCacheHost string `json:"redisCacheHost,omitempty"`
|
||||
}
|
||||
|
||||
// CreateConfig creates the default plugin configuration.
|
||||
func CreateConfig() *Config {
|
||||
return &Config{
|
||||
Enabled: false,
|
||||
LogLevel: "INFO",
|
||||
CrowdsecMode: liveMode,
|
||||
CrowdsecLapiScheme: "http",
|
||||
CrowdsecLapiHost: "crowdsec:8080",
|
||||
CrowdsecLapiKey: "",
|
||||
UpdateIntervalSeconds: 60,
|
||||
DefaultDecisionSeconds: 60,
|
||||
ClientTrustedIPs: []string{},
|
||||
ForwardedHeadersTrustedIPs: []string{},
|
||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||
RedisCacheEnabled: false,
|
||||
RedisCacheHost: "redis:6379",
|
||||
Enabled: false,
|
||||
LogLevel: "INFO",
|
||||
CrowdsecMode: liveMode,
|
||||
CrowdsecLapiScheme: "http",
|
||||
CrowdsecLapiHost: "crowdsec:8080",
|
||||
CrowdsecLapiKey: "",
|
||||
CrowdsecLapiTLSInsecureVerify: false,
|
||||
UpdateIntervalSeconds: 60,
|
||||
DefaultDecisionSeconds: 60,
|
||||
ForwardedHeadersCustomName: "X-Forwarded-For",
|
||||
ForwardedHeadersTrustedIPs: []string{},
|
||||
ClientTrustedIPs: []string{},
|
||||
RedisCacheEnabled: false,
|
||||
RedisCacheHost: "redis:6379",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,13 +110,24 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
logger.Init(config.LogLevel)
|
||||
err := validateParams(config)
|
||||
if err != nil {
|
||||
logger.Info(err.Error())
|
||||
logger.Info(fmt.Sprintf("New:validateParams %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverChecker, _ := ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||
clientChecker, _ := ip.NewChecker(config.ClientTrustedIPs)
|
||||
|
||||
tlsConfig, err := getTLSConfigCrowdsec(config)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("New:getTLSConfigCrowdsec fail to get tlsConfig %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
apiKey, err := getVariable(config, "CrowdsecLapiKey")
|
||||
if err != nil && len(tlsConfig.Certificates) == 0 {
|
||||
logger.Error(fmt.Sprintf("New:crowdsecLapiKey fail to get CrowdsecLapiKey and no client certificate setup %s", err.Error()))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bouncer := &Bouncer{
|
||||
next: next,
|
||||
name: name,
|
||||
@@ -111,7 +137,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
crowdsecMode: config.CrowdsecMode,
|
||||
crowdsecScheme: config.CrowdsecLapiScheme,
|
||||
crowdsecHost: config.CrowdsecLapiHost,
|
||||
crowdsecKey: config.CrowdsecLapiKey,
|
||||
crowdsecKey: apiKey,
|
||||
updateInterval: config.UpdateIntervalSeconds,
|
||||
customHeader: config.ForwardedHeadersCustomName,
|
||||
defaultDecisionTimeout: config.DefaultDecisionSeconds,
|
||||
@@ -125,6 +151,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
Timeout: 2 * time.Second,
|
||||
},
|
||||
@@ -365,11 +392,158 @@ func crowdsecQuery(bouncer *Bouncer, stringURL string) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func getTLSConfigCrowdsec(config *Config) (*tls.Config, error) {
|
||||
tlsConfig := new(tls.Config)
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
//nolint:gocritic
|
||||
if config.CrowdsecLapiScheme != "https" {
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiScheme not https")
|
||||
return tlsConfig, nil
|
||||
} else if config.CrowdsecLapiTLSInsecureVerify {
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSInsecureVerify is true")
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
// If we return here and still want to use client auth this won't work
|
||||
// return tlsConfig, nil
|
||||
} else {
|
||||
certAuthority, err := getVariable(config, "CrowdsecLapiTLSCertificateAuthority")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert := []byte(certAuthority)
|
||||
if !tlsConfig.RootCAs.AppendCertsFromPEM(cert) {
|
||||
logger.Debug("getTLSConfigCrowdsec:CrowdsecLapiTLSCertificateAuthority read cert failed")
|
||||
// here we return because if CrowdsecLapiTLSInsecureVerify is false
|
||||
// and CA not load, we can't communicate with https
|
||||
return nil, errors.New("getTLSConfigCrowdsec:cannot load CA and verify cert is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
certBouncer, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certBouncerKey, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if certBouncer == "" || certBouncerKey == "" {
|
||||
return tlsConfig, nil
|
||||
}
|
||||
clientCert, err := tls.X509KeyPair([]byte(certBouncer), []byte(certBouncerKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getTLSClientConfigCrowdsec impossible to generate ClientCert %w", err)
|
||||
}
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCert)
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func getVariable(config *Config, key string) (string, error) {
|
||||
value := ""
|
||||
object := reflect.Indirect(reflect.ValueOf(config))
|
||||
field := object.FieldByName(fmt.Sprintf("%sFile", key))
|
||||
// Here linter say you should simplify this code, but lets not, performance is important not clarity and complexity
|
||||
fp := field.String()
|
||||
if fp != "" {
|
||||
file, err := os.Stat(fp)
|
||||
if err != nil {
|
||||
return value, fmt.Errorf("%s:%s invalid path %w", key, fp, err)
|
||||
}
|
||||
if file.IsDir() {
|
||||
return value, fmt.Errorf("%s:%s path must be a file", key, fp)
|
||||
}
|
||||
fileValue, err := os.ReadFile(filepath.Clean(fp))
|
||||
if err != nil {
|
||||
return value, fmt.Errorf("%s:%s read file path failed %w", key, fp, err)
|
||||
}
|
||||
value = string(fileValue)
|
||||
return value, nil
|
||||
}
|
||||
field = object.FieldByName(key)
|
||||
value = field.String()
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func validateParams(config *Config) error {
|
||||
if err := validateParamsRequired(config); err != nil {
|
||||
return err
|
||||
}
|
||||
testURL := url.URL{
|
||||
Scheme: config.CrowdsecLapiScheme,
|
||||
Host: config.CrowdsecLapiHost,
|
||||
}
|
||||
// This only check that the format of the URL scheme:// is correct and do not make requests
|
||||
|
||||
if _, err := http.NewRequest(http.MethodGet, testURL.String(), nil); err != nil {
|
||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||
}
|
||||
|
||||
if err := validateParamsIPs(config.ForwardedHeadersTrustedIPs, "ForwardedHeadersTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateParamsIPs(config.ClientTrustedIPs, "ClientTrustedIPs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lapiKey, err := getVariable(config, "CrowdsecLapiKey")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certBouncer, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncer")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certBouncerKey, err := getVariable(config, "CrowdsecLapiTLSCertificateBouncerKey")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We need to either have crowdsecLapiKey defined or the BouncerCert and Bouncerkey
|
||||
if lapiKey == "" && (certBouncer == "" || certBouncerKey == "") {
|
||||
return fmt.Errorf("CrowdsecLapiKey || (CrowdsecLapiTLSCertificateBouncer && CrowdsecLapiTLSCertificateBouncerKey): cannot be both empty")
|
||||
}
|
||||
|
||||
// Case https to contact Crowdsec LAPI and certificate must be provided
|
||||
if config.CrowdsecLapiScheme == "https" && !config.CrowdsecLapiTLSInsecureVerify {
|
||||
err = validateParamsTLS(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParamsTLS(config *Config) error {
|
||||
certAuth, err := getVariable(config, "CrowdsecLapiTLSCertificateAuthority")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certAuth == "" {
|
||||
return fmt.Errorf("CrowdsecLapiTLSCertificateAuthority must be specified when CrowdsecLapiScheme='https' and CrowdsecLapiTLSInsecureVerify=false")
|
||||
}
|
||||
tlsConfig := new(tls.Config)
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(certAuth)) {
|
||||
return fmt.Errorf("failed parsing pem file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParamsIPs(listIP []string, key string) error {
|
||||
if len(listIP) > 0 {
|
||||
if _, err := ip.NewChecker(listIP); err != nil {
|
||||
return fmt.Errorf("%s must be a list of IP/CIDR :%w", key, err)
|
||||
}
|
||||
} else {
|
||||
logger.Debug(fmt.Sprintf("No IP provided for %s", key))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParamsRequired(config *Config) error {
|
||||
requiredStrings := map[string]string{
|
||||
"CrowdsecLapiScheme": config.CrowdsecLapiScheme,
|
||||
"CrowdsecLapiHost": config.CrowdsecLapiHost,
|
||||
"CrowdsecLapiKey": config.CrowdsecLapiKey,
|
||||
"CrowdsecMode": config.CrowdsecMode,
|
||||
}
|
||||
requiredInt := map[string]int64{
|
||||
@@ -392,30 +566,5 @@ func validateParams(config *Config) error {
|
||||
if !contains([]string{"http", "https"}, config.CrowdsecLapiScheme) {
|
||||
return fmt.Errorf("CrowdsecLapiScheme: must be one of 'http' or 'https'")
|
||||
}
|
||||
testURL := url.URL{
|
||||
Scheme: config.CrowdsecLapiScheme,
|
||||
Host: config.CrowdsecLapiHost,
|
||||
}
|
||||
_, err := http.NewRequest(http.MethodGet, testURL.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CrowdsecLapiScheme://CrowdsecLapiHost: '%v://%v' must be an URL", config.CrowdsecLapiScheme, config.CrowdsecLapiHost)
|
||||
}
|
||||
if len(config.ForwardedHeadersTrustedIPs) > 0 {
|
||||
_, err = ip.NewChecker(config.ForwardedHeadersTrustedIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ForwardedHeadersTrustedIPs must be a list of IP/CIDR :%w", err)
|
||||
}
|
||||
} else {
|
||||
logger.Debug("No IP provided for ForwardedHeadersTrustedIPs")
|
||||
}
|
||||
if len(config.ClientTrustedIPs) > 0 {
|
||||
_, err = ip.NewChecker(config.ClientTrustedIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TrustedIPs must be a list of IP/CIDR :%w", err)
|
||||
}
|
||||
} else {
|
||||
logger.Debug("No IP provided for TrustedIPs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user