This is an automated email from the ASF dual-hosted git repository. tallison pushed a commit to branch TIKA-4642 in repository https://gitbox.apache.org/repos/asf/tika.git
commit f2f324d8d2c3185e1232cc23249a136eff43e273 Author: tallison <[email protected]> AuthorDate: Sat Jan 31 18:03:07 2026 -0500 TIKA-4642 - improve tls configuration and documentation --- 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() {
