From 1414f8993a616a8dc2d5430e3a646e3075b87b57 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 03 Sep 2026 08:07:08 +0000
Subject: [PATCH] [#905] Warn when a replication handshake fails and document CA-signed certificates (#906)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/ReplSessionSecurity.java |   91 +++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 87 insertions(+), 4 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/ReplSessionSecurity.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/ReplSessionSecurity.java
index 3799ab6..b953df7 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/ReplSessionSecurity.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/protocol/ReplSessionSecurity.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.replication.protocol;
 
@@ -22,6 +23,8 @@
 import java.io.IOException;
 import java.net.Socket;
 import java.util.SortedSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLException;
@@ -49,6 +52,29 @@
   private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
   /**
+   * Minimum interval, in minutes, between two warnings about a failed SSL handshake
+   * on the replication port. Every connection which is not a replication peer fails
+   * the handshake, network probes included, so only the first failure of an interval
+   * is logged as a warning and the following ones are logged at debug level.
+   */
+  private static final long HANDSHAKE_FAILURE_WARN_INTERVAL_MINUTES = 5;
+
+  /** Package private for testing. */
+  static final long HANDSHAKE_FAILURE_WARN_INTERVAL_NANOS =
+      TimeUnit.MINUTES.toNanos(HANDSHAKE_FAILURE_WARN_INTERVAL_MINUTES);
+
+  /**
+   * Value of {@link System#nanoTime()} at which the last handshake failure was
+   * logged as a warning. It starts one interval in the past so that the first
+   * failure is warned about.
+   */
+  private final AtomicLong lastHandshakeFailureWarnNanos =
+      new AtomicLong(System.nanoTime() - HANDSHAKE_FAILURE_WARN_INTERVAL_NANOS);
+
+  /** Number of handshake failures logged at debug level since the last warning. */
+  private final AtomicLong suppressedHandshakeFailures = new AtomicLong();
+
+  /**
    * Whether replication sessions use SSL encryption.
    */
   private final boolean sslEncryption;
@@ -253,10 +279,10 @@
     }
     catch (final SSLException e)
     {
-      // This is probably a connection attempt from an unexpected client
-      // log that to warn the administrator.
-      logger.debug(INFO_SSL_SERVER_CON_ATTEMPT_ERROR, socket.getRemoteSocketAddress(),
-          socket.getLocalSocketAddress(), e.getLocalizedMessage());
+      // This may be a connection attempt from an unexpected client, but it is
+      // also how a certificate misconfiguration shows up, so warn the
+      // administrator instead of failing silently.
+      logHandshakeFailure(socket, e);
       return null;
     }
     finally
@@ -272,6 +298,63 @@
 
 
   /**
+   * Logs a failed SSL handshake on the replication port, as a warning for the
+   * first failure of each {@link #HANDSHAKE_FAILURE_WARN_INTERVAL_MINUTES}
+   * interval and at debug level for the following ones. The warning reports how
+   * many failures were logged at debug level before it, so that a single line
+   * cannot be mistaken for a single failed connection. That count looks backwards
+   * only: the failures which follow the last warning of a burst are counted but
+   * never reported, as nothing flushes the count when the failures stop.
+   *
+   * @param socket
+   *          The socket the handshake failed on.
+   * @param e
+   *          The handshake failure.
+   */
+  private void logHandshakeFailure(final Socket socket, final SSLException e)
+  {
+    final long recorded = recordHandshakeFailure(System.nanoTime());
+    if (recorded >= 0)
+    {
+      logger.warn(WARN_SSL_SERVER_CON_ATTEMPT_ERROR, socket.getRemoteSocketAddress(),
+          socket.getLocalSocketAddress(), HANDSHAKE_FAILURE_WARN_INTERVAL_MINUTES,
+          recorded, e.getLocalizedMessage());
+    }
+    else
+    {
+      logger.debug(WARN_SSL_SERVER_CON_ATTEMPT_ERROR, socket.getRemoteSocketAddress(),
+          socket.getLocalSocketAddress(), HANDSHAKE_FAILURE_WARN_INTERVAL_MINUTES,
+          -recorded - 1, e.getLocalizedMessage());
+    }
+  }
+
+  /**
+   * Records a handshake failure which happened at the provided time and tells how it
+   * must be logged, together with the number of failures logged at debug level since
+   * the previous warning.
+   * <p>
+   * Package private for testing.
+   *
+   * @param nowNanos
+   *          The value of {@link System#nanoTime()} at which the handshake failed.
+   * @return A number greater than or equal to zero if this failure is to be logged as a
+   *         warning, which is then the number of failures logged at debug level since
+   *         the previous warning, or {@code -count - 1} if this failure is itself to be
+   *         logged at debug level, where {@code count} is the number of failures logged
+   *         at debug level since the previous warning, this one included.
+   */
+  long recordHandshakeFailure(final long nowNanos)
+  {
+    final long lastWarn = lastHandshakeFailureWarnNanos.get();
+    if (nowNanos - lastWarn >= HANDSHAKE_FAILURE_WARN_INTERVAL_NANOS
+        && lastHandshakeFailureWarnNanos.compareAndSet(lastWarn, nowNanos))
+    {
+      return suppressedHandshakeFailures.getAndSet(0);
+    }
+    return -suppressedHandshakeFailures.incrementAndGet() - 1;
+  }
+
+  /**
    * Determine whether sessions to a given replication server should be
    * encrypted.
    *

--
Gitblit v1.10.0