Skip to content

Commit

Permalink
Merge pull request #57 from hslatman/appsec
Browse files Browse the repository at this point in the history
Add `AppSec` integration
  • Loading branch information
hslatman authored Dec 4, 2024
2 parents ebefe4f + f7ebfa3 commit 982ac18
Show file tree
Hide file tree
Showing 26 changed files with 1,560 additions and 286 deletions.
166 changes: 56 additions & 110 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ A [Caddy](https://caddyserver.com/) module that blocks malicious traffic based o

## Description

__This repository is currently a WIP. Things may change a bit.__

CrowdSec is a free and open source security automation tool that uses local logs and a set of scenarios to infer malicious intent.
In addition to operating locally, an optional community integration is also available, through which crowd-sourced IP reputation lists are distributed.

Expand All @@ -14,27 +12,33 @@ At its core is the CrowdSec Agent, which keeps track of all data and related sys
Bouncers are pieces of software that perform specific actions based on the decisions of the Agent.

This repository contains a custom CrowdSec Bouncer that can be embedded as a Caddy module.
It consists of the follwing three main pieces:
It consists of the following four main pieces:

* A Caddy App
* A Caddy HTTP Handler
* A Caddy Bouncer HTTP Handler
* A Caddy [Layer 4](https://github.com/mholt/caddy-l4) Connection Matcher
* A Caddy AppSec HTTP Handler

The App is responsible for communicating with a CrowdSec Agent via the CrowdSec *Local API* and keeping track of the decisions of the Agent.
The HTTP Handler checks client IPs of incoming requests against the decisions stored by the App.
This way, multiple independent HTTP Handlers or Connection Matchers can use the storage exposed by the App.
The Bouncer HTTP Handler checks client IPs of incoming requests against the decisions stored by the App.
This way, multiple independent HTTP Handlers and Connection Matchers can use the storage exposed by the App.
The App can be configured to use either the StreamBouncer, which gets decisions via a HTTP polling mechanism, or the LiveBouncer, which sends a request on every incoming HTTP request or Layer 4 connection setup.
The Layer 4 Connection Matcher matches TCP and UDP IP addresses against the CrowdSec *Local API*.
Finally, the AppSec HTTP Handler communicates with an AppSec component configured on your CrowdSec deployment, and will check incoming HTTP requests against the rulesets configured.

## Usage

Get the module

```bash
# get the http handler
# get the CrowdSec Bouncer HTTP handler
go get github.com/hslatman/caddy-crowdsec-bouncer/http

# get the layer4 connection matcher (only required if you need support for TCP/UDP level blocking)
# get the CrowdSec layer4 connection matcher (only required if you need support for TCP/UDP level blocking)
go get github.com/hslatman/caddy-crowdsec-bouncer/layer4

# get the AppSec HTTP handler (only required if you want CrowdSec AppSec support)
go get github.com/hslatman/caddy-crowdsec-bouncer/appsec
```

Create a (custom) Caddy server (or use *xcaddy*)
Expand All @@ -45,135 +49,77 @@ package main
import (
cmd "github.com/caddyserver/caddy/v2/cmd"
_ "github.com/caddyserver/caddy/v2/modules/standard"
// import the http handler
// import the bouncer HTTP handler
_ "github.com/hslatman/caddy-crowdsec-bouncer/http"
// import the layer4 matcher (in case you want to block connections to layer4 servers using CrowdSec)
_ "github.com/hslatman/caddy-crowdsec-bouncer/layer4"
// import the appsec HTTP handler (in case you want to block requests using the CrowdSec AppSec component)
_ "github.com/hslatman/caddy-crowdsec-bouncer/appsec"
)

func main() {
cmd.Main()
}
```

Configuration using a Caddyfile is supported for HTTP handlers and Layer 4 matchers.
You'll also need to use a recent version of Caddy (i.e. 2.7.3 and newer) and Go 1.20 (or newer).

Example Caddyfile:

```
{
debug
crowdsec {
api_url http://localhost:8080
api_key <api_key>
ticker_interval 15s
#disable_streaming
#enable_hard_fails
debug
crowdsec {
api_url http://localhost:8080
api_key <api_key>
ticker_interval 15s
appsec_url http://localhost:7422
#disable_streaming
#enable_hard_fails
}
layer4 {
localhost:4444 {
@crowdsec crowdsec
route @crowdsec {
proxy {
upstream localhost:6443
}
}
}
}
}
localhost {
route {
crowdsec
respond "Allowed by CrowdSec!"
}
localhost:8443 {
route {
crowdsec
respond "Allowed by Bouncer!"
}
}
```
Configuration using a Caddyfile is only supported for HTTP handlers.
You'll also need to use a recent version of Caddy (i.e. 2.7.3 and newer) and Go 1.20 (or newer).
In case you want to use the CrowdSec bouncer on TCP or UDP level, you'll need to configure Caddy using the native JSON format.
An example configuration is shown below:

```json
{
"apps": {
"crowdsec": {
"api_key": "<insert_crowdsec_local_api_key_here>",
"api_url": "http://127.0.0.1:8080/",
"ticker_interval": "10s",
"enable_streaming": true,
"enable_hard_fails": false,
},
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"example": {
"listen": [
"127.0.0.1:9443"
],
"routes": [
{
"group": "example-group",
"match": [
{
"path": [
"/*"
]
}
],
"handle": [
{
"handler": "crowdsec"
},
{
"handler": "static_response",
"status_code": "200",
"body": "Hello World!"
},
{
"handler": "headers",
"response": {
"set": {
"Server": ["caddy-cs-bouncer-example-server"]
}
}
}
]
}
],
"logs": {}
}
}
},
"layer4": {
"servers": {
"https_proxy": {
"listen": ["localhost:8443"],
"routes": [
{
"match": [
{
"crowdsec": {},
"tls": {}
}
],
"handle": [
{
"handler": "proxy",
"upstreams": [
{
"dial": ["localhost:9443"]
}
]
}
]
}
]
}
}
},
}
localhost:7443 {
route {
appsec
respond "Allowed by AppSec!"
}
}
localhost:6443 {
route {
crowdsec
appsec
respond "Allowed by Bouncer and AppSec!"
}
}
```

Run the Caddy server

```bash
# with a Caddyfile
go run main.go run -config Caddyfile

# with JSON configuration
go run main.go run -config config.json
```

## Demo
Expand Down
134 changes: 134 additions & 0 deletions appsec/appsec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2024 Herman Slatman
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package appsec

import (
"errors"
"fmt"
"net/http"
"net/netip"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"

"github.com/hslatman/caddy-crowdsec-bouncer/crowdsec"
"github.com/hslatman/caddy-crowdsec-bouncer/internal/bouncer"
"github.com/hslatman/caddy-crowdsec-bouncer/internal/httputils"
)

func init() {
caddy.RegisterModule(Handler{})
httpcaddyfile.RegisterHandlerDirective("appsec", parseCaddyfileHandlerDirective)
}

// Handler checks the CrowdSec AppSec component decided whether
// an HTTP request is blocked or not.
type Handler struct {
logger *zap.Logger
crowdsec *crowdsec.CrowdSec
}

// CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.appsec",
New: func() caddy.Module { return new(Handler) },
}
}

// Provision sets up the CrowdSec AppSec handler.
func (h *Handler) Provision(ctx caddy.Context) error {
crowdsecAppIface, err := ctx.App("crowdsec")
if err != nil {
return fmt.Errorf("getting crowdsec app: %v", err)
}
h.crowdsec = crowdsecAppIface.(*crowdsec.CrowdSec)

h.logger = ctx.Logger(h)

return nil
}

// Validate ensures the app's configuration is valid.
func (h *Handler) Validate() error {
if h.crowdsec == nil {
return errors.New("crowdsec app not available")
}

return nil
}

// Cleanup cleans up resources when the module is being stopped.
func (h *Handler) Cleanup() error {
h.logger.Sync() // nolint

return nil
}

// ServeHTTP is the Caddy handler for serving HTTP requests.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
var (
ctx = r.Context()
ip netip.Addr
)

ctx, ip = httputils.EnsureIP(ctx)
if err := h.crowdsec.CheckRequest(ctx, r); err != nil {
a := &bouncer.AppSecError{}
if !errors.As(err, &a) {
return err
}

switch a.Action {
case "allow":
// nothing to do
case "log":
h.logger.Info("appsec rule triggered", zap.String("ip", ip.String()), zap.String("action", a.Action))
default:
return httputils.WriteResponse(w, h.logger, a.Action, ip.String(), a.Duration, a.StatusCode)
}
}

// Continue down the handler stack
if err := next.ServeHTTP(w, r.WithContext(ctx)); err != nil {
return err
}

return nil
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}

// parseCaddyfileHandlerDirective parses the `crowdsec` Caddyfile directive
func parseCaddyfileHandlerDirective(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var handler Handler
err := handler.UnmarshalCaddyfile(h.Dispenser)
return &handler, err
}

// Interface guards
var (
_ caddy.Module = (*Handler)(nil)
_ caddy.Provisioner = (*Handler)(nil)
_ caddy.Validator = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ caddyfile.Unmarshaler = (*Handler)(nil)
)
17 changes: 16 additions & 1 deletion crowdsec/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package crowdsec
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -70,8 +71,22 @@ func parseCrowdSec(d *caddyfile.Dispenser, existingVal any) (any, error) {
return nil, d.ArgErr()
}
cs.EnableHardFails = &tv
case "appsec_url":
if !d.NextArg() {
return nil, d.ArgErr()
}
cs.AppSecUrl = d.Val()
case "appsec_max_body_bytes":
if !d.NextArg() {
return nil, d.ArgErr()
}
v, err := strconv.Atoi(d.Val())
if err != nil {
return nil, d.Errf("invalid maximum number of bytes %q: %v", d.Val(), err)
}
cs.AppSecMaxBodySize = v
default:
return nil, d.Errf("invalid configuration token provided: %s", d.Val())
return nil, d.Errf("invalid configuration token %q provided", d.Val())
}
}

Expand Down
Loading

0 comments on commit 982ac18

Please sign in to comment.