github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/middlewares/v1/api_key.go (about)

     1  package v1
     2  
     3  import (
     4  	"crypto/rand"
     5  	"crypto/sha512"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/gin-gonic/gin"
    12  	log "github.com/sirupsen/logrus"
    13  
    14  	"github.com/crowdsecurity/crowdsec/pkg/database"
    15  	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
    16  	"github.com/crowdsecurity/crowdsec/pkg/types"
    17  )
    18  
    19  const (
    20  	APIKeyHeader      = "X-Api-Key"
    21  	BouncerContextKey = "bouncer_info"
    22  	dummyAPIKeySize   = 54
    23  	// max allowed by bcrypt 72 = 54 bytes in base64
    24  )
    25  
    26  type APIKey struct {
    27  	HeaderName string
    28  	DbClient   *database.Client
    29  	TlsAuth    *TLSAuth
    30  }
    31  
    32  func GenerateAPIKey(n int) (string, error) {
    33  	bytes := make([]byte, n)
    34  	if _, err := rand.Read(bytes); err != nil {
    35  		return "", err
    36  	}
    37  
    38  	encoded := base64.StdEncoding.EncodeToString(bytes)
    39  
    40  	// the '=' can cause issues on some bouncers
    41  	return strings.TrimRight(encoded, "="), nil
    42  }
    43  
    44  func NewAPIKey(dbClient *database.Client) *APIKey {
    45  	return &APIKey{
    46  		HeaderName: APIKeyHeader,
    47  		DbClient:   dbClient,
    48  		TlsAuth:    &TLSAuth{},
    49  	}
    50  }
    51  
    52  func HashSHA512(str string) string {
    53  	hashedKey := sha512.New()
    54  	hashedKey.Write([]byte(str))
    55  
    56  	hashStr := fmt.Sprintf("%x", hashedKey.Sum(nil))
    57  
    58  	return hashStr
    59  }
    60  
    61  func (a *APIKey) authTLS(c *gin.Context, logger *log.Entry) *ent.Bouncer {
    62  	if a.TlsAuth == nil {
    63  		logger.Error("TLS Auth is not configured but client presented a certificate")
    64  		return nil
    65  	}
    66  
    67  	validCert, extractedCN, err := a.TlsAuth.ValidateCert(c)
    68  	if !validCert {
    69  		logger.Error(err)
    70  		return nil
    71  	}
    72  
    73  	if err != nil {
    74  		logger.Error(err)
    75  		return nil
    76  	}
    77  
    78  	logger = logger.WithFields(log.Fields{
    79  		"cn": extractedCN,
    80  	})
    81  
    82  	bouncerName := fmt.Sprintf("%s@%s", extractedCN, c.ClientIP())
    83  	bouncer, err := a.DbClient.SelectBouncerByName(bouncerName)
    84  
    85  	// This is likely not the proper way, but isNotFound does not seem to work
    86  	if err != nil && strings.Contains(err.Error(), "bouncer not found") {
    87  		// Because we have a valid cert, automatically create the bouncer in the database if it does not exist
    88  		// Set a random API key, but it will never be used
    89  		apiKey, err := GenerateAPIKey(dummyAPIKeySize)
    90  		if err != nil {
    91  			logger.Errorf("error generating mock api key: %s", err)
    92  			return nil
    93  		}
    94  
    95  		logger.Infof("Creating bouncer %s", bouncerName)
    96  
    97  		bouncer, err = a.DbClient.CreateBouncer(bouncerName, c.ClientIP(), HashSHA512(apiKey), types.TlsAuthType)
    98  		if err != nil {
    99  			logger.Errorf("while creating bouncer db entry: %s", err)
   100  			return nil
   101  		}
   102  	} else if err != nil {
   103  		// error while selecting bouncer
   104  		logger.Errorf("while selecting bouncers: %s", err)
   105  		return nil
   106  	} else if bouncer.AuthType != types.TlsAuthType {
   107  		// bouncer was found in DB
   108  		logger.Errorf("bouncer isn't allowed to auth by TLS")
   109  		return nil
   110  	}
   111  
   112  	return bouncer
   113  }
   114  
   115  func (a *APIKey) authPlain(c *gin.Context, logger *log.Entry) *ent.Bouncer {
   116  	val, ok := c.Request.Header[APIKeyHeader]
   117  	if !ok {
   118  		logger.Errorf("API key not found")
   119  		return nil
   120  	}
   121  
   122  	hashStr := HashSHA512(val[0])
   123  
   124  	bouncer, err := a.DbClient.SelectBouncer(hashStr)
   125  	if err != nil {
   126  		logger.Errorf("while fetching bouncer info: %s", err)
   127  		return nil
   128  	}
   129  
   130  	if bouncer.AuthType != types.ApiKeyAuthType {
   131  		logger.Errorf("bouncer %s attempted to login using an API key but it is configured to auth with %s", bouncer.Name, bouncer.AuthType)
   132  		return nil
   133  	}
   134  
   135  	return bouncer
   136  }
   137  
   138  func (a *APIKey) MiddlewareFunc() gin.HandlerFunc {
   139  	return func(c *gin.Context) {
   140  		var bouncer *ent.Bouncer
   141  
   142  		clientIP := c.ClientIP()
   143  
   144  		logger := log.WithFields(log.Fields{
   145  			"ip": clientIP,
   146  		})
   147  
   148  		if c.Request.TLS != nil && len(c.Request.TLS.PeerCertificates) > 0 {
   149  			bouncer = a.authTLS(c, logger)
   150  		} else {
   151  			bouncer = a.authPlain(c, logger)
   152  		}
   153  
   154  		if bouncer == nil {
   155  			c.JSON(http.StatusForbidden, gin.H{"message": "access forbidden"})
   156  			c.Abort()
   157  
   158  			return
   159  		}
   160  
   161  		logger = logger.WithFields(log.Fields{
   162  			"name": bouncer.Name,
   163  		})
   164  
   165  		if bouncer.IPAddress == "" {
   166  			if err := a.DbClient.UpdateBouncerIP(clientIP, bouncer.ID); err != nil {
   167  				logger.Errorf("Failed to update ip address for '%s': %s\n", bouncer.Name, err)
   168  				c.JSON(http.StatusForbidden, gin.H{"message": "access forbidden"})
   169  				c.Abort()
   170  
   171  				return
   172  			}
   173  		}
   174  
   175  		// Don't update IP on HEAD request, as it's used by the appsec to check the validity of the API key provided
   176  		if bouncer.IPAddress != clientIP && bouncer.IPAddress != "" && c.Request.Method != http.MethodHead {
   177  			log.Warningf("new IP address detected for bouncer '%s': %s (old: %s)", bouncer.Name, clientIP, bouncer.IPAddress)
   178  
   179  			if err := a.DbClient.UpdateBouncerIP(clientIP, bouncer.ID); err != nil {
   180  				logger.Errorf("Failed to update ip address for '%s': %s\n", bouncer.Name, err)
   181  				c.JSON(http.StatusForbidden, gin.H{"message": "access forbidden"})
   182  				c.Abort()
   183  
   184  				return
   185  			}
   186  		}
   187  
   188  		useragent := strings.Split(c.Request.UserAgent(), "/")
   189  		if len(useragent) != 2 {
   190  			logger.Warningf("bad user agent '%s'", c.Request.UserAgent())
   191  			useragent = []string{c.Request.UserAgent(), "N/A"}
   192  		}
   193  
   194  		if bouncer.Version != useragent[1] || bouncer.Type != useragent[0] {
   195  			if err := a.DbClient.UpdateBouncerTypeAndVersion(useragent[0], useragent[1], bouncer.ID); err != nil {
   196  				logger.Errorf("failed to update bouncer version and type: %s", err)
   197  				c.JSON(http.StatusForbidden, gin.H{"message": "bad user agent"})
   198  				c.Abort()
   199  
   200  				return
   201  			}
   202  		}
   203  
   204  		c.Set(BouncerContextKey, bouncer)
   205  	}
   206  }