Skip to content

Commit

Permalink
feat: cleanup code
Browse files Browse the repository at this point in the history
  • Loading branch information
GoliathLabs committed Jun 8, 2024
1 parent 441664f commit 8704fb9
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support

Expand Down
19 changes: 19 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Title for the gitleaks configuration file.
title = "Gitleaks configuration"

# Extend the base (this) configuration. When you extend a configuration
# the base rules take precendence over the extended rules. I.e, if there are
# duplicate rules in both the base configuration and the extended configuration
# the base rules will override the extended rules.
# Another thing to know with extending configurations is you can chain together
# multiple configuration files to a depth of 2. Allowlist arrays are appended
# and can contain duplicates.
# useDefault and path can NOT be used at the same time. Choose one.
[extend]
# useDefault will extend the base configuration with the default gitleaks config:
# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml
useDefault = true

[allowlist]
description = "ignore commit 04a37c7014c951bdf6bc3c8bbc802c9a772bf96a"
commits = [ "04a37c7014c951bdf6bc3c8bbc802c9a772bf96a", "3effed4e2c73249b9cb35718efdadbda9614a6b3"]
108 changes: 57 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

![GitHub](https://img.shields.io/github/license/fbonalair/traefik-crowdsec-bouncer)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/fbonalair/traefik-crowdsec-bouncer)
[![Go Report Card](https://goreportcard.com/badge/github.com/fbonalair/traefik-crowdsec-bouncer)](https://goreportcard.com/report/github.com/fbonalair/traefik-crowdsec-bouncer)
Expand All @@ -7,79 +8,84 @@
![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/fbonalair/traefik-crowdsec-bouncer)

# traefik-crowdsec-bouncer
A http service to verify request and bounce them according to decisions made by CrowdSec.

A HTTP service to verify requests and bounce them according to decisions made by CrowdSec.

# Description
This repository aim to implement a [CrowdSec](https://doc.crowdsec.net/) bouncer for the router [Traefik](https://doc.traefik.io/traefik/) to block malicious IP to access your services.
For this it leverages [Traefik v2 ForwardAuth middleware](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) and query CrowdSec with client IP.
If the client IP is on ban list, it will get a http code 403 response. Otherwise, request will continue as usual.

This repository aims to implement a [CrowdSec](https://doc.crowdsec.net/) bouncer for the router [Traefik](https://doc.traefik.io/traefik/) to block malicious IPs from accessing your services. It leverages the [Traefik v2 ForwardAuth middleware](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) and queries CrowdSec with the client IP. If the client IP is on the ban list, it will receive a HTTP code 403 response. Otherwise, the request will continue as usual.

# Demo

## Prerequisites
[Docker](https://docs.docker.com/get-docker/) and [Docker-compose](https://docs.docker.com/compose/install/) installed.
You can use the docker-compose in the examples' folder as a starting point.
Through traefik it exposes the whoami countainer on port 80, with the bouncer accepting and rejecting client IP.
Launch your all services except the bouncer with the follow commands:

Ensure [Docker](https://docs.docker.com/get-docker/) and [Docker-compose](https://docs.docker.com/compose/install/) are installed. You can use the docker-compose file in the examples folder as a starting point. Through Traefik, it exposes the whoami container on port 80, with the bouncer accepting and rejecting client IPs.

Launch all services except the bouncer with the following commands:

```bash
git clone https://github.com/fbonalair/traefik-crowdsec-bouncer.git && \
cd traefik-crowdsec-bouncer/examples && \
docker-compose up -d traefik crowdsec whoami
```

## Procedure
1. Get a bouncer API key from CrowdSec with command `docker exec crowdsec-example cscli bouncers add traefik-bouncer`
2. Copy the API key printed. You **_WON'T_** be able the get it again.
3. Paste this API key as the value for bouncer environment variable `CROWDSEC_BOUNCER_API_KEY`, instead of "MyApiKey"
4. Start bouncer in attach mode with `docker-compose up bouncer`
5. Visit <http://localhost/>. You will see the container whoami page, copy your IP address from `X-Real-Ip` line (i.e. 192.168.128.1).
In your console, you will see lines showing your authorized request (i.e. "status":200).
6. In another console, ban your IP with command `docker exec crowdsec-example cscli decisions add --ip 192.168.128.1`, modify the IP with your address.
7. Visit <http://localhost/> again, in your browser you will see "Forbidden" since this time since you've been banned.
Though the console you will see "status":403.

1. Get a bouncer API key from CrowdSec with the command `docker exec crowdsec-example cscli bouncers add traefik-bouncer`
2. Copy the printed API key. You **_WON'T_** be able to retrieve it again.
3. Paste this API key as the value for the bouncer environment variable `CROWDSEC_BOUNCER_API_KEY`, instead of "MyApiKey"
4. Start the bouncer in attach mode with `docker-compose up bouncer`
5. Visit <http://localhost/>. You will see the container whoami page. Copy your IP address from the `X-Real-Ip` line (e.g., 192.168.128.1).
In your console, you will see lines showing your authorized request (i.e., "status": 200).
6. In another console, ban your IP with the command `docker exec crowdsec-example cscli decisions add --ip 192.168.128.1`, replacing the IP with your address.
7. Visit <http://localhost/> again. In your browser, you will see "Forbidden" since you have been banned.
In the console, you will see "status": 403.
8. Unban yourself with `docker exec crowdsec-example cscli decisions delete --ip 192.168.128.1`
9. Visit <http://localhost/> one last time, you will have access to the container whoami.
9. Visit <http://localhost/> one last time. You will have access to the container whoami.

Enjoy!

# Usage
For now, this web service is mainly fought to be used as a container.
If you need to build from source, you can get some inspiration from the Dockerfile.

For now, this web service is mainly intended to be used as a container. If you need to build from source, you can get some inspiration from the Dockerfile.

## Prerequisites
You should have Traefik v2 and a CrowdSec instance running.
The container is available on docker as image `fbonalair/traefik-crowdsec-bouncer`. Host it as you see fit, though it must have access to CrowdSec and be accessible by Traefik.
Follow [traefik v2 ForwardAuth middleware](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) documentation to create a forwardAuth middle pointing to your bouncer host.
Generate a bouncer API key following [CrowdSec documentation](https://doc.crowdsec.net/docs/cscli/cscli_bouncers_add)

You should have Traefik v2 and a CrowdSec instance running. The container is available on Docker as the image `fbonalair/traefik-crowdsec-bouncer`. Host it as you see fit, though it must have access to CrowdSec and be accessible by Traefik. Follow the [Traefik v2 ForwardAuth middleware](https://doc.traefik.io/traefik/middlewares/http/forwardauth/) documentation to create a forwardAuth middle pointing to your bouncer host. Generate a bouncer API key following [CrowdSec documentation](https://doc.crowdsec.net/docs/cscli/cscli_bouncers_add).

## Configuration
The webservice configuration is made via environment variables:

* `CROWDSEC_BOUNCER_API_KEY` - CrowdSec bouncer API key required to be authorized to request local API (required)`
* `CROWDSEC_AGENT_HOST` - Host and port of CrowdSec agent, i.e. crowdsec-agent:8080 (required)`
* `CROWDSEC_BOUNCER_SCHEME` - Scheme to query CrowdSec agent. Expected value: http, https. Default to http`
* `CROWDSEC_BOUNCER_LOG_LEVEL` - Minimum log level for bouncer. Expected value [zerolog levels](https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging). Default to 1
* `CROWDSEC_BOUNCER_BAN_RESPONSE_CODE` - HTTP code to respond in case of ban. Default to 403
* `CROWDSEC_BOUNCER_BAN_RESPONSE_MSG` - HTTP body as message to respond in case of ban. Default to Forbidden
* `HEALTH_CHECKER_TIMEOUT_DURATION` - [Golang string represation of a duration](https://pkg.go.dev/time#ParseDuration) to wait for bouncer's answer before failing health check. Default to 2s
* `PORT` - Change listening port of web server. Default listen on 8080
* `GIN_MODE` - By default, run app in "debug" mode. Set it to "release" in production
* `TRUSTED_PROXIES` - List of trusted proxies IP addresses in CIDR format, delimited by ','. Default of 0.0.0.0/0 should be fine for most use cases, but you HAVE to add them directly in Traefik.

## Exposed routes
The webservice exposes some routes:

* GET `/api/v1/forwardAuth` - Main route to be used by Traefik: query CrowdSec agent with the header `X-Real-Ip` as client IP`
* GET `/api/v1/ping` - Simple health route that respond pong with http 200`
* GET `/api/v1/healthz` - Another health route that query CrowdSec agent with localhost (127.0.0.1)`
* GET `/api/v1/metrics` - Prometheus route to scrap metrics

The web service configuration is managed via environment variables:

* `CROWDSEC_BOUNCER_API_KEY` - CrowdSec bouncer API key required to authorize requests to the local API (required)
* `CROWDSEC_AGENT_HOST` - Host and port of the CrowdSec agent, e.g., crowdsec-agent:8080 (required)
* `CROWDSEC_BOUNCER_SCHEME` - Scheme to query the CrowdSec agent. Expected values: http, https. Defaults to http
* `CROWDSEC_BOUNCER_LOG_LEVEL` - Minimum log level for the bouncer. Expected values: [zerolog levels](https://pkg.go.dev/github.com/rs/zerolog#readme-leveled-logging). Defaults to 1
* `CROWDSEC_BOUNCER_BAN_RESPONSE_CODE` - HTTP code to respond in case of a ban. Defaults to 403
* `CROWDSEC_BOUNCER_BAN_RESPONSE_MSG` - HTTP body message to respond in case of a ban. Defaults to "Forbidden"
* `HEALTH_CHECKER_TIMEOUT_DURATION` - [Golang string representation of a duration](https://pkg.go.dev/time#ParseDuration) to wait for the bouncer's answer before failing the health check. Defaults to 2s
* `PORT` - Change the listening port of the web server. Defaults to 8080
* `GIN_MODE` - By default, runs the app in "debug" mode. Set it to "release" in production
* `TRUSTED_PROXIES` - List of trusted proxies' IP addresses in CIDR format, delimited by commas. Default is 0.0.0.0/0, which should be fine for most use cases, but you MUST add them directly in Traefik.

## Exposed Routes

The web service exposes the following routes:

* GET `/api/v1/forwardAuth` - Main route to be used by Traefik: queries the CrowdSec agent with the header `X-Real-Ip` as the client IP
* GET `/api/v1/ping` - Simple health route that responds with "pong" and HTTP 200
* GET `/api/v1/healthz` - Another health route that queries the CrowdSec agent with localhost (127.0.0.1)
* GET `/api/v1/metrics` - Prometheus route to scrape metrics

# Contribution
Any constructive feedback is welcome, fill free to add an issue or a pull request. I will review it and integrate it to the code.

Any constructive feedback is welcome. Feel free to add an issue or a pull request. I will review it and integrate it into the code.

## Local Setup
1. Start docker compose with docker-compose up -d
2. Create `_test.env` from template `_test.env.example` such as `cp _test.env.example _test.env`
3. Get an API key for your bouncer with : ` docker exec traefik-crowdsec-bouncer-crowdsec-1 cscli bouncers add traefik-bouncer`
4. In `_test.env` replace `<your_generated_api_key>` with the previously generated key
5. Adding a banned IP to your crodwsec instance with : `docker exec traefik-crowdsec-bouncer-crowdsec-1 cscli decisions add -i 1.2.3.4`
6. Run test with `godotenv -f ./_test.env go test -cover`

1. Start docker-compose with `docker-compose up -d`
2. Create `_test.env` from the template `_test.env.example` with the command `cp _test.env.example _test.env`
3. Get an API key for your bouncer with the command `docker exec traefik-crowdsec-bouncer-crowdsec-1 cscli bouncers add traefik-bouncer`
4. In `_test.env`, replace `<your_generated_api_key>` with the previously generated key
5. Add a banned IP to your CrowdSec instance with the command `docker exec traefik-crowdsec-bouncer-crowdsec-1 cscli decisions add -i 1.2.3.4`
6. Run tests with `godotenv -f ./_test.env go test -cover`
10 changes: 5 additions & 5 deletions bouncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"os"

. "github.com/fbonalair/traefik-crowdsec-bouncer/config"
"github.com/fbonalair/traefik-crowdsec-bouncer/controler"
"github.com/fbonalair/traefik-crowdsec-bouncer/controller"
"github.com/gin-contrib/logger"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
Expand Down Expand Up @@ -58,9 +58,9 @@ func setupRouter() (*gin.Engine, error) {
router.Use(logger.SetLogger(
logger.WithSkipPath([]string{"/api/v1/ping", "/api/v1/healthz"}),
))
router.GET("/api/v1/ping", controler.Ping)
router.GET("/api/v1/healthz", controler.Healthz)
router.GET("/api/v1/forwardAuth", controler.ForwardAuth)
router.GET("/api/v1/metrics", controler.Metrics)
router.GET("/api/v1/ping", controller.Ping)
router.GET("/api/v1/healthz", controller.Healthz)
router.GET("/api/v1/forwardAuth", controller.ForwardAuth)
router.GET("/api/v1/metrics", controller.Metrics)
return router, nil
}
31 changes: 13 additions & 18 deletions controler/controler.go → controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package controler
package controller

import (
"bytes"
Expand All @@ -8,7 +8,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -52,7 +51,7 @@ var client = &http.Client{
Call Crowdsec local IP and with realIP and return true if IP does NOT have a ban decisions.
*/
func isIpAuthorized(clientIP string) (bool, error) {
// Generating crowdsec API request
// Generate Crowdsec API request
decisionUrl := url.URL{
Scheme: crowdsecBouncerScheme,
Host: crowdsecBouncerHost,
Expand All @@ -67,42 +66,38 @@ func isIpAuthorized(clientIP string) (bool, error) {
log.Debug().
Str("method", http.MethodGet).
Str("url", decisionUrl.String()).
Msg("Request Crowdsec's decision Local API")
Msg("Requesting Crowdsec's decision Local API")

// Calling crowdsec API
// Call Crowdsec API
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusForbidden {
return false, err
return false, nil
}

// Parsing response
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Err(err).Msg("An error occurred while closing body reader")
}
}(resp.Body)
reqBody, err := ioutil.ReadAll(resp.Body)
// Parse response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
if bytes.Equal(reqBody, []byte("null")) {
if bytes.Equal(respBody, []byte("null")) {
log.Debug().Msgf("No decision for IP %q. Accepting", clientIP)
return true, nil
}

log.Debug().RawJSON("decisions", reqBody).Msg("Found Crowdsec's decision(s), evaluating ...")
log.Debug().RawJSON("decisions", respBody).Msg("Found Crowdsec's decision(s), evaluating ...")
var decisions []model.Decision
err = json.Unmarshal(reqBody, &decisions)
err = json.Unmarshal(respBody, &decisions)
if err != nil {
return false, err
}

// Authorization logic
return len(decisions) < 0, nil
return len(decisions) == 0, nil
}

/*
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/fbonalair/traefik-crowdsec-bouncer

go 1.22
go 1.21

toolchain go1.22.2

require (
github.com/gin-contrib/logger v1.1.2
Expand Down Expand Up @@ -52,4 +54,4 @@ replace github.com/fbonalair/traefik-crowdsec-bouncer/config => ../config

replace github.com/fbonalair/traefik-crowdsec-bouncer/model => ../model

replace github.com/fbonalair/traefik-crowdsec-bouncer/controler => ../controler
replace github.com/fbonalair/traefik-crowdsec-bouncer/controller => ../controller
2 changes: 1 addition & 1 deletion healthcheck/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/fbonalair/healthcheck

go 1.22
go 1.21

0 comments on commit 8704fb9

Please sign in to comment.