This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit a444f47c9502466105cd0a88fe667ded5ab78e03 Author: Quan Tran <[email protected]> AuthorDate: Tue Jan 9 16:14:40 2024 +0700 JAMES-3897 CrowdSec integration test for SMTP --- third-party/crowdsec/pom.xml | 5 + .../java/org/apache/james/CrowdsecExtension.java | 65 +++++- .../org/apache/james/CrowdsecIntegrationTest.java | 220 +++++++++++++++++++++ ...rowdsecExtension.java => HAProxyExtension.java} | 49 ++--- .../src/test/resources/crowdsec/acquis.yaml | 5 + .../test/resources/crowdsec/collections/james.yaml | 7 + .../resources/crowdsec/parsers/james-auth.yaml | 75 +++++++ .../crowdsec/parsers/james-dictionary-attack.yaml | 25 +++ .../resources/crowdsec/parsers/syslog-logs.yaml | 75 +++++++ .../crowdsec/scenarios/james-bf-auth.yaml | 14 ++ .../scenarios/james-dictionary-attack.yaml | 14 ++ .../crowdsec/src/test/resources/imapserver.xml | 64 ++++++ .../crowdsec/src/test/resources/listeners.xml | 22 +++ .../crowdsec/src/test/resources/log/james.log | 0 .../crowdsec/src/test/resources/logback-test.xml | 59 ++++++ .../src/test/resources/mailetcontainer.xml | 155 +++++++++++++++ .../crowdsec/src/test/resources/pop3server.xml | 23 +++ .../crowdsec/src/test/resources/smtpserver.xml | 84 ++++++++ .../src/test/resources/usersrepository.xml | 28 +++ 19 files changed, 958 insertions(+), 31 deletions(-) diff --git a/third-party/crowdsec/pom.xml b/third-party/crowdsec/pom.xml index 3711c5d2c8..b552553773 100644 --- a/third-party/crowdsec/pom.xml +++ b/third-party/crowdsec/pom.xml @@ -42,6 +42,11 @@ <version>${project.version}</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-memory-app</artifactId> + <scope>test</scope> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>testing-base</artifactId> diff --git a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java index f683c5673e..08da9021c3 100644 --- a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java +++ b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java @@ -19,21 +19,32 @@ package org.apache.james; +import static org.apache.james.model.CrowdsecClientConfiguration.DEFAULT_API_KEY; + import java.io.IOException; +import java.net.URL; import java.time.Duration; import java.util.UUID; +import javax.inject.Singleton; + +import org.apache.james.model.CrowdsecClientConfiguration; +import org.apache.james.model.CrowdsecHttpClient; import org.apache.james.util.docker.RateLimiters; import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; +import org.testcontainers.utility.MountableFile; -import com.google.common.collect.ImmutableList; +import com.github.fge.lambdas.Throwing; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.Provides; public class CrowdsecExtension implements GuiceModuleTestExtension { public static final Duration STARTUP_TIMEOUT = Duration.ofMinutes(5); public static final int CROWDSEC_PORT = 8080; - public static final int EXPOSED_PORT = 8082; public static final String CROWDSEC_IMAGE = "crowdsecurity/crowdsec:v1.5.4"; private final GenericContainer<?> crowdsecContainer; @@ -43,26 +54,74 @@ public class CrowdsecExtension implements GuiceModuleTestExtension { .withCreateContainerCmdModifier(cmd -> cmd.withName("james-crowdsec-test-" + UUID.randomUUID())) .withExposedPorts(CROWDSEC_PORT) .withStartupTimeout(STARTUP_TIMEOUT) + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/acquis.yaml"), "/etc/crowdsec/") + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/parsers/syslog-logs.yaml"), "/etc/crowdsec/parsers/s00-raw/") + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/parsers/james-auth.yaml"), "/etc/crowdsec/parsers/s01-parse/") + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/scenarios/james-bf-auth.yaml"), "/etc/crowdsec/scenarios/") + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/scenarios/james-dictionary-attack.yaml"), "/etc/crowdsec/scenarios/") + .withCopyFileToContainer(MountableFile.forClasspathResource("crowdsec/collections/james.yaml"), "/etc/crowdsec/collections/") + .withFileSystemBind("src/test/resources/log", "/var/log", BindMode.READ_WRITE) .waitingFor(new HostPortWaitStrategy().withRateLimiter(RateLimiters.TWENTIES_PER_SECOND)); } @Override - public void beforeAll(ExtensionContext context) { + public void beforeAll(ExtensionContext context) throws Exception { if (!crowdsecContainer.isRunning()) { crowdsecContainer.start(); } + + setApiKey(); + } + + private void setApiKey() throws IOException, InterruptedException { + crowdsecContainer.execInContainer("cscli", "bouncer", "add", "bouncer", "-k", DEFAULT_API_KEY); } @Override public void afterEach(ExtensionContext extensionContext) throws IOException, InterruptedException { + resetCrowdSecBanDecisions(); + resetJamesLogFile(); + } + + private void resetCrowdSecBanDecisions() throws IOException, InterruptedException { crowdsecContainer.execInContainer("cscli", "decision", "delete", "--all"); } + private void resetJamesLogFile() throws IOException, InterruptedException { + crowdsecContainer.execInContainer("truncate", "-s", "0", "/var/log/james.log"); + } + @Override public void afterAll(ExtensionContext extensionContext) { } + @Override + public Module getModule() { + URL crowdSecUrl = getCrowdSecUrl(); + + return new AbstractModule() { + @Provides + @Singleton + public CrowdsecClientConfiguration crowdsecClientConfiguration() { + return new CrowdsecClientConfiguration(crowdSecUrl, DEFAULT_API_KEY); + } + + @Provides + @Singleton + public CrowdsecHttpClient crowdsecHttpClient(CrowdsecClientConfiguration crowdsecClientConfiguration) { + return new CrowdsecHttpClient(crowdsecClientConfiguration); + } + }; + } + + public URL getCrowdSecUrl() { + return Throwing.supplier(() -> new URL("http", + crowdsecContainer.getHost(), + crowdsecContainer.getMappedPort(CROWDSEC_PORT), + "/v1")).get(); + } + public GenericContainer<?> getCrowdsecContainer() { return crowdsecContainer; } diff --git a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecIntegrationTest.java b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecIntegrationTest.java new file mode 100644 index 0000000000..24a9dc5bd6 --- /dev/null +++ b/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecIntegrationTest.java @@ -0,0 +1,220 @@ +/**************************************************************** + * 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. * + ****************************************************************/ + +package org.apache.james; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT; +import static org.apache.james.model.CrowdsecClientConfiguration.DEFAULT_API_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.stream.IntStream; + +import org.apache.commons.net.smtp.SMTPClient; +import org.apache.james.model.CrowdsecClientConfiguration; +import org.apache.james.model.CrowdsecDecision; +import org.apache.james.model.CrowdsecHttpClient; +import org.apache.james.modules.protocols.SmtpGuiceProbe; +import org.apache.james.utils.DataProbeImpl; +import org.apache.james.utils.TestIMAPClient; +import org.assertj.core.api.SoftAssertions; +import org.awaitility.Awaitility; +import org.awaitility.Durations; +import org.awaitility.core.ConditionFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.utility.MountableFile; + +import com.github.fge.lambdas.Throwing; + +class CrowdsecIntegrationTest { + @RegisterExtension + static CrowdsecExtension crowdsecExtension = new CrowdsecExtension(); + + @RegisterExtension + static JamesServerExtension testExtension = new JamesServerBuilder<MemoryJamesConfiguration>(tmpDir -> + MemoryJamesConfiguration.builder() + .workingDirectory(tmpDir) + .configurationFromClasspath() + .usersRepository(DEFAULT) + .build()) + .server(MemoryJamesServerMain::createServer) + .extension(crowdsecExtension) + .lifeCycle(JamesServerExtension.Lifecycle.PER_TEST) + .build(); + + @RegisterExtension + public TestIMAPClient testIMAPClient = new TestIMAPClient(); + + private static final String LOCALHOST_IP = "127.0.0.1"; + private static final String DOMAIN = "domain.tld"; + private static final String BOB = "bob@" + DOMAIN; + private static final String BOB_PASSWORD = "bobPassword"; + private static final String BAD_PASSWORD = "badPassword"; + private static final ConditionFactory CALMLY_AWAIT = Awaitility + .with().pollInterval(ONE_HUNDRED_MILLISECONDS) + .and().pollDelay(ONE_HUNDRED_MILLISECONDS) + .await(); + + private HAProxyExtension haProxyExtension; + private SMTPClient smtpProtocol; + private CrowdsecHttpClient crowdsecClient; + + @BeforeEach + void setup(GuiceJamesServer server, @TempDir Path tempDir) throws Exception { + server.getProbe(DataProbeImpl.class).fluent() + .addDomain(DOMAIN) + .addUser(BOB, BOB_PASSWORD); + + haProxyExtension = new HAProxyExtension(MountableFile.forHostPath(createHaProxyConfigFile(server, tempDir).toString())); + haProxyExtension.start(); + + smtpProtocol = new SMTPClient(); + crowdsecClient = new CrowdsecHttpClient(new CrowdsecClientConfiguration(crowdsecExtension.getCrowdSecUrl(), DEFAULT_API_KEY)); + } + + private Path createHaProxyConfigFile(GuiceJamesServer server, Path tempDir) throws IOException { + String smtpServerWithProxySupportIp = crowdsecExtension.getCrowdsecContainer().getContainerInfo().getNetworkSettings() + .getGateway(); // James server listens at the docker bridge network's gateway IP + int smtpWithProxySupportPort = server.getProbe(SmtpGuiceProbe.class).getSmtpAuthRequiredPort().getValue(); + String haproxyConfigContent = String.format("global\n" + + " log stdout format raw local0 info\n" + + "\n" + + "defaults\n" + + " mode tcp\n" + + " timeout client 1800s\n" + + " timeout connect 5s\n" + + " timeout server 1800s\n" + + " log global\n" + + " option tcplog\n" + + "\n" + + "frontend smtp-frontend\n" + + " bind :25\n" + + " default_backend james-server-smtp\n" + + "\n" + + "backend james-server-smtp\n" + + " server james1 %s:%d send-proxy\n", + smtpServerWithProxySupportIp, smtpWithProxySupportPort); + Path haProxyConfigFile = tempDir.resolve("haproxy.cfg"); + Files.write(haProxyConfigFile, haproxyConfigContent.getBytes()); + + return haProxyConfigFile; + } + + @AfterEach + void teardown() { + haProxyExtension.stop(); + } + + @Nested + class SMTP { + @Test + void ehloShouldRejectAfterThreeFailedSMTPAuthentications(GuiceJamesServer server) { + // GIVEN a client failed to log in 3 consecutive times in a short period + IntStream.range(0, 3) + .forEach(Throwing.intConsumer(any -> { + smtpProtocol.connect(LOCALHOST_IP, server.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + smtpProtocol.sendCommand("AUTH PLAIN"); + smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + BOB + "\0" + BAD_PASSWORD + "\0").getBytes(UTF_8))); + assertThat(smtpProtocol.getReplyString()) + .contains("535 Authentication Failed"); + })); + + // THEN SMTP usage from the IP would be blocked upon the EHLO command. CrowdSec takes time to processing the ban decision therefore the await below. + CALMLY_AWAIT.atMost(Durations.TEN_SECONDS) + .untilAsserted(() -> { + smtpProtocol.connect(LOCALHOST_IP, server.getProbe(SmtpGuiceProbe.class).getSmtpPort().getValue()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + assertThat(smtpProtocol.getReplyString()) + .contains("554 Email rejected"); + }); + } + + @Test + void ehloShouldRejectAfterThreeFailedSMTPAuthenticationsViaProxy() { + // GIVEN a client connected via proxy failed to log in 3 consecutive times in a short period + IntStream.range(0, 3) + .forEach(Throwing.intConsumer(any -> { + smtpProtocol.connect(LOCALHOST_IP, haProxyExtension.getProxiedSmtpPort()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + smtpProtocol.sendCommand("AUTH PLAIN"); + smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + BOB + "\0" + BAD_PASSWORD + "\0").getBytes(UTF_8))); + assertThat(smtpProtocol.getReplyString()) + .contains("535 Authentication Failed"); + })); + + // THEN SMTP usage from the client would be blocked upon the EHLO command. + CALMLY_AWAIT.atMost(Durations.TEN_SECONDS) + .untilAsserted(() -> { + smtpProtocol.connect(LOCALHOST_IP, haProxyExtension.getProxiedSmtpPort()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + assertThat(smtpProtocol.getReplyString()) + .contains("554 Email rejected"); + }); + } + + @Test + void shouldBanRealClientIpAndNotProxyIp() { + // GIVEN a client connected via proxy failed to log in 3 consecutive times in a short period + IntStream.range(0, 3) + .forEach(Throwing.intConsumer(any -> { + smtpProtocol.connect(LOCALHOST_IP, haProxyExtension.getProxiedSmtpPort()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + smtpProtocol.sendCommand("AUTH PLAIN"); + smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + BOB + "\0" + BAD_PASSWORD + "\0").getBytes(UTF_8))); + assertThat(smtpProtocol.getReplyString()) + .contains("535 Authentication Failed"); + })); + + CALMLY_AWAIT.atMost(Durations.TEN_SECONDS) + .untilAsserted(() -> { + smtpProtocol.connect(LOCALHOST_IP, haProxyExtension.getProxiedSmtpPort()); + smtpProtocol.sendCommand("EHLO", InetAddress.getLocalHost().toString()); + assertThat(smtpProtocol.getReplyString()) + .contains("554 Email rejected"); + }); + + // THEN real client IP must be banned, not proxy IP + String realClientIp = haProxyExtension.getHaproxyContainer().getContainerInfo().getNetworkSettings() + .getGateway(); // client connect to HAProxy container via the docker bridge network's gateway IP + String haProxyIp = haProxyExtension.getHaproxyContainer().getContainerInfo().getNetworkSettings() + .getIpAddress(); + + List<CrowdsecDecision> decisions = crowdsecClient.getCrowdsecDecisions().block(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(decisions).hasSize(1); + softly.assertThat(decisions.get(0).getValue()).isEqualTo(realClientIp); + softly.assertThat(decisions.get(0).getValue()).isNotEqualTo(haProxyIp); + }); + } + + } +} diff --git a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java b/third-party/crowdsec/src/test/java/org/apache/james/HAProxyExtension.java similarity index 55% copy from third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java copy to third-party/crowdsec/src/test/java/org/apache/james/HAProxyExtension.java index f683c5673e..31f556cfe7 100644 --- a/third-party/crowdsec/src/test/java/org/apache/james/CrowdsecExtension.java +++ b/third-party/crowdsec/src/test/java/org/apache/james/HAProxyExtension.java @@ -19,51 +19,44 @@ package org.apache.james; -import java.io.IOException; -import java.time.Duration; import java.util.UUID; import org.apache.james.util.docker.RateLimiters; -import org.junit.jupiter.api.extension.ExtensionContext; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; +import org.testcontainers.utility.MountableFile; -import com.google.common.collect.ImmutableList; +public class HAProxyExtension { + private static final String HAPROXY_IMAGE = "haproxytech/haproxy-alpine:2.9.1"; + private static final int SMTP_PORT = 25; -public class CrowdsecExtension implements GuiceModuleTestExtension { - public static final Duration STARTUP_TIMEOUT = Duration.ofMinutes(5); - public static final int CROWDSEC_PORT = 8080; - public static final int EXPOSED_PORT = 8082; - public static final String CROWDSEC_IMAGE = "crowdsecurity/crowdsec:v1.5.4"; + private final GenericContainer<?> haproxyContainer; - private final GenericContainer<?> crowdsecContainer; - - public CrowdsecExtension() { - this.crowdsecContainer = new GenericContainer<>(CROWDSEC_IMAGE) - .withCreateContainerCmdModifier(cmd -> cmd.withName("james-crowdsec-test-" + UUID.randomUUID())) - .withExposedPorts(CROWDSEC_PORT) - .withStartupTimeout(STARTUP_TIMEOUT) + public HAProxyExtension(MountableFile haProxyConfigFile) { + this.haproxyContainer = new GenericContainer<>(HAPROXY_IMAGE) + .withCreateContainerCmdModifier(cmd -> cmd.withName("james-haproxy-test-" + UUID.randomUUID())) + .withExposedPorts(SMTP_PORT) + .withCopyFileToContainer(haProxyConfigFile, "/usr/local/etc/haproxy/") .waitingFor(new HostPortWaitStrategy().withRateLimiter(RateLimiters.TWENTIES_PER_SECOND)); } - @Override - public void beforeAll(ExtensionContext context) { - if (!crowdsecContainer.isRunning()) { - crowdsecContainer.start(); + public void start() { + if (!haproxyContainer.isRunning()) { + haproxyContainer.start(); } } - @Override - public void afterEach(ExtensionContext extensionContext) throws IOException, InterruptedException { - crowdsecContainer.execInContainer("cscli", "decision", "delete", "--all"); + public void stop() { + if (haproxyContainer.isRunning()) { + haproxyContainer.stop(); + } } - @Override - public void afterAll(ExtensionContext extensionContext) { - + public int getProxiedSmtpPort() { + return haproxyContainer.getMappedPort(SMTP_PORT); } - public GenericContainer<?> getCrowdsecContainer() { - return crowdsecContainer; + public GenericContainer<?> getHaproxyContainer() { + return this.haproxyContainer; } } diff --git a/third-party/crowdsec/src/test/resources/crowdsec/acquis.yaml b/third-party/crowdsec/src/test/resources/crowdsec/acquis.yaml new file mode 100644 index 0000000000..c58183a0f7 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/acquis.yaml @@ -0,0 +1,5 @@ +source: file +filenames: + - /var/log/james.log +labels: + type: james \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/collections/james.yaml b/third-party/crowdsec/src/test/resources/crowdsec/collections/james.yaml new file mode 100644 index 0000000000..8d1e046dfd --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/collections/james.yaml @@ -0,0 +1,7 @@ +parsers: + - crowdsecurity/syslog-logs + - linagora/james-auth + - linagora/james-dictionary-attack +scenarios: + - apache-james/bf-auth +author: linagora \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-auth.yaml b/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-auth.yaml new file mode 100644 index 0000000000..52bea127f1 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-auth.yaml @@ -0,0 +1,75 @@ +onsuccess: next_stage +debug: true +filter: "evt.Parsed.program == 'james'" +name: linagora/james-auth +description: "Parser for James IMAP and SMTP authentication " + +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.' +nodes: + - grok: + name: "IMAP_AUTH_FAIL_BAD_CREDENTIALS" + apply_on: message + statics: + - meta: log_type + value: imap-auth-fail + - meta: timestamp + expression: evt.Parsed.timestamp + - meta: level + expression: evt.Parsed.level + - meta: source_ip + expression: evt.Parsed.mdc_ip + - meta: host + expression: evt.Parsed.mdc_host + - meta: user + expression: evt.Parsed.user + - grok: + name: "IMAP_AUTH_FAIL_DELEGATION_BAD_CREDENTIALS" + apply_on: message + statics: + - meta: log_type + value: imap-auth-fail + - meta: timestamp + expression: evt.Parsed.timestamp + - meta: level + expression: evt.Parsed.level + - meta: source_ip + expression: evt.Parsed.mdc_ip + - meta: host + expression: evt.Parsed.mdc_host + - meta: user + expression: evt.Parsed.user + - grok: + name: "IMAP_AUTH_FAIL_NO_EXISTING_DELEGATION" + apply_on: message + statics: + - meta: log_type + value: imap-auth-fail + - meta: timestamp + expression: evt.Parsed.timestamp + - meta: level + expression: evt.Parsed.level + - meta: source_ip + expression: evt.Parsed.mdc_ip + - meta: host + expression: evt.Parsed.mdc_host + - meta: user + expression: evt.Parsed.user + - grok: + name: "SMTP_AUTH_FAIL" + apply_on: message + statics: + - meta: log_type + value: smtp-auth-fail + - meta: timestamp + expression: evt.Parsed.timestamp + - meta: level + expression: evt.Parsed.level + - meta: source_ip + expression: evt.Parsed.mdc_remoteIP + - meta: user + expression: evt.Parsed.mdc_username \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-dictionary-attack.yaml b/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-dictionary-attack.yaml new file mode 100644 index 0000000000..1d7a5763dc --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/parsers/james-dictionary-attack.yaml @@ -0,0 +1,25 @@ +onsuccess: next_stage +debug: true +filter: "evt.Parsed.program == 'james'" +name: linagora/james-dictionary-attack +description: "Parser for James dictionary attack" + +pattern_syntax: + DICTIONARY_ATTACK: 'Rejected message. Unknown user: %{EMAILADDRESS:rcpt}' +nodes: + - grok: + name: "DICTIONARY_ATTACK" + apply_on: message + statics: + - meta: log_type + value: dictionary-attack + - meta: timestamp + expression: evt.Parsed.timestamp + - meta: level + expression: evt.Parsed.level + - meta: source_ip + expression: evt.Parsed.mdc_ip + - meta: user + expression: evt.Parsed.mdc_user + - meta: rcpt + expression: evt.Parsed.rcpt \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/parsers/syslog-logs.yaml b/third-party/crowdsec/src/test/resources/crowdsec/parsers/syslog-logs.yaml new file mode 100644 index 0000000000..1b4f50f34e --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/parsers/syslog-logs.yaml @@ -0,0 +1,75 @@ +#If it's syslog, we are going to extract progname from it +filter: "evt.Line.Labels.type == 'syslog'" +onsuccess: next_stage +pattern_syntax: + RAW_SYSLOG_PREFIX: '^<%{NUMBER:stuff1}>%{NUMBER:stuff2} %{SYSLOGBASE2} %{DATA:program} %{NUMBER:pid}' + RAW_SYSLOG_META: '\[meta sequenceId="%{NOTDQUOTE:seq_id}"\]' +name: crowdsecurity/syslog-logs +nodes: + - grok: + #this is a named regular expression. grok patterns can be kept into separate files for readability + pattern: "^%{SYSLOGLINE}" + #This is the field of the `Event` to which the regexp should be applied + apply_on: Line.Raw + - grok: + #a second pattern for unparsed syslog lines, as saw in opnsense + pattern: '%{RAW_SYSLOG_PREFIX} - %{RAW_SYSLOG_META} %{GREEDYDATA:message}' + apply_on: Line.Raw +#if the node was successfull, statics will be applied. +statics: + - meta: machine + expression: evt.Parsed.logsource + - parsed: "logsource" + value: "syslog" +# syslog date can be in two different fields (one of hte assignment will fail) + - target: evt.StrTime + expression: evt.Parsed.timestamp + - target: evt.StrTime + expression: evt.Parsed.timestamp8601 + - meta: datasource_path + expression: evt.Line.Src + - meta: datasource_type + expression: evt.Line.Module +--- +#if it's not syslog, the type is the progname +filter: "evt.Line.Labels.type != 'syslog'" +onsuccess: next_stage +name: crowdsecurity/non-syslog +debug: true +statics: + - parsed: json_parsed + expression: UnmarshalJSON(evt.Line.Raw, evt.Unmarshaled, "message") + - parsed: program + expression: evt.Line.Labels.type + - parsed: timestamp + expression: evt.Unmarshaled.message.timestamp + - parsed: level + expression: evt.Unmarshaled.message.level + - parsed: thread + expression: evt.Unmarshaled.message.thread + - parsed: mdc_protocol + expression: evt.Unmarshaled.message.mdc.protocol + - parsed: mdc_ip + expression: evt.Unmarshaled.message.mdc.ip + - parsed: mdc_host + expression: evt.Unmarshaled.message.mdc.host + - parsed: mdc_action + expression: evt.Unmarshaled.message.mdc.action + - parsed: mdc_sessionId + expression: evt.Unmarshaled.message.mdc.sessionId + - parsed: mdc_user + expression: evt.Unmarshaled.message.mdc.user + - parsed: mdc_remoteIP + expression: evt.Unmarshaled.message.mdc.remoteIP + - parsed: mdc_username + expression: evt.Unmarshaled.message.mdc.username + - parsed: logger + expression: evt.Unmarshaled.message.logger + - parsed: message + expression: evt.Unmarshaled.message.message + - parsed: context + expression: evt.Unmarshaled.message.context + - meta: datasource_path + expression: evt.Line.Src + - meta: datasource_type + expression: evt.Line.Module \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-bf-auth.yaml b/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-bf-auth.yaml new file mode 100644 index 0000000000..f7d02cec73 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-bf-auth.yaml @@ -0,0 +1,14 @@ +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'" +leakspeed: "5m" +capacity: 2 # 3rd failed authentication would trigger the ban decision +groupby: evt.Meta.source_ip +blackhole: 0m # to enable rapidly trigger new ban decision across tests. cf https://docs.crowdsec.net/docs/scenarios/format/#blackhole +reprocess: true +labels: + service: ssh + type: bruteforce + remediation: true \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-dictionary-attack.yaml b/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-dictionary-attack.yaml new file mode 100644 index 0000000000..380b912774 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/crowdsec/scenarios/james-dictionary-attack.yaml @@ -0,0 +1,14 @@ +type: leaky +name: linagora/james-dictionary-attack +debug: true +description: "Detect login james bruteforce" +filter: "evt.Meta.log_type == 'dictionary-attack'" +leakspeed: "1m" +capacity: 5 +groupby: evt.Meta.source_ip +blackhole: 1m +reprocess: true +labels: + service: ssh + type: bruteforce + remediation: true \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/imapserver.xml b/third-party/crowdsec/src/test/resources/imapserver.xml new file mode 100644 index 0000000000..a2ee96c8c9 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/imapserver.xml @@ -0,0 +1,64 @@ +<?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. +--> + + +<imapservers> + <imapserver enabled="true"> + <jmxName>imapserver</jmxName> + <bind>0.0.0.0:0</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> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <gracefulShutdown>false</gracefulShutdown> + <idleTimeInterval>120</idleTimeInterval> + <idleTimeIntervalUnit>SECONDS</idleTimeIntervalUnit> + <enableIdle>true</enableIdle> + <plainAuthDisallowed>false</plainAuthDisallowed> + </imapserver> + <imapserver enabled="true"> + <jmxName>imapserver-ssl</jmxName> + <bind>0.0.0.0:0</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> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <gracefulShutdown>false</gracefulShutdown> + <idleTimeInterval>120</idleTimeInterval> + <idleTimeIntervalUnit>SECONDS</idleTimeIntervalUnit> + <enableIdle>true</enableIdle> + <plainAuthDisallowed>false</plainAuthDisallowed> + </imapserver> +</imapservers> diff --git a/third-party/crowdsec/src/test/resources/listeners.xml b/third-party/crowdsec/src/test/resources/listeners.xml new file mode 100644 index 0000000000..ae5937f1fa --- /dev/null +++ b/third-party/crowdsec/src/test/resources/listeners.xml @@ -0,0 +1,22 @@ +<?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. + --> + +<listeners> +</listeners> \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/log/james.log b/third-party/crowdsec/src/test/resources/log/james.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third-party/crowdsec/src/test/resources/logback-test.xml b/third-party/crowdsec/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..ad3f309937 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/logback-test.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> + +<configuration> + + <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"> + <resetJUL>true</resetJUL> + </contextListener> + + <appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>./src/test/resources/log/james.log</file> + + <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"> + <fileNamePattern>./src/test/resources/log/james.%i.log.tar.gz</fileNamePattern> + <minIndex>1</minIndex> + <maxIndex>3</maxIndex> + </rollingPolicy> + + <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> + <maxFileSize>100MB</maxFileSize> + </triggeringPolicy> + + <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> + <layout class="ch.qos.logback.contrib.json.classic.JsonLayout"> + <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSSX</timestampFormat> + <timestampFormatTimezoneId>Etc/UTC</timestampFormatTimezoneId> + + <!-- Importance for handling multiple lines log --> + <appendLineSeparator>true</appendLineSeparator> + + <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter"> + <prettyPrint>false</prettyPrint> + </jsonFormatter> + </layout> + </encoder> + </appender> + <root level="WARN"> + <appender-ref ref="LOG_FILE" /> + </root> + + <logger name="org.apache.james" level="INFO" /> +</configuration> \ No newline at end of file diff --git a/third-party/crowdsec/src/test/resources/mailetcontainer.xml b/third-party/crowdsec/src/test/resources/mailetcontainer.xml new file mode 100644 index 0000000000..a721c7cde5 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/mailetcontainer.xml @@ -0,0 +1,155 @@ +<?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. + --> + +<mailetcontainer enableJmx="false"> + + <context> + <postmaster>postmaster</postmaster> + </context> + + <spooler> + <threads>20</threads> + <errorRepository>memory://var/mail/error/</errorRepository> + </spooler> + + <processors> + <processor state="root" enableJmx="false"> + <mailet match="All" class="PostmasterAlias"/> + <mailet match="RelayLimit=30" class="Null"/> + <mailet match="All" class="ToProcessor"> + <processor>transport</processor> + </mailet> + </processor> + + <processor state="error" enableJmx="false"> + <mailet match="All" class="Bounce"> + <onMailetException>ignore</onMailetException> + </mailet> + <mailet match="All" class="ToRepository"> + <repositoryPath>memory://var/mail/error/</repositoryPath> + <onMailetException>propagate</onMailetException> + </mailet> + </processor> + + + <processor state="transport" enableJmx="false"> + <mailet match="SMTPAuthSuccessful" class="SetMimeHeader"> + <name>X-UserIsAuth</name> + <value>true</value> + </mailet> + <mailet match="All" class="RemoveMimeHeader"> + <name>bcc</name> + </mailet> + <mailet match="All" class="RecipientRewriteTable"> + <errorProcessor>rrt-error</errorProcessor> + </mailet> + <mailet match="RecipientIsLocal" class="ToProcessor"> + <processor>local-delivery</processor> + </mailet> + <mailet match="HostIsLocal" class="ToProcessor"> + <processor>local-address-error</processor> + <notice>550 - Requested action not taken: no such user here</notice> + </mailet> + <mailet match="SMTPAuthSuccessful" class="SetMailAttribute"> + <RelayAllowed>true</RelayAllowed> + </mailet> + <mailet match="SMTPIsAuthNetwork" class="SetMailAttribute"> + <RelayAllowed>true</RelayAllowed> + </mailet> + <mailet match="SentByMailet" class="SetMailAttribute"> + <RelayAllowed>true</RelayAllowed> + </mailet> + <mailet match="org.apache.james.jmap.mailet.SentByJmap" class="SetMailAttribute"> + <RelayAllowed>true</RelayAllowed> + </mailet> + <mailet match="HasMailAttribute=RelayAllowed" class="RemoteDelivery"> + <outgoingQueue>outgoing</outgoingQueue> + <delayTime>5000, 100000, 500000</delayTime> + <maxRetries>3</maxRetries> + <maxDnsProblemRetries>0</maxDnsProblemRetries> + <deliveryThreads>10</deliveryThreads> + <sendpartial>true</sendpartial> + <bounceProcessor>bounces</bounceProcessor> + </mailet> + <mailet match="All" class="ToProcessor"> + <processor>relay-denied</processor> + </mailet> + </processor> + + <processor state="local-delivery" enableJmx="true"> + <mailet match="All" class="VacationMailet"> + <onMailetException>ignore</onMailetException> + </mailet> + <mailet match="All" class="Sieve"> + <onMailetException>ignore</onMailetException> + </mailet> + <mailet match="All" class="AddDeliveredToHeader"/> + <mailet match="All" class="org.apache.james.jmap.mailet.filter.JMAPFiltering"> + <onMailetException>ignore</onMailetException> + </mailet> + <mailet match="All" class="LocalDelivery"/> + </processor> + + <processor state="spam" enableJmx="false"> + <mailet match="All" class="ToRepository"> + <repositoryPath>memory://var/mail/spam/</repositoryPath> + </mailet> + </processor> + + <processor state="local-address-error" enableJmx="false"> + <mailet match="All" class="Bounce"> + <attachment>none</attachment> + </mailet> + <mailet match="All" class="ToRepository"> + <repositoryPath>memory://var/mail/address-error/</repositoryPath> + </mailet> + </processor> + + <processor state="relay-denied" enableJmx="false"> + <mailet match="All" class="Bounce"> + <attachment>none</attachment> + </mailet> + <mailet match="All" class="ToRepository"> + <repositoryPath>memory://var/mail/relay-denied/</repositoryPath> + <notice>Warning: You are sending an e-mail to a remote server. You must be authentified to perform such an operation</notice> + </mailet> + </processor> + + <processor state="bounces" enableJmx="false"> + <mailet match="All" class="DSNBounce"> + <passThrough>false</passThrough> + </mailet> + </processor> + + <processor state="rrt-error" enableJmx="false"> + <mailet match="All" class="ToRepository"> + <repositoryPath>memory://var/mail/rrt-error/</repositoryPath> + <passThrough>true</passThrough> + </mailet> + <mailet match="IsSenderInRRTLoop" class="Null"/> + <mailet match="All" class="Bounce"/> + </processor> + + </processors> + +</mailetcontainer> + + diff --git a/third-party/crowdsec/src/test/resources/pop3server.xml b/third-party/crowdsec/src/test/resources/pop3server.xml new file mode 100644 index 0000000000..bec385ae30 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/pop3server.xml @@ -0,0 +1,23 @@ +<?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> +</pop3servers> diff --git a/third-party/crowdsec/src/test/resources/smtpserver.xml b/third-party/crowdsec/src/test/resources/smtpserver.xml new file mode 100644 index 0000000000..abd07ea441 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/smtpserver.xml @@ -0,0 +1,84 @@ +<?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. + --> + +<smtpservers> + <smtpserver enabled="true"> + <jmxName>smtpserver-global</jmxName> + <bind>0.0.0.0:0</bind> + <connectionBacklog>200</connectionBacklog> + <tls socketTLS="false" startTLS="false"> + <keystore>file://conf/keystore</keystore> + <secret>james72laBalle</secret> + <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider> + <algorithm>SunX509</algorithm> + </tls> + <connectiontimeout>360</connectiontimeout> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <auth> + <announce>never</announce> + <requireSSL>false</requireSSL> + <plainAuthEnabled>true</plainAuthEnabled> + </auth> + <verifyIdentity>false</verifyIdentity> + <maxmessagesize>0</maxmessagesize> + <addressBracketsEnforcement>true</addressBracketsEnforcement> + <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting> + <handlerchain> + <handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + <handler class="org.apache.james.CrowdsecEhloHook"/> + </handlerchain> + <gracefulShutdown>false</gracefulShutdown> + </smtpserver> + <smtpserver enabled="true"> + <jmxName>smtpserver-TLS</jmxName> + <bind>0.0.0.0:0</bind> + <connectionBacklog>200</connectionBacklog> + <tls socketTLS="false" startTLS="false"> + <keystore>file://conf/keystore</keystore> + <secret>james72laBalle</secret> + <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider> + <algorithm>SunX509</algorithm> + </tls> + <connectiontimeout>360</connectiontimeout> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <auth> + <announce>forUnauthorizedAddresses</announce> + <requireSSL>false</requireSSL> + </auth> + <!-- Trust authenticated users --> + <verifyIdentity>false</verifyIdentity> + <maxmessagesize>0</maxmessagesize> + <addressBracketsEnforcement>true</addressBracketsEnforcement> + <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting> + <handlerchain> + <handler class="org.apache.james.smtpserver.fastfail.ValidRcptHandler"/> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + <handler class="org.apache.james.CrowdsecEhloHook"/> + </handlerchain> + <gracefulShutdown>false</gracefulShutdown> + <proxyRequired>true</proxyRequired> + </smtpserver> +</smtpservers> + + diff --git a/third-party/crowdsec/src/test/resources/usersrepository.xml b/third-party/crowdsec/src/test/resources/usersrepository.xml new file mode 100644 index 0000000000..a5390d7140 --- /dev/null +++ b/third-party/crowdsec/src/test/resources/usersrepository.xml @@ -0,0 +1,28 @@ +<?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. + --> + +<!-- Read https://james.apache.org/server/config-users.html for further details --> + +<usersrepository name="LocalUsers"> + <algorithm>PBKDF2-SHA512</algorithm> + <enableVirtualHosting>true</enableVirtualHosting> + <enableForwarding>true</enableForwarding> +</usersrepository> + --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
