Initial Commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Markus Pesch 2023-10-02 12:50:34 +02:00
commit 3998f9e2c2
Signed by: volker.raschek
GPG Key ID: 852BCC170D81A982
38 changed files with 3485 additions and 0 deletions

832
.drone.yml Normal file
View File

@ -0,0 +1,832 @@
---
kind: pipeline
type: kubernetes
name: linter
clone:
disable: true
platform:
os: linux
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: markdown lint
commands:
- markdownlint *.md
image: git.cryptic.systems/volker.raschek/markdownlint:0.36.0
resources:
limits:
cpu: 150
memory: 150M
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
resources:
limits:
cpu: 150
memory: 150M
when:
status:
- changed
- failure
trigger:
event:
exclude:
- tag
---
kind: pipeline
type: docker
name: unit-test-amd64
clone:
disable: true
platform:
arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: unit-test
commands:
- go test -v ./...
image: docker.io/library/golang:1.21.0
trigger:
event:
exclude:
- tag
---
kind: pipeline
type: docker
name: unit-test-arm-v7
clone:
disable: true
platform:
arch: arm
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: unit-test
commands:
- go test -v ./...
image: docker.io/library/golang:1.21.0
trigger:
event:
include:
- pull_request
- push
exclude:
- tag
---
kind: pipeline
type: docker
name: unit-test-arm64
clone:
disable: true
platform:
arch: arm64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: unit-test
commands:
- go test -v ./...
image: docker.io/library/golang:1.21.0
trigger:
event:
include:
- pull_request
- push
exclude:
- tag
---
kind: pipeline
type: docker
name: dry-run-amd64
clone:
disable: true
depends_on:
- linter
- unit-test-amd64
platform:
os: linux
arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
dry_run: true
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-amd64
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
exclude:
- master
event:
- pull_request
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: dry-run-arm-v7
clone:
disable: true
depends_on:
- linter
- unit-test-arm-v7
platform:
os: linux
arch: arm
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
dry_run: true
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-arm-v7
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
exclude:
- master
event:
- pull_request
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: dry-run-arm64-v8
clone:
disable: true
depends_on:
- linter
- unit-test-arm64
platform:
os: linux
arch: arm64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
dry_run: true
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-arm64-v8
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
exclude:
- master
event:
- pull_request
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: latest-amd64
clone:
disable: true
depends_on:
- linter
- unit-test-amd64
platform:
os: linux
arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-amd64
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
- master
event:
- cron
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: latest-arm-v7
clone:
disable: true
depends_on:
- linter
- unit-test-arm-v7
platform:
os: linux
arch: arm
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-arm-v7
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
- master
event:
- cron
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: latest-arm64-v8
clone:
disable: true
depends_on:
- linter
- unit-test-arm64
platform:
os: linux
arch: arm64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: false
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
tags: latest-arm64-v8
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
branch:
- master
event:
- cron
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: kubernetes
name: latest-manifest
clone:
disable: true
depends_on:
- latest-amd64
- latest-arm-v7
- latest-arm64-v8
# docker.io/plugins/manifest only for amd64 architectures available
node_selector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build-manifest
image: docker.io/plugins/manifest:1.4.0
settings:
auto_tag: false
ignore_missing: true
spec: manifest.tmpl
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
resources:
limits:
cpu: 150
memory: 150M
when:
status:
- changed
- failure
trigger:
branch:
- master
event:
- cron
- push
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: tagged-amd64
clone:
disable: true
platform:
os: linux
arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: true
auto_tag_suffix: amd64
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
build_args:
- VERSION=${DRONE_TAG}
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
event:
- tag
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: tagged-arm-v7
clone:
disable: true
platform:
os: linux
arch: arm
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: true
auto_tag_suffix: arm-v7
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
build_args:
- VERSION=${DRONE_TAG}
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
event:
- tag
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: docker
name: tagged-arm64-v8
clone:
disable: true
platform:
os: linux
arch: arm64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build
image: docker.io/plugins/docker:20.10.9
settings:
auto_tag: true
auto_tag_suffix: arm64-v8
dockerfile: Dockerfile
force_tag: true
no_cache: true
purge: true
mirror:
from_secret: docker_io_mirror
registry: git.cryptic.systems
repo: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
build_args:
- VERSION=${DRONE_TAG}
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
when:
status:
- changed
- failure
trigger:
event:
- tag
repo:
- volker.raschek/prometheus-fail2ban-exporter
---
kind: pipeline
type: kubernetes
name: tagged-manifest
clone:
disable: true
depends_on:
- tagged-amd64
- tagged-arm-v7
- tagged-arm64-v8
# docker.io/plugins/manifest only for amd64 architectures available
node_selector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
steps:
- name: clone
image: git.cryptic.systems/volker.raschek/git:1.3.1
- name: build-manifest
image: docker.io/plugins/manifest:1.4.0
settings:
auto_tag: true
ignore_missing: true
spec: manifest.tmpl
username:
from_secret: git_cryptic_systems_container_registry_user
password:
from_secret: git_cryptic_systems_container_registry_password
- name: email-notification
environment:
SMTP_FROM_ADDRESS:
from_secret: smtp_from_address
SMTP_FROM_NAME:
from_secret: smtp_from_name
SMTP_HOST:
from_secret: smtp_host
SMTP_USERNAME:
from_secret: smtp_username
SMTP_PASSWORD:
from_secret: smtp_password
image: git.cryptic.systems/volker.raschek/drone-email:0.1.5
resources:
limits:
cpu: 150
memory: 150M
when:
status:
- changed
- failure
trigger:
event:
- tag
repo:
- volker.raschek/prometheus-fail2ban-exporter

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
[Makefile]
indent_style = tab

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea/
vendor/
*.iml
build/
dist/
prometheus-fail2ban-exporter

143
.markdownlint.yaml Normal file
View File

