mirror of
https://github.com/aykhans/my-self-host-services.git
synced 2026-05-29 15:35:59 +00:00
Compare commits
25 Commits
54e3854c0d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| daa56d72ef | |||
| fc73b80930 | |||
| a3d051c71a | |||
| ba03407420 | |||
| 8092079047 | |||
| c9b2d723d8 | |||
| 59a62a3cc7 | |||
| cb4d12822d | |||
| 1581be1722 | |||
| 2e132075b3 | |||
| 4ebbe30b4a | |||
| 73829dd5a4 | |||
| ed68045605 | |||
| b19d315f9a | |||
| 8250e08871 | |||
| 5a2c6e2d50 | |||
| 504619b7b4 | |||
| 90b8b5a3c3 | |||
| 61b76934da | |||
| 121ce2ed78 | |||
| 58e247f770 | |||
| 846ca875fe | |||
| a5c389457d | |||
| 62568e476e | |||
| e11b03ebaf |
@@ -1,43 +1,50 @@
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Bash**
|
- **Bash**
|
||||||
- **Docker**
|
- **Docker**
|
||||||
- **Docker compose**
|
- **Docker compose**
|
||||||
- **Ports:**
|
- **Ports:**
|
||||||
- **Caddy**
|
- **Caddy**
|
||||||
- 80/tcp (HTTP)
|
- 80/tcp (HTTP)
|
||||||
- 443/tcp (HTTPS)
|
- 443/tcp (HTTPS)
|
||||||
- **Stalwart**
|
- **Stalwart**
|
||||||
- 25/tcp (SMTP)
|
- 25/tcp (SMTP)
|
||||||
- 110/tcp (POP3)
|
- 110/tcp (POP3)
|
||||||
- 995/tcp (POP3S)
|
- 995/tcp (POP3S)
|
||||||
- 143/tcp (IMAP)
|
- 143/tcp (IMAP)
|
||||||
- 993/tcp (IMAPS)
|
- 993/tcp (IMAPS)
|
||||||
- 465/tcp (SMTPS)
|
- 465/tcp (SMTPS)
|
||||||
- 587/tcp (SUBMISSION)
|
- 587/tcp (SUBMISSION)
|
||||||
- 4190/tcp (ManageSieve)
|
- 4190/tcp (ManageSieve)
|
||||||
- **Croc**
|
- **Croc**
|
||||||
- 9009-9013/tcp (relay)
|
- 9009-9013/tcp (relay)
|
||||||
- **SFTPGo**
|
- **SFTPGo**
|
||||||
- 2022/tcp (SFTP)
|
- 2022/tcp (SFTP)
|
||||||
- **WireGuard Easy**
|
- **WireGuard Easy**
|
||||||
- 51820/udp (WireGuard)
|
- 51820/udp (WireGuard)
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Follow these steps to set up and start the services:
|
Follow these steps to set up and start the services:
|
||||||
|
|
||||||
### 1. Grant Execute Permissions
|
### 1. Grant Execute Permissions
|
||||||
|
|
||||||
Ensure the `main.sh` script has the necessary permissions:
|
Ensure the `main.sh` script has the necessary permissions:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
chmod +x main.sh
|
chmod +x main.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Generate Environment Files
|
### 2. Generate Environment Files
|
||||||
|
|
||||||
Create `.env` configuration files with the following command:
|
Create `.env` configuration files with the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./main.sh generate-env
|
./main.sh generate-env
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configure Environment Variables
|
### 3. Configure Environment Variables
|
||||||
|
|
||||||
Edit the generated `.env` files to fill in the required fields:
|
Edit the generated `.env` files to fill in the required fields:
|
||||||
|
|
||||||
- `./gitea/.env`
|
- `./gitea/.env`
|
||||||
@@ -50,16 +57,67 @@ Edit the generated `.env` files to fill in the required fields:
|
|||||||
- `./croc/.env`
|
- `./croc/.env`
|
||||||
- `./stalwart/.env`
|
- `./stalwart/.env`
|
||||||
- `./caddy/.env`
|
- `./caddy/.env`
|
||||||
|
- `./crowdsec/.env`
|
||||||
- `./caddy/Caddyfile.private`
|
- `./caddy/Caddyfile.private`
|
||||||
|
|
||||||
### 4. Start Services
|
### 4. Bouncer Keys (CrowdSec)
|
||||||
|
|
||||||
|
Generate two keys and write them into the matching `.env` files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CADDY_KEY=$(openssl rand -hex 32)
|
||||||
|
FW_KEY=$(openssl rand -hex 32)
|
||||||
|
|
||||||
|
# crowdsec/.env
|
||||||
|
sed -i "s|^CROWDSEC_BOUNCER_KEY_CADDY=.*|CROWDSEC_BOUNCER_KEY_CADDY=$CADDY_KEY|" ./crowdsec/.env
|
||||||
|
sed -i "s|^CROWDSEC_BOUNCER_KEY_FW=.*|CROWDSEC_BOUNCER_KEY_FW=$FW_KEY|" ./crowdsec/.env
|
||||||
|
|
||||||
|
# caddy/.env (same value as CADDY key above)
|
||||||
|
sed -i "s|^CROWDSEC_API_KEY=.*|CROWDSEC_API_KEY=$CADDY_KEY|" ./caddy/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
(Optional) get a Console enroll key from https://app.crowdsec.net and put it in `CROWDSEC_ENROLL_KEY`.
|
||||||
|
|
||||||
|
### 5. Start Services
|
||||||
|
|
||||||
Launch all services with the following command:
|
Launch all services with the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./main.sh start
|
./main.sh start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 6. Host Firewall Bouncer (CrowdSec, nftables)
|
||||||
|
|
||||||
|
The Caddy bouncer protects HTTP services. Stalwart's mail ports (25/465/587/143/993/110/995/4190) bypass Caddy, so install a firewall bouncer on the host. CrowdSec packages live on PackageCloud, not in the default apt repos, so add the repo first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
|
||||||
|
sudo apt install crowdsec-firewall-bouncer-nftables
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT use `install.crowdsec.net` (that installs the engine too, which we already run in Docker).
|
||||||
|
|
||||||
|
Patch the default config (the package writes `api_url: http://127.0.0.1:8080/` but our LAPI is on 18080):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
FW_KEY=$(grep '^CROWDSEC_BOUNCER_KEY_FW=' ./crowdsec/.env | cut -d= -f2)
|
||||||
|
sudo sed -i "s|^api_url:.*|api_url: http://127.0.0.1:18080/|" /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
|
||||||
|
sudo sed -i "s|^api_key:.*|api_key: $FW_KEY|" /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
|
||||||
|
sudo systemctl enable --now crowdsec-firewall-bouncer
|
||||||
|
sudo systemctl status crowdsec-firewall-bouncer --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker exec crowdsec cscli bouncers list # 'firewall' should appear with a non-empty IP and recent 'Last API pull'
|
||||||
|
sudo nft list ruleset | grep crowdsec # kernel-level rules in place
|
||||||
|
```
|
||||||
|
|
||||||
## Stopping Services
|
## Stopping Services
|
||||||
|
|
||||||
To stop all running services, use:
|
To stop all running services, use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./main.sh stop
|
./main.sh stop
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -44,3 +44,7 @@ GOPKG_PROXY_DOMAIN=
|
|||||||
|
|
||||||
############# Ech0 #############
|
############# Ech0 #############
|
||||||
ECH0_DOMAIN=
|
ECH0_DOMAIN=
|
||||||
|
|
||||||
|
############# CrowdSec #############
|
||||||
|
# Same value as CROWDSEC_BOUNCER_KEY_CADDY in crowdsec/.env
|
||||||
|
CROWDSEC_API_KEY=
|
||||||
|
|||||||
+133
-53
@@ -3,11 +3,32 @@
|
|||||||
metrics {
|
metrics {
|
||||||
per_host
|
per_host
|
||||||
}
|
}
|
||||||
# log {
|
|
||||||
# output file /var/log/caddy/access.log
|
crowdsec {
|
||||||
# format json
|
api_url http://crowdsec:8080
|
||||||
# level DEBUG
|
api_key {env.CROWDSEC_API_KEY}
|
||||||
# }
|
ticker_interval 15s
|
||||||
|
appsec_url http://crowdsec:7422
|
||||||
|
appsec_fail_open
|
||||||
|
appsec_timeout 1s
|
||||||
|
}
|
||||||
|
|
||||||
|
servers {
|
||||||
|
trusted_proxies static private_ranges
|
||||||
|
client_ip_headers X-Forwarded-For
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(access-log) {
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(security) {
|
||||||
|
crowdsec
|
||||||
|
appsec
|
||||||
}
|
}
|
||||||
|
|
||||||
:2019 {
|
:2019 {
|
||||||
@@ -16,158 +37,217 @@
|
|||||||
|
|
||||||
############## grafana ##############
|
############## grafana ##############
|
||||||
{$GRAFANA_DOMAIN} {
|
{$GRAFANA_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 2048MB
|
max_size 2048MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://grafana:3000
|
import security
|
||||||
|
reverse_proxy http://grafana:3000 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## gitea ##############
|
############## gitea ##############
|
||||||
{$GITEA_DOMAIN} {
|
{$GITEA_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 512MB
|
max_size 2048MB
|
||||||
|
}
|
||||||
|
@not_registry not path /v2/*
|
||||||
|
route {
|
||||||
|
crowdsec
|
||||||
|
appsec @not_registry
|
||||||
|
reverse_proxy http://gitea:3000 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reverse_proxy http://gitea:3000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
############## slash ##############
|
############## slash ##############
|
||||||
{$SLASH_DOMAIN} {
|
{$SLASH_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 10MB
|
max_size 10MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://slash:5231
|
import security
|
||||||
|
reverse_proxy http://slash:5231 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## memos ##############
|
############## memos ##############
|
||||||
{$MEMOS_DOMAIN} {
|
{$MEMOS_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 1024MB
|
max_size 1024MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://memos:5230
|
import security
|
||||||
|
reverse_proxy http://memos:5230 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## wg-easy ##############
|
############## wg-easy ##############
|
||||||
{$WG_EASY_DOMAIN} {
|
{$WG_EASY_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 10MB
|
max_size 10MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://wg-easy:51821
|
import security
|
||||||
|
reverse_proxy http://wg-easy:51821 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## vaultwarden ##############
|
############## vaultwarden ##############
|
||||||
{$VAULTWARDEN_DOMAIN} {
|
{$VAULTWARDEN_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 128MB
|
max_size 128MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://vaultwarden
|
import security
|
||||||
|
reverse_proxy http://vaultwarden {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## sftpgo ##############
|
############## sftpgo ##############
|
||||||
{$SFTPGO_DOMAIN} {
|
{$SFTPGO_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 8120MB
|
max_size 8120MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://sftpgo:8080
|
import security
|
||||||
|
reverse_proxy http://sftpgo:8080 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## glance ##############
|
############## glance ##############
|
||||||
{$GLANCE_DOMAIN} {
|
{$GLANCE_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 64MB
|
max_size 64MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://glance:8080
|
import security
|
||||||
|
reverse_proxy http://glance:8080 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## ghost ##############
|
############## ghost ##############
|
||||||
{$GHOST_DOMAIN} {
|
{$GHOST_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 124MB
|
max_size 124MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://ghost:2368 {
|
import security
|
||||||
header_up X-Forwarded-Proto {http.request.scheme}
|
reverse_proxy http://ghost:2368 {
|
||||||
header_up Host {http.request.host}
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## immich ##############
|
############## immich ##############
|
||||||
{$IMMICH_DOMAIN} {
|
{$IMMICH_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 1024MB
|
max_size 1024MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://immich_server:2283 {
|
import security
|
||||||
header_up X-Forwarded-Proto {http.request.scheme}
|
reverse_proxy http://immich_server:2283 {
|
||||||
header_up Host {http.request.host}
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## uptime-kuma ##############
|
############## uptime-kuma ##############
|
||||||
{$UPTIME_KUMA_DOMAIN} {
|
{$UPTIME_KUMA_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 1024MB
|
max_size 1024MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://uptime_kuma:3001 {
|
import security
|
||||||
header_up X-Forwarded-Proto {http.request.scheme}
|
reverse_proxy http://uptime_kuma:3001 {
|
||||||
header_up Host {http.request.host}
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## stalwart ##############
|
############## stalwart ##############
|
||||||
{$STALWART_DOMAIN}, {$STALWART_AUTOCONFIG_DOMAIN}, {$STALWART_AUTODISCOVER_DOMAIN} {
|
{$STALWART_DOMAIN}, {$STALWART_AUTOCONFIG_DOMAIN}, {$STALWART_AUTODISCOVER_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 4048MB
|
max_size 4048MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://stalwart:8080 {
|
import security
|
||||||
header_up X-Forwarded-Proto {http.request.scheme}
|
reverse_proxy http://stalwart:8080 {
|
||||||
header_up X-Forwarded-For {http.request.remote_host}
|
header_up Host {http.request.host}
|
||||||
header_up Host {http.request.host}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## textarea ##############
|
############## textarea ##############
|
||||||
{$TEXTAREA_DOMAIN} {
|
{$TEXTAREA_DOMAIN} {
|
||||||
root * /volume/textarea
|
import access-log
|
||||||
file_server {
|
route {
|
||||||
browse off
|
import security
|
||||||
|
root * /volume/textarea
|
||||||
|
file_server {
|
||||||
|
browse off
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## gopkg-proxy ##############
|
############## gopkg-proxy ##############
|
||||||
{$GOPKG_PROXY_DOMAIN} {
|
{$GOPKG_PROXY_DOMAIN} {
|
||||||
|
import access-log
|
||||||
request_body {
|
request_body {
|
||||||
max_size 2MB
|
max_size 2MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
reverse_proxy http://gopkg_proxy:8421
|
import security
|
||||||
|
reverse_proxy http://gopkg_proxy:8421 {
|
||||||
|
header_up Host {http.request.host}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
############## ech0 ##############
|
############## ech0 ##############
|
||||||
{$ECH0_DOMAIN} {
|
{$ECH0_DOMAIN} {
|
||||||
|
import access-log
|
||||||
header -Server
|
header -Server
|
||||||
|
|
||||||
request_body {
|
request_body {
|
||||||
max_size 5MB
|
max_size 5MB
|
||||||
}
|
}
|
||||||
|
route {
|
||||||
|
import security
|
||||||
|
reverse_proxy http://ech0:8421 {
|
||||||
|
header_up -X-Forwarded-Host
|
||||||
|
header_up -X-Forwarded-Proto
|
||||||
|
header_up -Via
|
||||||
|
|
||||||
reverse_proxy http://ech0:8421 {
|
transport http {
|
||||||
# wheader_up -X-Forwarded-For
|
compression off
|
||||||
header_up -X-Forwarded-Host
|
}
|
||||||
header_up -X-Forwarded-Proto
|
|
||||||
header_up -Via
|
|
||||||
|
|
||||||
transport http {
|
|
||||||
compression off
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM caddy:2-builder-alpine AS builder
|
||||||
|
RUN xcaddy build \
|
||||||
|
--with github.com/hslatman/caddy-crowdsec-bouncer/http \
|
||||||
|
--with github.com/hslatman/caddy-crowdsec-bouncer/appsec
|
||||||
|
|
||||||
|
FROM caddy:2-alpine
|
||||||
|
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
|
||||||
@@ -6,11 +6,9 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
caddy:
|
caddy:
|
||||||
image: caddy:2-alpine
|
build: .
|
||||||
container_name: caddy
|
container_name: caddy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
|
||||||
networks:
|
networks:
|
||||||
- caddy
|
- caddy
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
############# Bouncer keys #############
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
# These are PRE-SEEDED into CrowdSec on first boot. Use the same value here
|
||||||
|
# and in caddy/.env (CADDY) and the host firewall bouncer config (FW).
|
||||||
|
CROWDSEC_BOUNCER_KEY_CADDY=
|
||||||
|
CROWDSEC_BOUNCER_KEY_FW=
|
||||||
|
CROWDSEC_BOUNCER_KEY_EXPORTER=
|
||||||
|
|
||||||
|
############# Console enrollment #############
|
||||||
|
# Enroll key from https://app.crowdsec.net (free).
|
||||||
|
# Leave blank to run without Console (no community blocklist subscriptions).
|
||||||
|
CROWDSEC_ENROLL_KEY=
|
||||||
|
CROWDSEC_ENROLL_INSTANCE_NAME=
|
||||||
|
|
||||||
|
############# MaxMind GeoIP #############
|
||||||
|
# Free account: https://www.maxmind.com/en/geolite2/signup
|
||||||
|
# After signup: Manage License Keys -> Generate new license key.
|
||||||
|
# Used by the geoipupdate sidecar to download/refresh GeoLite2-City.mmdb,
|
||||||
|
# consumed by the exporter for the Country column in the Local bans table.
|
||||||
|
# Leave blank to disable: exporter still runs, country labels will be empty.
|
||||||
|
MAXMIND_ACCOUNT_ID=
|
||||||
|
MAXMIND_LICENSE_KEY=
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
appsec_configs:
|
||||||
|
- crowdsecurity/appsec-default
|
||||||
|
labels:
|
||||||
|
type: appsec
|
||||||
|
listen_addr: 0.0.0.0:7422
|
||||||
|
source: appsec
|
||||||
|
name: caddy-appsec
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
source: docker
|
||||||
|
container_name:
|
||||||
|
- caddy
|
||||||
|
labels:
|
||||||
|
type: caddy
|
||||||
|
follow_stdout: true
|
||||||
|
follow_stderr: true
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
source: docker
|
||||||
|
container_name:
|
||||||
|
- gitea
|
||||||
|
labels:
|
||||||
|
type: gitea
|
||||||
|
follow_stdout: true
|
||||||
|
follow_stderr: true
|
||||||
|
---
|
||||||
|
source: docker
|
||||||
|
container_name:
|
||||||
|
- vaultwarden
|
||||||
|
labels:
|
||||||
|
type: vaultwarden
|
||||||
|
follow_stdout: true
|
||||||
|
follow_stderr: true
|
||||||
|
---
|
||||||
|
source: docker
|
||||||
|
container_name:
|
||||||
|
- stalwart
|
||||||
|
labels:
|
||||||
|
type: stalwart
|
||||||
|
follow_stdout: true
|
||||||
|
follow_stderr: true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
source: journalctl
|
||||||
|
journalctl_filter:
|
||||||
|
- "_SYSTEMD_UNIT=ssh.service"
|
||||||
|
labels:
|
||||||
|
type: syslog
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/db/*
|
||||||
|
/config/*
|
||||||
|
/geoip/*
|
||||||
|
!.gitkeep
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
name: caddy
|
||||||
|
driver: bridge
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
crowdsec:
|
||||||
|
image: crowdsecurity/crowdsec:latest-debian
|
||||||
|
container_name: crowdsec
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
environment:
|
||||||
|
TZ: "Etc/UTC"
|
||||||
|
USE_WAL: "true"
|
||||||
|
COLLECTIONS: >-
|
||||||
|
crowdsecurity/linux
|
||||||
|
crowdsecurity/caddy
|
||||||
|
crowdsecurity/http-cve
|
||||||
|
crowdsecurity/sshd
|
||||||
|
crowdsecurity/whitelist-good-actors
|
||||||
|
crowdsecurity/appsec-virtual-patching
|
||||||
|
crowdsecurity/appsec-generic-rules
|
||||||
|
LePresidente/gitea
|
||||||
|
Dominic-Wagner/vaultwarden
|
||||||
|
BOUNCER_KEY_caddy: "${CROWDSEC_BOUNCER_KEY_CADDY}"
|
||||||
|
BOUNCER_KEY_firewall: "${CROWDSEC_BOUNCER_KEY_FW}"
|
||||||
|
BOUNCER_KEY_exporter: "${CROWDSEC_BOUNCER_KEY_EXPORTER}"
|
||||||
|
ENROLL_KEY: "${CROWDSEC_ENROLL_KEY:-}"
|
||||||
|
ENROLL_INSTANCE_NAME: "${CROWDSEC_ENROLL_INSTANCE_NAME:-aykhans-prod}"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18080:8080"
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
volumes:
|
||||||
|
- ./data/db:/var/lib/crowdsec/data
|
||||||
|
- ./data/config:/etc/crowdsec
|
||||||
|
- ./acquis.d:/etc/crowdsec/acquis.d:ro
|
||||||
|
- ./profiles/profiles.yaml:/etc/crowdsec/profiles.yaml:ro
|
||||||
|
- ./parsers/s00-raw/stalwart-logs.yaml:/etc/crowdsec/parsers/s00-raw/stalwart-logs.yaml:ro
|
||||||
|
- ./parsers/s01-parse/stalwart-logs-extended.yaml:/etc/crowdsec/parsers/s01-parse/stalwart-logs-extended.yaml:ro
|
||||||
|
- ./parsers/s02-enrich/whitelist-trusted.yaml:/etc/crowdsec/parsers/s02-enrich/whitelist-trusted.yaml:ro
|
||||||
|
- ./scenarios/stalwart-smtp-bruteforce.yaml:/etc/crowdsec/scenarios/stalwart-smtp-bruteforce.yaml:ro
|
||||||
|
- ./scenarios/stalwart-auth-bruteforce.yaml:/etc/crowdsec/scenarios/stalwart-auth-bruteforce.yaml:ro
|
||||||
|
- ./scenarios/http-admin-interface-probing.yaml:/etc/crowdsec/scenarios/http-admin-interface-probing.yaml:ro
|
||||||
|
- /var/log/journal:/var/log/journal:ro
|
||||||
|
- /run/log/journal:/run/log/journal:ro
|
||||||
|
- /etc/machine-id:/etc/machine-id:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "100m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
exporter:
|
||||||
|
build: ./exporter
|
||||||
|
container_name: crowdsec_exporter
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
environment:
|
||||||
|
CROWDSEC_LAPI_URL: "http://crowdsec:8080"
|
||||||
|
CROWDSEC_API_KEY: "${CROWDSEC_BOUNCER_KEY_EXPORTER}"
|
||||||
|
POLL_INTERVAL_SECS: "30"
|
||||||
|
LISTEN_PORT: "9100"
|
||||||
|
GEOIP_CITY_DB: "/geoip/GeoLite2-City.mmdb"
|
||||||
|
volumes:
|
||||||
|
- ./data/geoip:/geoip:ro
|
||||||
|
depends_on:
|
||||||
|
- crowdsec
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
geoipupdate:
|
||||||
|
image: ghcr.io/maxmind/geoipupdate:v7
|
||||||
|
container_name: crowdsec_geoipupdate
|
||||||
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
environment:
|
||||||
|
GEOIPUPDATE_ACCOUNT_ID: "${MAXMIND_ACCOUNT_ID}"
|
||||||
|
GEOIPUPDATE_LICENSE_KEY: "${MAXMIND_LICENSE_KEY}"
|
||||||
|
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City"
|
||||||
|
GEOIPUPDATE_FREQUENCY: "24"
|
||||||
|
volumes:
|
||||||
|
- ./data/geoip:/usr/share/GeoIP
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "2"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY main.go ./
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -o /exporter .
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /exporter /exporter
|
||||||
|
EXPOSE 9100
|
||||||
|
ENTRYPOINT ["/exporter"]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
module crowdsec-exporter
|
||||||
|
|
||||||
|
go 1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0
|
||||||
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang/v2"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var geoDB *geoip2.Reader
|
||||||
|
|
||||||
|
// countryFlag converts a 2-letter ISO-3166-1 alpha-2 country code into its
|
||||||
|
// emoji flag using the Unicode regional indicator pairs trick (A-Z -> 0x1F1E6-FF).
|
||||||
|
func countryFlag(iso string) string {
|
||||||
|
if len(iso) != 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
a, b := iso[0], iso[1]
|
||||||
|
if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string([]rune{rune(a-'A') + 0x1F1E6, rune(b-'A') + 0x1F1E6})
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupGeo returns "<flag> <country name> (<city>)" for the given IP, falling
|
||||||
|
// back gracefully (empty string, missing flag, missing city) when the DB is
|
||||||
|
// unavailable or the IP can't be resolved.
|
||||||
|
func lookupGeo(ip string) string {
|
||||||
|
if geoDB == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
addr, err := netip.ParseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rec, err := geoDB.City(addr)
|
||||||
|
if err != nil || rec == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
iso := rec.Country.ISOCode
|
||||||
|
name := rec.Country.Names.English
|
||||||
|
if name == "" {
|
||||||
|
name = iso
|
||||||
|
}
|
||||||
|
out := name
|
||||||
|
if flag := countryFlag(iso); flag != "" {
|
||||||
|
out = flag + " " + name
|
||||||
|
}
|
||||||
|
if city := rec.City.Names.English; city != "" {
|
||||||
|
out += " (" + city + ")"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
decisionsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_decisions_active",
|
||||||
|
Help: "Active CrowdSec decisions counted directly from LAPI (no gauge drift).",
|
||||||
|
}, []string{"origin", "scenario", "type"})
|
||||||
|
|
||||||
|
decisionsUniqueIPs = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_decisions_unique_ips",
|
||||||
|
Help: "Unique IPs with at least one active decision, grouped by origin.",
|
||||||
|
}, []string{"origin"})
|
||||||
|
|
||||||
|
decisionsUniqueIPsTotal = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_decisions_unique_ips_total",
|
||||||
|
Help: "Unique IPs with at least one active decision across all origins.",
|
||||||
|
})
|
||||||
|
|
||||||
|
decisionRemaining = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_decision_remaining_seconds",
|
||||||
|
Help: "Remaining seconds before each local decision expires. Only origin=crowdsec is exposed to keep cardinality bounded (CAPI/lists can contain tens of thousands of IPs).",
|
||||||
|
}, []string{"origin", "ip", "country", "scenario", "type"})
|
||||||
|
|
||||||
|
lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_exporter_last_fetch_unix",
|
||||||
|
Help: "Unix timestamp of the last successful LAPI fetch.",
|
||||||
|
})
|
||||||
|
|
||||||
|
fetchErrors = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "crowdsec_exporter_fetch_errors",
|
||||||
|
Help: "1 if the most recent fetch errored, else 0.",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type decision struct {
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Scenario string `json:"scenario"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(
|
||||||
|
decisionsActive,
|
||||||
|
decisionsUniqueIPs,
|
||||||
|
decisionsUniqueIPsTotal,
|
||||||
|
decisionRemaining,
|
||||||
|
lastFetchUnix,
|
||||||
|
fetchErrors,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(k, def string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) {
|
||||||
|
req, err := http.NewRequest("GET", lapiURL+"/v1/decisions", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[error] build request failed: %v", err)
|
||||||
|
fetchErrors.Set(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Api-Key", apiKey)
|
||||||
|
req.Header.Set("User-Agent", "crowdsec-exporter/1.0")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[error] fetch failed: %v", err)
|
||||||
|
fetchErrors.Set(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Printf("[error] LAPI returned status %d", resp.StatusCode)
|
||||||
|
fetchErrors.Set(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []decision
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||||
|
log.Printf("[error] decode failed: %v", err)
|
||||||
|
fetchErrors.Set(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type key struct{ origin, scenario, dtype string }
|
||||||
|
counts := map[key]int{}
|
||||||
|
ips := map[string]map[string]struct{}{}
|
||||||
|
allIPs := map[string]struct{}{}
|
||||||
|
type localDecision struct {
|
||||||
|
origin, ip, country, scenario, dtype string
|
||||||
|
remaining float64
|
||||||
|
}
|
||||||
|
var locals []localDecision
|
||||||
|
|
||||||
|
for _, d := range data {
|
||||||
|
origin := d.Origin
|
||||||
|
if origin == "" {
|
||||||
|
origin = "unknown"
|
||||||
|
}
|
||||||
|
scenario := d.Scenario
|
||||||
|
if scenario == "" {
|
||||||
|
scenario = "unknown"
|
||||||
|
}
|
||||||
|
dtype := d.Type
|
||||||
|
if dtype == "" {
|
||||||
|
dtype = "unknown"
|
||||||
|
}
|
||||||
|
counts[key{origin, scenario, dtype}]++
|
||||||
|
if d.Value != "" {
|
||||||
|
if _, ok := ips[origin]; !ok {
|
||||||
|
ips[origin] = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
ips[origin][d.Value] = struct{}{}
|
||||||
|
allIPs[d.Value] = struct{}{}
|
||||||
|
}
|
||||||
|
if origin == "crowdsec" {
|
||||||
|
dur, perr := time.ParseDuration(d.Duration)
|
||||||
|
if perr != nil {
|
||||||
|
dur = 0
|
||||||
|
}
|
||||||
|
locals = append(locals, localDecision{origin, d.Value, lookupGeo(d.Value), scenario, dtype, dur.Seconds()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decisionsActive.Reset()
|
||||||
|
decisionsUniqueIPs.Reset()
|
||||||
|
decisionRemaining.Reset()
|
||||||
|
for k, n := range counts {
|
||||||
|
decisionsActive.WithLabelValues(k.origin, k.scenario, k.dtype).Set(float64(n))
|
||||||
|
}
|
||||||
|
for origin, s := range ips {
|
||||||
|
decisionsUniqueIPs.WithLabelValues(origin).Set(float64(len(s)))
|
||||||
|
}
|
||||||
|
decisionsUniqueIPsTotal.Set(float64(len(allIPs)))
|
||||||
|
for _, l := range locals {
|
||||||
|
decisionRemaining.WithLabelValues(l.origin, l.ip, l.country, l.scenario, l.dtype).Set(l.remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchErrors.Set(0)
|
||||||
|
lastFetchUnix.Set(float64(time.Now().Unix()))
|
||||||
|
|
||||||
|
log.Printf("[ok] %d decisions, %d unique IPs across %d origins",
|
||||||
|
len(data), len(allIPs), len(ips))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lapiURL := getenv("CROWDSEC_LAPI_URL", "http://crowdsec:8080")
|
||||||
|
apiKey := os.Getenv("CROWDSEC_API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
log.Fatal("CROWDSEC_API_KEY env var required")
|
||||||
|
}
|
||||||
|
intervalStr := getenv("POLL_INTERVAL_SECS", "30")
|
||||||
|
interval, err := strconv.Atoi(intervalStr)
|
||||||
|
if err != nil || interval < 1 {
|
||||||
|
log.Fatalf("invalid POLL_INTERVAL_SECS: %q", intervalStr)
|
||||||
|
}
|
||||||
|
port := getenv("LISTEN_PORT", "9100")
|
||||||
|
|
||||||
|
geoDBPath := getenv("GEOIP_CITY_DB", "")
|
||||||
|
if geoDBPath != "" {
|
||||||
|
if db, err := geoip2.Open(geoDBPath); err == nil {
|
||||||
|
geoDB = db
|
||||||
|
defer geoDB.Close()
|
||||||
|
log.Printf("[start] GeoIP DB loaded: %s", geoDBPath)
|
||||||
|
} else {
|
||||||
|
log.Printf("[warn] GeoIP DB unavailable (%v); country label will be empty", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
fetchAndUpdate(client, lapiURL, apiKey)
|
||||||
|
time.Sleep(time.Duration(interval) * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
log.Printf("[start] listening :%s, polling %s every %ds", port, lapiURL, interval)
|
||||||
|
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
name: stalwart/parse-logs
|
||||||
|
description: Raw parser for Stalwart logs
|
||||||
|
stage: s00-raw
|
||||||
|
onsuccess: next_stage
|
||||||
|
filter: "evt.Parsed.program == 'stalwart' || evt.Line.Labels.type == 'stalwart'"
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
- grok:
|
||||||
|
apply_on: Line.Raw
|
||||||
|
pattern: '^%{TIMESTAMP_ISO8601:timestamp}\s+%{WORD:log_level}\s+%{DATA:message_text}\s*\(%{DATA:event_type}\)\s*(%{GREEDYDATA:kvpairs})?$'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
name: stalwart/parse-extended
|
||||||
|
description: Parse Stalwart logs including key fields without kv parser
|
||||||
|
stage: s01-parse
|
||||||
|
onsuccess: next_stage
|
||||||
|
filter: "evt.Parsed.program == 'stalwart' || evt.Line.Labels.type == 'stalwart'"
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
- grok:
|
||||||
|
apply_on: Line.Raw
|
||||||
|
pattern: '^%{TIMESTAMP_ISO8601:timestamp}\s+%{WORD:log_level}\s+%{DATA:message_text}\s+\(%{DATA:event_type}\)\s*(?:listenerId\s*=\s*"%{DATA:listenerId}",\s*)?(?:localPort\s*=\s*%{INT:localPort},\s*)?(?:remoteIp\s*=\s*%{IP:remoteIp},\s*)?(?:remotePort\s*=\s*%{INT:remotePort},\s*)?(?:reason\s*=\s*"%{DATA:reason}")?.*$'
|
||||||
|
statics:
|
||||||
|
- meta: source_ip
|
||||||
|
expression: evt.Parsed.remoteIp
|
||||||
|
- meta: service
|
||||||
|
value: stalwart
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: aykhans/whitelist-trusted
|
||||||
|
description: Trusted operator IPs and Docker private network ranges
|
||||||
|
whitelist:
|
||||||
|
reason: "operator + internal docker networks"
|
||||||
|
ip:
|
||||||
|
- "127.0.0.1"
|
||||||
|
- "::1"
|
||||||
|
cidr:
|
||||||
|
- "10.0.0.0/8"
|
||||||
|
- "172.16.0.0/12"
|
||||||
|
- "192.168.0.0/16"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: default_ip_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
duration_expr: Sprintf('%dh', min(730, (GetDecisionsCount(Alert.GetValue())+1)*4))
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
|
||||||
|
---
|
||||||
|
name: default_range_remediation
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Range"
|
||||||
|
duration_expr: Sprintf('%dh', min(730, (GetDecisionsCount(Alert.GetValue())+1)*4))
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
type: leaky
|
||||||
|
name: crowdsecurity/http-admin-interface-probing
|
||||||
|
description: "Detect generic HTTP admin interface probing"
|
||||||
|
filter: |
|
||||||
|
evt.Meta.service == 'http' and
|
||||||
|
evt.Meta.log_type in ['http_access-log', 'http_error-log'] and
|
||||||
|
evt.Meta.http_status in ['404', '403'] and
|
||||||
|
any(File("admin_interfaces.txt"), { Lower(evt.Meta.http_path) contains #})
|
||||||
|
groupby: evt.Meta.source_ip
|
||||||
|
distinct: "evt.Meta.http_path"
|
||||||
|
data:
|
||||||
|
- source_url: https://hub-data.crowdsec.net/web/admin_interfaces.txt
|
||||||
|
dest_file: admin_interfaces.txt
|
||||||
|
type: string
|
||||||
|
capacity: 5
|
||||||
|
leakspeed: "10s"
|
||||||
|
blackhole: 1m
|
||||||
|
labels:
|
||||||
|
confidence: 3
|
||||||
|
spoofable: 0
|
||||||
|
classification:
|
||||||
|
- attack.T1595
|
||||||
|
behavior: "http:scan"
|
||||||
|
label: "HTTP Admin Interface Probing"
|
||||||
|
service: http
|
||||||
|
remediation: true
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
type: leaky
|
||||||
|
name: aykhans/stalwart-auth-bruteforce
|
||||||
|
description: Detect SMTP/IMAP/POP3 authentication brute force on Stalwart
|
||||||
|
filter: |
|
||||||
|
evt.Parsed.event_type == "auth.failed"
|
||||||
|
groupby: evt.Meta.source_ip
|
||||||
|
capacity: 3
|
||||||
|
leakspeed: 10m
|
||||||
|
blackhole: 1h
|
||||||
|
labels:
|
||||||
|
type: bruteforce
|
||||||
|
service: stalwart
|
||||||
|
remediation: true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
type: leaky
|
||||||
|
name: stalwart/smtp-bruteforce
|
||||||
|
description: Detect SMTP bruteforce and scanners on Stalwart logs
|
||||||
|
filter: |
|
||||||
|
evt.Parsed.event_type in [
|
||||||
|
"smtp.invalid-ehlo",
|
||||||
|
"smtp.auth-not-allowed",
|
||||||
|
"smtp.auth-mechanism-not-supported"
|
||||||
|
]
|
||||||
|
groupby: evt.Meta.source_ip
|
||||||
|
capacity: 5
|
||||||
|
leakspeed: 10m
|
||||||
|
blackhole: 1h
|
||||||
|
labels:
|
||||||
|
type: attack
|
||||||
|
service: smtp
|
||||||
|
remediation: true
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 1,
|
||||||
|
"id": null,
|
||||||
|
"uid": "crowdsec",
|
||||||
|
"title": "CrowdSec",
|
||||||
|
"tags": [
|
||||||
|
"crowdsec",
|
||||||
|
"security"
|
||||||
|
],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-24h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Total active bans",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "crowdsec_decisions_unique_ips_total",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0",
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "CAPI (community)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 6,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "crowdsec_decisions_unique_ips{origin=\"CAPI\"}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0",
|
||||||
|
"color": {
|
||||||
|
"mode": "purple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Blocklist subs",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "crowdsec_decisions_unique_ips{origin=\"lists\"}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0",
|
||||||
|
"color": {
|
||||||
|
"mode": "blue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Local detections",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 0,
|
||||||
|
"w": 6,
|
||||||
|
"h": 4
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "crowdsec_decisions_unique_ips{origin=\"crowdsec\"}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0",
|
||||||
|
"color": {
|
||||||
|
"mode": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "area",
|
||||||
|
"textMode": "value",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"type": "piechart",
|
||||||
|
"title": "Bans by origin",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 4,
|
||||||
|
"w": 12,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum by (origin) (crowdsec_decisions_unique_ips)",
|
||||||
|
"legendFormat": "{{origin}}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"pieType": "donut",
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"values": [
|
||||||
|
"value",
|
||||||
|
"percent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
},
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "piechart",
|
||||||
|
"title": "Top 10 bans by reason",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 12,
|
||||||
|
"y": 4,
|
||||||
|
"w": 12,
|
||||||
|
"h": 9
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "topk(10, sum by (scenario) (crowdsec_decisions_active))",
|
||||||
|
"legendFormat": "{{scenario}}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"pieType": "donut",
|
||||||
|
"legend": {
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"values": [
|
||||||
|
"value",
|
||||||
|
"percent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
},
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"type": "bargauge",
|
||||||
|
"title": "Top scenarios by alerts (total)",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 23,
|
||||||
|
"w": 24,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "topk(10, sum by (reason) (cs_alerts))",
|
||||||
|
"legendFormat": "{{reason}}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"displayMode": "gradient",
|
||||||
|
"showUnfilled": true,
|
||||||
|
"valueMode": "color",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"type": "table",
|
||||||
|
"title": "Local bans detail",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 13,
|
||||||
|
"w": 24,
|
||||||
|
"h": 10
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "crowdsec_decision_remaining_seconds{origin=\"crowdsec\"}",
|
||||||
|
"refId": "A",
|
||||||
|
"instant": true,
|
||||||
|
"format": "table",
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "prometheusdatasource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {
|
||||||
|
"Time": true,
|
||||||
|
"__name__": true,
|
||||||
|
"instance": true,
|
||||||
|
"job": true,
|
||||||
|
"origin": true
|
||||||
|
},
|
||||||
|
"indexByName": {
|
||||||
|
"country": 0,
|
||||||
|
"ip": 1,
|
||||||
|
"scenario": 2,
|
||||||
|
"type": 3,
|
||||||
|
"Value": 4
|
||||||
|
},
|
||||||
|
"renameByName": {
|
||||||
|
"country": "Country",
|
||||||
|
"ip": "IP",
|
||||||
|
"scenario": "Scenario",
|
||||||
|
"type": "Type",
|
||||||
|
"Value": "Time remaining"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"custom": {
|
||||||
|
"align": "auto",
|
||||||
|
"filterable": true,
|
||||||
|
"inspect": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Time remaining"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "unit",
|
||||||
|
"value": "s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "custom.cellOptions",
|
||||||
|
"value": {
|
||||||
|
"type": "color-text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"value": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": [
|
||||||
|
{
|
||||||
|
"displayName": "Time remaining",
|
||||||
|
"desc": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer": {
|
||||||
|
"show": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,9 +14,9 @@ providers:
|
|||||||
disableDeletion: false
|
disableDeletion: false
|
||||||
# Optional: Allow users to edit dashboards in the Grafana UI.
|
# Optional: Allow users to edit dashboards in the Grafana UI.
|
||||||
# Changes will be overwritten on restart unless you save them elsewhere.
|
# Changes will be overwritten on restart unless you save them elsewhere.
|
||||||
editable: true
|
editable: false
|
||||||
updateIntervalSeconds: 10
|
updateIntervalSeconds: 10
|
||||||
allowUiUpdates: true
|
allowUiUpdates: false
|
||||||
# Options specific to the 'file' type provider
|
# Options specific to the 'file' type provider
|
||||||
options:
|
options:
|
||||||
# Path inside the container where Grafana should look for dashboard JSON files.
|
# Path inside the container where Grafana should look for dashboard JSON files.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ generate_env_files() {
|
|||||||
cp --update=none ./uptime_kuma/.env.example ./uptime_kuma/.env
|
cp --update=none ./uptime_kuma/.env.example ./uptime_kuma/.env
|
||||||
cp --update=none ./croc/.env.example ./croc/.env
|
cp --update=none ./croc/.env.example ./croc/.env
|
||||||
cp --update=none ./stalwart/.env.example ./stalwart/.env
|
cp --update=none ./stalwart/.env.example ./stalwart/.env
|
||||||
|
cp --update=none ./crowdsec/.env.example ./crowdsec/.env
|
||||||
cp --update=none ./caddy/Caddyfile.private.example ./caddy/Caddyfile.private
|
cp --update=none ./caddy/Caddyfile.private.example ./caddy/Caddyfile.private
|
||||||
print_success ".env files generated."
|
print_success ".env files generated."
|
||||||
}
|
}
|
||||||
@@ -190,8 +191,17 @@ start_services() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Starting crowdsec..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND -f ./crowdsec/docker-compose.yaml up --pull always -d
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "Crowdsec started successfully."
|
||||||
|
else
|
||||||
|
print_error "failed to start Crowdsec!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting caddy..."
|
echo "Starting caddy..."
|
||||||
$DOCKER_COMPOSE_COMMAND -f ./caddy/docker-compose.yaml up --pull always -d
|
$DOCKER_COMPOSE_COMMAND -f ./caddy/docker-compose.yaml up --pull always --build -d
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
print_success "Caddy started successfully."
|
print_success "Caddy started successfully."
|
||||||
else
|
else
|
||||||
@@ -368,6 +378,15 @@ stop_services() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Stopping crowdsec..."
|
||||||
|
$DOCKER_COMPOSE_COMMAND -f ./crowdsec/docker-compose.yaml down
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
print_success "Crowdsec stopped successfully."
|
||||||
|
else
|
||||||
|
print_error "failed to stop Crowdsec!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Stopping watchtower..."
|
echo "Stopping watchtower..."
|
||||||
$DOCKER_COMPOSE_COMMAND -f ./watchtower/docker-compose.yaml down
|
$DOCKER_COMPOSE_COMMAND -f ./watchtower/docker-compose.yaml down
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
memos:
|
memos:
|
||||||
image: neosmemo/memos:0.27
|
image: neosmemo/memos:0.29
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: memos
|
container_name: memos
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -16,3 +16,13 @@ scrape_configs:
|
|||||||
scrape_interval: 2s
|
scrape_interval: 2s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["caddy:2019"]
|
- targets: ["caddy:2019"]
|
||||||
|
|
||||||
|
- job_name: crowdsec
|
||||||
|
scrape_interval: 10s
|
||||||
|
static_configs:
|
||||||
|
- targets: ["crowdsec:6060"]
|
||||||
|
|
||||||
|
- job_name: crowdsec_exporter
|
||||||
|
scrape_interval: 30s
|
||||||
|
static_configs:
|
||||||
|
- targets: ["crowdsec_exporter:9100"]
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
server:
|
||||||
image: vaultwarden/server:1.35.7
|
image: vaultwarden/server:1.36.0
|
||||||
container_name: vaultwarden
|
container_name: vaultwarden
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- DOMAIN=${DOMAIN}
|
- DOMAIN=${DOMAIN}
|
||||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
||||||
|
- LOG_TIMESTAMP_FORMAT=%Y-%m-%d %H:%M:%S.%3f%:z
|
||||||
networks:
|
networks:
|
||||||
- caddy
|
- caddy
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
- "--label-enable"
|
- "--label-enable"
|
||||||
- "--cleanup"
|
- "--cleanup"
|
||||||
- "--interval"
|
- "--interval"
|
||||||
- "600" # 10 minutes
|
- "28800" # 8 hours
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|||||||
Reference in New Issue
Block a user