This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/main by this push:
     new e2626aafa Use OS certificates for replication
e2626aafa is described below

commit e2626aafa47fe465eb3a9ac0ed7652334d93a728
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Fri Sep 5 19:06:02 2025 -0400

    Use OS certificates for replication
    
    Since OTP 25 there is an easy way to load OS provided CA certificates on 
all of
    our supported platforms. Use that to simplify peer verification in the
    replicator.
    
    If the user already provided a path to the CA certificates file, load that. 
If
    there isn't a CA cert path, attempt to use the OS CA certs, and if that 
fails
    too, then crash with an error and do not continue.
    
    To help users diagnose the issue early, explicitly load the OS CA certs in 
the
    replicator connection pool gen_server on startup. Emit an info log about the
    number of loaded certs or an error if it fails. CA certs after the first
    successful load are cached in a persistent term by the OTP.
    
    To prevent CA certificate from become stale, at least every 24 hours clear 
the
    CA certs in memory cache and force reload it from disk. The period is
    configurable via `cacert_reload_interval_hours` setting.
    
    Issue: https://github.com/apache/couchdb/issues/5638
---
 rel/overlay/etc/default.ini                        |  4 ++
 .../src/couch_replicator_connection.erl            |  3 ++
 .../src/couch_replicator_parse.erl                 | 25 ++++++++---
 .../src/couch_replicator_utils.erl                 | 50 +++++++++++++++++++++-
 src/docs/src/config/replicator.rst                 | 16 ++++++-
 5 files changed, 90 insertions(+), 8 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index f972a0a10..6cffc12fa 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -699,6 +699,10 @@ partitioned||* = true
 ; Maximum peer certificate depth (must be set even if certificate validation 
is off).
 ;ssl_certificate_max_depth = 3
 
+; How often to reload operating system CA certificates (in hours). The default
+; is 24 hours.
+;cacert_reload_interval_hours = 24
+
 ; Maximum document ID length for replication.
 ;max_document_id_length = infinity
 
diff --git a/src/couch_replicator/src/couch_replicator_connection.erl 
b/src/couch_replicator/src/couch_replicator_connection.erl
index 978962bd7..110c792bb 100644
--- a/src/couch_replicator/src/couch_replicator_connection.erl
+++ b/src/couch_replicator/src/couch_replicator_connection.erl
@@ -77,6 +77,9 @@ init([]) ->
         {inactivity_timeout, Interval},
         {worker_trap_exits, false}
     ]),
+    % Try loading all the OS CA certs to give users an early indication in the
+    % logs if there is an error.
+    couch_replicator_utils:cacert_get(),
     {ok, #state{close_interval = Interval, timer = Timer}}.
 
 acquire(Url) ->
diff --git a/src/couch_replicator/src/couch_replicator_parse.erl 
b/src/couch_replicator/src/couch_replicator_parse.erl
index b72e1f576..5f8437992 100644
--- a/src/couch_replicator/src/couch_replicator_parse.erl
+++ b/src/couch_replicator/src/couch_replicator_parse.erl
@@ -488,15 +488,28 @@ ssl_params(Url) ->
 
 -spec ssl_verify_options(true | false) -> [_].
 ssl_verify_options(true) ->
-    CAFile = cfg("ssl_trusted_certificates_file"),
-    [
-        {verify, verify_peer},
-        {customize_hostname_check, [{match_fun, 
public_key:pkix_verify_hostname_match_fun(https)}]},
-        {cacertfile, CAFile}
-    ];
+    % 
https://security.erlef.org/secure_coding_and_deployment_hardening/ssl.html
+    ssl_ca_cert_opts() ++
+        [
+            {verify, verify_peer},
+            {customize_hostname_check, [
+                {match_fun, public_key:pkix_verify_hostname_match_fun(https)}
+            ]}
+        ];
 ssl_verify_options(false) ->
     [{verify, verify_none}].
 
+ssl_ca_cert_opts() ->
+    % Try to use the CA cert file from config first, and if not specified, use
+    % the CA certificates from the OS. If those can't be loaded either, then
+    % crash: cacerts_get/0 raises an error in that case and we do not catch it.
+    case cfg("ssl_trusted_certificates_file") of
+        undefined ->
+            [{cacerts, public_key:cacerts_get()}];
+        CAFile when is_list(CAFile) ->
+            [{cacertfile, CAFile}]
+    end.
+
 get_value(Key, Props) ->
     couch_util:get_value(Key, Props).
 
diff --git a/src/couch_replicator/src/couch_replicator_utils.erl 
b/src/couch_replicator/src/couch_replicator_utils.erl
index 9ce786646..e776f4dc0 100644
--- a/src/couch_replicator/src/couch_replicator_utils.erl
+++ b/src/couch_replicator/src/couch_replicator_utils.erl
@@ -30,7 +30,8 @@
     normalize_basic_auth/1,
     seq_encode/1,
     valid_endpoint_protocols_log/1,
-    verify_ssl_certificates_log/1
+    verify_ssl_certificates_log/1,
+    cacert_get/0
 ]).
 
 -include_lib("ibrowse/include/ibrowse.hrl").
