Although you didn't state your OS, I'm going to assume we're talking about Linux. As others mentioned, reading hardening guides is a first step. Those will tell you how to avoid the most common configuration footguns and reduce attack surface.
Obviously a good firewall (ufw suffices) is a must. Using a reverse proxy web server back to your web apps (I prefer nginx, but caddy is also another good one). In your reverse proxy web server, also setting up web application firewall config to flag suspicious things (for example, anyone going to a URL with `../` or `/etc/passwd` is clearly up to no good, as is a user-agent that's a known scraping tool). In particular, I like using the custom HTTP 444 response code with nginx, so I can instantly flag the worst offenders.
Then, use fail2ban to blacklist hosts that are up to no good. If you can create a regex for something in a logfile, you can automatically ban almost anything. However, fail2ban is a very powerful footgun, and if misconfigured you can easily ban yourself from your own server! However, fail2ban is probably the best hardening tool, since you greatly reduce the number of tries that someone has to exploit something, and severely slow them down.
Finally, regular monitoring and patching. I swear by check_mk for monitoring, so I can see every suspicious query that's coming through, and instantly identify most ongoing attacks. Fail2ban takes care of 99% of the work if it's configured correctly, but the worst attacks are the ones that you don't know are happening.
I've been self-hosting most of the webapps I depend on for almost 10 years now, and I can say that self-hosting is extremely fun, but does require a a decent time commitment to maintain your infrastructure. However, if you are willing to invest time in automation with some basic shell scripting, you can get this down to less than an hour per week, which is mainly just checking your monitoring console and scheduled jobs.
If this sounds too daunting but you still want to go this route, check out yunohost: https://yunohost.org/#/
It might be slightly easier to use sshguard instead of fail2ban for protecting against ssh attacks.
Using passwordless (key only) login is a given.
As soon as your server is provisioned, log in with the password, first setup ssh, and disable password login.
You can use fail2ban jails for different services (like nginx). You need to decide how strict you need to be.
On FreeBSD, using blacklistd might also be a better idea than using fail2ban.
To quote from the internet - "fail2ban and sshguard are both log scrapers. Log scrapers are gross hacks.
blacklistd as an integrated solution is what should have
happened many years ago."
FreeBSD jails also provide excellent protection. It can be a good idea to run each service in its own jail. E.g. a separate jail for nginx, one for your webserver, another for your db servers. This way you can also limit the resources that are allocated/dedicated to each jail.
Also, while running pf (or whatever firewall you have), you can limit the number of requests (rate limiting) to somewhat protect yourself.
Using Cloudflare or something else on the front can help against ddos. Also, some providers like OVH and Hetzner have ddos protection built in for free. Some like Vultr have it as a paid service, iirc.
SSHGuard looks cool, wasn't aware that existed. Haven't messed with FreeBSD jails, but I use unprivileged lxc containers on Linux, iirc that's the closest Linux equivalent? Those help me sleep better at night.
lxc was rather new when I started looking into this stuff. Since jails were much older, I just went with FreeBSD. Also because I was a little biased against Linux because on my personal computers Ubuntu and Fedora had crashed occasionally.
Now I think lxd is supposed to be a better user experience than lxc. Same backend better frontend. Like ezjail or iocage for FreeBSD makes the management easier than doing it all directly.
> Obviously a good firewall (ufw suffices) is a must
I keep hearing this, but I don't get why. I make sure my services just bind to localhost if they should not be public and I make sure to not run services I don't need. I portscan my server after everything is setup to make sure I didn't miss anything.
Does a firewall really buy me anything in that case?
It gives you a little bit more control over what happens when someone hits a closed port (do you want to drop or reject a packet?), this affects how different nmap scans interpret the port being open/closed/filtered. Also, it's a failsafe in case you don't configure something correctly and something is listening on more than just the loopack (localhost) network interface that you intended. Validating your config with portscanning is great.
Most importantly, you can rate limit and log.
For example, when I have a ufw default deny rule, I get these in my kern.log, and then I can apply a fail2ban rule if someone hits too many closed ports in a certain period (to detect someone portscanning):
[Definition]
# Option: failregex
# Notes: Looks for attempts on ports not open in your firewall. Expects the
# iptables logging utility to be used. Add the following to your iptables
# config, as the last item before you DROP or REJECT:
# -A <chain_name> -j LOG --log-prefix "PORT DENIED: " --log-level 5 --log-ip-options --log-tcp-options --log-tcp-sequence
# This will place a notice in /var/log/messages about any attempt on a port that isn't open.
failregex = \[UFW BLOCK\] .\* SRC=<HOST>
ignoreregex =
Then if you want to rate limit something (to help prevent DOS), you can do an iptables rule like this (not sure what the equivalent ufw config is):
-A INPUT -m limit --limit 1/sec -j LOG --log-prefix "INPUT:REJECT:"
Is there some point to blocking portscans? If nothing is listening on the ports then there is little harm to just letting them scan, right?
What I'm getting at is basically, if I have good rate limiting for expensive API's (which I need anyway), and I configure all my services the right way (which I want to do anyway since some software like docker likes to punch through ufw), and I don't care about somebody portscanning a bunch of ports that aren't open anyway is a firewall still a must in your opinion?
I would say so, anything you can to prevent an attacker from gaining useful information is a good deterrent (just like the comment that mentioned hiding version numbers from being displayed). The more deterrents you have, the more you frustrate an attacker and dissuade them from wanting to invest time in exploiting you. So to figure out what open ports you have, if you're using something to block/limit port scanning, it means you're exponentially increasing the time to figure out what ports are open, and possibly forcing them to rotate IP addresses that get banned.
I also don't like wasting CPU or bandwidth, however minuscule, on illegitimate traffic. Unless they are collecting data for a research project or you've hired someone to pentest you, someone port scanning you almost always indicates malicious intent.
If it shows up as blocked in a portscan the attacker will just try again from a different IP, right? think of it as a 403 vs a 404 in HTTP. A 404 says "there is actually nothing here" where as a "403" says "you didn't get to know if there is anything here". I'd rather just tell them that my open ports are 22, 80 and 443. Have at it, since it's cheaper for both of us to let whoever know immediately, and everyone who wants to know will know.
Also, on the CPU/bandwidth part, I don't think sending traffic to a non-listened port is any more expensive than sending it to a firewall. Both are stopped at the kernel, no?
Is there any point to block traffic to a non-listening port any more than is done by just not listening to the port?
Changing IPs is more expensive and requires more effort than being able to scan all ports from one IP, so yes they can certainly do that if they are determined, but the more expensive you make it for them to do that (resources, time, energy), the better a position you are in. Attackers can try anything, but it's defeatist to just let them instead of creating an obstacle.
You have a point that showing you're hiding something could potentially pique someone's interest, but also showing that you have few defenses and obstacles also makes you more appealing to an attacker since it makes you more worth their time. I don't think HTTP status codes are a good analogy, since they are more informative than a port being open/closed. If a port scan shows a port as closed, you don't have any information about whether a service is actually running there, just that you can't access it on a given IP address. If a scan shows filtered for a port then you can infer that it's there, but only accepts requests from a different IP. Also, running SSH on port 22 without any rate limiting is pretty much asking for brute forcing and makes you appealing as a target, even if they have no chance of succeeding it's still wasting resources. I like putting that on a high numbered port if possible, so someone has to port scan (with limited attempts) to even find it.
Depending on how aggressive and thorough you get with nmap (using the -T 5 option, OS/service version detection, and scripting that can actually run active exploits versus just reconnaissance), you can absolutely use up a good amount of CPU/bandwidth. This is a frequent problem for us at work when we are getting pentested, it can be very noisy and disruptive.
Though that resource usage comes more from the volume of traffic versus how it's rejected. Reduce the volume of ports they can try with one IP or in a given time frame, and reduce the resources you spend defending.
However, my understanding is that a firewall would help prevent nmap from doing complete TCP handshake simulations, depending on what type of nmap scan is being done and how the firewall is blocking traffic. So yes, each individual request is almost nothing if that's happening in kernel, but that could easily add up if someone's scanning all 65535 ports and doing an invasive scan. And I haven't even talked about scanning for UDP services, which take more energy on both sides since you don't have a TCP handshake to help you infer information easily.
Obviously a good firewall (ufw suffices) is a must. Using a reverse proxy web server back to your web apps (I prefer nginx, but caddy is also another good one). In your reverse proxy web server, also setting up web application firewall config to flag suspicious things (for example, anyone going to a URL with `../` or `/etc/passwd` is clearly up to no good, as is a user-agent that's a known scraping tool). In particular, I like using the custom HTTP 444 response code with nginx, so I can instantly flag the worst offenders.
Then, use fail2ban to blacklist hosts that are up to no good. If you can create a regex for something in a logfile, you can automatically ban almost anything. However, fail2ban is a very powerful footgun, and if misconfigured you can easily ban yourself from your own server! However, fail2ban is probably the best hardening tool, since you greatly reduce the number of tries that someone has to exploit something, and severely slow them down.
Finally, regular monitoring and patching. I swear by check_mk for monitoring, so I can see every suspicious query that's coming through, and instantly identify most ongoing attacks. Fail2ban takes care of 99% of the work if it's configured correctly, but the worst attacks are the ones that you don't know are happening.
I've been self-hosting most of the webapps I depend on for almost 10 years now, and I can say that self-hosting is extremely fun, but does require a a decent time commitment to maintain your infrastructure. However, if you are willing to invest time in automation with some basic shell scripting, you can get this down to less than an hour per week, which is mainly just checking your monitoring console and scheduled jobs.
If this sounds too daunting but you still want to go this route, check out yunohost: https://yunohost.org/#/