Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JAMES-3897 CrowdSec support for POP3 #1936

Merged
merged 6 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
package org.apache.james.modules.protocols;

import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.function.Predicate;

import javax.inject.Inject;

import org.apache.james.pop3server.netty.POP3ServerFactory;
import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
import org.apache.james.utils.GuiceProbe;


Expand All @@ -42,4 +45,12 @@ public int getPop3Port() {
.map(InetSocketAddress::getPort)
.orElseThrow(() -> new IllegalStateException("POP3 server not defined"));
}

public Optional<Integer> getPort(Predicate<? super AbstractConfigurableAsyncServer> filter) {
return pop3ServerFactory.getServers().stream()
.filter(filter)
.findFirst()
.flatMap(server -> server.getListenAddresses().stream().findFirst())
.map(InetSocketAddress::getPort);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private Mailbox auth(POP3Session session, String password) throws IOException {
} catch (BadCredentialsException e) {
LOGGER.info("Bad credential supplied for {} with remote address {}",
session.getUsername().asString(),
session.getRemoteAddress().getAddress());
session.getRemoteAddress().getAddress().getHostAddress());
return null;
} catch (MailboxException e) {
throw new IOException("Unable to access mailbox for user " + session.getUsername().asString(), e);
Expand Down
34 changes: 29 additions & 5 deletions third-party/crowdsec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,50 @@ This module is for developing and delivering extensions to James for the [Crowds

- The Crowdsec extension requires an extra configuration file `crowdsec.properties` to configure Crowdsec connection
Configuration parameters:
- `crowdsecUrl` : URL defining the Crowdsec's bouncer. Eg: http://crowdsec:8080/v1
- `apiKey` : Api key for pass authentication when request to Crowdsec's bouncer.
- `crowdsecUrl` : String. Required. URL defining the Crowdsec's bouncer. Eg: http://crowdsec:8080/v1
- `apiKey` : String. Required. Api key for pass authentication when request to Crowdsec.
- `timeout` : Duration. Optional. Default to `500ms`. Timeout questioning to CrowdSec.
E.g. `500ms`, `1 second`,...

- Declare the `extensions.properties` for this module.

```
guice.extension.module=org.apache.james.module.CrowdsecModule
guice.extension.module=org.apache.james.crowdsec.module.CrowdsecModule
```

### CrowdSec support for SMTP
- Declare the Crowdsec EhloHook in `smtpserver.xml`. Eg:

```
<handlerchain>
<handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/>
<handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.CrowdsecEhloHook"/>
<handler class="org.apache.james.crowdsec.CrowdsecEhloHook"/>
</handlerchain>
```

### CrowdSec support for IMAP
- Declare the `CrowdsecImapConnectionCheck` in `imapserver.xml`. Eg:

```
<imapserver enabled="true">
...
<additionalConnectionChecks>org.apache.james.crowdsec.CrowdsecImapConnectionCheck</additionalConnectionChecks>
</imapserver>
```

### CrowdSec support for POP3
- Declare the `CrowdsecPOP3CheckHandler` in `pop3server.xml`. Eg:
-
```
<pop3server enabled="true">
<handlerchain>
<handler class="org.apache.james.pop3server.core.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.crowdsec.CrowdsecPOP3CheckHandler"/>
</handlerchain>
</pop3server>
```

- Docker compose file example: [docker-compose.yml](docker-compose.yml).
- The sample-configuration: [sample-configuration](sample-configuration)
- For running docker-compose, first compile this project
Expand All @@ -47,7 +72,6 @@ curl -XGET http://localhost:8080/v1/decisions -H "X-Api-Key: default_api_key" -H
Response codes:
- 200: Success
- 403: Invalid apikey. Try with a different value for apikey.
- 404: Crowdsec's url is not found. Maybe there is no bouncer in Crowdsec, check it and create a new one.

Responses:
- It will be null if there is no decision.
Expand Down
1 change: 1 addition & 0 deletions third-party/crowdsec/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- ./sample-configuration/smtpserver.xml:/root/conf/smtpserver.xml
- ./sample-configuration/crowdsec.properties:/root/conf/crowdsec.properties
- ./sample-configuration/imapserver.xml:/root/conf/imapserver.xml
- ./sample-configuration/pop3server.xml:/root/conf/pop3server.xml
networks:
- james
ports:
Expand Down
5 changes: 5 additions & 0 deletions third-party/crowdsec/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
<artifactId>protocols-imap</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${james.protocols.groupId}</groupId>
<artifactId>protocols-pop3</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${james.protocols.groupId}</groupId>
<artifactId>protocols-smtp</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
guice.extension.module=org.apache.james.module.CrowdsecModule
guice.extension.module=org.apache.james.crowdsec.module.CrowdsecModule
2 changes: 1 addition & 1 deletion third-party/crowdsec/sample-configuration/imapserver.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ under the License.
<customProperties>pong.response=customImapParameter</customProperties>
<customProperties>prop.b=anotherValue</customProperties>
<gracefulShutdown>false</gracefulShutdown>
<additionalConnectionChecks>org.apache.james.CrowdsecImapConnectionCheck</additionalConnectionChecks>
<additionalConnectionChecks>org.apache.james.crowdsec.CrowdsecImapConnectionCheck</additionalConnectionChecks>
</imapserver>
</imapservers>
12 changes: 10 additions & 2 deletions third-party/crowdsec/sample-configuration/parsers/james-auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pattern_syntax:
IMAP_AUTH_FAIL_BAD_CREDENTIALS: 'IMAP Authentication failed%{DATA:data}because of bad credentials.'
IMAP_AUTH_FAIL_DELEGATION_BAD_CREDENTIALS: 'IMAP Authentication with delegation failed%{DATA:data}because of bad credentials.'
IMAP_AUTH_FAIL_NO_EXISTING_DELEGATION: 'IMAP Authentication with delegation failed%{DATA:data}because of non existing delegation.'

SMTP_AUTH_FAIL: 'SMTP Authentication%{DATA:data}failed.'
POP3_AUTH_FAIL: 'Bad credential supplied for %{DATA:user} with remote address %{IP:source_ip}'
nodes:
- grok:
name: "IMAP_AUTH_FAIL_BAD_CREDENTIALS"
Expand Down Expand Up @@ -72,4 +72,12 @@ nodes:
- meta: source_ip
expression: evt.Parsed.mdc_remoteIP
- meta: user
expression: evt.Parsed.mdc_username
expression: evt.Parsed.mdc_username
- grok:
name: "POP3_AUTH_FAIL"
apply_on: message
statics:
- meta: log_type
value: pop3-auth-fail
- meta: source_ip
expression: evt.Parsed.source_ip
44 changes: 44 additions & 0 deletions third-party/crowdsec/sample-configuration/pop3server.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->


<pop3servers>
<pop3server enabled="true">
<jmxName>pop3server</jmxName>
<bind>0.0.0.0:110</bind>
<connectionBacklog>200</connectionBacklog>
<tls socketTLS="false" startTLS="false">
<!-- To create a new keystore execute:
keytool -genkey -alias james -keyalg RSA -keystore /path/to/james/conf/keystore
-->
<keystore>file://conf/keystore</keystore>
<secret>james72laBalle</secret>
<provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
</tls>
<connectiontimeout>1200</connectiontimeout>
<connectionLimit>0</connectionLimit>
<connectionLimitPerIP>0</connectionLimitPerIP>
<handlerchain>
<handler class="org.apache.james.pop3server.core.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.crowdsec.CrowdsecPOP3CheckHandler"/>
</handlerchain>
<gracefulShutdown>false</gracefulShutdown>
</pop3server>
</pop3servers>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ type: leaky
name: apache-james/bf-auth
debug: true
description: "Detect login james bruteforce"
filter: "evt.Meta.log_type == 'imap-auth-fail' || evt.Meta.log_type == 'smtp-auth-fail'"
filter: "evt.Meta.log_type == 'imap-auth-fail' || evt.Meta.log_type == 'smtp-auth-fail' || evt.Meta.log_type == 'pop3-auth-fail'"
leakspeed: "1m"
capacity: 5
groupby: evt.Meta.source_ip
Expand Down
6 changes: 3 additions & 3 deletions third-party/crowdsec/sample-configuration/smtpserver.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<handlerchain>
<handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/>
<handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.CrowdsecEhloHook"/>
<handler class="org.apache.james.crowdsec.CrowdsecEhloHook"/>
</handlerchain>
</smtpserver>
<smtpserver enabled="true">
Expand Down Expand Up @@ -106,7 +106,7 @@
<handlerchain>
<handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/>
<handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.CrowdsecEhloHook"/>
<handler class="org.apache.james.crowdsec.CrowdsecEhloHook"/>
</handlerchain>
</smtpserver>
<smtpserver enabled="true">
Expand Down Expand Up @@ -154,7 +154,7 @@
<handlerchain>
<handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/>
<handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
<handler class="org.apache.james.CrowdsecEhloHook"/>
<handler class="org.apache.james.crowdsec.CrowdsecEhloHook"/>
</handlerchain>
</smtpserver>
</smtpservers>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@
* under the License. *
****************************************************************/

package org.apache.james;
package org.apache.james.crowdsec;

import static org.apache.james.crowdsec.CrowdsecUtils.isBanned;

import java.util.List;

import javax.inject.Inject;

import org.apache.commons.net.util.SubnetUtils;
import org.apache.james.model.CrowdsecClientConfiguration;
import org.apache.james.model.CrowdsecDecision;
import org.apache.james.model.CrowdsecHttpClient;
import org.apache.james.crowdsec.client.CrowdsecClientConfiguration;
import org.apache.james.crowdsec.client.CrowdsecHttpClient;
import org.apache.james.crowdsec.model.CrowdsecDecision;
import org.apache.james.protocols.smtp.SMTPSession;
import org.apache.james.protocols.smtp.hook.HeloHook;
import org.apache.james.protocols.smtp.hook.HookResult;
Expand All @@ -46,23 +47,6 @@ public HookResult doHelo(SMTPSession session, String helo) {
.map(decisions -> apply(decisions, ip)).block();
}

private boolean isBanned(CrowdsecDecision decision, String ip) {
if (decision.getScope().equals("Ip") && ip.contains(decision.getValue())) {
return true;
}
if (decision.getScope().equals("Range") && belongToNetwork(decision.getValue(), ip)) {
return true;
}
return false;
}

private boolean belongToNetwork(String value, String ip) {
SubnetUtils subnetUtils = new SubnetUtils(value);
subnetUtils.setInclusiveHostCount(true);

return subnetUtils.getInfo().isInRange(ip);
}

private HookResult apply(List<CrowdsecDecision> decisions, String ip) {
return decisions.stream()
.filter(decision -> isBanned(decision, ip))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,21 @@
* under the License. *
****************************************************************/

package org.apache.james;
package org.apache.james.crowdsec;

import static org.apache.james.model.CrowdsecClientConfiguration.DEFAULT_TIMEOUT;
import static org.apache.james.crowdsec.CrowdsecUtils.isBanned;

import java.net.InetSocketAddress;
import java.util.concurrent.TimeoutException;

import javax.inject.Inject;

import org.apache.commons.net.util.SubnetUtils;
import org.apache.james.exception.CrowdsecException;
import org.apache.james.crowdsec.client.CrowdsecClientConfiguration;
import org.apache.james.crowdsec.client.CrowdsecHttpClient;
import org.apache.james.crowdsec.exception.CrowdsecException;
import org.apache.james.imap.api.ConnectionCheck;
import org.apache.james.model.CrowdsecClientConfiguration;
import org.apache.james.model.CrowdsecDecision;
import org.apache.james.model.CrowdsecHttpClient;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import reactor.core.publisher.Mono;

public class CrowdsecImapConnectionCheck implements ConnectionCheck {
private static final Logger LOGGER = LoggerFactory.getLogger(CrowdsecImapConnectionCheck.class);

private final CrowdsecHttpClient client;

@Inject
Expand All @@ -53,28 +44,7 @@ public Publisher<Void> validate(InetSocketAddress remoteAddress) {
String ip = remoteAddress.getAddress().getHostAddress();

return client.getCrowdsecDecisions()
.timeout(DEFAULT_TIMEOUT)
.onErrorResume(TimeoutException.class, e -> Mono.fromRunnable(() -> LOGGER.warn("Timeout while questioning to CrowdSec. May need to check the CrowdSec configuration.")))
.filter(decisions -> decisions.stream().anyMatch(decision -> isBanned(decision, ip)))
.handle((crowdsecDecisions, synchronousSink) -> synchronousSink.error(new CrowdsecException("Ip " + ip + " is not allowed to connect to IMAP server by Crowdsec")));
}

private boolean isBanned(CrowdsecDecision decision, String ip) {
if (decision.getScope().equals("Ip") && ip.contains(decision.getValue())) {
LOGGER.warn("Connection from IP {} has been blocked by CrowdSec for duration {}", ip, decision.getDuration());
return true;
}
if (decision.getScope().equals("Range") && belongToNetwork(decision.getValue(), ip)) {
LOGGER.warn("Connection from IP {} has been blocked by CrowdSec for duration {}", ip, decision.getDuration());
return true;
}
return false;
}

private boolean belongToNetwork(String value, String ip) {
SubnetUtils subnetUtils = new SubnetUtils(value);
subnetUtils.setInclusiveHostCount(true);

return subnetUtils.getInfo().isInRange(ip);
}
}
Loading