@ -0,0 +1,143 @@
# markdownlint YAML configuration
# https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
# Default state for all rules
default: true
# Path to configuration file to extend
extends: null
# MD003/heading-style/header-style - Heading style
MD003:
# Heading style
style: "atx"
# MD004/ul-style - Unordered list style
MD004:
style: "dash"
# MD007/ul-indent - Unordered list indentation
MD007:
# Spaces for indent
indent: 2
# Whether to indent the first level of the list
start_indented: false
# MD009/no-trailing-spaces - Trailing spaces
MD009:
# Spaces for line break
br_spaces: 2
# Allow spaces for empty lines in list items
list_item_empty_lines: false
# Include unnecessary breaks
strict: false
# MD010/no-hard-tabs - Hard tabs
MD010:
# Include code blocks
code_blocks: true
# MD012/no-multiple-blanks - Multiple consecutive blank lines
MD012:
# Consecutive blank lines
maximum: 1
# MD013/line-length - Line length
MD013:
# Number of characters
line_length: 80
# Number of characters for headings
heading_line_length: 80
# Number of characters for code blocks
code_block_line_length: 80
# Include code blocks
code_blocks: false
# Include tables
tables: false
# Include headings
headings: true
# Include headings
headers: true
# Strict length checking
strict: false
# Stern length checking
stern: false
# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines
MD022:
# Blank lines above heading
lines_above: 1
# Blank lines below heading
lines_below: 1
# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
MD024:
# Only check sibling headings
allow_different_nesting: true
# MD025/single-title/single-h1 - Multiple top-level headings in the same document
MD025:
# Heading level
level: 1
# RegExp for matching title in front matter
front_matter_title: "^\\s*title\\s*[:=]"
# MD026/no-trailing-punctuation - Trailing punctuation in heading
MD026:
# Punctuation characters
punctuation: ".,;:!。,;:!"
# MD029/ol-prefix - Ordered list item prefix
MD029:
# List style
style: "one_or_ordered"
# MD030/list-marker-space - Spaces after list markers
MD030:
# Spaces for single-line unordered list items
ul_single: 1
# Spaces for single-line ordered list items
ol_single: 1
# Spaces for multi-line unordered list items
ul_multi: 1
# Spaces for multi-line ordered list items
ol_multi: 1
# MD033/no-inline-html - Inline HTML
MD033:
# Allowed elements
allowed_elements: []
# MD035/hr-style - Horizontal rule style
MD035:
# Horizontal rule style
style: "---"
# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading
MD036:
# Punctuation characters
punctuation: ".,;:!?。,;:!?"
# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading
MD041:
# Heading level
level: 1
# RegExp for matching title in front matter
front_matter_title: "^\\s*title\\s*[:=]"
# MD044/proper-names - Proper names should have the correct capitalization
MD044:
# List of proper names
names: []
# Include code blocks
code_blocks: false
# MD046/code-block-style - Code block style
MD046:
# Block style
style: "fenced"
# MD048/code-fence-style - Code fence style
MD048:
# Code fence syle
style: "backtick"

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM docker.io/library/golang:1.20.5-buster AS build
WORKDIR /workspace
ADD . /workspace
RUN apt update --yes && \
apt install --yes build-essential && \
make install \
PREFIX=/usr \
DESTDIR=/app \
EXECUTABLE=prometheus-fail2ban-exporter
FROM docker.io/library/debian:10-slim
COPY --from=build /app /
EXPOSE 9191
ENTRYPOINT [ "/usr/bin/prometheus-fail2ban-exporter" ]

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Hector
Copyright (c) 2023 Markus Pesch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

85
Makefile Normal file
View File

