Check the first part on how to prepare and deploy an ASP.NET Core application on Ubuntu with Nginx and MySQL. This is the second part of the guide.
The current setup is an ASP.NET Core app already hosted and running on Ubuntu with nginx. This article focuses on the security side.
Block requests by IP
To stop nginx from serving requests on bare IP (no hostname):
sudo nano /etc/nginx/sites-available/default
Add the following as the first server directive:
server {
listen 80;
listen 443;
server_name "";
return 444;
}
Check for syntax errors: sudo nginx -t, then reload: sudo systemctl restart nginx
Firewall
ufw is the default firewall configuration tool for Ubuntu.
Allow HTTP and the SSH port you configured in part 1 (example: 4743):
sudo ufw allow 4743
sudo ufw allow http
Enable ufw: sudo ufw enable
Check status: sudo ufw status verbose
Fail2ban
Fail2ban monitors server logs for failed authentication attempts and suspicious activity, and can take automated action based on configured rules.
If not already installed: sudo apt-get install fail2ban
Navigate to /etc/fail2ban. The two relevant files:
jail.conf— base config, may be overwritten on updatesjail.local— your actual configuration, always reads first
Open jail.local:
sudo nano /etc/fail2ban/jail.local
Key options explained:
ignoreip— IP addresses to never ban (e.g., your own IP)bantime— how long in seconds to ban an IP (default: 600)maxretry— failures allowed before a banfindtime— time window in whichmaxretryfailures trigger a ban
Here’s a working jail.local configuration:
[DEFAULT]
ignoreip = 127.0.0.1/8
bantime = 1800
findtime = 600
maxretry = 4
[ssh]
enabled = true
port = 4743
filter = sshd
logpath = /var/log/auth.log
[nginx-noproxy]
enabled = true
port = http,https
filter = nginx-noproxy
logpath = /var/log/nginx/access.log
maxretry = 2
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 2
action = cloudflare-firewall
[nginx-noscript]
enabled = true
port = http,https
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 2
action = cloudflare-firewall
[nginx-nohome]
enabled = true
port = http,https
filter = nginx-nohome
logpath = /var/log/nginx/access.log
maxretry = 2
[nginx-444]
enabled = true
port = http,https
filter = nginx-444
logpath = /var/log/nginx/access.log
maxretry = 2
Now create the filter definitions. Navigate to filter.d and create each file:
cd /etc/fail2ban/filter.d
nginx-noproxy.conf — blocks open proxy abuse:
[Definition]
failregex = ^<HOST> -.*GET http.*
ignoreregex =
nginx-badbots.conf — reuse the Apache badbots filter:
sudo cp apache-badbots.conf nginx-badbots.conf
nginx-noscript.conf — blocks script/exploit scanners:
[Definition]
failregex = ^<HOST> -.*GET.*(\.php|\.asp|\.exe|\.pl|\.cgi|\.scgi)
ignoreregex =
nginx-nohome.conf — blocks home-directory access attempts:
[Definition]
failregex = ^<HOST> -.*GET .*/~.*
ignoreregex =
nginx-444.conf — bans IPs that got a 444 (IP-based request blocked):
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD).*HTTP.*" 444
ignoreregex =
Restart fail2ban: sudo systemctl restart fail2ban
Check active jails: sudo fail2ban-client status
If you accidentally lock yourself out while testing: sudo fail2ban-client set <JAIL_NAME> unbanip <YOUR_IP>
Fail2ban and Cloudflare integration
Proxying through Cloudflare provides fast global caching and an easy way to automatically block offending IPs via API.
Get the real client IP from Cloudflare in nginx
Create a config file inside conf.d (nginx will pick it up automatically):
sudo nano /etc/nginx/conf.d/cloudflare-ips.conf
# From https://www.cloudflare.com/ips
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;
real_ip_header CF-Connecting-IP;
Test and restart nginx: sudo nginx -t && sudo systemctl restart nginx
Bash script for Cloudflare Firewall API
(Credits: Antoine Aflalo)
Install jq (JSON processor for bash):
sudo apt-get install jq
Create the script:
sudo nano /usr/local/sbin/cloudflare-firewall
#!/bin/bash
# Create/remove an IP ban on CloudFlare via API
#
# usage: cloudflare-firewall <cfuser> <cftoken> <cfzoneid> <add|remove> <ip> [note]
add() {
local IP="${1}"; shift
local NOTE="$@"
curl -g -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONEID}/firewall/access_rules/rules" \
-H "X-Auth-Email: $CF_USER" \
-H "X-Auth-Key: $CF_TOKEN" \
-H "Content-Type: application/json" \
--data @- << EOF
{"mode":"challenge","configuration":{"target":"ip","value":"$IP"},"notes":"$NOTE"}
EOF
}
remove() {
local IP="${1}"
local RULE_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${CF_ZONEID}/firewall/access_rules/rules?configuration_target=ip&configuration_value=${IP}" \
-H "X-Auth-Email: $CF_USER" \
-H "X-Auth-Key: $CF_TOKEN" \
-H "Content-Type: application/json" | jq ".result|.[]|.id")
RULE_ID="${RULE_ID%\"}"
RULE_ID="${RULE_ID#\"}"
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${CF_ZONEID}/firewall/access_rules/rules/${RULE_ID}" \
-H "X-Auth-Email: $CF_USER" \
-H "X-Auth-Key: $CF_TOKEN" \
-H "Content-Type: application/json" \
--data '{"cascade":"basic"}'
}
CF_USER="$1"; shift
CF_TOKEN="$1"; shift
CF_ZONEID="$1"; shift
HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(add|remove)$ ]]; then
"$HANDLER" "$@"
fi
Mark it executable:
sudo chmod +x /usr/local/sbin/cloudflare-firewall
Fail2ban custom Cloudflare action
Get your Cloudflare email, global API key, and Zone ID from the Cloudflare dashboard (website → Overview tab).
Create the action file:
sudo nano /etc/fail2ban/action.d/cloudflare-firewall.conf
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = /usr/local/sbin/cloudflare-firewall <cfuser> <cftoken> <cfzone> add <ip> "<name> after <failures> failures at <time>"
actionunban = /usr/local/sbin/cloudflare-firewall <cfuser> <cftoken> <cfzone> remove <ip>
[Init]
cftoken = YOUR_CLOUDFLARE_API_TOKEN_HERE
cfuser = YOUR_EMAIL_FOR_CLOUDFLARE_HERE
cfzone = YOUR_ZONE_ID_HERE
name = fail2ban
Restart fail2ban: sudo systemctl restart fail2ban
Check status — if every step completed correctly, nginx-badbots and nginx-noscript jails should now have the cloudflare-firewall action active.
“You either have been hacked, or you don’t know it yet.”
Security is a never-ending process. It usually depends on how much comfort and performance you’re willing to sacrifice for it. Follow best practices, keep your tools updated, and hire a security expert if the stakes are high.