This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 8e6949312e TIKA-4642 - improve tls configuration and documentation
(#2564)
8e6949312e is described below
commit 8e6949312ec2673583f33e2d20e6ebde86fa15a8
Author: Tim Allison <[email protected]>
AuthorDate: Sat Jan 31 19:22:28 2026 -0500
TIKA-4642 - improve tls configuration and documentation (#2564)
---
docs/modules/ROOT/nav.adoc | 1 +
.../ROOT/pages/using-tika/server/index.adoc | 6 +-
docs/modules/ROOT/pages/using-tika/server/tls.adoc | 651 +++++++++++++++++++++
.../apache/tika/server/core/TikaServerProcess.java | 25 +
.../org/apache/tika/server/core/TlsConfig.java | 229 +++++++-
5 files changed, 892 insertions(+), 20 deletions(-)
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index ef0165c69b..8cf1e26a0a 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -17,6 +17,7 @@
* xref:using-tika/index.adoc[Using Tika]
** xref:using-tika/java-api/index.adoc[Java API]
** xref:using-tika/server/index.adoc[Tika Server]
+*** xref:using-tika/server/tls.adoc[TLS/SSL Configuration]
** xref:using-tika/cli/index.adoc[Command Line]
** xref:using-tika/grpc/index.adoc[gRPC]
* xref:pipes/index.adoc[Pipes]
diff --git a/docs/modules/ROOT/pages/using-tika/server/index.adoc
b/docs/modules/ROOT/pages/using-tika/server/index.adoc
index accfc02700..59017b943b 100644
--- a/docs/modules/ROOT/pages/using-tika/server/index.adoc
+++ b/docs/modules/ROOT/pages/using-tika/server/index.adoc
@@ -35,8 +35,4 @@ The server starts on port 9998 by default.
== Topics
-// Add links to specific topics as they are created
-// * link:installation.html[Installation]
-// * link:endpoints.html[REST Endpoints]
-// * link:configuration.html[Configuration]
-// * link:docker.html[Docker Deployment]
+* xref:using-tika/server/tls.adoc[TLS/SSL Configuration] - Secure your server
with TLS and mutual authentication
diff --git a/docs/modules/ROOT/pages/using-tika/server/tls.adoc
b/docs/modules/ROOT/pages/using-tika/server/tls.adoc
new file mode 100644
index 0000000000..8823b9a4f5
--- /dev/null
+++ b/docs/modules/ROOT/pages/using-tika/server/tls.adoc
@@ -0,0 +1,651 @@
+//
+// 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.
+//
+
+= TLS/SSL Configuration
+:toc:
+:toclevels: 3
+
+Tika Server supports TLS (Transport Layer Security) for encrypted communication
+and mutual authentication (2-way TLS / mTLS).
+
+== Choosing a Security Approach
+
+For most deployments, we recommend placing Tika Server behind a **reverse
proxy** (such as nginx, Apache httpd, or Traefik) that handles TLS termination.
This approach simplifies certificate management, enables additional features
like rate limiting and authentication, and follows a well-established pattern
with extensive documentation. In this setup, the reverse proxy handles HTTPS
connections from clients, and Tika Server runs on plain HTTP in a private
network or on localhost. If you [...]
+
+Direct TLS configuration on Tika Server (described in this document) is useful
when you cannot use a reverse proxy, need client certificate authentication
(2-way TLS/mTLS), or prefer a simpler single-process deployment. If you only
need to restrict access without TLS, consider network-level controls such as
binding to localhost, firewall rules, or running in a private subnet.
+
+== Overview
+
+TLS configuration options:
+
+* **1-Way TLS**: Server presents its certificate; clients verify the server
+* **2-Way TLS (mTLS)**: Both server and client present certificates for mutual
authentication
+
+== Quick Start
+
+=== 1-Way TLS
+
+Server authenticates to clients, but clients are not required to present
certificates.
+
+[source,json]
+----
+{
+ "server": {
+ "host": "localhost",
+ "port": 9998
+ },
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "clientAuthenticationWanted": false,
+ "clientAuthenticationRequired": false
+ }
+}
+----
+
+=== 2-Way TLS (mTLS)
+
+Both server and client must present valid certificates.
+
+[source,json]
+----
+{
+ "server": {
+ "host": "localhost",
+ "port": 9998
+ },
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "trustStoreType": "PKCS12",
+ "trustStoreFile": "/path/to/server-truststore.p12",
+ "trustStorePassword": "your-password",
+ "clientAuthenticationWanted": true,
+ "clientAuthenticationRequired": true
+ }
+}
+----
+
+== Docker Deployment
+
+When running Tika Server in Docker with TLS, you need to:
+
+1. Create a configuration file with TLS settings
+2. Mount the configuration file into the container
+3. Mount the keystore (and truststore for 2-way TLS) into the container
+4. Start the container with the `-c` flag pointing to the config file
+
+=== Directory Structure
+
+Create a directory structure on your host machine:
+
+[source]
+----
+tika-tls/
+├── config/
+│ └── tika-config.json
+└── certs/
+ ├── server-keystore.p12
+ └── server-truststore.p12 # (for 2-way TLS)
+----
+
+=== Configuration File
+
+Create `tika-tls/config/tika-config.json`:
+
+[source,json]
+----
+{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 9998
+ },
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/tika/certs/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "trustStoreType": "PKCS12",
+ "trustStoreFile": "/tika/certs/server-truststore.p12",
+ "trustStorePassword": "your-password",
+ "clientAuthenticationRequired": true
+ }
+}
+----
+
+NOTE: Use `0.0.0.0` as the host to bind to all interfaces inside the container.
+The paths (`/tika/certs/...`) must match where you mount the files in the
container.
+
+=== Running with Docker
+
+[source,bash]
+----
+docker run -d \
+ --name tika-server \
+ -p 9998:9998 \
+ -v $(pwd)/tika-tls/config:/tika/config:ro \
+ -v $(pwd)/tika-tls/certs:/tika/certs:ro \
+ apache/tika:<version> \
+ -c /tika/config/tika-config.json
+----
+
+Options explained:
+
+* `-p 9998:9998` - Expose the HTTPS port
+* `-v .../config:/tika/config:ro` - Mount config directory (read-only)
+* `-v .../certs:/tika/certs:ro` - Mount certificates directory (read-only)
+* `-c /tika/config/tika-config.json` - Path to config file inside container
+
+=== Docker Compose
+
+For more complex deployments, use Docker Compose:
+
+[source,yaml]
+----
+# docker-compose.yml
+version: '3.8'
+
+services:
+ tika:
+ image: apache/tika:latest
+ ports:
+ - "9998:9998"
+ volumes:
+ - ./config:/tika/config:ro
+ - ./certs:/tika/certs:ro
+ command: ["-c", "/tika/config/tika-config.json"]
+ healthcheck:
+ test: ["CMD", "curl", "-f", "--cacert", "/tika/certs/server.crt",
"https://localhost:9998/tika"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+----
+
+Run with:
+
+[source,bash]
+----
+docker-compose up -d
+----
+
+=== Using Secrets (Docker Swarm / Kubernetes)
+
+For production deployments, avoid storing passwords in plain text config files.
+
+==== Docker Swarm
+
+Use Docker secrets for sensitive data:
+
+[source,bash]
+----
+# Create secrets
+echo "your-keystore-password" | docker secret create tika_keystore_password -
+echo "your-truststore-password" | docker secret create
tika_truststore_password -
+
+# Reference in docker-compose.yml (swarm mode)
+services:
+ tika:
+ image: apache/tika:latest
+ secrets:
+ - tika_keystore_password
+ - tika_truststore_password
+ # Read password from /run/secrets/tika_keystore_password in entrypoint
script
+----
+
+==== Kubernetes
+
+Use Kubernetes secrets:
+
+[source,yaml]
+----
+# Create secret for certificates
+kubectl create secret generic tika-tls-certs \
+ --from-file=server-keystore.p12=./certs/server-keystore.p12 \
+ --from-file=server-truststore.p12=./certs/server-truststore.p12
+
+# Create secret for passwords
+kubectl create secret generic tika-tls-passwords \
+ --from-literal=keystore-password=your-password \
+ --from-literal=truststore-password=your-password
+----
+
+[source,yaml]
+----
+# deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: tika-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: tika
+ template:
+ metadata:
+ labels:
+ app: tika
+ spec:
+ containers:
+ - name: tika
+ image: apache/tika:latest
+ ports:
+ - containerPort: 9998
+ args: ["-c", "/tika/config/tika-config.json"]
+ volumeMounts:
+ - name: config
+ mountPath: /tika/config
+ readOnly: true
+ - name: certs
+ mountPath: /tika/certs
+ readOnly: true
+ env:
+ - name: KEYSTORE_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: tika-tls-passwords
+ key: keystore-password
+ volumes:
+ - name: config
+ configMap:
+ name: tika-config
+ - name: certs
+ secret:
+ secretName: tika-tls-certs
+----
+
+=== Testing Docker TLS Setup
+
+Test that TLS is working:
+
+[source,bash]
+----
+# 1-way TLS (trust server certificate)
+curl --cacert ./certs/server.crt https://localhost:9998/tika
+
+# 2-way TLS (provide client certificate)
+curl --cacert ./certs/server.crt \
+ --cert ./certs/client.crt \
+ --key ./certs/client-key.pem \
+ https://localhost:9998/tika
+----
+
+If using self-signed certificates and just testing:
+
+[source,bash]
+----
+# Skip certificate verification (NOT for production!)
+curl -k https://localhost:9998/tika
+----
+
+== Configuration Reference
+
+=== tlsConfig Properties
+
+[cols="2,1,1,4"]
+|===
+|Property |Type |Default |Description
+
+|`active`
+|boolean
+|false
+|Enable TLS. When true, the server uses HTTPS.
+
+|`keyStoreType`
+|string
+|null
+|Keystore format: `PKCS12` (recommended) or `JKS`.
+
+|`keyStoreFile`
+|string
+|null
+|Path to the server's keystore file containing its private key and certificate.
+
+|`keyStorePassword`
+|string
+|null
+|Password for the keystore.
+
+|`trustStoreType`
+|string
+|null
+|Truststore format: `PKCS12` (recommended) or `JKS`.
+
+|`trustStoreFile`
+|string
+|null
+|Path to the truststore containing trusted client certificates (for 2-way TLS).
+
+|`trustStorePassword`
+|string
+|null
+|Password for the truststore.
+
+|`clientAuthenticationWanted`
+|boolean
+|false
+|Request client certificates but don't require them. Clients without
certificates can still connect.
+
+|`clientAuthenticationRequired`
+|boolean
+|false
+|Require client certificates. Clients must present a valid certificate trusted
by the server.
+
+|`includedProtocols`
+|list
+|["TLSv1.2", "TLSv1.3"]
+|TLS protocol versions to enable. Default only allows TLS 1.2 and 1.3.
+
+|`excludedProtocols`
+|list
+|null
+|TLS protocol versions to explicitly disable.
+
+|`includedCipherSuites`
+|list
+|null
+|Cipher suites to enable. If null, uses JVM defaults.
+
+|`excludedCipherSuites`
+|list
+|null
+|Cipher suites to disable.
+
+|`certExpirationWarningDays`
+|integer
+|30
+|Days before certificate expiration to log warnings. Set to 0 to disable.
+|===
+
+== Certificate Expiration Warnings
+
+Tika Server automatically checks certificate expiration at startup and logs
warnings
+for certificates that will expire soon. This helps prevent unexpected outages
due to
+expired certificates.
+
+=== Warning Levels
+
+* **ERROR**: Certificate has already expired or is not yet valid - TLS will
fail
+* **WARN**: Certificate expires within the configured threshold (default: 30
days)
+* **DEBUG**: Certificate is valid (logged at debug level)
+
+=== Example Log Output
+
+[source]
+----
+WARN TlsConfig - Certificate 'server' in keystore expires in 15 days on Sat
Feb 15 12:00:00 UTC 2026. Consider renewing soon.
+ERROR TlsConfig - Certificate 'server' in keystore has EXPIRED on Mon Jan 01
12:00:00 UTC 2026. TLS connections will fail!
+----
+
+=== Configuring the Warning Threshold
+
+To change when warnings are logged (default is 30 days before expiration):
+
+[source,json]
+----
+{
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "certExpirationWarningDays": 60
+ }
+}
+----
+
+Set to `0` to disable expiration checking.
+
+== Generating Certificates
+
+This section shows how to generate self-signed certificates for testing.
+For production, use certificates from a trusted Certificate Authority (CA).
+
+=== Generate Server Keystore
+
+[source,bash]
+----
+# Generate server private key and certificate
+keytool -genkeypair \
+ -alias server \
+ -keyalg RSA \
+ -keysize 2048 \
+ -validity 365 \
+ -keystore server-keystore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -dname "CN=localhost,OU=Tika,O=Apache,L=Unknown,ST=Unknown,C=US"
+
+# Export server certificate (for clients to trust)
+keytool -exportcert \
+ -alias server \
+ -keystore server-keystore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -file server.crt
+----
+
+=== Generate Client Keystore (for 2-Way TLS)
+
+[source,bash]
+----
+# Generate client private key and certificate
+keytool -genkeypair \
+ -alias client \
+ -keyalg RSA \
+ -keysize 2048 \
+ -validity 365 \
+ -keystore client-keystore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -dname "CN=TikaClient,OU=Tika,O=Apache,L=Unknown,ST=Unknown,C=US"
+
+# Export client certificate (for server to trust)
+keytool -exportcert \
+ -alias client \
+ -keystore client-keystore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -file client.crt
+----
+
+=== Create Truststores
+
+[source,bash]
+----
+# Create server truststore (import client certificate)
+keytool -importcert \
+ -alias client \
+ -file client.crt \
+ -keystore server-truststore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -noprompt
+
+# Create client truststore (import server certificate)
+keytool -importcert \
+ -alias server \
+ -file server.crt \
+ -keystore client-truststore.p12 \
+ -storetype PKCS12 \
+ -storepass changeit \
+ -noprompt
+----
+
+== Protocol and Cipher Configuration
+
+=== Restricting TLS Versions
+
+By default, only TLS 1.2 and TLS 1.3 are enabled. TLS 1.0 and 1.1 are
considered
+insecure and are not enabled by default.
+
+To use only TLS 1.3:
+
+[source,json]
+----
+{
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "includedProtocols": ["TLSv1.3"]
+ }
+}
+----
+
+=== Restricting Cipher Suites
+
+To allow only specific cipher suites:
+
+[source,json]
+----
+{
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "includedCipherSuites": [
+ "TLS_AES_256_GCM_SHA384",
+ "TLS_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
+ ]
+ }
+}
+----
+
+To exclude weak cipher suites:
+
+[source,json]
+----
+{
+ "tlsConfig": {
+ "active": true,
+ "keyStoreType": "PKCS12",
+ "keyStoreFile": "/path/to/server-keystore.p12",
+ "keyStorePassword": "your-password",
+ "excludedCipherSuites": [
+ ".*CBC.*",
+ ".*RC4.*",
+ ".*3DES.*",
+ ".*NULL.*"
+ ]
+ }
+}
+----
+
+== Testing TLS Configuration
+
+=== Test with curl (1-Way TLS)
+
+[source,bash]
+----
+# Trust the server certificate
+curl --cacert server.crt https://localhost:9998/tika
+----
+
+=== Test with curl (2-Way TLS)
+
+[source,bash]
+----
+# Provide client certificate and trust server certificate
+curl --cacert server.crt \
+ --cert client.crt \
+ --key client-key.pem \
+ https://localhost:9998/tika
+----
+
+=== Test with Java Client
+
+[source,java]
+----
+// Configure SSL context for 2-way TLS
+SSLContext sslContext = SSLContexts.custom()
+ .loadKeyMaterial(
+ new File("client-keystore.p12"),
+ "changeit".toCharArray(),
+ "changeit".toCharArray())
+ .loadTrustMaterial(
+ new File("client-truststore.p12"),
+ "changeit".toCharArray())
+ .build();
+
+HttpClient client = HttpClients.custom()
+ .setSSLContext(sslContext)
+ .build();
+
+HttpGet request = new HttpGet("https://localhost:9998/tika");
+HttpResponse response = client.execute(request);
+----
+
+== Security Best Practices
+
+1. **Use strong passwords** for keystores and truststores
+2. **Use TLS 1.2 or 1.3** only (default configuration)
+3. **Prefer PKCS12** format over JKS (better interoperability)
+4. **Use certificates from a trusted CA** in production
+5. **Rotate certificates** before they expire
+6. **Restrict cipher suites** to strong, modern algorithms
+7. **Use 2-way TLS** when clients need to be authenticated
+8. **Protect keystore files** with appropriate file system permissions
+9. **Never commit passwords** to version control; use environment variables or
secrets management
+
+== Troubleshooting
+
+=== Common Errors
+
+**"keyStoreFile does not exist or is not a file"**
+
+The specified keystore file path is incorrect or the file doesn't exist.
+Verify the path is absolute or relative to the working directory.
+
+**"Partial truststore configuration detected"**
+
+You've set some but not all truststore properties. Either set all three
+(`trustStoreType`, `trustStoreFile`, `trustStorePassword`) or none.
+
+**"requiring client authentication, but no trust store has been specified"**
+
+You've set `clientAuthenticationRequired: true` but haven't configured a
truststore.
+The server needs a truststore to verify client certificates.
+
+**SSL handshake failures**
+
+- Verify certificates are valid and not expired
+- Ensure client trusts server certificate (for 1-way TLS)
+- Ensure server trusts client certificate (for 2-way TLS)
+- Check that protocol versions and cipher suites are compatible
+
+=== Enable SSL Debug Logging
+
+Add this JVM argument to see detailed SSL handshake information:
+
+[source,bash]
+----
+java -Djavax.net.debug=ssl:handshake -jar tika-server-standard.jar -c
config.json
+----
+
+== See Also
+
+* xref:security.adoc[Security] - General security considerations
+* xref:advanced/robustness.adoc[Robustness] - Process isolation and fault
tolerance
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index 4edf08bc9a..23e44852ea 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -38,6 +38,7 @@ import org.apache.cxf.binding.BindingFactoryManager;
import org.apache.cxf.configuration.jsse.TLSParameterJaxBUtils;
import org.apache.cxf.configuration.jsse.TLSServerParameters;
import org.apache.cxf.configuration.security.ClientAuthentication;
+import org.apache.cxf.configuration.security.FiltersType;
import org.apache.cxf.configuration.security.KeyManagersType;
import org.apache.cxf.configuration.security.KeyStoreType;
import org.apache.cxf.configuration.security.TrustManagersType;
@@ -209,6 +210,8 @@ public class TikaServerProcess {
.getTlsConfig()
.isActive()) {
LOG.warn("The TLS configuration is in BETA and might change " +
"dramatically in future releases.");
+ // Check for expiring certificates and log warnings
+ tikaServerConfig.getTlsConfig().checkCertificateExpiration();
TLSServerParameters tlsParams =
getTlsParams(tikaServerConfig.getTlsConfig());
JettyHTTPServerEngineFactory factory = new
JettyHTTPServerEngineFactory();
factory.setBus(sf.getBus());
@@ -250,6 +253,28 @@ public class TikaServerProcess {
clientAuthentication.setRequired(tlsConfig.isClientAuthenticationRequired());
clientAuthentication.setWant(tlsConfig.isClientAuthenticationWanted());
parameters.setClientAuthentication(clientAuthentication);
+
+ // Configure TLS protocols
+ if (tlsConfig.getIncludedProtocols() != null &&
!tlsConfig.getIncludedProtocols().isEmpty()) {
+ parameters.setIncludeProtocols(tlsConfig.getIncludedProtocols());
+ }
+ if (tlsConfig.getExcludedProtocols() != null &&
!tlsConfig.getExcludedProtocols().isEmpty()) {
+ parameters.setExcludeProtocols(tlsConfig.getExcludedProtocols());
+ }
+
+ // Configure cipher suites
+ if ((tlsConfig.getIncludedCipherSuites() != null &&
!tlsConfig.getIncludedCipherSuites().isEmpty()) ||
+ (tlsConfig.getExcludedCipherSuites() != null &&
!tlsConfig.getExcludedCipherSuites().isEmpty())) {
+ FiltersType cipherSuitesFilter = new FiltersType();
+ if (tlsConfig.getIncludedCipherSuites() != null) {
+
cipherSuitesFilter.getInclude().addAll(tlsConfig.getIncludedCipherSuites());
+ }
+ if (tlsConfig.getExcludedCipherSuites() != null) {
+
cipherSuitesFilter.getExclude().addAll(tlsConfig.getExcludedCipherSuites());
+ }
+ parameters.setCipherSuitesFilter(cipherSuitesFilter);
+ }
+
return parameters;
}
diff --git
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TlsConfig.java
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TlsConfig.java
index c0adb23bd5..5fbfb43820 100644
---
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TlsConfig.java
+++
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TlsConfig.java
@@ -16,14 +16,45 @@
*/
package org.apache.tika.server.core;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.utils.StringUtils;
public class TlsConfig {
- //TODO make this configurable
- private final boolean passwordsAESEncrypted = false;
+ private static final Logger LOG = LoggerFactory.getLogger(TlsConfig.class);
+
+ /**
+ * Default TLS protocols - only TLS 1.2 and 1.3 are enabled by default.
+ * TLS 1.0 and 1.1 are considered insecure and should not be used.
+ */
+ public static final List<String> DEFAULT_PROTOCOLS =
Arrays.asList("TLSv1.2", "TLSv1.3");
+
+ /**
+ * Default warning threshold for certificate expiration (30 days).
+ */
+ public static final int DEFAULT_CERT_EXPIRATION_WARNING_DAYS = 30;
+
private boolean active = false;
+ private int certExpirationWarningDays =
DEFAULT_CERT_EXPIRATION_WARNING_DAYS;
private String keyStoreType = null;
private String keyStorePassword = null;
private String keyStoreFile = null;
@@ -32,9 +63,16 @@ public class TlsConfig {
private String trustStoreFile = null;
private boolean clientAuthenticationWanted = false;
-
private boolean clientAuthenticationRequired = false;
+ // TLS protocol configuration
+ private List<String> includedProtocols = DEFAULT_PROTOCOLS;
+ private List<String> excludedProtocols = null;
+
+ // Cipher suite configuration
+ private List<String> includedCipherSuites = null;
+ private List<String> excludedCipherSuites = null;
+
public boolean isActive() {
return active;
}
@@ -43,10 +81,6 @@ public class TlsConfig {
this.active = active;
}
- public boolean isPasswordsAESEncrypted() {
- return passwordsAESEncrypted;
- }
-
public String getKeyStoreType() {
return keyStoreType;
}
@@ -97,6 +131,7 @@ public class TlsConfig {
public void checkInitialization() throws TikaConfigException {
if (active) {
+ // Validate keystore configuration
if (StringUtils.isBlank(keyStoreType)) {
throw new TikaConfigException("must initialize keyStoreType");
} else if (StringUtils.isBlank(keyStoreFile)) {
@@ -104,21 +139,59 @@ public class TlsConfig {
} else if (StringUtils.isBlank(keyStorePassword)) {
throw new TikaConfigException("must initialize
keyStorePassword");
}
+
+ // Validate keystore file exists
+ File ksFile = new File(keyStoreFile);
+ if (!ksFile.isFile()) {
+ throw new TikaConfigException("keyStoreFile does not exist or
is not a file: " + keyStoreFile);
+ }
+
+ // Validate truststore configuration
if (hasTrustStore()) {
if (StringUtils.isBlank(trustStoreType)) {
- throw new TikaConfigException("must initialize
trustStoreType " + "if there's any trustStore info");
+ throw new TikaConfigException("must initialize
trustStoreType if there's any trustStore info");
} else if (StringUtils.isBlank(trustStoreFile)) {
- throw new TikaConfigException("must initialize
trustStoreFile " + "if there's any trustStore info");
+ throw new TikaConfigException("must initialize
trustStoreFile if there's any trustStore info");
} else if (StringUtils.isBlank(trustStorePassword)) {
- throw new TikaConfigException("must initialize
trustStorePassword " + "if there's any trustStore info");
+ throw new TikaConfigException("must initialize
trustStorePassword if there's any trustStore info");
+ }
+
+ // Validate truststore file exists
+ File tsFile = new File(trustStoreFile);
+ if (!tsFile.isFile()) {
+ throw new TikaConfigException("trustStoreFile does not
exist or is not a file: " + trustStoreFile);
}
}
+
+ // Warn about partial truststore configuration
+ checkPartialTrustStoreConfig();
+
if (!hasTrustStore() && isClientAuthenticationRequired()) {
- throw new TikaConfigException("requiring client
authentication, but no trust " + "store has been specified?!");
+ throw new TikaConfigException("requiring client
authentication, but no trust store has been specified");
}
}
}
+ /**
+ * Check for partial truststore configuration and throw an exception.
+ * If only some truststore fields are set, this is likely a configuration
error.
+ */
+ private void checkPartialTrustStoreConfig() throws TikaConfigException {
+ boolean hasType = !StringUtils.isBlank(trustStoreType);
+ boolean hasFile = !StringUtils.isBlank(trustStoreFile);
+ boolean hasPassword = !StringUtils.isBlank(trustStorePassword);
+
+ // If any field is set but not all, that's a configuration error
+ int setCount = (hasType ? 1 : 0) + (hasFile ? 1 : 0) + (hasPassword ?
1 : 0);
+ if (setCount > 0 && setCount < 3) {
+ StringBuilder missing = new StringBuilder("Partial truststore
configuration detected. Missing: ");
+ if (!hasType) missing.append("trustStoreType ");
+ if (!hasFile) missing.append("trustStoreFile ");
+ if (!hasPassword) missing.append("trustStorePassword ");
+ throw new TikaConfigException(missing.toString().trim());
+ }
+ }
+
public boolean isClientAuthenticationWanted() {
return clientAuthenticationWanted;
}
@@ -135,12 +208,138 @@ public class TlsConfig {
this.clientAuthenticationRequired = clientAuthenticationRequired;
}
+ public List<String> getIncludedProtocols() {
+ return includedProtocols;
+ }
+
+ public void setIncludedProtocols(List<String> includedProtocols) {
+ this.includedProtocols = includedProtocols;
+ }
+
+ public List<String> getExcludedProtocols() {
+ return excludedProtocols;
+ }
+
+ public void setExcludedProtocols(List<String> excludedProtocols) {
+ this.excludedProtocols = excludedProtocols;
+ }
+
+ public List<String> getIncludedCipherSuites() {
+ return includedCipherSuites;
+ }
+
+ public void setIncludedCipherSuites(List<String> includedCipherSuites) {
+ this.includedCipherSuites = includedCipherSuites;
+ }
+
+ public List<String> getExcludedCipherSuites() {
+ return excludedCipherSuites;
+ }
+
+ public void setExcludedCipherSuites(List<String> excludedCipherSuites) {
+ this.excludedCipherSuites = excludedCipherSuites;
+ }
+
+ public int getCertExpirationWarningDays() {
+ return certExpirationWarningDays;
+ }
+
+ public void setCertExpirationWarningDays(int certExpirationWarningDays) {
+ this.certExpirationWarningDays = certExpirationWarningDays;
+ }
+
+ /**
+ * Check certificate expiration dates and log warnings for certificates
+ * expiring within the configured threshold.
+ * <p>
+ * This method should be called after {@link #checkInitialization()} to
+ * warn about upcoming certificate expirations.
+ */
+ public void checkCertificateExpiration() {
+ if (!active || certExpirationWarningDays <= 0) {
+ return;
+ }
+
+ Instant warningThreshold =
Instant.now().plus(Duration.ofDays(certExpirationWarningDays));
+
+ // Check keystore certificates
+ if (!StringUtils.isBlank(keyStoreFile)) {
+ checkKeystoreExpiration(keyStoreFile, keyStoreType,
keyStorePassword, "keystore", warningThreshold);
+ }
+
+ // Check truststore certificates
+ if (hasTrustStore()) {
+ checkKeystoreExpiration(trustStoreFile, trustStoreType,
trustStorePassword, "truststore", warningThreshold);
+ }
+ }
+
+ private void checkKeystoreExpiration(String file, String type, String
password,
+ String storeName, Instant
warningThreshold) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ KeyStore keyStore = KeyStore.getInstance(type);
+ keyStore.load(fis, password.toCharArray());
+
+ Enumeration<String> aliases = keyStore.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ Certificate cert = keyStore.getCertificate(alias);
+
+ if (cert instanceof X509Certificate) {
+ X509Certificate x509 = (X509Certificate) cert;
+ Date notAfter = x509.getNotAfter();
+ Date notBefore = x509.getNotBefore();
+ Instant now = Instant.now();
+
+ // Check if already expired
+ if (notAfter.toInstant().isBefore(now)) {
+ LOG.error("Certificate '{}' in {} has EXPIRED on {}. "
+
+ "TLS connections will fail!",
+ alias, storeName, notAfter);
+ }
+ // Check if not yet valid
+ else if (notBefore.toInstant().isAfter(now)) {
+ LOG.error("Certificate '{}' in {} is not yet valid
until {}. " +
+ "TLS connections will fail!",
+ alias, storeName, notBefore);
+ }
+ // Check if expiring soon
+ else if (notAfter.toInstant().isBefore(warningThreshold)) {
+ long daysUntilExpiry = Duration.between(now,
notAfter.toInstant()).toDays();
+ LOG.warn("Certificate '{}' in {} expires in {} days on
{}. " +
+ "Consider renewing soon.",
+ alias, storeName, daysUntilExpiry, notAfter);
+ } else {
+ LOG.debug("Certificate '{}' in {} is valid until {}",
+ alias, storeName, notAfter);
+ }
+ }
+ }
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException |
CertificateException e) {
+ LOG.warn("Unable to check certificate expiration for {}: {}",
storeName, e.getMessage());
+ }
+ }
+
@Override
public String toString() {
- return "TlsConfig{" + "active=" + active + ", passwordsAESEncrypted="
+ passwordsAESEncrypted + ", keyStoreType='" + keyStoreType + '\'' + ",
keyStorePassword='" +
- keyStorePassword + '\'' + ", keyStoreFile='" + keyStoreFile +
'\'' + ", trustStoreType='" + trustStoreType + '\'' + ", trustStorePassword='"
+ trustStorePassword +
- '\'' + ", trustStoreFile='" + trustStoreFile + '\'' + ",
clientAuthenticationWanted=" + clientAuthenticationWanted + ",
isClientAuthenticationRequired=" +
- clientAuthenticationRequired + '}';
+ return "TlsConfig{" +
+ "active=" + active +
+ ", keyStoreType='" + keyStoreType + '\'' +
+ ", keyStorePassword='" + maskPassword(keyStorePassword) + '\''
+
+ ", keyStoreFile='" + keyStoreFile + '\'' +
+ ", trustStoreType='" + trustStoreType + '\'' +
+ ", trustStorePassword='" + maskPassword(trustStorePassword) +
'\'' +
+ ", trustStoreFile='" + trustStoreFile + '\'' +
+ ", clientAuthenticationWanted=" + clientAuthenticationWanted +
+ ", clientAuthenticationRequired=" +
clientAuthenticationRequired +
+ ", includedProtocols=" + includedProtocols +
+ ", excludedProtocols=" + excludedProtocols +
+ ", includedCipherSuites=" + includedCipherSuites +
+ ", excludedCipherSuites=" + excludedCipherSuites +
+ '}';
+ }
+
+ private static String maskPassword(String password) {
+ return password == null ? "null" : "****";
}
public boolean hasTrustStore() {