@ -0,0 +1,85 @@
DESTDIR?=
PREFIX?=/usr/local
EXECUTABLE?=prometheus-fail2ban-exporter
CONTAINER_RUNTIME?=$(shell which podman)
# List make commands
.PHONY: ls
ls:
cat Makefile | grep "^[a-zA-Z#].*" | cut -d ":" -f 1 | sed s';#;\n#;'g
# Download dependencies
.PHONY: download
download:
go mod download
# Update project dependencies
.PHONY: update
update:
go get -u
go mod download
go mod tidy
# Run project tests
.PHONY: test
test: download
go test ./... -v -race
# Look for "suspicious constructs" in source code
.PHONY: vet
vet: download
go vet ./...
# Format code
.PHONY: fmt
fmt: download
go mod tidy
go fmt ./...
# Check for unformatted go code
.PHONY: check/fmt
check/fmt: download
test -z $(shell gofmt -l .)
# Build project
.PHONY: build
build:
CGO_ENABLED=0 go build \
-ldflags "\
-X main.version=${shell git describe --tags} \
-X main.commit=${shell git rev-parse HEAD} \
-X main.date=${shell date --iso-8601=seconds} \
-X main.builtBy=manual \
" \
-trimpath \
-o ${EXECUTABLE} \
exporter.go
# build container-image
.PHONY: build/container-image
build/container-image:
${CONTAINER_RUNTIME} build \
--tag ${EXECUTABLE} \
.
.PHONY: install
install: build
mkdir --parents ${DESTDIR}/usr/lib/systemd/system
sed -e "s/EXECUTABLE/${EXECUTABLE}/gm" systemd/systemd.service > ${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service
chmod 0644 ${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service
install -D --mode 0755 --target-directory ${DESTDIR}${PREFIX}/bin ${EXECUTABLE}
# NOTE: Set restrict file permissions by default to protect optional basic auth credentials
install -D --mode 0600 env ${DESTDIR}/etc/conf.d/${EXECUTABLE}
install -D --mode 0755 --target-directory ${DESTDIR}${PREFIX}/share/licenses/${EXECUTABLE} LICENSE
.PHONY: uninstall
uninstall:
-rm --recursive --force \
${DESTDIR}${PREFIX}/bin/${EXECUTABLE} \
${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service \
${DESTDIR}/etc/conf.d/${EXECUTABLE} \
${DESTDIR}${PREFIX}/share/licenses/${EXECUTABLE}/LICENSE

232
README.md Normal file
View File

@ -0,0 +1,232 @@
# prometheus-fail2ban-exporter
[![Build Status](https://drone.cryptic.systems/api/badges/volker.raschek/prometheus-fail2ban-exporter/status.svg)](https://drone.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter)
This is a fork of Hector's fail2ban
[exporter](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter). This
fork contains some changes to get the application running in a kubernetes
cluster.
## Table of Contents
1. Quick Start
2. Metrics
3. Configuration
4. Building from source
5. Textfile metrics
## 1. Quick Start
The exporter can be run as a standalone binary or a docker container.
### 1.1. Standalone
The following command will start collecting metrics from the
`/var/run/fail2ban/fail2ban.sock` file and expose them on port `9191`.
```bash
$ fail2ban_exporter --collector.f2b.socket=/var/run/fail2ban/fail2ban.sock --web.listen-address=":9191"
2022/02/20 09:54:06 fail2ban exporter version 0.8.1
2022/02/20 09:54:06 starting server at :9191
2022/02/20 09:54:06 reading metrics from fail2ban socket: /var/run/fail2ban/fail2ban.sock
2022/02/20 09:54:06 metrics available at '/metrics'
2022/02/20 09:54:06 ready
```
Binary files for each release can be found on the
[releases](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/releases)
page.
There is also an [example systemd service
file](/_examples/systemd/fail2ban_exporter.service) included in the repository.
This is a starting point to run the exporter as a service.
### 1.2. Docker
#### Docker run
```bash
$ docker run -d \
--name "fail2ban-exporter" \
-v /var/run/fail2ban:/var/run/fail2ban:ro \
-p "9191:9191" \
registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
```
#### Docker compose
```yaml
version: "2"
services:
exporter:
image: registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
volumes:
- /var/run/fail2ban/:/var/run/fail2ban:ro
ports:
- "9191:9191"
```
Use the `:latest` tag to get the latest stable release. See the [registry
page](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/container_registry)
for all available tags.
**NOTE:** While it is possible to mount the `fail2ban.sock` file directly, it is
recommended to mount the parent folder instead. The `.sock` file is deleted by
fail2ban on shutdown and re-created on startup and this causes problems for the
docker mount. See [this
reply](https://gitlab.com/hectorjsmith/fail2ban-prometheus-exporter/-/issues/11#note_665003499)
for more details.
## 2. Metrics
The exporter exposes the following metrics:
*All metric names are prefixed with `f2b_`*
| Metric | Description | Example |
|------------------------------|------------------------------------------------------------------------------------|-----------------------------------------------------|
| `up` | Returns 1 if the exporter is up and running | `f2b_up 1` |
| `errors` | Count the number of errors since startup by type | |
| `errors{type="socket_conn"}` | Errors connecting to the fail2ban socket (e.g. connection refused) | `f2b_errors{type="socket_conn"} 0` |
| `errors{type="socket_req"}` | Errors sending requests to the fail2ban server (e.g. invalid responses) | `f2b_errors{type="socket_req"} 0` |
| `jail_count` | Number of jails configured in fail2ban | `f2b_jail_count 2` |
| `jail_banned_current` | Number of IPs currently banned per jail | `f2b_jail_banned_current{jail="sshd"} 15` |
| `jail_banned_total` | Total number of banned IPs since fail2ban startup per jail (includes expired bans) | `f2b_jail_banned_total{jail="sshd"} 31` |
| `jail_failed_current` | Number of current failures per jail | `f2b_jail_failed_current{jail="sshd"} 6` |
| `jail_failed_total` | Total number of failures since fail2ban startup per jail | `f2b_jail_failed_total{jail="sshd"} 125` |
| `jail_config_ban_time` | How long an IP is banned for in this jail (in seconds) | `f2b_config_jail_ban_time{jail="sshd"} 600` |
| `jail_config_find_time` | How far back the filter will look for failures in this jail (in seconds) | `f2b_config_jail_find_time{jail="sshd"} 600` |
| `jail_config_max_retry` | The max number of failures allowed before banning an IP in this jail | `f2b_config_jail_max_retries{jail="sshd"} 5` |
| `version` | Version string of the exporter and fail2ban | `f2b_version{exporter="0.5.0",fail2ban="0.11.1"} 1` |
The metrics above correspond to the matching fields in the `fail2ban-client
status <jail>` command:
```text
Status for the jail: sshd
|- Filter
| |- Currently failed: 6
| |- Total failed: 125
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 15
|- Total banned: 31
`- Banned IP list: ...
```
### 2.1. Grafana
The metrics exported by this tool are compatible with Prometheus and Grafana. A
sample grafana dashboard can be found in the
[grafana.json](/_examples/grafana/dashboard.json) file. Just import the contents
of this file into a new Grafana dashboard to get started.
The dashboard supports displaying data from multiple exporters. Use the
`instance` dashboard variable to select which ones to display.
*(Sample dashboard is compatible with Grafana `9.1.8` and above)*
## 3. Configuration
The exporter is configured with CLI flags and environment variables.
There are no configuration files.
### CLI flags
```text
🚀 Collect prometheus metrics from a running Fail2Ban instance
Flags:
-h, --help Show context-sensitive help.
-v, --version Show version info and exit
--dry-run Attempt to connect to the fail2ban socket then exit
before starting the server
--web.listen-address=":9191" Address to use for the metrics server
($F2B_WEB_LISTEN_ADDRESS)
--collector.f2b.socket="/var/run/fail2ban/fail2ban.sock"
Path to the fail2ban server socket
($F2B_COLLECTOR_SOCKET)
--collector.f2b.exit-on-socket-connection-error
When set to true the exporter will immediately
exit on a fail2ban socket connection error
($F2B_EXIT_ON_SOCKET_CONN_ERROR)
--collector.textfile.directory=STRING
Directory to read text files with metrics from
($F2B_COLLECTOR_TEXT_PATH)
--web.basic-auth.username=STRING
Username to use to protect endpoints with basic auth
($F2B_WEB_BASICAUTH_USER)
--web.basic-auth.password=STRING
Password to use to protect endpoints with basic auth
($F2B_WEB_BASICAUTH_PASS)
```
### Environment variables
Each environment variable corresponds to a CLI flag.
If both are specified, the CLI flag takes precedence.
| Environment variable | Corresponding CLI flag |
|---------------------------------|---------------------------------------------------|
| `F2B_COLLECTOR_SOCKET` | `--collector.f2b.socket` |
| `F2B_COLLECTOR_TEXT_PATH` | `--collector.textfile.directory` |
| `F2B_WEB_LISTEN_ADDRESS` | `--web.listen-address` |
| `F2B_WEB_BASICAUTH_USER` | `--web.basic-auth.username` |
| `F2B_WEB_BASICAUTH_PASS` | `--web.basic-auth.password` |
| `F2B_EXIT_ON_SOCKET_CONN_ERROR` | `--collector.f2b.exit-on-socket-connection-error` |
## 4. Building from source
Building from source has the following dependencies:
- Go v1.20
- Make
From there, simply run `make build`
This will download the necessary dependencies and build a `fail2ban_exporter`
binary in the root of the project.
## 5. Textfile metrics
For more flexibility the exporter also allows exporting metrics collected from a
text file.
To enable textfile metrics provide the directory to read files from with the
`--collector.textfile.directory` flag.
Metrics collected from these files will be exposed directly alongside the other
metrics without any additional processing. This means that it is the
responsibility of the file creator to ensure the format is correct.
By exporting textfile metrics an extra metric is also exported with an error
count for each file:
```text
# HELP textfile_error Checks for errors while reading text files
# TYPE textfile_error gauge
textfile_error{path="file.prom"} 0
```
**NOTE:** Any file not ending with `.prom` will be ignored.
### Running in Docker
To collect textfile metrics inside a docker container, a couple of things need
to be done:
1. Mount the folder with the metrics files
2. Set the `F2B_COLLECTOR_TEXT_PATH` environment variable
*For example:*
```bash
$ docker run -d \
--name "fail2ban-exporter" \
-v /var/run/fail2ban:/var/run/fail2ban:ro \
-v /path/to/metrics:/app/metrics/:ro \
-e F2B_COLLECTOR_TEXT_PATH=/app/metrics \
-p "9191:9191" \
registry.gitlab.com/hectorjsmith/fail2ban-prometheus-exporter:latest
```

View File

@ -0,0 +1,877 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__elements": {},
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "9.1.8"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 2,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": ".*Time"
},
"properties": [
{
"id": "unit",
"value": "s"
}
]
}
]
},
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 0
},
"id": 206,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "9.1.8",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_max_retries{instance=~\"$instance\"}",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_ban_time{instance=~\"$instance\"}",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": false,
"expr": "f2b_config_jail_find_time{instance=~\"$instance\"}",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{jail}}",
"refId": "C"
}
],
"title": "F2B Config",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "groupBy",
"options": {
"fields": {
"Value #A": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"Value #B": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"Value #C": {
"aggregations": [
"lastNotNull"
],
"operation": "aggregate"
},
"instance": {
"aggregations": [],
"operation": "groupby"
},
"jail": {
"aggregations": [],
"operation": "groupby"
}
}
}
},
{
"id": "organize",
"options": {
"excludeByName": {},
"indexByName": {},
"renameByName": {
"Value #A (lastNotNull)": "Max Retries",
"Value #B (lastNotNull)": "Ban Time",
"Value #C (lastNotNull)": "Find Time",
"jail": "Jail"
}
}
}
],
"transparent": true,
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"id": 190,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_failed_total{instance=~\"$instance\"}",
"hide": false,
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Failures (Total)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"id": 191,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_banned_total{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Bans (Total)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 14
},
"id": 208,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_failed_current{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Failures (Current)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 14
},
"id": 209,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_jail_banned_current{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{jail}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Bans (Current)",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 22
},
"id": 203,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_up{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "Up ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Up",
"transparent": true,
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 22
},
"id": 204,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.2.1",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"exemplar": true,
"expr": "f2b_errors{instance=~\"$instance\"}",
"interval": "",
"legendFormat": "{{type}} ({{instance}})",
"range": true,
"refId": "A"
}
],
"title": "Fail2Ban Exporter Errors",
"transparent": true,
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "Prometheus",
"value": "Prometheus"
},
"hide": 0,
"includeAll": false,
"label": "Data Source",
"multi": false,
"name": "DataSource",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "f2b_up",
"description": "Select which instance(s) to show",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": true,
"name": "instance",
"options": [],
"query": {
"query": "f2b_up",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "/.*instance=\"([^\"]+)\"/",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "F2B",
"uid": "cTkH9AT7z",
"version": 3,
"weekStart": ""
}

View File

@ -0,0 +1,7 @@
# Systemd
The `.service` file in this directory should be copied to the `/etc/systemd/system/` folder.
- It expects the binary file to be installed at `/usr/sbin/fail2ban_exporter`.
- It expects a user named `fail2ban_exporter` to exist. This user should not have a shell or any special privileges aside from read-access to the fail2ban socket file.
The `ExecStart` line can be modified to add any custom CLI flags.

View File

@ -0,0 +1,9 @@
[Unit]
Description=Fail2Ban Exporter
[Service]
User=fail2ban_exporter
ExecStart=/usr/sbin/fail2ban_exporter
[Install]
WantedBy=multi-user.target

29
auth/basic.go Normal file
View File

@ -0,0 +1,29 @@
package auth
import (
"fmt"
"net/http"
)
func NewBasicAuthProvider(username, password string) AuthProvider {
return &basicAuthProvider{
hashedAuth: encodeBasicAuth(username, password),
}
}
type basicAuthProvider struct {
hashedAuth string
}
func (p *basicAuthProvider) IsAllowed(request *http.Request) bool {
username, password, ok := request.BasicAuth()
if !ok {
return false
}
requestAuth := encodeBasicAuth(username, password)
return p.hashedAuth == requestAuth
}
func encodeBasicAuth(username, password string) string {
return HashString(fmt.Sprintf("%s:%s", username, password))
}

53
auth/basic_test.go Normal file
View File

@ -0,0 +1,53 @@
package auth
import (
"net/http"
"net/http/httptest"
"testing"
)
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithCorrectCreds_THEN_TrueReturned(t *testing.T) {
// assemble
username := "u1"
password := HashString("abc")
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth(username, password)
provider := NewBasicAuthProvider(username, password)
// act
result := provider.IsAllowed(request)
// assert
if !result {
t.Errorf("expected request to be allowed, but failed")
}
}
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithoutCreds_THEN_FalseReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
provider := NewBasicAuthProvider("u1", "p1")
// act
result := provider.IsAllowed(request)
// assert
if result {
t.Errorf("expected request to be denied, but was allowed")
}
}
func Test_GIVEN_BasicAuthSet_WHEN_CallingIsAllowedWithWrongCreds_THEN_FalseReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth("wrong", "pw")
provider := NewBasicAuthProvider("u1", "p1")
// act
result := provider.IsAllowed(request)
// assert
if result {
t.Errorf("expected request to be denied, but was allowed")
}
}

14
auth/empty.go Normal file
View File

@ -0,0 +1,14 @@
package auth
import "net/http"
func NewEmptyAuthProvider() AuthProvider {
return &emptyAuthProvider{}
}
type emptyAuthProvider struct {
}
func (p *emptyAuthProvider) IsAllowed(request *http.Request) bool {
return true
}

36
auth/empty_test.go Normal file
View File

@ -0,0 +1,36 @@
package auth
import (
"net/http"
"net/http/httptest"
"testing"
)
func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithoutAuth_THEN_TrueReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
provider := NewEmptyAuthProvider()
// act
response := provider.IsAllowed(request)
// assert
if !response {
t.Errorf("expected request to be allowed, but failed")
}
}
func Test_GIVEN_EmptyAuth_WHEN_CallingIsAllowedWithAuth_THEN_TrueReturned(t *testing.T) {
// assemble
request := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
request.SetBasicAuth("user", "pass")
provider := NewEmptyAuthProvider()
// act
response := provider.IsAllowed(request)
// assert
if !response {
t.Errorf("expected request to be allowed, but failed")
}
}

18
auth/hash.go Normal file
View File

@ -0,0 +1,18 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
)
func hash(data []byte) []byte {
if len(data) == 0 {
return []byte{}
}
b := sha256.Sum256(data)
return b[:]
}
func HashString(data string) string {
return hex.EncodeToString(hash([]byte(data)))
}

26
auth/hash_test.go Normal file
View File

@ -0,0 +1,26 @@
package auth
import (
"reflect"
"testing"
)
func TestHashString(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{"Happy path #1", "123", "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"},
{"Happy path #2", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"},
{"Happy path #3", "H3Ll0_W0RLD", "d58a27fe9a6e73a1d8a67189fb8acace047e7a1a795276a0056d3717ad61bd0e"},
{"Blank string", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HashString(tt.args); !reflect.DeepEqual(got, tt.want) {
t.Errorf("HashString() = %v, want %v", got, tt.want)
}
})
}
}

9
auth/provider.go Normal file
View File

@ -0,0 +1,9 @@
package auth
import (
"net/http"
)
type AuthProvider interface {
IsAllowed(*http.Request) bool
}

84
cfg/cfg.go Normal file
View File

@ -0,0 +1,84 @@
package cfg
import (
"fmt"
"log"
"os"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth"
"github.com/alecthomas/kong"
)
var cliStruct struct {
VersionMode bool `name:"version" short:"v" help:"Show version info and exit"`
DryRunMode bool `name:"dry-run" help:"Attempt to connect to the fail2ban socket then exit before starting the server"`
ServerAddress string `name:"web.listen-address" env:"F2B_WEB_LISTEN_ADDRESS" help:"Address to use for the metrics server" default:"${default_address}"`
F2bSocketPath string `name:"collector.f2b.socket" env:"F2B_COLLECTOR_SOCKET" help:"Path to the fail2ban server socket" default:"${default_socket}"`
ExitOnSocketError bool `name:"collector.f2b.exit-on-socket-connection-error" env:"F2B_EXIT_ON_SOCKET_CONN_ERROR" help:"When set to true the exporter will immediately exit on a fail2ban socket connection error"`
TextFileExporterPath string `name:"collector.textfile.directory" env:"F2B_COLLECTOR_TEXT_PATH" help:"Directory to read text files with metrics from"`
BasicAuthUser string `name:"web.basic-auth.username" env:"F2B_WEB_BASICAUTH_USER" help:"Username to use to protect endpoints with basic auth"`
BasicAuthPass string `name:"web.basic-auth.password" env:"F2B_WEB_BASICAUTH_PASS" help:"Password to use to protect endpoints with basic auth"`
}
func Parse() *AppSettings {
ctx := kong.Parse(
&cliStruct,
kong.Vars{
"default_socket": "/var/run/fail2ban/fail2ban.sock",
"default_address": ":9191",
},
kong.Name("fail2ban_exporter"),
kong.Description("🚀 Export prometheus metrics from a running Fail2Ban instance"),
kong.UsageOnError(),
)
validateFlags(ctx)
settings := &AppSettings{
VersionMode: cliStruct.VersionMode,
DryRunMode: cliStruct.DryRunMode,
MetricsAddress: cliStruct.ServerAddress,
Fail2BanSocketPath: cliStruct.F2bSocketPath,
FileCollectorPath: cliStruct.TextFileExporterPath,
ExitOnSocketConnError: cliStruct.ExitOnSocketError,
AuthProvider: createAuthProvider(),
}
return settings
}
func createAuthProvider() auth.AuthProvider {
username := cliStruct.BasicAuthUser
password := cliStruct.BasicAuthPass
if len(username) == 0 && len(password) == 0 {
return auth.NewEmptyAuthProvider()
}
log.Print("basic auth enabled")
return auth.NewBasicAuthProvider(username, password)
}
func validateFlags(cliCtx *kong.Context) {
var flagsValid = true
var messages = []string{}
if !cliStruct.VersionMode {
if cliStruct.F2bSocketPath == "" {
messages = append(messages, "error: fail2ban socket path must not be blank")
flagsValid = false
}
if cliStruct.ServerAddress == "" {
messages = append(messages, "error: invalid server address, must not be blank")
flagsValid = false
}
if (len(cliStruct.BasicAuthUser) > 0) != (len(cliStruct.BasicAuthPass) > 0) {
messages = append(messages, "error: to enable basic auth both the username and the password must be provided")
flagsValid = false
}
}
if !flagsValid {
cliCtx.PrintUsage(false)
fmt.Println()
for i := 0; i < len(messages); i++ {
fmt.Println(messages[i])
}
os.Exit(1)
}
}

13
cfg/settings.go Normal file
View File

@ -0,0 +1,13 @@
package cfg
import "git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth"
type AppSettings struct {
VersionMode bool
DryRunMode bool
MetricsAddress string
Fail2BanSocketPath string
FileCollectorPath string
AuthProvider auth.AuthProvider
ExitOnSocketConnError bool
}

View File

@ -0,0 +1,76 @@
package f2b
import (
"log"
"os"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/socket"
"github.com/prometheus/client_golang/prometheus"
)
type Collector struct {
socketPath string
exporterVersion string
lastError error
socketConnectionErrorCount int
socketRequestErrorCount int
exitOnSocketConnError bool
}
func NewExporter(appSettings *cfg.AppSettings, exporterVersion string) *Collector {
log.Printf("reading fail2ban metrics from socket file: %s", appSettings.Fail2BanSocketPath)
printFail2BanServerVersion(appSettings.Fail2BanSocketPath)
return &Collector{
socketPath: appSettings.Fail2BanSocketPath,
exporterVersion: exporterVersion,
lastError: nil,
socketConnectionErrorCount: 0,
socketRequestErrorCount: 0,
exitOnSocketConnError: appSettings.ExitOnSocketConnError,
}
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- metricServerUp
ch <- metricJailCount
ch <- metricJailFailedCurrent
ch <- metricJailFailedTotal
ch <- metricJailBannedCurrent
ch <- metricJailBannedTotal
ch <- metricErrorCount
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
s, err := socket.ConnectToSocket(c.socketPath)
if err != nil {
log.Printf("error opening socket: %v", err)
c.socketConnectionErrorCount++
if c.exitOnSocketConnError {
os.Exit(1)
}
} else {
defer s.Close()
}
c.collectServerUpMetric(ch, s)
if err == nil && s != nil {
c.collectJailMetrics(ch, s)
c.collectVersionMetric(ch, s)
}
c.collectErrorCountMetric(ch)
}
func printFail2BanServerVersion(socketPath string) {
s, err := socket.ConnectToSocket(socketPath)
if err != nil {
log.Printf("error connecting to socket: %v", err)
} else {
version, err := s.GetServerVersion()
if err != nil {
log.Printf("error interacting with socket: %v", err)
} else {
log.Printf("successfully connected to fail2ban socket! fail2ban version: %s", version)
}
}
}

180
collector/f2b/socket.go Normal file
View File

@ -0,0 +1,180 @@
package f2b
import (
"log"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/socket"
"github.com/prometheus/client_golang/prometheus"
)
const (
namespace = "f2b"
)
var (
metricErrorCount = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "errors"),
"Number of errors found since startup",
[]string{"type"}, nil,
)
metricServerUp = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "up"),
"Check if the fail2ban server is up",
nil, nil,
)
metricJailCount = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_count"),
"Number of defined jails",
nil, nil,
)
metricJailFailedCurrent = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_failed_current"),
"Number of current failures on this jail's filter",
[]string{"jail"}, nil,
)
metricJailFailedTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_failed_total"),
"Number of total failures on this jail's filter",
[]string{"jail"}, nil,
)
metricJailBannedCurrent = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_banned_current"),
"Number of IPs currently banned in this jail",
[]string{"jail"}, nil,
)
metricJailBannedTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "jail_banned_total"),
"Total number of IPs banned by this jail (includes expired bans)",
[]string{"jail"}, nil,
)
metricJailBanTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_ban_time"),
"How long an IP is banned for in this jail (in seconds)",
[]string{"jail"}, nil,
)
metricJailFindTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_find_time"),
"How far back will the filter look for failures in this jail (in seconds)",
[]string{"jail"}, nil,
)
metricJailMaxRetry = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "config", "jail_max_retries"),
"The number of failures allowed until the IP is banned by this jail",
[]string{"jail"}, nil,
)
metricVersionInfo = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "version"),
"Version of the exporter and fail2ban server",
[]string{"exporter", "fail2ban"}, nil,
)
)
func (c *Collector) collectErrorCountMetric(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(c.socketConnectionErrorCount), "socket_conn",
)
ch <- prometheus.MustNewConstMetric(
metricErrorCount, prometheus.CounterValue, float64(c.socketRequestErrorCount), "socket_req",
)
}
func (c *Collector) collectServerUpMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
var serverUp float64 = 0
if s != nil {
pingSuccess, err := s.Ping()
if err != nil {
c.socketRequestErrorCount++
log.Print(err)
}
if err == nil && pingSuccess {
serverUp = 1
}
}
ch <- prometheus.MustNewConstMetric(
metricServerUp, prometheus.GaugeValue, serverUp,
)
}
func (c *Collector) collectJailMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
jails, err := s.GetJails()
var count float64 = 0
if err != nil {
c.socketRequestErrorCount++
log.Print(err)
}
if err == nil {
count = float64(len(jails))
}
ch <- prometheus.MustNewConstMetric(
metricJailCount, prometheus.GaugeValue, count,
)
for i := range jails {
c.collectJailStatsMetric(ch, s, jails[i])
c.collectJailConfigMetrics(ch, s, jails[i])
}
}
func (c *Collector) collectJailStatsMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) {
stats, err := s.GetJailStats(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get stats for jail %s: %v", jail, err)
return
}
ch <- prometheus.MustNewConstMetric(
metricJailFailedCurrent, prometheus.GaugeValue, float64(stats.FailedCurrent), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailFailedTotal, prometheus.GaugeValue, float64(stats.FailedTotal), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailBannedCurrent, prometheus.GaugeValue, float64(stats.BannedCurrent), jail,
)
ch <- prometheus.MustNewConstMetric(
metricJailBannedTotal, prometheus.GaugeValue, float64(stats.BannedTotal), jail,
)
}
func (c *Collector) collectJailConfigMetrics(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket, jail string) {
banTime, err := s.GetJailBanTime(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get ban time for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailBanTime, prometheus.GaugeValue, float64(banTime), jail,
)
}
findTime, err := s.GetJailFindTime(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get find time for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailFindTime, prometheus.GaugeValue, float64(findTime), jail,
)
}
maxRetry, err := s.GetJailMaxRetries(jail)
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get max retries for jail %s: %v", jail, err)
} else {
ch <- prometheus.MustNewConstMetric(
metricJailMaxRetry, prometheus.GaugeValue, float64(maxRetry), jail,
)
}
}
func (c *Collector) collectVersionMetric(ch chan<- prometheus.Metric, s *socket.Fail2BanSocket) {
fail2banVersion, err := s.GetServerVersion()
if err != nil {
c.socketRequestErrorCount++
log.Printf("failed to get fail2ban server version: %v", err)
}
ch <- prometheus.MustNewConstMetric(
metricVersionInfo, prometheus.GaugeValue, float64(1), c.exporterVersion, fail2banVersion,
)
}

View File

@ -0,0 +1,49 @@
package textfile
import (
"log"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg"
"github.com/prometheus/client_golang/prometheus"
)
type Collector struct {
enabled bool
folderPath string
fileMap map[string]*fileData
}
type fileData struct {
readErrors int
fileName string
fileContents []byte
}
func NewCollector(appSettings *cfg.AppSettings) *Collector {
collector := &Collector{
enabled: appSettings.FileCollectorPath != "",
folderPath: appSettings.FileCollectorPath,
fileMap: make(map[string]*fileData),
}
if collector.enabled {
log.Printf("reading textfile metrics from: %s", collector.folderPath)
}
return collector
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
if c.enabled {
ch <- metricReadError
}
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
if c.enabled {
c.collectFileContents()
c.collectFileErrors(ch)
}
}
func (c *Collector) appendErrorForPath(path string) {
c.fileMap[path].readErrors++
}

View File

@ -0,0 +1,56 @@
package textfile
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/prometheus/client_golang/prometheus"
)
const namespace = "textfile"
var (
metricReadError = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "error"),
"Checks for errors while reading text files",
[]string{"path"}, nil,
)
)
func (c *Collector) collectFileContents() {
files, err := os.ReadDir(c.folderPath)
if err != nil {
log.Printf("error reading directory '%s': %v", c.folderPath, err)
return
}
for _, file := range files {
fileName := file.Name()
if !strings.HasSuffix(strings.ToLower(fileName), ".prom") {
continue
}
c.fileMap[fileName] = &fileData{
readErrors: 0,
fileName: fileName,
}
fullPath := filepath.Join(c.folderPath, fileName)
content, err := os.ReadFile(fullPath)
if err != nil {
c.appendErrorForPath(fileName)
log.Printf("error reading contents of file '%s': %v", fileName, err)
}
c.fileMap[fileName].fileContents = content
}
}
func (c *Collector) collectFileErrors(ch chan<- prometheus.Metric) {
for _, f := range c.fileMap {
ch <- prometheus.MustNewConstMetric(
metricReadError, prometheus.GaugeValue, float64(f.readErrors), f.fileName,
)
}
}

View File

@ -0,0 +1,20 @@
package textfile
import (
"log"
"net/http"
)
func (c *Collector) WriteTextFileMetrics(w http.ResponseWriter, r *http.Request) {
if !c.enabled {
return
}
for _, f := range c.fileMap {
_, err := w.Write(f.fileContents)
if err != nil {
c.appendErrorForPath(f.fileName)
log.Printf("error writing file contents to response writer '%s': %v", f.fileName, err)
}
}
}

6
env Normal file
View File

@ -0,0 +1,6 @@
# F2B_COLLECTOR_SOCKET=""
# F2B_COLLECTOR_TEXT_PATH=""
# F2B_WEB_LISTEN_ADDRESS=""
# F2B_WEB_BASICAUTH_USER=""
# F2B_WEB_BASICAUTH_PASS=""
# F2B_EXIT_ON_SOCKET_CONN_ERROR=""

66
exporter.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/f2b"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/server"
"github.com/prometheus/client_golang/prometheus"
)
var (
version = "dev"
commit = "none"
date = "unknown"
builtBy = "unknown"
)
func printAppVersion() {
fmt.Println(version)
fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy)
}
func main() {
appSettings := cfg.Parse()
if appSettings.VersionMode {
printAppVersion()
return
}
handleGracefulShutdown()
log.Printf("fail2ban exporter version %s", version)
log.Printf("starting server at %s", appSettings.MetricsAddress)
f2bCollector := f2b.NewExporter(appSettings, version)
prometheus.MustRegister(f2bCollector)
textFileCollector := textfile.NewCollector(appSettings)
prometheus.MustRegister(textFileCollector)
if !appSettings.DryRunMode {
svrErr := server.StartServer(appSettings, textFileCollector)
err := <-svrErr
log.Fatal(err)
} else {
log.Print("running in dry-run mode - exiting")
}
}
func handleGracefulShutdown() {
var signals = make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGTERM)
signal.Notify(signals, syscall.SIGINT)
go func() {
sig := <-signals
log.Printf("caught signal: %+v", sig)
os.Exit(0)
}()
}

22
go.mod Normal file
View File

@ -0,0 +1,22 @@
module git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter
go 1.20
require (
github.com/alecthomas/kong v0.8.0
github.com/kisielk/og-rek v1.2.0
github.com/nlpodyssey/gopickle v0.2.0
github.com/prometheus/client_golang v1.16.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)

38
go.sum Normal file
View File

@ -0,0 +1,38 @@
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s=
github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/kisielk/og-rek v1.2.0 h1:CTvDIin+YnetsSQAYbe+QNAxXU3B50C5hseEz8xEoJw=
github.com/kisielk/og-rek v1.2.0/go.mod h1:6ihsOSzSAxR/65S3Bn9zNihoEqRquhDQZ2c6I2+MG3c=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/nlpodyssey/gopickle v0.2.0 h1:4naD2DVylYJupQLbCQFdwo6yiXEmPyp+0xf5MVlrBDY=
github.com/nlpodyssey/gopickle v0.2.0/go.mod h1:YIUwjJ2O7+vnBsxUN+MHAAI3N+adqEGiw+nDpwW95bY=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

26
manifest.tmpl Normal file
View File

@ -0,0 +1,26 @@
image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
- "latest"
{{/if}}
manifests:
-
image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-amd64
platform:
architecture: amd64
os: linux
-
image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-arm-v7
platform:
architecture: arm
os: linux
variant: v7
-
image: git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}-arm64-v8
platform:
architecture: arm64
os: linux
variant: v8

17
server/auth.go Normal file
View File

@ -0,0 +1,17 @@
package server
import (
"net/http"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/auth"
)
func AuthMiddleware(handlerFunc http.HandlerFunc, authProvider auth.AuthProvider) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if authProvider.IsAllowed(r) {
handlerFunc.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
}

46
server/auth_test.go Normal file
View File

@ -0,0 +1,46 @@
package server
import (
"net/http"
"net/http/httptest"
"testing"
)
type testAuthProvider struct {
match bool
}
func (p testAuthProvider) IsAllowed(request *http.Request) bool {
return p.match
}
func newTestRequest() *http.Request {
return httptest.NewRequest(http.MethodGet, "http://example.com", nil)
}
func executeAuthMiddlewareTest(t *testing.T, authMatches bool, expectedCode int, expectedCallCount int) {
callCount := 0
testHandler := func(w http.ResponseWriter, r *http.Request) {
callCount++
}
handler := AuthMiddleware(testHandler, testAuthProvider{match: authMatches})
recorder := httptest.NewRecorder()
request := newTestRequest()
handler.ServeHTTP(recorder, request)
if recorder.Code != expectedCode {
t.Errorf("statusCode = %v, want %v", recorder.Code, expectedCode)
}
if callCount != expectedCallCount {
t.Errorf("callCount = %v, want %v", callCount, expectedCallCount)
}
}
func Test_GIVEN_MatchingBasicAuth_WHEN_MethodCalled_THEN_RequestProcessed(t *testing.T) {
executeAuthMiddlewareTest(t, true, http.StatusOK, 1)
}
func Test_GIVEN_NonMatchingBasicAuth_WHEN_MethodCalled_THEN_RequestRejected(t *testing.T) {
executeAuthMiddlewareTest(t, false, http.StatusUnauthorized, 0)
}

33
server/handler.go Normal file
View File

@ -0,0 +1,33 @@
package server
import (
"log"
"net/http"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
metricsPath = "/metrics"
)
func rootHtmlHandler(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(
`<html>
<head><title>Fail2Ban Exporter</title></head>
<body>
<h1>Fail2Ban Exporter</h1>
<p><a href="` + metricsPath + `">Metrics</a></p>
</body>
</html>`))
if err != nil {
log.Printf("error handling root url: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func metricHandler(w http.ResponseWriter, r *http.Request, collector *textfile.Collector) {
promhttp.Handler().ServeHTTP(w, r)
collector.WriteTextFileMetrics(w, r)
}

41
server/server.go Normal file
View File

@ -0,0 +1,41 @@
package server
import (
"log"
"net/http"
"time"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/cfg"
"git.cryptic.systems/volker.raschek/prometheus-fail2ban-exporter/collector/textfile"
)
func StartServer(
appSettings *cfg.AppSettings,
textFileCollector *textfile.Collector,
) chan error {
http.HandleFunc("/", AuthMiddleware(
rootHtmlHandler,
appSettings.AuthProvider,
))
http.HandleFunc(metricsPath, AuthMiddleware(
func(w http.ResponseWriter, r *http.Request) {
metricHandler(w, r, textFileCollector)
},
appSettings.AuthProvider,
))
log.Printf("metrics available at '%s'", metricsPath)
svrErr := make(chan error)
go func() {
httpServer := &http.Server{
Addr: appSettings.MetricsAddress,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}
svrErr <- httpServer.ListenAndServe()
}()
log.Print("ready")
return svrErr
}

8
socket/decoder.go Normal file
View File

@ -0,0 +1,8 @@
package socket
// Py_builtins_str is used by the pickle decoder to parse the server response into a format Go can understand
type Py_builtins_str struct{}
func (c Py_builtins_str) Call(args ...interface{}) (interface{}, error) {
return args[0], nil
}

179
socket/fail2banSocket.go Normal file
View File

@ -0,0 +1,179 @@
package socket
import (
"fmt"
"github.com/kisielk/og-rek"
"github.com/nlpodyssey/gopickle/types"
"net"
"strings"
)
type Fail2BanSocket struct {
socket net.Conn
encoder *ogórek.Encoder
}
type JailStats struct {
FailedCurrent int
FailedTotal int
BannedCurrent int
BannedTotal int
}
func ConnectToSocket(path string) (*Fail2BanSocket, error) {
c, err := net.Dial("unix", path)
if err != nil {
return nil, err
}
return &Fail2BanSocket{
socket: c,
encoder: ogórek.NewEncoder(c),
}, nil
}
func (s *Fail2BanSocket) Close() error {
return s.socket.Close()
}
func (s *Fail2BanSocket) Ping() (bool, error) {
response, err := s.sendCommand([]string{pingCommand, "100"})
if err != nil {
return false, newConnectionError(pingCommand, err)
}
if t, ok := response.(*types.Tuple); ok {
if (*t)[1] == "pong" {
return true, nil
}
return false, fmt.Errorf("unexpected response data (expecting 'pong'): %s", (*t)[1])
}
return false, newBadFormatError(pingCommand, response)
}
func (s *Fail2BanSocket) GetJails() ([]string, error) {
response, err := s.sendCommand([]string{statusCommand})
if err != nil {
return nil, err
}
if lvl1, ok := response.(*types.Tuple); ok {
if lvl2, ok := lvl1.Get(1).(*types.List); ok {
if lvl3, ok := lvl2.Get(1).(*types.Tuple); ok {
if lvl4, ok := lvl3.Get(1).(string); ok {
splitJails := strings.Split(lvl4, ",")
return trimSpaceForAll(splitJails), nil
}
}
}
}
return nil, newBadFormatError(statusCommand, response)
}
func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) {
response, err := s.sendCommand([]string{statusCommand, jail})
if err != nil {
return JailStats{}, err
}
stats := JailStats{
FailedCurrent: -1,
FailedTotal: -1,
BannedCurrent: -1,
BannedTotal: -1,
}
if lvl1, ok := response.(*types.Tuple); ok {
if lvl2, ok := lvl1.Get(1).(*types.List); ok {
if filter, ok := lvl2.Get(0).(*types.Tuple); ok {
if filterLvl1, ok := filter.Get(1).(*types.List); ok {
if filterCurrentTuple, ok := filterLvl1.Get(0).(*types.Tuple); ok {
if filterCurrent, ok := filterCurrentTuple.Get(1).(int); ok {
stats.FailedCurrent = filterCurrent
}
}
if filterTotalTuple, ok := filterLvl1.Get(1).(*types.Tuple); ok {
if filterTotal, ok := filterTotalTuple.Get(1).(int); ok {
stats.FailedTotal = filterTotal
}
}
}
}
if actions, ok := lvl2.Get(1).(*types.Tuple); ok {
if actionsLvl1, ok := actions.Get(1).(*types.List); ok {
if actionsCurrentTuple, ok := actionsLvl1.Get(0).(*types.Tuple); ok {
if actionsCurrent, ok := actionsCurrentTuple.Get(1).(int); ok {
stats.BannedCurrent = actionsCurrent
}
}
if actionsTotalTuple, ok := actionsLvl1.Get(1).(*types.Tuple); ok {
if actionsTotal, ok := actionsTotalTuple.Get(1).(int); ok {
stats.BannedTotal = actionsTotal
}
}
}
}
return stats, nil
}
}
return stats, newBadFormatError(statusCommand, response)
}
func (s *Fail2BanSocket) GetJailBanTime(jail string) (int, error) {
command := fmt.Sprintf(banTimeCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetJailFindTime(jail string) (int, error) {
command := fmt.Sprintf(findTimeCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetJailMaxRetries(jail string) (int, error) {
command := fmt.Sprintf(maxRetriesCommandFmt, jail)
return s.sendSimpleIntCommand(command)
}
func (s *Fail2BanSocket) GetServerVersion() (string, error) {
response, err := s.sendCommand([]string{versionCommand})
if err != nil {
return "", err
}
if lvl1, ok := response.(*types.Tuple); ok {
if versionStr, ok := lvl1.Get(1).(string); ok {
return versionStr, nil
}
}
return "", newBadFormatError(versionCommand, response)
}
// sendSimpleIntCommand sends a command to the fail2ban socket and parses the response to extract an int.
// This command assumes that the response data is in the format of `(d, d)` where `d` is a number.
func (s *Fail2BanSocket) sendSimpleIntCommand(command string) (int, error) {
response, err := s.sendCommand(strings.Split(command, " "))
if err != nil {
return -1, err
}
if lvl1, ok := response.(*types.Tuple); ok {
if banTime, ok := lvl1.Get(1).(int); ok {
return banTime, nil
}
}
return -1, newBadFormatError(command, response)
}
func newBadFormatError(command string, data interface{}) error {
return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data)
}
func newConnectionError(command string, err error) error {
return fmt.Errorf("(%s) failed to send command through socket: %v", command, err)
}
func trimSpaceForAll(slice []string) []string {
for i := range slice {
slice[i] = strings.TrimSpace(slice[i])
}
return slice
}

69
socket/protocol.go Normal file
View File

@ -0,0 +1,69 @@
package socket
import (
"bufio"
"bytes"
"fmt"
"github.com/nlpodyssey/gopickle/pickle"
)
const (
commandTerminator = "<F2B_END_COMMAND>"
pingCommand = "ping"
statusCommand = "status"
versionCommand = "version"
banTimeCommandFmt = "get %s bantime"
findTimeCommandFmt = "get %s findtime"
maxRetriesCommandFmt = "get %s maxretry"
socketReadBufferSize = 1024
)
func (s *Fail2BanSocket) sendCommand(command []string) (interface{}, error) {
err := s.write(command)
if err != nil {
return nil, err
}
return s.read()
}
func (s *Fail2BanSocket) write(command []string) error {
err := s.encoder.Encode(command)
if err != nil {
return err
}
_, err = s.socket.Write([]byte(commandTerminator))
if err != nil {
return err
}
return nil
}
func (s *Fail2BanSocket) read() (interface{}, error) {
reader := bufio.NewReader(s.socket)
data := []byte{}
for {
buf := make([]byte, socketReadBufferSize)
_, err := reader.Read(buf)
if err != nil {
return nil, err
}
data = append(data, buf...)
containsTerminator := bytes.Contains(data, []byte(commandTerminator))
if containsTerminator {
break
}
}
bufReader := bytes.NewReader(data)
unpickler := pickle.NewUnpickler(bufReader)
unpickler.FindClass = func(module, name string) (interface{}, error) {
if (module == "builtins" || module == "__builtin__") && name == "str" {
return &Py_builtins_str{}, nil
}
return nil, fmt.Errorf("class not found: " + module + " : " + name)
}
return unpickler.Load()
}

22
systemd/systemd.service Normal file
View File

@ -0,0 +1,22 @@
[Unit]
Description=Prometheus exporter for fail2ban metrics
Requires=network-online.target
After=network-online.target
[Service]
EnvironmentFile=/etc/conf.d/EXECUTABLE
ExecStart=/usr/bin/EXECUTABLE
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
# NOTE: Would be great to create and use a dedicated user/group via
# sysusers.conf to access the fail2ban socket, but currently it is no possible
# without manual configuration of the fail2ban daemon.
User=root
Group=root
[Install]
WantedBy=multi-user.target