This document covers security features available across all Gumdrop servers, including identity and access management, rate limiting, and transport security.
HTTP services add default security headers (X-Frame-Options,
X-Content-Type-Options) to responses unless the application sets
them. See HTTP Configuration for the
add-security-headers property.
All Gumdrop servers support TLS for encrypted connections. For TCP-based
protocols (HTTP/1.x, HTTP/2, SMTP, IMAP, POP3, FTP), Gumdrop uses the JVM's
built-in SSLEngine and standard Java keystores (PKCS#12, JKS).
QUIC/HTTP/3 uses a separate TLS stack (BoringSSL via quiche) with different
certificate requirements, described below.
The HTTPS listener requires a PKCS#12 keystore and the HTTP/3 (QUIC) listener requires PEM certificate and key files (see QUIC / HTTP/3 Certificate Requirements for details on why these differ). These are not checked in to the repository; you must generate them before running the server.
The easiest way for local development is mkcert, which creates certificates trusted by your browser:
# One-time: install the local CA into your system trust store
mkcert -install
# Generate certificate and key for localhost into etc/
mkcert -cert-file etc/localhost+2.pem -key-file etc/localhost+2-key.pem \
localhost 127.0.0.1 ::1
This produces etc/localhost+2.pem and etc/localhost+2-key.pem.
Then generate a PKCS#12 keystore from the same certificate for the HTTPS
listener:
openssl pkcs12 -export -in etc/localhost+2.pem -inkey etc/localhost+2-key.pem \
-out etc/localhost+2.p12 -name localhost -passout pass:changeit
The configuration files in etc/ reference these by filename (e.g.
localhost+2.p12); they are not checked in to the repository.
Connection is encrypted from the start (e.g., SMTPS port 465, IMAPS port 993):
<service id="imaps" class="org.bluezoo.gumdrop.imap.DefaultIMAPService">
<listener class="org.bluezoo.gumdrop.imap.IMAPListener">
<property name="port">993</property>
<property name="secure">true</property>
<property name="keystore-file">keystore.p12</property>
<property name="keystore-pass">secret</property>
</listener>
</service>
Connection starts unencrypted and upgrades to TLS on client request:
<service id="smtp" class="org.bluezoo.gumdrop.smtp.SimpleRelayService">
<listener class="org.bluezoo.gumdrop.smtp.SMTPListener">
<property name="port">25</property>
<property name="keystore-file">keystore.p12</property>
<property name="keystore-pass">secret</property>
<!-- Advertises STARTTLS capability -->
</listener>
</service>
Servers can require or request client certificates for mutual TLS (mTLS):
<service id="smtp" class="org.bluezoo.gumdrop.smtp.SimpleRelayService">
<listener class="org.bluezoo.gumdrop.smtp.SMTPListener">
<property name="need-client-auth">true</property>
<property name="truststore-file">truststore.p12</property>
<property name="truststore-pass">secret</property>
</listener>
</service>
SNI allows a server to present different certificates based on the hostname the client requests during the TLS handshake. This enables hosting multiple secure domains on a single IP address and port.
<service id="https" class="org.bluezoo.gumdrop.servlet.ServletService">
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">443</property>
<property name="secure">true</property>
<property name="keystore-file">multi-domain.p12</property>
<property name="keystore-pass">secret</property>
<property name="sni-hostnames">
<map>
<entry key="example.com" value="example-cert"/>
<entry key="*.example.com" value="example-wildcard"/>
<entry key="other.com" value="other-cert"/>
</map>
</property>
<property name="sni-default-alias">default-cert</property>
</listener>
</service>
The sniHostnames map specifies hostname-to-certificate-alias mappings.
Hostnames can be exact (example.com) or wildcard patterns
(*.example.com). The keystore must contain certificates with the
specified aliases. If no match is found, sniDefaultAlias is used.
QUIC mandates TLS 1.3 — there is no unencrypted mode and no STARTTLS upgrade. The TLS handshake is integrated into the QUIC connection establishment, so every QUIC connection is encrypted from the first packet.
Because Gumdrop's QUIC implementation uses BoringSSL (via the quiche
library) rather than the JVM's SSLEngine, its certificate
configuration differs from TCP-based listeners:
| TCP/TLS (HTTP/1.x, HTTP/2, SMTP, etc.) | QUIC (HTTP/3) | |
|---|---|---|
| TLS implementation | JVM SSLEngine | BoringSSL (via quiche) |
| Certificate format | Java keystore (PKCS#12, JKS) | PEM files |
| Certificate property | keystore-file |
cert-file, key-file |
| Password | keystore-pass (keystore password) |
N/A (key file is unencrypted) |
| TLS versions | TLS 1.0 – 1.3 (configurable) | TLS 1.3 only (QUIC requirement) |
QUIC listeners require certificates and keys in PEM format — Java keystores (PKCS#12, JKS) are not supported. The certificate file must contain the full certificate chain (server certificate followed by any intermediate CA certificates). The private key file must be unencrypted PEM (PKCS#8 or traditional format).
For development, mkcert generates locally-trusted PEM certificates. See TLS Certificates for Local Development above for the complete setup including the PKCS#12 keystore required for HTTPS.
For production, most certificate authorities (Let's Encrypt, etc.) issue PEM files directly. If you already have a Java keystore, you can export to PEM:
# Extract certificate chain
keytool -exportcert -alias myalias -keystore keystore.p12 \
-storepass secret -rfc -file cert.pem
# Extract private key (via PKCS#12 + OpenSSL)
openssl pkcs12 -in keystore.p12 -nocerts -nodes -out key.pem
A typical deployment shares the same port (443 or 8443) between an HTTPS listener on TCP and an HTTP/3 listener on UDP. Because they use different TLS stacks, they require separate certificate configurations:
<service id="http" class="org.bluezoo.gumdrop.servlet.ServletService">
<property name="container" ref="#mainContainer"/>
<!-- HTTPS on TCP (Java SSLEngine, PKCS#12 keystore) -->
<listener class="org.bluezoo.gumdrop.http.HTTPListener">
<property name="port">8443</property>
<property name="secure">true</property>
<property name="keystore-file">keystore.p12</property>
<property name="keystore-pass">secret</property>
</listener>
<!-- HTTP/3 on UDP (BoringSSL, PEM files) -->
<listener class="org.bluezoo.gumdrop.http.h3.HTTP3Listener">
<property name="port">8443</property>
<property name="cert-file">cert.pem</property>
<property name="key-file">key.pem</property>
</listener>
</service>
Both listeners must present the same certificate identity (same
subject/SAN) to avoid browser warnings when upgrading from HTTP/2 to HTTP/3.
The browser discovers HTTP/3 support via the Alt-Svc response
header on the initial HTTP/2 connection and will only attempt QUIC if the
certificate matches.
QUIC listeners support BoringSSL cipher suite and named group configuration:
<listener class="org.bluezoo.gumdrop.http.h3.HTTP3Listener">
<property name="port">443</property>
<property name="cert-file">cert.pem</property>
<property name="key-file">key.pem</property>
<property name="cipher-suites">TLS_AES_256_GCM_SHA384</property>
<property name="named-groups">X25519</property>
</listener>
The default cipher suites are BoringSSL's defaults (AES-128-GCM and AES-256-GCM with SHA-256/384). The default named group is X25519.
PinnedCertTrustManager wraps an X509TrustManager and
validates server certificates against a set of SHA-256 fingerprints. If the
presented certificate does not match any pinned fingerprint, the TLS handshake
is rejected.
All client classes support certificate pinning via
setTrustManager(X509TrustManager):
HTTPClient, SMTPClient, IMAPClient,
POP3Client, WebSocketClient, RedisClient,
LDAPClient.
Gumdrop provides a unified identity and access management framework used across
all protocol implementations. At its core is the Realm interface,
which abstracts credential storage and verification.
A Realm represents a collection of authenticatable principals with
associated passwords and roles. The interface supports multiple authentication
mechanisms without exposing stored credentials:
public interface Realm {
Realm forSelectorLoop(SelectorLoop loop);
Set<SASLMechanism> getSupportedSASLMechanisms();
boolean passwordMatch(String username, String password);
String getDigestHA1(String username, String realmName);
boolean isUserInRole(String username, String role);
boolean userExists(String username);
String getCramMD5Response(String username, String challenge);
ScramCredentials getScramCredentials(String username);
TokenValidationResult validateBearerToken(String token);
TokenValidationResult validateOAuthToken(String accessToken);
CertificateAuthenticationResult authenticateCertificate(X509Certificate cert);
boolean authorizeAs(String authenticatedUser, String requestedUser);
}
Most methods are default and throw UnsupportedOperationException
— realms need only implement the mechanisms they support. This design allows
the realm to store passwords in any form (plaintext, hashed, or derived keys)
without requiring the authentication layer to know the storage format.
The built-in BasicRealm reads users and groups from an XML file:
<realm> <!-- Group definitions --> <group id="readers" name="mail-read imap-read pop3-read"/> <group id="writers" name="mail-read mail-write imap-read imap-write"/> <group id="admins" name="admin mail-admin ftp-admin"/> <!-- User definitions --> <user name="alice" password="secret123" groups="#readers #writers"/> <user name="bob" password="password456" groups="#readers"/> <user name="admin" password="adminpass" groups="#admins"/> <!-- Inline roles (legacy format) --> <user name="guest" password="guest" roles="guest read-only"/> </realm>
Groups are defined with an id for referencing and a name
containing space-separated role names. Users reference groups using the
#id syntax in their groups attribute.
<realm id="mailRealm" class="org.bluezoo.gumdrop.auth.BasicRealm">
<property name="href">users.xml</property>
</realm>
<!-- Or inline -->
<realm id="simpleRealm" class="org.bluezoo.gumdrop.auth.BasicRealm">
<user name="testuser" password="testpass" roles="user"/>
</realm>
BasicRealm with plaintext passwords is intended for development and testing only. For production, use LDAPRealm or OAuthRealm. A warning is logged at startup when plaintext passwords are loaded.
Passwords may use LDAP-style hashed formats for improved security. Supported
schemes: {SHA}, {SSHA}, {SHA256},
{SSHA256}. Values can be exported from LDAP userPassword
and used directly. Hashed users support only PLAIN and LOGIN SASL mechanisms;
CRAM-MD5, SCRAM, Digest, and APOP require plaintext and will not work.
<user name="alice" password="{SSHA256}base64digest+salt" groups="readers"/>
Use slappasswd -h {SSHA256} -s password (OpenLDAP) or equivalent
tools to generate hashed values.
OAuthRealm validates OAuth 2.0 tokens via
RFC 7662 (token
introspection) and optionally via local
RFC 7519 JWT
validation. Constructed with a Properties instance specifying
the introspection endpoint, client credentials, and role-scope mappings. Not
directly DI-instantiable; use a factory or programmatic setup.
When oauth.jwt.enabled=true, tokens that look like JWTs
(three Base64url-encoded dot-separated segments) are validated locally
before falling back to the introspection endpoint. This eliminates the
round-trip for self-contained JWTs.
Supported signature algorithms (RFC 7518 ยง3):
oauth.jwt.secret)oauth.jwt.public.key)oauth.jwt.public.key)Standard claims are verified: exp (expiration),
nbf (not-before) with configurable clock skew tolerance,
iss (issuer), and aud (audience).
# Enable local JWT validation oauth.jwt.enabled=true oauth.jwt.secret=my-shared-secret # Optional claim validation oauth.jwt.issuer=https://auth.example.com oauth.jwt.audience=my-service oauth.jwt.clock.skew=30
isUserInRole() checks cached token scopes against role-scope
mappings. See the oauth-servlet-auth
example for a working configuration.
For production deployments, implement the Realm interface to
integrate with your identity provider:
OAuthRealm or custom implementationSMTP, IMAP, POP3, and FTP servers support SASL (Simple Authentication and Security Layer) mechanisms for secure credential exchange.
| Mechanism | Security | Requirements | Recommended |
|---|---|---|---|
| PLAIN | Password transmitted (base64) | TLS required | Over TLS only |
| LOGIN | Password transmitted (base64) | TLS required | Legacy compatibility |
| CRAM-MD5 | Challenge-response, no password sent | Realm must store plaintext | No |
| DIGEST-MD5 | Digest authentication | Realm must provide H(A1) | No (deprecated) |
| SCRAM-SHA-256 | Mutual auth, no password sent | Realm provides derived keys | Yes |
| OAUTHBEARER | Bearer token | OAuth 2.0 infrastructure | For OAuth deployments |
| GSSAPI | Kerberos V5 mutual auth | Keytab + service principal | For Kerberos/enterprise SSO |
| EXTERNAL | TLS client certificate | mTLS configured | For certificate-based auth |
SCRAM-SHA-256 is the recommended mechanism because:
The realm stores StoredKey and ServerKey instead of
the password. Even if the credential store is compromised, attackers cannot
recover the original password.
GSSAPI enables Kerberos V5 single sign-on authentication for IMAP, POP3, and SMTP. Each service requires a keytab file and service principal.
Server-side GSSAPI authentication is event-loop safe: the
acceptSecContext() operation decrypts the service ticket using the
local keytab with no KDC network I/O. The LDAP client's GSSAPI bind
automatically offloads the first initSecContext() call to a worker
thread since it may contact the KDC.
Gumdrop negotiates "no security layer" (RFC 4752 §3.1) since TLS already provides confidentiality and integrity.
// Programmatic
imapListener.configureGSSAPI(
Path.of("/etc/krb5.keytab"),
"imap/mail.example.com@EXAMPLE.COM");
// Or via setGSSAPIServer() for shared keytab
GSSAPIServer gss = new GSSAPIServer(
Path.of("/etc/krb5.keytab"),
"imap/mail.example.com@EXAMPLE.COM");
imapListener.setGSSAPIServer(gss);
gumdroprc XML configuration:
<service id="imap" class="org.bluezoo.gumdrop.imap.IMAPService">
<property name="gssapiKeytab" value="/etc/krb5.keytab"/>
<property name="gssapiPrincipal" value="imap/mail.example.com@EXAMPLE.COM"/>
</service>
After successful GSSAPI authentication, the authenticated Kerberos principal
(e.g. user@EXAMPLE.COM) is mapped to a local username via the
Realm's mapKerberosPrincipal() method. The default implementation
strips the realm portion.
Many Gumdrop servers support role-based access control (RBAC) for fine-grained authorization. Roles are assigned to users in the realm and checked by the server during operations.
| Protocol | Roles | Description |
|---|---|---|
| FTP | ftp-read | Download files, list directories |
| ftp-write | Upload files (implies ftp-read) | |
| ftp-delete | Delete files/directories (implies ftp-write) | |
| ftp-admin | Full access, SITE commands, quota management | |
| Servlet | (custom) | Roles defined in web.xml security constraints |
| manager | Access to manager application |
<!-- Define role groups --> <realm> <group id="readers" name="ftp-read"/> <group id="uploaders" name="ftp-read ftp-write"/> <group id="managers" name="ftp-read ftp-write ftp-delete"/> <group id="admins" name="ftp-admin"/> <user name="guest" password="guest" groups="#readers"/> <user name="contributor" password="secret" groups="#uploaders"/> <user name="editor" password="secret" groups="#managers"/> <user name="admin" password="secret" groups="#admins"/> </realm>
Gumdrop provides comprehensive rate limiting to protect servers from abuse, denial-of-service attacks, and brute-force authentication attempts.
The ConnectionRateLimiter provides two types of protection:
Limits the number of simultaneous connections from a single IP address, preventing resource exhaustion from connection flooding.
<service id="smtp" class="org.bluezoo.gumdrop.smtp.SimpleRelayService">
<property name="max-connections-per-ip">10</property>
<listener class="org.bluezoo.gumdrop.smtp.SMTPListener" port="25"/>
</service>
Limits the number of connection attempts per IP address within a time window, preventing rapid reconnection attacks.
<service id="ftp" class="org.bluezoo.gumdrop.ftp.file.RoleBasedFTPService">
<property name="rate-limit">100/60s</property>
<listener class="org.bluezoo.gumdrop.ftp.FTPListener" port="21"/>
</service>
The rate-limit property accepts the format count/duration:
100/60s - 100 per minute1000/1h - 1000 per hour50/30s - 50 per 30 secondsRate limiting uses a sliding window algorithm that efficiently tracks connection timestamps. Old entries are automatically expired, and a background cleanup task removes stale tracking data to prevent memory leaks.
The AuthenticationRateLimiter protects against brute-force password
attacks by tracking failed authentication attempts and temporarily locking out
offending clients.
<service id="imap" class="org.bluezoo.gumdrop.imap.DefaultIMAPService">
<property name="max-auth-failures">5</property>
<property name="auth-lockout-time">5m</property>
<listener class="org.bluezoo.gumdrop.imap.IMAPListener" port="143"/>
</service>
By default, lockout duration increases exponentially for repeat offenders:
Authentication failures can be tracked by:
SMTP servers support CIDR-based network filtering for allow/block lists:
<service id="smtp" class="org.bluezoo.gumdrop.smtp.SimpleRelayService">
<property name="allowed-networks">
192.168.0.0/16,10.0.0.0/8,2001:db8::/32
</property>
<property name="blocked-networks">
192.168.100.0/24,2001:db8:bad::/48
</property>
<listener class="org.bluezoo.gumdrop.smtp.SMTPListener" port="25"/>
</service>
Blocked networks are checked first, then allowed networks. If allowed networks are configured, only connections from those networks are accepted.
Security-related events are integrated with Gumdrop's telemetry system for monitoring and alerting:
ErrorCategory.RATE_LIMITED - connection/command rate limitingErrorCategory.AUTHENTICATION_FAILED - auth failuresErrorCategory.AUTHORIZATION_DENIED - permission deniedErrorCategory.TLS_ERROR - TLS handshake failuresAUTH_SUCCESS - successful authenticationAUTH_FAILURE - failed authentication attemptAUTH_LOCKED - account locked outCONNECTION_REJECTED - rate limit or policy rejectionConfigure your OpenTelemetry backend to alert on authentication failures:
// Prometheus query example
sum(rate(span_events{event="AUTH_FAILURE"}[5m])) by (server) > 10
← Back to Main Page | SMTP Server | IMAP Server | FTP Server | Telemetry
Gumdrop Security