I have shared the last couple of posts of this website on Lemmy and despite the very little audience I have, I have noticed that since I (i.e., this blog) stopped being completely invisible and became only mostly invisible (fun fact: On Kagi.com this blog pops up on the first page when searching for a few things), I started getting a little more junk traffic than usual.
By junk traffic I mean the kind of stuff that start popping up the moment you get a public IP or you create a new DNS record: scanners, web-crawlers, bots, smartasses who launch their tools against everything because they now "use" Kali.
On the very homepage of this website I mention that this blog is a static site, I go even more in detail in this blog post about the technical setup. Even among static sites it is a minimal one: it has almost nothing at all, no Javascript and the whole website can run in a 11MB Docker image - of which 8.5MB are the web-server binary. The reason why I am mentioning this is because essentially this junk traffic doesn't represent a risk at all for this website. The only possibility to have this website compromised is essentially via a vulnerability in the web server, possible but unlikely.
Despite all this, your setup might be different, your attack surface larger and therefore all these things might matter for you. I have then decided to anyway implement some level of security for my website to create a guide (see a rant about security guides to keep me accountable) that can possibly be useful to non-security specialists who still run a public server and a few websites.
This post will then cover the following:
- Basic SSH hardening
- Web Application security via Crowdsec
This post will not cover the following:
- Obvious common practices: use unique and strong passwords, use 2FA whenever possible, keep your software updated, don't expose services that don't need to be exposed to the internet.
- How to install tools (I will instead link the documentation).
- "Use Cloudflare" and similar useless suggestions that completely outsource the problem.
- Linux hardening - this will be maybe a future post.
- Supply-chain security.
Introduction - The Attack Surface
Before designing our security strategy, we need to understand what we are actually trying to protect, and from who/what. Since I need to make some assumptions, I will be assuming that your setup is extremely simple (and kind-of-similar to mine): you have/rent a Linux box with a public IP, and you have a few websites running on top of it, which you want people to access globally (maybe a blog, your portfolio, etc.). I will also assume that you are not on the NSA watchlist and that no sophisticated attacker or state-sponsored group is after you. If that is the case, any suggestion in this blog is going to be useless and you should probably avoid the internet completely.
In cases where my assumptions hold, you should pretty much consider that your surface is the following:
- Any service that is exposed publicly can have vulnerabilities, independently from how you configure it. Each service is by default part of the attack surface.
- Not exposed services could still be exposed by a mistake in the configuration. Let's consider them as well.
- Each web application can have vulnerabilities that can affect both your server, your data and your users' security.
- Each interface where you can login (SSH, but also Wordpress admin console, your alternative CMS console, etc.) is subject to brute-force attacks, credential stuffing attacks (maybe your user/pass combo was leaked in the past?), and similar attacks where someone else is going to try to guess your credentials.
- Anything that you host publicly can be found. It doesn't matter if you keep
the DNS name secret (e.g., you use
$(RANDOM-STRING).mydomain.com
), or if you use secrets link such asmydomain.com/super-secret-directory
or things like that. The moment something is public, assume it will be found. - Technically, every software you run exposes you as well to supply-chain risk. Maybe the repository for that nice tool you use gets compromised and the next update is going to make you install malware. I won't cover this in this post, but for some tangential information, consider a hardened runtime environment, for example well configured containers.
Threat Actors and Threats
In terms of the who (threat actors) and what (threats) we are trying to protect ourselves from, I would say it is relatively simple:
- Opportunistic attackers: people who scan the internet automatically and might attack you simply because you appear to be vulnerable, or your credentials were leaked, or the tool you use happen to have a new vulnerability discovered. These represent the most significant part of the threat actors that most of us should care about.
- Unsophisticated attackers who might target you specifically. While rare, it's not impossible that someone might be pissed at you specifically for whatever reason. I am going to assume that the motivation and financial investment is small, so you won't have an APT (Advanced Persistent Threat) after you, but simply someone that in the moment would like to take specifically your website down to spite you or something like that. You know, like the dude you diffed in League on Legends and who discovered your website and starts searching "how to deface website tutorial".
Given the actors we are dealing with, the kind of attacks are pretty much:
- Automated scans of your applications looking for vulnerabilities.
- Automated brute-forcing/probing/attempts against your authentication interfaces (i.e., logins).
- Random attempts to guess sensitive files and directories to discover hidden content or to gather more information about your versions, applications, etc..
- Exploits for known vulnerabilities of the software you run (whether you run vulnerable versions or not).
- Random tests in your web applications for generic vulnerabilities (e.g., Cross-site Scripting, (NO)SQL Injections, Deserialization, Command Injections etc.).
As we can see, the threats are not sophisticated or particularly dangerous. However, the risk is not 0 and there is no reason for not applying equally basic security measures that can substantially reduce the risk of compromise (or at least of something breaking).
(In retrospective, I see that the above is very close to a threat model. Maybe I will make a followup post describing how to build simple threat models. I don't know, but if you are interested let me know, that might motivate me.)
Security Strategy - The Plan
Now that we have a clearer idea of the situation, we can look at a basic plan to follow.
- First of all, the SSH service will be protected. I will discuss a few options on how to do it.
- Then, basic brute-force/scanning protections will be applied to both SSH and your web applications.
- Finally, basic mitigations for simple web application attacks are going to be implemented.
The last two steps of the plan are going to be done using Crowdsec, everything else is going to be done using simple configuration changes.
SSH Hardening
The SSH service is generally a secure service to expose: it is meant to be exposed publicly and its attack surface is fairly limited. However, vulnerabilities still happen, and in fact less than a month ago a serious one was discovered.
This already places you in front of a choice: expose SSH directly or instead expose a VPN service and then expose SSH only through the VPN.
VPN services like Wireguard historically have had way less vulnerabilities discovered, their attack surface is smaller and can be considered more secure to expose than SSH (it also means you are now using an additional authentication - the VPN credentials). However, this also means that you will only be able to access your server if you can establish the VPN connection, plus you need to maintain another service (a very low-maintenance one in this case).
Whatever decision you will take, consider the VPN connection an extra layer of security, not a replacement for any of the hardening measures we are going to discuss now.
Configuration Hardening
All the changes discussed here should be done to /etc/ssh/sshd_config
file.
- Disable password authentication. You should use keys to login into your
server. Passwords can be guessed and need to be also typed all the time. Keys
are more convenient and more secure. For extra security consider protecting
your private key with a password (
ssh-keygen
will ask you one) →PasswordAuthentication no
,PubkeyAuthentication yes
. - Disable authentication modes that you don't use. This helps more with reducing
the SSH attack surface in terms of components that can have vulnerabilities
than with anything else. →
KerberosAuthentication no
,GSSAPIAuthentication no
,ChallengeResponseAuthentication
. - Disable X11 Forwarding. Your server in 99% of the case won't support this, and
it is unnecessary attack surface. →
X11Forwarding no
. This option is needed if you want to use some graphical applications via SSH. Weird, but if that's your thing, skip this step. - Disable
root
login. Use a low-privileged user withsudo
access (which requires a password). The root user is not really needed and it's just unnecessarily risky to use it for remote connections, especially if you happen to use an image for your server which happen to have a defaultroot
password and you forget to change it. →PermitRootLogin no
.
There are more settings, and if you feel like it, you can simply follow a best-practice guide, since this is nothing creative and it's mostly about following the book. For example:
- One example (and a million more that you can easily find).
- CIS Benchmarks (free - but you will need registration) contain whole sections about SSH hardening (and much more stuff).
Some people also swear by other measures, like changing the SSH port to
something else. Most people end up using 2222
to easily remember. This is
borderline useless, as you can see for yourself.
We are going to implement anyway brute-force protection later, so in my opinion
this is not worth the extra hassle of choosing and remembering a new port and
configuring every client accordingly.
There are also many more options related to cryptographic settings. While these are important in general, none of the attackers in our current threat model is ever going to exploit weak Diffie-Hellmann or things like this. It doesn't matter relative to our threat actors.
If you want to copy paste, these are all the options above in a single block:
PasswordAuthentication no
PubkeyAuthentication yes
KerberosAuthentication no
GSSAPIAuthentication no
ChallengeResponseAuthentication no
X11Forwarding no
PeermitRootLogin no
Slap these in your /etc/ssh/sshd_config
file and restart the service. Your SSH
service is now protected against 90% of whatever you will ever face.
Remember that you can check if your configuration is valid with sshd -t
.
Protecting from Dumb Attackers with Crowdsec
Until recently I used fail2ban to do most of the job that I will be doing with Crowdsec, so I want to start from why using one or the other.
From my point of view, the pros/cons are the following:
fail2ban
has been around forever, chances of bugs are smaller.crowdsec
is written in Go,fail2ban
in Python, which means the latter is generally going to be slower.crowdsec
has a distributed aspect and uses "bad IPs" from other users to automatically populate your blocklist. It also means that bad IPs that attack you are shared with others (it's both a pro or a cons depending on your preference).crowdsec
has a nice modular and modern architecture that makes it easier and more flexible to work with. It also has nice perks like an API and metrics exposed.fail2ban
is fully FOSS (GPL license), Crowdsec has a company behind with a paid product, but the product is Open Source with MIT license.
Given the above, I decided to go with Crowdsec, so that's what I am going to cover here.
Crowdsec Basics - Architecture
Do you remember when I mentioned Crowdsec having a more modular architecture? Well, this also means it has a slightly steeper learning curve. Thankfully it has also excellent documentation, although not too many end-to-end examples (which is partially the reason why I decided to write all of this).
Crowdsec is composed by a bunch of pieces, which I will try to recap here in the way I understood them:
- The main binary does two main things: it processes sources (log files) and
implements a local API that exposes some of Crowdsec features (e.g., metrics,
banned IPs, etc.)
- This also uses configuration files for parsers, acquisitions and scenarios. Parsers are what interprets a specific log type. Acquisitions are ways to describe file paths or streams to use as sources and how to categorize them. Scenarios are the attacks that we want to block. There are tons of pre-made artifacts for all of these.
- A CLI tool (
cscli
) can be used to interact (via the local API) with crowdsec and manually ban/unban IPs, showing metrics and much more. It can also be used to install additional "pieces", such as collections, parsers and bouncers. - The components that actually do things (as in "add the IP to firewall" or "block this IP") are called "bouncers" or "remediation components" and are separate pieces of software with their own configuration, which need a separate installation.
- Finally, there is the CAPI (Central API), which is the Crowdsec hosted service where bad IPs are pushed (i.e., shared) and from where crowdsourced bad IPs are taken. I hope that the crowdsource nature of the tool forces the company to keep the tool Open Source as it is now, as their business model benefits from all the people gathering threat intelligence in exchange for good software. The data shared upstream is minimal: just the IP(s) to ban and the scenario it triggered.
Following the installation
instructions, most of the Crowdsec configuration
lives in /etc/crowdsec
. Here we can find:
tree -L 1
.
├── acquis.d
├── acquis.yaml
├── bouncers
├── collections
├── config.yaml
├── console
├── console.yaml
├── contexts
├── hub
├── local_api_credentials.yaml
├── notifications
├── online_api_credentials.yaml
├── parsers
├── patterns
├── profiles.yaml
├── scenarios
└── simulation.yaml
Note that there is an installation wizard that might create some of the stuff for you, based on your answers.
Looking at the above:
acquis.d
→ Directory where additional acquisition configurations can be stored, to keep them in order.acquis.yaml
→ Main acquisition configuration.bouncers
→ Directory where the configuration files for all the bouncers (also called remediation components) are stored.config.yaml
→ Main crowdsec configuration file.console
/console.yaml
→ Crowdsec also hosts a service (called in fact "console"), that can be used to register your installation and see insights for your deployment (like blocked IPs etc.). These are related to this service, I suppose.contexts
→ In this directory there are configurations related to this feature. The idea is that for each type of alert some additional fields (a context) can be added to provide more details (for example, a username for the SSH authentication failures).hub
→ In this directory quite some data is stored, which is coming from the hub. The hub is basically a community library of pre-made parsers, scenarios, bouncers or groups of all of these (called "collections").local_api_credentials.yaml
→ I have already initialized a bouncer at this time, which automatically created local API credentials. This file contains those credentials. Bouncers use these credentials to query the local crowdsec API for IPs to ban and IPs to unban (because their timeout expired).notifications
→ This directory contains the configuration file for the notification endpoints supported by crowdsec. If you want to send notification to email/Slack etc. when a new action is triggered, here is where templates and configurations are stored.online_api_credentials.yaml
→ These are credentials for the Central API, and were automatically created by the CLI tool to submit signals for malicious IPs that attacked my server.parsers
→ This directory contains the configuration for all the installed parsers. The configuration uses a Grok format and allows to rename and modify fields. Note that parsing happens in stages, and you will find in this directory three folders (raw, parse, enrich) for the configuration of parsers that work in the respective stages. This is all explained in the documentation.patterns
→ This directory stores raw patterns that can be used elsewhere (presumably, in the parsers).profiles.yaml
→ This configuration allows to define some default parameters for actions to take when some rule triggers (for example, the ban duration).scenarios
→ This is one of the most important paths, as it contains the list of "scenarios" i.e., the rules that are used to determine when an alert is actually triggered (and therefore a response action is invoked). Lots of scenarios exist in the hub, or it's very easy to write your own.simulation.yaml
→ This configuration can be used to test scenarios via the CLI in "dry run" mode (i.e., no action is actually taken).
Crowdsec in Action - Traefik
In order to better elaborate all the different moving parts, I will run through the scenario that I had to implement myself: using crowdsec for web related attacks and enumeration attempts when traefik is used as reverse proxy.
This scenario is going to be very similar to what most of people would have, and
the situation doesn't change much compared to -say- if you are using nginx
or
caddy
as reverse proxy.
By default there are no rules related to Traefik, which means when you run the configuration wizard Traefik won't be presented as an option. However, there is a "collection" for Traefik which is officially maintained.
In general, considering what we have discussed above, we will need:
- A parser that is able to extract the proper fields from a Traefik log file.
- An acquisition configuration that tells crowdsec to follow the needed log file.
- A bunch of scenarios that are relevant. Note that the scenarios are identical whatever reverse proxy or server you are running, as they are HTTP-related.
The collection above in fact contains exactly what's needed:
- There is a parser that you will find in the
parsers/s01-parse
directory. - It automatically includes the
base-http-scenarios
. This is yet another collection which in turn includes another parser to extract from HTTP logs even more detailed fields (like file extension) and a bunch of scenarios.
The acquisition can be configured manually. In my case I have added a file in
the acquis.d
directory:
cat acquis.d/traefik.yaml
filenames:
- /$(PATH_TO_TRAEFIK)/traefik/logs/traefik.log
labels:
type: traefik
Curating Scenarios
If you opt for using the collection to install everything related to Traefik
(instead of installing the parser separately and then manually install
scenarios), you will end up with a number of scenario configurations in the
scenarios
directory.
Each scenario is pretty self-explained, but I will take one as a model so that you can decide to create additional ones that might fit your use-case.
# http-wordpress-scan.yaml
# Doc at https://doc.crowdsec.net/docs/scenarios/format
# Use a leaky bucket for the counter in this scenario. Other
# options are simple counters, direct triggers and more (see doc).
type: leaky
name: crowdsecurity/http-wordpress-scan
description: "Detect WordPress scan: vuln hunting"
filter: |
# Only apply to logs with the meta.service tag set to http
evt.Meta.service == 'http' and
# ... and the log_type set to http_access_log or http_error_log
evt.Meta.log_type in ['http_access-log', 'http_error-log'] and
# ... and where the status of the request (extracted by the parser) is 403 or 404
evt.Meta.http_status in ['404', '403'] and
# ... and the path for the URL contains the string "/wp-"
Lower(evt.Meta.http_path) contains "/wp-" and
# ... and ends for ".php"
Lower(evt.Meta.http_path) endsWith ".php"
# Events are grouped by source IP (i.e., the bucket is partitioned per source IP)
groupby: evt.Meta.source_ip
# Deduplicate event in a bucket (partition) based on the path.
# 10 log lines for the same "wp-login.php" URL and from the same IP will only
# enter once in the bucket
distinct: evt.Meta.http_path
# Bucket capacity. After this the bucket is overflown and the alert triggers
capacity: 3
# The timer after which an event in the bucket is dropped.
leakspeed: "10s"
# Silence a bucket triggering for this amount of time. This is used to reduce
# spam from triggered bucket (e.g., the same IP overflowing the bucket 10 times
# in a few seconds). The IP should be anyway banned, so there is no value in
# re-triggering the alert.
blackhole: 5m
# Add context to alerts
labels:
# Indicate that the offending IP should be banned
remediation: true
# ATT&CK classification
classification:
- attack.T1595
# A specific behavior. Part of a fixed taxonomy
# See https://github.com/crowdsecurity/hub/blob/scenario_taxonomy/taxonomy/behaviors.json
behavior: "http:scan"
label: "WordPress Vuln Hunting"
# 0→3. 0 means the attacker could not be spoofing the attack, 3 means spoofable.
spoofable: 0
service: wordpress
# 0→3. Confidence this scenario does not trigger false positives.
# 0 means false-positive prone, 3 means high confidence in the alert
confidence: 3
The configuration format for the scenarios allows also to fetch lists remotely, for example:
# [...]
any(File("trendy_cves.txt"), { Lower(evt.Meta.http_path) contains #})
# [...]
data:
- source_url: https://hub-data.crowdsec.net/web/trendy_cves.txt
dest_file: trendy_cves.txt
type: string
In this case a file hosted by crowdsec is used to source paths that are indicative of recent CVEs being exploited.
Selecting Scenarios
For the use-case considered in this post, I would say that the basic scenarios that are included in the base-http-scenarios collection are already sufficient. Obviously you might be running a specific application that might have its own set of valid scenarios. In the hub there are quite a lot of community-provided configuration for a number of services. If there are not, you might want to create your own.
Considering the threats we have identified above, it's important to catch:
- Attackers looking for potential hidden files or performing general
enumeration. These will generate naturally a high amount of
404
errors, which means thehttp-probing
will cover most of these cases. This scenario triggers for400
,403
and404
errors over a short period of time. - Attackers attempting to use CVE exploits. These might generate very different
traffic, but most likely will use known exploit vectors. The
http-cve-probing
attempts to track recent CVEs, but is not going to be infallible. Unfortunately there is no silver-bullet against recent exploits. - Attackers attempting to find generic web application vulnerabilities. There are a number of scenarios for path traversal, XSS, SQL injections etc. This is far from a full WAF (Web Application Firewall), but it will definitely catch low-complexity attacks (and the most common ones).
- Attackers trying to force their way into your administrative page (e.g.,
Wordpress admin console). The
http-admin-interface-probing
will trigger for failed logins to some of the most common administrative interfaces. If you are using a tool that has a login interface that is not listed, you should definitely create a dedicated scenario for your particular case.
As a benefit, there are also a few more rules that block known bad User-agents. Most of these rules are mostly going to reduce noise by blocking IPs that misbehave. Keep in mind that these rules are public, so feel free to change the time parameters to make it harder for attackers to rate limit their traffic right under the rule threshold. Also remember that this is not a fully fledged WAF, although crowdsec does have also some WAF capabilities for virtual patching (i.e., building a rule that blocks exploits rather than fixing the vulnerability) and uses ModSecurity rules (standard in the industry, but awful to work with, sorry!).
What about SSH?
We discussed a lot about SSH previously, so what about SSH brute-force and attacks?
Well, this is quite easy to do with crowdsec because everything is pre-made, so
it's boring to discuss.
However, it's important to have the ssh-bf
, ssh-slow-bf
and since it's
recent, also the ssh-cve-2024-6387
scenarios (regarding the last, you should
update OpenSSH rather than counting on this rule to protect you).
All of this is done automatically if you run the configuration wizard and select SSH as one of the possible targets. Alternatively, you can just install this collection.
Bouncing IPs
The (almost) final piece of the puzzle requires configuring the type of actions that you want to have taken in response to any of your scenarios triggering.
In my case, I chose the firewall bouncer, which simply adds the offending IPs (remember, the one you encounter and all the ones gathered by the rest of crowdsec users!).
Other options including Cloudflare Firewall, or Haproxy ACLs etc. I am probably going to have some fun writing a custom bouncer that adds IPs to Hetzner firewalls, even if I absolutely do not need it.
Installing the pre-made components is quite straightforward, but observing it in
action is quite interesting! For example, using the iptables
one, crowdsec
curates an ipset, where IPs are continually
added and removed. Note that these IPs are blocked fully!
pkts bytes target prot opt in out source destination
277 16352 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set crowdsec-blacklists src
This means that if you by mistake block your IP to test a rule you will also get kicked out of an established SSH session (ask me how I know!). You can use the CLI to unban yourself if you don't want to wait (it needs a console, for example through your provider website).
To inspect the current banned IPs you can check the current "decisions":
cscli decision list
╭────────┬──────────┬───────────────────┬───────────────────────────────────┬────────┬─────────┬───────────────────────┬────────┬────────────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │ AS │ Events │ expiration │ Alert ID │
├────────┼──────────┼───────────────────┼───────────────────────────────────┼────────┼─────────┼───────────────────────┼────────┼────────────────────┼──────────┤
│ 555412 │ crowdsec │ Ip:206.168.34.218 │ crowdsecurity/http-bad-user-agent │ ban │ US │ 398324 CENSYS-ARIN-01 │ 2 │ 3h49m22.407970114s │ 449 │
│ 540411 │ crowdsec │ Ip:107.173.200.14 │ crowdsecurity/http-bad-user-agent │ ban │ US │ 36352 AS-COLOCROSSING │ 2 │ 3h26m27.50365056s │ 447 │
╰────────┴──────────┴───────────────────┴───────────────────────────────────┴────────┴─────────┴───────────────────────┴────────┴────────────────────┴──────────╯
These are the ones that triggered from my own servers. However, this is not the whole list of banned IPs, remember that there are the crowdsourced ones!
sudo ipset list crowdsec-blacklists | awk '{print $1}' | tail +9 | sort | uniq | wc -l
27215
We can also check using the cscli decisions list -a
the full list.
Keeping an Eye on Crowdsec
You can use cscli metrics
to look at how rules and parsers are performing.
These metrics are also exposed, see the main config.yaml
:
prometheus:
enabled: true
level: full
listen_addr: 127.0.0.1
listen_port: 6060
It's also generally important to monitor how the rules are performing, as it is to add more scenarios over time, for example if a serious vulnerability is discovered for a specific technology you use.
Conclusion
This post ended up being a bit more generic than I expected, but that's the result of taking into consideration the basic scenarios that are relevant for most of us, and also of the fact that crowdsec already covers many scenarios automatically, so there is no much that we need to handcraft unless we need to cover more corner cases.
I hope that it was still a useful introduction to the tool and to the general approach I recommend for implementing the bare minimum level of security for your websites.
If you find an error, want to propose a correction, or you simply have any kind of comment and observation, feel free to reach out via email or via Mastodon. If you have any suggestion about other topics to cover, or maybe you would like me to elaborate better something I have already written about, let me know!