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() {


Reply via email to