What is Portainer and why you need to ensure its security
Portainer is a tool that can manage Docker containers and images with GUI. It is a web application and can be exposed to the world, which, may raise doubts about its security. After all, with Portainer you can create, delete or modify containers and images.
Notice
To be clear, you need to be aware of the danger posed by exposing such important services open to the world. The following guide will not make your instance completely safe, but it will reduce the risk of unauthorized access using dictonary attacks by bots.
Integration between Portainer and Fail2ban
Portainer collects application logs, including information about unsuccessful authorization, but it does not contain the IP address from which the request was sent and does not look like it will change, so it cannot be used. This is what it looks like:
time="2022-10-15T15:49:14Z" level=info msg="2022/10/15 15:49:14 http error: Invalid credentials (err=Unauthorized) (code=422)"
There is another solution. We assume that you are using Portainer Community Edition 2.15.1 exposed via Nginx reverse proxy and you see real query IP if using Cloudflare. So you can use access.log
file instead. This is what the query fragment with incorrect login details looks like:
127.0.0.1 - - [26/Oct/2022:20:55:11 +0200] "POST /api/auth HTTP/2.0" 422 59 "https://portainer.example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"
127.0.0.1
is a real request IP, and https://portainer.example.com/
is URL of your Portainer instance.
Configuration
Go to /etc/fail2ban/filter.d
and create portainer.conf
file. Paste the following content into it:
[Definition]
failregex = ^<HOST>.*POST /api/auth HTTP/2.0" 422.*."https://portainer.example.com/"
Be sure to replace https://portainer.example.com
with the address of your Portainer instance.
Next, go to /etc/fail2ban/jail.d
and create portainer.local
file with such content:
[portainer]
backend = auto
enabled = true
port = 80,443
protocol = tcp
filter = portainer
action = cloudflare
maxretry = 3
bantime = 1800
findtime = 1800
logpath = /var/log/nginx/access.log
If you are exposing Portainer to a port other than the default, set your port there. Configure maxretry
, bantime
and findtime
as you want. Make sure that the path given in logpath
leads to the access.log
file with the Portainer webserver logs.
As you may have noticed, there is a information about the cloudflare
action in the above file. Now we need to set up it.
You have to block requests using Cloudflare instead of iptables, as only Nginx can see the real IP of users - iptables cannot.
Go to /etc/fail2ban/action.d
. Find cloudflare.conf
file there. If for some reason there is no such file, get it from here.
#
# Author: Mike Rushton
#
# IMPORTANT
#
# Please set jail.local's permission to 640 because it contains your CF API key.
#
# This action depends on curl (and optionally jq).
# Referenced from http://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE
#
# To get your CloudFlare API Key: https://www.cloudflare.com/a/account/my-account
#
# CloudFlare API error codes: https://www.cloudflare.com/docs/host-api.html#s4.2
[Definition]
# Option: actionstart
# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).
# Values: CMD
#
actionstart =
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop =
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v1
#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
-d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
<_cf_api_url>
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
# API v1
#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4
actionunban = id=$(curl -s -X GET <_cf_api_prms> \
"<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1¬es=Fail2Ban%%20<name>" \
| { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
_cf_api_url = https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules
_cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Content-Type: application/json'
[Init]
# If you like to use this action with mailing whois lines, you could use the composite action
# action_cf_mwl predefined in jail.conf, just define in your jail:
#
# action = %(action_cf_mwl)s
# # Your CF account e-mail
# cfemail =
# # Your CF API Key
# cfapikey =
cftoken = YOUR_CLOUDFLARE_API_KEY
cfuser = YOUR_CLOUDFLARE_EMAIL
Set up your Cloudflare API Key and Cloudflare Account Email.
After completing the configuration, restart Fail2ban using systemctl restart fail2ban
and be sure to check if the restart was successful with systemctl status fail2ban
. If something is wrong, the restart will not return this information and fail2ban will not work.
Then check the status of the newly created jail using fail2ban-client status portainer
. It should look something like this:
Status for the jail: portainer
|- Filter
| |- Currently failed: 3
| |- Total failed: 22
| `- File list: /var/log/nginx/access.log
`- Actions
|- Currently banned: 0
|- Total banned: 2
`- Banned IP list:
Conclusion
Fail2ban should now secure your Portainer instance and temporarily ban the address after several invalid login attempts. Sometimes you have to wait a few seconds before the cloudflare rule takes effect.