diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f27e3cb..41d9cf1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -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 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..6c055bc --- /dev/null +++ b/.gitleaks.toml @@ -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"] diff --git a/README.md b/README.md index 90ca91d..9f1fc65 100644 --- a/README.md +++ b/README.md @@ -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) @@ -7,19 +8,21 @@ ![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 && \ @@ -27,59 +30,62 @@ git clone https://github.com/fbonalair/traefik-crowdsec-bouncer.git && \ ``` ## 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 . 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 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 . 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 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 one last time, you will have access to the container whoami. +9. Visit 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 `` 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 `` 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` diff --git a/bouncer.go b/bouncer.go index 134ebb3..ddb3fb8 100644 --- a/bouncer.go +++ b/bouncer.go @@ -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" @@ -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 } diff --git a/controler/controler.go b/controller/controller.go similarity index 86% rename from controler/controler.go rename to controller/controller.go index 2f6aae0..200a02f 100644 --- a/controler/controler.go +++ b/controller/controller.go @@ -1,4 +1,4 @@ -package controler +package controller import ( "bytes" @@ -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" @@ -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, @@ -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 } /* diff --git a/go.mod b/go.mod index a456dc7..e094d83 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/healthcheck/go.mod b/healthcheck/go.mod index 3882538..cf5b620 100644 --- a/healthcheck/go.mod +++ b/healthcheck/go.mod @@ -1,3 +1,3 @@ module github.com/fbonalair/healthcheck -go 1.22 +go 1.21