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