diff --git a/.assets/Example.png b/.assets/Example.png new file mode 100644 index 00000000..e8f607ea Binary files /dev/null and b/.assets/Example.png differ diff --git a/.travis.yml b/.travis.yml index e6e5b1fb..4fd38f53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: shell branches: only: - - - #replace variables, omit brackets + - letsencrypt-f2bdiscord #replace variables, omit brackets services: - docker @@ -12,8 +12,8 @@ services: env: global: - DOCKERHUB="linuxserver/mods" #don't modify - - BASEIMAGE="baseimagename" #replace - - MODNAME="modname" #replace + - BASEIMAGE="letsencrypt" #replace + - MODNAME="f2bdiscord" #replace jobs: include: diff --git a/Dockerfile b/Dockerfile index 2f24b209..a27e5cde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ FROM scratch +LABEL maintainer="Roxedus" + # copy local files COPY root/ / diff --git a/Dockerfile.complex b/Dockerfile.complex deleted file mode 100644 index 4463d838..00000000 --- a/Dockerfile.complex +++ /dev/null @@ -1,21 +0,0 @@ -## Buildstage ## -FROM lsiobase/alpine:3.9 as buildstage - -RUN \ - echo "**** install packages ****" && \ - apk add --no-cache \ - curl && \ - echo "**** grab rclone ****" && \ - mkdir -p /root-layer && \ - curl -o \ - /root-layer/rclone.deb -L \ - "https://downloads.rclone.org/v1.47.0/rclone-v1.47.0-linux-amd64.deb" - -# copy local files -COPY root/ /root-layer/ - -## Single layer deployed image ## -FROM scratch - -# Add files from buildstage -COPY --from=buildstage /root-layer/ / diff --git a/README.md b/README.md index 2418e530..8d6cf2b0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,38 @@ -# Rsync - Docker mod for openssh-server +# F2B Discord Notification - Docker mod which allows Fail2Ban Discord embeds -This mod adds rsync to openssh-server, to be installed/updated during container start. +This mod enhances the Letsencrypt container adding better Fail2Ban notifications for discord. -In openssh-server docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:openssh-server-rsync` +## Configuration -If adding multiple mods, enter them in an array separated by `|`, such as `DOCKER_MODS=linuxserver/mods:openssh-server-rsync|linuxserver/mods:openssh-server-mod2` +### Enable -# Mod creation instructions +In letsencrypt docker arguments, set an environment variable `DOCKER_MODS=linuxserver/mods:letsencrypt-f2bdiscord` to enable. -* Ask the team to create a new branch named `-`. Baseimage should be the name of the image the mod will be applied to. The new branch will be based on the `template` branch. -* Fork the repo, checkout the newly created branch. -* Edit the `Dockerfile` for the mod. `Dockerfile.complex` is only an example and included for reference; it should be deleted when done. -* Inspect the `root` folder contents. Edit, add and remove as necessary. -* Edit this readme with pertinent info, delete these instructions. -* Finally edit the `travis.yml`. Customize the build branch, and the vars for `BASEIMAGE` and `MODNAME`. -* Submit PR against the branch created by the team. \ No newline at end of file +### Mod configuration + +**Environment variables used by this mod:** + +[Discord webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks), it just need the last parts. `-e DISC_HOOK=40832456738934/7DcEpWr5V24OIEIELjg-KkHky86SrOgTqA` +[Your discord ID](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-). `-e DISC_ME=120970603556503552` +[Map API Key](https://developer.mapquest.com/), get a key from mapquest. `-e DISC_API=YourKey` + +#### Jail configuration example + +```ini +[bitwarden] + +filter = bitwarden +enabled = true +logpath = /fail2ban/bw/bitwarden.log +action = discordEmbed[bantime=24] + iptables-allports[name=Bitwarden] + +``` + +Action arguments: + +`bantime`(hour) is optional, but defaults to 24 when not set. Just reflects in the message, does not change the ban time + +## Example + +![Example picture](.assets/Example.png) diff --git a/root/AwesomeFolder/Fail2Ban.py b/root/AwesomeFolder/Fail2Ban.py new file mode 100644 index 00000000..64faca24 --- /dev/null +++ b/root/AwesomeFolder/Fail2Ban.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +import argparse +import collections +import datetime +import geoip2.database +import os +import requests + +class Discord: + def __init__(self, data, action): + self.action = action + self.base = "https://discordapp.com/api/webhooks/" + self.data = data + self.token = os.getenv('DISC_HOOK', "") # If not setting enviroment variables, edit this + self.you = os.getenv('DISC_ME', "120970603556503552") # If not setting enviroment variables, edit this + + def create_payload(self): + webhook = { + "username":"Fail2Ban", + "content": f"<@{self.you}>", + "embeds": [{}] + } + webhook["embeds"][0]["author"] = {"name": "Fail2Ban"} + webhook["embeds"][0]["timestamp"] = f"{datetime.datetime.utcnow()}" + if "ban" in self.action.action: + webhook["embeds"][0]["url"] = f"https://db-ip.com/{self.data['ip']}" + webhook["embeds"][0]["image"] = {"url": f"{self.data['map-img']}"} + webhook["embeds"][0]["fields"] = [{}] + webhook["embeds"][0]["fields"][0]["name"] = f":flag_{self.data['iso'].lower()}:" + webhook["embeds"][0]["fields"][0]["value"] = self.data["city"] or self.data["name"] + if self.action.action == "ban": + webhook["embeds"][0]["fields"].append({"name": f"Map", "value": f"[Link]({self.data['map-url']})"}) + webhook["embeds"][0]["fields"].append({"name": f"Unban cmd", "value": f"fail2ban-client unban {self.data['ip']}"}) + webhook["embeds"][0]["title"] = f"New ban on `{self.action.jail}`" + webhook["embeds"][0]["description"] = f"**{self.data['ip']}** got banned for `{self.action.time}` hours after `{self.action.fail}` tries" + webhook["embeds"][0]["color"] = 16194076 + elif self.action.action == "unban": + webhook["embeds"][0]["title"] = f"Revoked ban on `{self.action.jail}`" + webhook["embeds"][0]["description"] = f"**{self.data['ip']}** is now unbanned" + webhook["embeds"][0]["color"] = 845872 + elif self.action.action == "start": + webhook["content"] = "" + webhook["embeds"][0]["description"] = f"Started `{self.action.jail}`" + webhook["embeds"][0]["color"] = 845872 + elif self.action.action == "stopped": + webhook["content"] = "" + webhook["embeds"][0]["description"] = f"Stopped `{self.action.jail}`" + webhook["embeds"][0]["color"] = 16194076 + elif self.action.action == "test": + webhook["content"] = "" + webhook["embeds"][0]["description"] = f"I am working" + webhook["embeds"][0]["color"] = 845872 + else: + return None + return webhook + + def send(self, payload): + r = requests.post(url=f"{self.base}{self.token}", json=payload) + +class Helpers: + def __init__(self, ip): + self.data = {"ip": ip} + self.map_api = os.getenv('DISC_API', "") # If not setting enviroment variables, edit this + self.reader = geoip2.database.Reader('/config/geoip2db/GeoLite2-City.mmdb') + self.f2b() + self.map() + + def f2b(self): + r = self.reader.city(self.data['ip']) + self.data["iso"] = r.country.iso_code + self.data["name"] = r.country.name + self.data["city"] = r.city.name + self.data["lat"] = r.location.latitude + self.data["lon"] = r.location.longitude + + + def map(self): + img_params={"center":f"{self.data['lat']},{self.data['lon']}", "size":"500,300", "key": self.map_api} + img_r = requests.get('https://www.mapquestapi.com/staticmap/v5/map', params=img_params) + self.data["map-img"] = img_r.url + url_params={"center":f"{self.data['lat']},{self.data['lon']}", "size":"500,300"} + url_r = requests.get('https://mapquest.com/', params=url_params) + self.data["map-url"] = url_r.url + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Discord notifier for F2B') + parser.add_argument('-a', '--action', help="Which F2B action triggered the script", required=True) + parser.add_argument('-i', '--ip', help="ip which triggered the action", default="1.1.1.1") + parser.add_argument('-j', '--jail', help="jail which triggered the action") + parser.add_argument('-t', '--time', help="The time the action is valid") + parser.add_argument('-f', '--fail', help="Amount of attempts done") + + args = parser.parse_args() + + data = Helpers(args.ip).data + disc = Discord(data, args) + if (payload := disc.create_payload()): + disc.send(payload) diff --git a/root/AwesomeFolder/discordEmbed.conf b/root/AwesomeFolder/discordEmbed.conf new file mode 100644 index 00000000..d235891e --- /dev/null +++ b/root/AwesomeFolder/discordEmbed.conf @@ -0,0 +1,23 @@ +# Author: Roxedus +# Adapted Source: Gilbn from https://technicalramblings.com + +[Definition] + +# Notify on Startup +actionstart = python3 /config/fail2ban/Fail2Ban.py -a start -j + +# Notify on Shutdown +actionstop = python3 /config/fail2ban/Fail2Ban.py -a stopped -j + +# +actioncheck = + +# Notify on Banned +actionban = python3 /config/fail2ban/Fail2Ban.py -a ban -j -i -t -f + +# Notify on Unbanned +actionunban = python3 /config/fail2ban/Fail2Ban.py -a unban -j -i -t -f +[Init] + +# Name of the jail in your jail.local file. default = [your-jail-name] +name = default \ No newline at end of file diff --git a/root/etc/cont-init.d/49-F2BDiscord b/root/etc/cont-init.d/49-F2BDiscord new file mode 100644 index 00000000..c2bae95b --- /dev/null +++ b/root/etc/cont-init.d/49-F2BDiscord @@ -0,0 +1,20 @@ +#!/usr/bin/with-contenv bash + +if [ ! -d /usr/lib/python3.8/site-packages/geoip2 ]; then + echo '------------------------------------------------------------------------' + echo '| Running installation of required modules for letsencrypt-f2bdiscord' + echo '------------------------------------------------------------------------' + pip3 install --no-cache-dir -U \ + requests \ + argparse \ + geoip2 +fi + +if [ ! -f /config/fail2ban/Fail2Ban.py ]; then + cp /AwesomeFolder/Fail2Ban.py /config/fail2ban/ + chmod +x /config/fail2ban/Fail2Ban.py +fi + +if [ ! -f /config/fail2ban/action.d/discordEmbed.conf ]; then + cp /AwesomeFolder/discordEmbed.conf /config/fail2ban/action.d +fi \ No newline at end of file diff --git a/root/etc/cont-init.d/98-vpn-config b/root/etc/cont-init.d/98-vpn-config deleted file mode 100644 index a5f91276..00000000 --- a/root/etc/cont-init.d/98-vpn-config +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/with-contenv bash - -# Determine if setup is needed -if [ ! -f /usr/local/lib/python***/dist-packages/sshuttle ] && \ -[ -f /usr/bin/apt ]; then - ## Ubuntu - apt-get update - apt-get install --no-install-recommends -y \ - iptables \ - openssh-client \ - python3 \ - python3-pip - pip3 install sshuttle -fi -if [ ! -f /usr/lib/python***/site-packages/sshuttle ] && \ -[ -f /sbin/apk ]; then - # Alpine - apk add --no-cache \ - iptables \ - openssh \ - py3-pip \ - python3 - pip3 install sshuttle -fi - -chown -R root:root /root -chmod -R 600 /root/.ssh diff --git a/root/etc/services.d/sshvpn/run b/root/etc/services.d/sshvpn/run deleted file mode 100644 index 7d49e796..00000000 --- a/root/etc/services.d/sshvpn/run +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/with-contenv bash - -sshuttle --dns --remote root@${HOST}:${PORT} 0/0 -x 172.17.0.0/16