@@ -40,6 +41,10 @@
 -include_lib("couch_replicator/include/couch_replicator_api_wrap.hrl").
 -include_lib("public_key/include/public_key.hrl").
 
+-define(CACERT_KEY, {?MODULE, cacert_timestamp_key}).
+-define(CACERT_DEFAULT_TIMESTAMP, -(1 bsl 59)).
+-define(CACERT_DEFAULT_INTERVAL_HOURS, 24).
+
 -import(couch_util, [
     get_value/2,
     get_value/3
@@ -402,6 +407,34 @@ rep_principal(#rep{user_ctx = #user_ctx{name = Name}}) 
when is_binary(Name) ->
 rep_principal(#rep{}) ->
     "by unknown principal".
 
+cacert_get() ->
+    Now = erlang:monotonic_time(second),
+    Max = cacert_reload_interval_sec(),
+    TStamp = persistent_term:get(?CACERT_KEY, ?CACERT_DEFAULT_TIMESTAMP),
+    cacert_load(TStamp, Now, Max),
+    public_key:cacerts_get().
+
+cacert_load(TStamp, Now, Max) when (Now - TStamp) > Max ->
+    public_key:cacerts_clear(),
+    case public_key:cacerts_load() of
+        ok ->
+            Count = length(public_key:cacerts_get()),
+            InfoMsg = "~p : loaded ~p os ca certificates",
+            couch_log:info(InfoMsg, [?MODULE, Count]);
+        {error, Reason} ->
+            ErrMsg = "~p : error loading os ca certificates: ~p",
+            couch_log:error(ErrMsg, [?MODULE, Reason])
+    end,
+    persistent_term:put(?CACERT_KEY, Now),
+    loaded;
+cacert_load(_TStamp, _Now, _Max) ->
+    not_loaded.
+
+cacert_reload_interval_sec() ->
+    Default = ?CACERT_DEFAULT_INTERVAL_HOURS,
+    Hrs = config:get_integer("replicator", "cacert_reload_interval_hours", 
Default),
+    Hrs * 3600.
+
 -ifdef(TEST).
 
 -include_lib("couch/include/couch_eunit.hrl").
@@ -778,4 +811,19 @@ t_allow_canceling_transient_jobs(_) ->
     ?assertEqual(ok, valid_endpoint_protocols_log(#rep{})),
     ?assertEqual(0, meck:num_calls(couch_log, warning, 2)).
 
+cacert_test() ->
+    Old = ?CACERT_DEFAULT_TIMESTAMP,
+    Now = erlang:monotonic_time(second),
+    Max = 0,
+    ?assertEqual(loaded, cacert_load(Old, Now, Max)),
+    ?assertEqual(not_loaded, cacert_load(Now, Now, Max)),
+    try cacert_get() of
+        CACerts ->
+            ?assert(is_list(CACerts))
+    catch
+        error:_Err ->
+            % This is ok, some environments may not have OS certs
+            ?assert(true)
+    end.
+
 -endif.
diff --git a/src/docs/src/config/replicator.rst 
b/src/docs/src/config/replicator.rst
index 2fb3b2ca6..bca107ae3 100644
--- a/src/docs/src/config/replicator.rst
+++ b/src/docs/src/config/replicator.rst
@@ -303,7 +303,9 @@ Replicator Database Configuration
 
     .. config:option:: verify_ssl_certificates :: Check peer certificates
 
-        Set to true to validate peer certificates::
+        Set to true to validate peer certificates. If
+        ``ssl_trusted_certificates_file`` is set it will be used, otherwise the
+        operating system CA files will be used::
 
             [replicator]
             verify_ssl_certificates = false
@@ -325,6 +327,18 @@ Replicator Database Configuration
             [replicator]
             ssl_certificate_max_depth = 3
 
+    .. config:option:: cacert_reload_interval_hours :: CA certificates reload 
interval
+
+         How often to reload operating system CA certificates (in hours).
+         Erlang VM caches OS CA certificates in memory after they are loaded
+         the first time. This setting specifies how often to clear the cache
+         and force reload certificate from disk. This can be useful if the VM
+         node is up for a long time, and the the CA certificate files are
+         updated using operating system packaging system during that time::
+
+             [replicator]
+             cacert_reload_interval_hours = 24
+
     .. config:option:: auth_plugins :: List of replicator client 
authentication plugins
 
         .. versionadded:: 2.2

Reply via email to