You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2023/06/02 16:10:20 UTC

[couchdb] branch main updated: TLS: add `{verify, verify_peer}` to enable verification

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 416c1cd8d TLS: add `{verify, verify_peer}` to enable verification
416c1cd8d is described below

commit 416c1cd8d01b3c47bdbfd48e3899b295d05218c3
Author: jiahuili <li...@gmail.com>
AuthorDate: Mon May 22 09:45:07 2023 -0500

    TLS: add `{verify, verify_peer}` to enable verification
    
    When the CouchDB custom (couch) distribution is enabled, OTP 24 and 25,
    you will get a warning when using `remsh-tls` or `remsh -t`; but in OTP 26,
    this is an error. Because the default value of the verify option is `verify_peer`
    instead of `verify_none`. We need to provide the appropriate CA certificates to
    pass the validation.
    
    ```bash
    $ ./dev/remsh-tls
    =WARNING REPORT==== 4-May-2023::12:43:28.893022 ===
    Description: "Server authenticity is not verified since certificate path validation is not enabled"
         Reason: "The option {verify, verify_peer} and one of the options 'cacertfile' or 'cacerts' are required to enable this."
    ```
    
    ```bash
    $ ./dev/remsh-tls
    Could not connect to "node1@127.0.0.1"
    ```
    
    - Add `{verify, verify_peer}` to enable verification
    - Add `certs` in `couch_dist` app to generate proper certificates
    - Add `-t`/`--enable-tls` mode in ./dev/run to automatically generate
    vm.args, certificates and configuration files
    - Add `--no-tls <node>` option to specify node to use TCP only
    - Add README and documentation
    - Remove TLS certificate generation code from `configure`
    
    Greate thanks to Robert Newson for figuring out how to generate the
    correct certificates to pass verification!
    
    Ref:
    - https://www.erlang.org/doc/apps/ssl/ssl_distribution.html
    - https://www.erlang.org/doc/man/ssl_app.html
    - https://www.erlang.org/blog/otp-26-highlights/#ssl-safer-defaults
    - https://github.com/rnewson/elixir-certs
---
 .gitignore                                       |   2 -
 Makefile                                         |   2 +-
 configure                                        |  27 ---
 dev/remsh-tls                                    |   8 +-
 dev/run                                          |  36 +++-
 rel/overlay/bin/remsh                            |   2 +-
 src/couch_dist/.gitignore                        |   4 +
 src/couch_dist/README.md                         | 162 ++++++++++++++++++
 src/couch_dist/certs/certs                       |  11 ++
 src/couch_dist/certs/certs.exs                   | 209 +++++++++++++++++++++++
 src/couch_dist/certs/parse_cert.escript          |   8 +
 src/couch_dist/gen_certs                         |  43 +++++
 src/docs/src/cluster/tls_erlang_distribution.rst |  53 ++++--
 13 files changed, 519 insertions(+), 48 deletions(-)

diff --git a/.gitignore b/.gitignore
index eaa50a6a5..30aed7787 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,8 +30,6 @@ dev/*.beam
 dev/devnode.*
 dev/lib/
 dev/logs/
-dev/erlserver.pem
-dev/couch_ssl_dist.conf
 ebin/
 erl_crash.dump
 erln8.config
diff --git a/Makefile b/Makefile
index 183afd5e4..1615cac67 100644
--- a/Makefile
+++ b/Makefile
@@ -476,7 +476,7 @@ clean:
 	@rm -f src/couch/priv/couchspawnkillable
 	@rm -f src/couch/priv/couch_js/config.h
 	@rm -f dev/*.beam dev/devnode.* dev/pbkdf2.pyc log/crash.log
-	@rm -f dev/erlserver.pem dev/couch_ssl_dist.conf
+	@rm -f src/couch_dist/certs/out
 ifeq ($(with_nouveau), 1)
 	@cd nouveau && ./gradlew clean
 endif
diff --git a/configure b/configure
index 8b1e43d6f..f90bfce77 100755
--- a/configure
+++ b/configure
@@ -63,31 +63,11 @@ Options:
   --spidermonkey-version VSN  specify the version of SpiderMonkey to use (defaults to $SM_VSN)
   --skip-deps                 do not update erlang dependencies
   --rebar=PATH                use rebar by specified path (version >=2.6.0 && <3.0 required)
-  --generate-tls-dev-cert     generate a cert for TLS distribution (To enable TLS, change the vm.args file.)
   --rebar3=PATH               use rebar3 by specified path
   --erlfmt=PATH               use erlfmt by specified path
 EOF
 }
 
-# This is just an example to generate a certfile for TLS distribution.
-# This is not an endorsement of specific expiration limits, key sizes, or algorithms.
-generate_tls_dev_cert() {
-    if [ ! -e "${rootdir}/dev/erlserver.pem" ]; then
-        openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
-        cat key.pem cert.pem > dev/erlserver.pem && rm key.pem cert.pem
-    fi
-
-    if [ ! -e "${rootdir}/dev/couch_ssl_dist.conf" ]; then
-        cat > "${rootdir}/dev/couch_ssl_dist.conf" << EOF
-[{server,
-  [{certfile, "${rootdir}/dev/erlserver.pem"},
-   {secure_renegotiate, true}]},
- {client,
-  [{secure_renegotiate, true}]}].
-EOF
-    fi
-}
-
 parse_opts() {
     while :; do
         case $1 in
@@ -221,13 +201,6 @@ parse_opts() {
                 exit 1
                 ;;
 
-            --generate-tls-dev-cert)
-                echo "WARNING: To enable TLS distribution, don't forget to customize vm.args file."
-                generate_tls_dev_cert
-                shift
-                continue
-                ;;
-
             --) # End of options
                 shift
                 break
diff --git a/dev/remsh-tls b/dev/remsh-tls
index 089db669f..6ac4f1dd4 100755
--- a/dev/remsh-tls
+++ b/dev/remsh-tls
@@ -23,7 +23,11 @@ if [ -z $HOST ]; then
     HOST="127.0.0.1"
 fi
 
+if [ -z $OPTFILE ]; then
+    rootdir=`dirname "$(cd "${0%/*}" 2>/dev/null; echo "$PWD")"`
+    OPTFILE="$rootdir/src/couch_dist/certs/out/couch_dist.conf"
+fi
+
 NAME="remsh$$@$HOST"
 NODE="node$NODE@$HOST"
-rootdir="$(cd "${0%/*}" 2>/dev/null; echo "$PWD")"
-erl -name $NAME -remsh $NODE -hidden -proto_dist inet_tls -ssl_dist_optfile "${rootdir}/couch_ssl_dist.conf"
+erl -name $NAME -remsh $NODE -hidden -proto_dist inet_tls -ssl_dist_optfile $OPTFILE
diff --git a/dev/run b/dev/run
index 8ac76b7b2..3b28bc431 100755
--- a/dev/run
+++ b/dev/run
@@ -117,7 +117,7 @@ def setup_argparse():
 
 
 def get_args_parser():
-    parser = optparse.OptionParser(description="Runs CouchDB 2.0 dev cluster")
+    parser = optparse.OptionParser(description="Runs CouchDB dev cluster")
     parser.add_option(
         "-a",
         "--admin",
@@ -234,6 +234,20 @@ def get_args_parser():
         action="store_true",
         help="Start Nouveau server",
     )
+    parser.add_option(
+        "-t",
+        "--enable-tls",
+        dest="enable_tls",
+        default=False,
+        action="store_true",
+        help="Enable custom TLS distribution -- couch",
+    )
+    parser.add_option(
+        "--no-tls",
+        dest="no_tls",
+        default=None,
+        help="Use TCP for specified node when TLS distribution is enabled",
+    )
     return parser
 
 
@@ -264,6 +278,8 @@ def setup_context(opts, args):
         "auto_ports": opts.auto_ports,
         "locald_configs": opts.locald_configs,
         "with_nouveau": opts.with_nouveau,
+        "enable_tls": opts.enable_tls,
+        "no_tls": opts.no_tls,
     }
 
 
@@ -421,6 +437,8 @@ def cluster_port(ctx, n):
 def write_config(ctx, node, env):
     etc_src = os.path.join(ctx["rootdir"], "rel", "overlay", "etc")
     etc_tgt = ensure_dir_exists(ctx["devdir"], "lib", node, "etc")
+    if ctx["enable_tls"]:
+        sp.call(["./src/couch_dist/gen_certs"], cwd=ctx["rootdir"])
 
     for fname in glob.glob(os.path.join(etc_src, "*")):
         base = os.path.basename(fname)
@@ -440,6 +458,8 @@ def write_config(ctx, node, env):
             content = apply_config_overrides(ctx, content)
         elif base == "local.ini":
             content = hack_local_ini(ctx, content)
+        elif ctx["enable_tls"] and base == "vm.args":
+            content = hack_vm_args(ctx, node, content)
 
         with open(tgt, "w") as handle:
             handle.write(content)
@@ -524,6 +544,20 @@ def hack_local_ini(ctx, contents):
     return contents + "\n\n[chttpd_auth]\nsecret = %s\n" % COMMON_SALT
 
 
+def hack_vm_args(ctx, node, contents):
+    contents += f"""
+-proto_dist couch
+-couch_dist no_tls '"clouseau{node[-1]}@127.0.0.1"'
+-ssl_dist_optfile {ctx["rootdir"]}/src/couch_dist/certs/out/couch_dist.conf
+    """
+    if ctx["no_tls"]:
+        no_tls_nodes = ctx["no_tls"].split(",")
+        for node_name in no_tls_nodes:
+            node_name = node_name if "@" in node_name else f"{node_name}@127.0.0.1"
+            contents += f"""\n-couch_dist no_tls '"{node_name}"'"""
+    return contents
+
+
 def gen_password():
     # TODO: figure how to generate something more friendly here
     return base64.b64encode(os.urandom(6)).decode()
diff --git a/rel/overlay/bin/remsh b/rel/overlay/bin/remsh
index 1804336b5..117acafc0 100755
--- a/rel/overlay/bin/remsh
+++ b/rel/overlay/bin/remsh
@@ -95,7 +95,7 @@ while getopts ":hn:c:l:mvt:" optionName; do
     t)
       TLSCONF=$OPTARG
       if [ ! -f "$TLSCONF" ]; then
-        echo "ERROR: Could't find the file \"$TLSCONF\"." >&2
+        echo "ERROR: Couldn't find the file \"$TLSCONF\"." >&2
         exit 1
       fi
       ;;
diff --git a/src/couch_dist/.gitignore b/src/couch_dist/.gitignore
new file mode 100644
index 000000000..d7cb29ae4
--- /dev/null
+++ b/src/couch_dist/.gitignore
@@ -0,0 +1,4 @@
+.rebar/
+certs/*.pem
+certs/out/
+ebin/
diff --git a/src/couch_dist/README.md b/src/couch_dist/README.md
new file mode 100644
index 000000000..aabf4a5d9
--- /dev/null
+++ b/src/couch_dist/README.md
@@ -0,0 +1,162 @@
+# couch_dist
+
+Erlang communicates with its own protocol over TCP (Transmission Control Protocol).
+It can also be configured to run its protocol over a TLS (Transport Layer Security)
+connection which itself is over TCP.
+
+`couch_dist` implements a custom distribution protocol -- `couch`, which allows
+using TLS for Erlang distribution between nodes, with the ability to connect to
+some nodes using TCP as well.
+
+`TLS` can provide extra verification and security, but requires proper
+certificates and configuration to set up the environment.
+
+## Set up a custom Erlang distribution
+
+1. Specify the distribution protocol in `vm.args`
+2. Specify some nodes to use TCP only in `vm.args` (optional)
+3. Generate certificates using `certs`
+4. Specify security and other SSL options in `couch_dist.conf`
+
+Examples:
+
+1. `vm.args`:
+
+      ```vm.args
+      -proto_dist couch
+      -couch_dist no_tls '"clouseau@127.0.0.1"'
+      -ssl_dist_optfile </absolute_path/to/couch_dist.conf>
+      ```
+
+2. `couch_dist.conf`:
+
+    - `erlserver.pem`: contains the certificate and its private key.
+    - `{fail_if_no_peer_cert, true}`: In previous OTP versions it could be specified on both server side and client
+      side, but in OTP 26 it can only be used on server side,
+      see [OTP 26 Highlights](https://www.erlang.org/blog/otp-26-highlights/#ssl-improved-checking-of-options).
+
+       ```couch_dist.conf
+       [
+         {server, [
+           {cacertfile, "</absolute_path/to/ca-cert.pem>"},
+           {certfile,   "</absolute_path/to/erlserver.pem>"},
+           {secure_renegotiate, true},
+           {verify, verify_peer},
+           {fail_if_no_peer_cert, true}
+         ]},
+         {client, [
+           {cacertfile, "</absolute_path/to/ca-cert.pem>"},
+           {certfile,   "</absolute_path/to/cert.pem>"},
+           {keyfile,    "</absolute_path/to/key.pem>"},
+           {secure_renegotiate, true},
+           {verify, verify_peer}
+         ]}
+       ].
+       ```
+
+## Generate Certificate
+
+This is an example of using `elixir-certs` to generate certificates, but it is
+not an endorsement of a specific expiration limit, key size or algorithm.
+
+```bash
+cd src/couch_dist/certs
+
+# Generate CA certificate and key
+./certs self-signed \
+  --out-cert ca-cert.pem --out-key ca-key.pem \
+  --template root-ca \
+  --subject "/CN=CouchDB Root CA"
+
+# Generate node certificate and key
+./certs create-cert \
+  --issuer-cert ca-cert.pem --issuer-key ca-key.pem \
+  --out-cert cert.pem --out-key key.pem \
+  --template server \
+  --subject "/CN=127.0.0.1"
+
+# Generate `erlserver.pem`
+cat key.pem cert.pem >erlserver.pem
+
+# Parse certificate to verify:
+# Certificate needs to match the node's hostname
+./parse_cert.escript cert.pem
+["127.0.0.1"]
+```
+
+Thanks to Roger Lipscombe for creating [`elixir-certs`](https://github.com/rlipscombe/elixir-certs)
+which simplifies the process of generating `X.509` certificates.
+
+Also, thanks to Robert Newson for finding `elixir-certs` and adding the feature
+to [easily pass `host` and `node` parameters to certificates](https://github.com/rnewson/elixir-certs/).
+
+## Development
+
+You can run CouchDB with `--enable-tls` mode, which will automatically generate
+vm.args, certificates, and configuration files.
+
+```bash
+./configure --dev --spidermonkey-version 91 && make && ./dev/run -t
+./configure --dev --spidermonkey-version 91 && make && ./dev/run --enable-tls
+
+./dev/remsh-tls
+(node1@127.0.0.1)1> net_kernel:nodes_info().
+{ok,[{'node3@127.0.0.1',
+         [{owner,<0.679.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55013},"127.0.0.1",tls,inet}},
+          {type,normal},
+          {in,150},
+          {out,147}]},
+     {'node2@127.0.0.1',
+         [{owner,<0.655.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55011},"127.0.0.1",tls,inet}},
+          {type,normal},
+          {in,181},
+          {out,196}]},
+     {'remsh14066@127.0.0.1',
+         [{owner,<0.8558.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55075},"127.0.0.1",tls,inet}},
+          {type,hidden},
+          {in,10},
+          {out,15}]}]}
+```
+
+You can also set specific nodes to use TCP:
+
+```bash
+./configure --dev --spidermonkey-version 91 && make && ./dev/run -t --no-tls node2@127.0.0.1
+./configure --dev --spidermonkey-version 91 && make && ./dev/run -t --no-tls node2,node3
+
+./dev/remsh-tls
+(node1@127.0.0.1)1> net_kernel:nodes_info().
+{ok,[{'node2@127.0.0.1',
+         [{owner,<0.456.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55170},"127.0.0.1",tcp,inet}},
+          {type,normal},
+          {in,145},
+          {out,164}]},
+     {'node3@127.0.0.1',
+         [{owner,<0.461.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55172},"127.0.0.1",tcp,inet}},
+          {type,normal},
+          {in,141},
+          {out,169}]},
+     {'remsh17312@127.0.0.1',
+         [{owner,<0.1418.0>},
+          {state,up},
+          {address,
+              {net_address,{{127,0,0,1},55203},"127.0.0.1",tls,inet}},
+          {type,hidden},
+          {in,10},
+          {out,15}]}]}
+```
diff --git a/src/couch_dist/certs/certs b/src/couch_dist/certs/certs
new file mode 100755
index 000000000..b684c7e79
--- /dev/null
+++ b/src/couch_dist/certs/certs
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# Source code: https://github.com/rnewson/elixir-certs/
+# We want the keys to be u=rw,go=, but there's no way to do that in
+# Elixir without race conditions, afaict, so we use umask:
+umask 077
+
+SCRIPT=$(readlink -f "$0")
+SCRIPTPATH=$(dirname "$SCRIPT")
+
+exec elixir "$SCRIPTPATH/certs.exs" "$@"
diff --git a/src/couch_dist/certs/certs.exs b/src/couch_dist/certs/certs.exs
new file mode 100644
index 000000000..7ce09dbba
--- /dev/null
+++ b/src/couch_dist/certs/certs.exs
@@ -0,0 +1,209 @@
+# Source code: https://github.com/rnewson/elixir-certs/
+# credo:disable-for-this-file
+# Important: run this with the wrapper script, so that umask is set correctly.
+
+Mix.install([{:x509, "~> 0.8.3"}, {:optimus, "~> 0.2"}])
+
+defmodule Certs do
+  def main(argv) do
+    Certs.Options.new!() |> Optimus.parse!(argv) |> run()
+  end
+
+  defp run(
+         {[:self_signed],
+          %Optimus.ParseResult{
+            options: %{subject: subject, out_cert: out_cert, out_key: out_key, template: template}
+          }}
+       ) do
+    ca_key = X509.PrivateKey.new_ec(:secp256r1)
+
+    ca_crt = X509.Certificate.self_signed(ca_key, subject, template: template(template, subject, "", ""))
+
+    File.write!(out_key, X509.PrivateKey.to_pem(ca_key), [:exclusive])
+    File.chmod!(out_key, 0o400)
+
+    File.write!(out_cert, X509.Certificate.to_pem(ca_crt), [:exclusive])
+    File.chmod!(out_cert, 0o444)
+  end
+
+  defp run(
+         {[:create_cert],
+          %Optimus.ParseResult{
+            options: %{
+              subject: subject,
+              host: host,
+              node: node,
+              issuer_cert: issuer_cert,
+              issuer_key: issuer_key,
+              out_cert: out_cert,
+              out_key: out_key,
+              template: template
+            }
+          }}
+       ) do
+    issuer_cert = File.read!(issuer_cert) |> X509.Certificate.from_pem!()
+    issuer_key = File.read!(issuer_key) |> X509.PrivateKey.from_pem!()
+
+    key = X509.PrivateKey.new_ec(:secp256r1)
+    pub = X509.PublicKey.derive(key)
+
+    crt =
+      X509.Certificate.new(pub, subject, issuer_cert, issuer_key,
+        template: template(template, subject, host, node)
+      )
+
+    File.write!(out_key, X509.PrivateKey.to_pem(key), [:exclusive])
+    File.chmod!(out_key, 0o400)
+
+    File.write!(out_cert, X509.Certificate.to_pem(crt), [:exclusive])
+    File.chmod!(out_cert, 0o444)
+  end
+
+  defp run(_) do
+    Certs.Options.new!() |> Optimus.help() |> IO.puts()
+  end
+
+  defp template("root-ca", _subject, _host, _node), do: :root_ca
+
+  defp template("server", subject, _host, _node) do
+    [commonName] =
+      X509.RDNSequence.new(subject)
+      |> X509.RDNSequence.get_attr(:commonName)
+
+    import X509.Certificate.Extension
+
+    %X509.Certificate.Template{
+      # 1 year, plus a 30 days grace period
+      validity: 365 + 30,
+      hash: :sha256,
+      extensions: [
+        basic_constraints: basic_constraints(false),
+        key_usage: key_usage([:digitalSignature, :keyEncipherment]),
+        ext_key_usage: ext_key_usage([:serverAuth, :clientAuth]),
+        subject_key_identifier: true,
+        authority_key_identifier: true,
+        subject_alt_name: subject_alt_name([commonName])
+      ]
+    }
+  end
+
+  defp template("node", _subject, host, node) do
+    import X509.Certificate.Extension
+
+    %X509.Certificate.Template{
+      # 1 year, plus a 30 days grace period
+      validity: 365 + 30,
+      hash: :sha256,
+      extensions: [
+        basic_constraints: basic_constraints(false),
+        key_usage: key_usage([:digitalSignature, :keyEncipherment]),
+        ext_key_usage: ext_key_usage([:serverAuth, :clientAuth]),
+        subject_key_identifier: true,
+        authority_key_identifier: true,
+        subject_alt_name: subject_alt_name([host]),
+        subject_alt_name: subject_alt_name([{:directoryName, X509.RDNSequence.new("CN=" <> node, :otp)}])
+      ]
+    }
+  end
+end
+
+defmodule Certs.Options do
+  def new!() do
+    Optimus.new!(
+      name: "certs",
+      description: "certs",
+      version: "0.2",
+      allow_unknown_args: false,
+      parse_double_dash: true,
+      subcommands: [
+        self_signed: [
+          name: "self-signed",
+          about: "Create a self-signed certificate",
+          options: [
+            subject: [
+              long: "--subject",
+              value_name: "SUBJECT",
+              required: true,
+              parser: :string
+            ],
+            out_cert: [
+              long: "--out-cert",
+              value_name: "OUT_CRT",
+              required: true,
+              parser: :string
+            ],
+            out_key: [
+              long: "--out-key",
+              value_name: "OUT_KEY",
+              required: true,
+              parser: :string
+            ],
+            template: [
+              long: "--template",
+              value_name: "TEMPLATE",
+              required: true,
+              parser: :string
+            ]
+          ]
+        ],
+        create_cert: [
+          name: "create-cert",
+          options: [
+            subject: [
+              long: "--subject",
+              value_name: "SUBJECT",
+              required: true,
+              parser: :string
+            ],
+            host: [
+              long: "--host",
+              value_name: "HOST",
+              required: false,
+              parser: :string
+            ],
+            node: [
+              long: "--node",
+              value_name: "NODE",
+              required: false,
+              parser: :string
+            ],
+            issuer_cert: [
+              long: "--issuer-cert",
+              value_name: "ISSUER_CRT",
+              required: true,
+              parser: :string
+            ],
+            issuer_key: [
+              long: "--issuer-key",
+              value_name: "ISSUER_KEY",
+              required: true,
+              parser: :string
+            ],
+            out_cert: [
+              long: "--out-cert",
+              value_name: "OUT_CRT",
+              required: true,
+              parser: :string
+            ],
+            out_key: [
+              long: "--out-key",
+              value_name: "OUT_KEY",
+              required: true,
+              parser: :string
+            ],
+            template: [
+              long: "--template",
+              value_name: "TEMPLATE",
+              required: true,
+              parser: :string
+            ]
+          ]
+        ]
+      ]
+    )
+  end
+end
+
+Certs.main(System.argv())
+
+# vim: set ft=elixir
diff --git a/src/couch_dist/certs/parse_cert.escript b/src/couch_dist/certs/parse_cert.escript
new file mode 100755
index 000000000..7d5dbca2b
--- /dev/null
+++ b/src/couch_dist/certs/parse_cert.escript
@@ -0,0 +1,8 @@
+#!/usr/bin/env escript
+-mode(compile).
+
+main(File) ->
+    {ok, PemBin} = file:read_file(File),
+    [{_, DerCert, _}] = public_key:pem_decode(PemBin),
+    OTPCert = public_key:pkix_decode_cert(DerCert, otp),
+    io:format("~p~n", [inet_tls_dist:cert_nodes(OTPCert)]).
diff --git a/src/couch_dist/gen_certs b/src/couch_dist/gen_certs
new file mode 100755
index 000000000..b7eaf5146
--- /dev/null
+++ b/src/couch_dist/gen_certs
@@ -0,0 +1,43 @@
+#!/bin/sh
+set -e
+
+certs_dir="$(cd "${0%/*}" 2>/dev/null; echo "${PWD}")/certs"
+cd "${certs_dir}"
+mkdir -p "${certs_dir}/out"
+
+if [ ! -e "${certs_dir}/out/ca-cert.pem" ]; then
+  ./certs self-signed \
+    --out-cert out/ca-cert.pem --out-key out/ca-key.pem \
+    --template root-ca \
+    --subject "/CN=CouchDB Root CA"
+fi
+
+if [ ! -e "${certs_dir}/out/cert.pem" ]; then
+  ./certs create-cert \
+    --issuer-cert out/ca-cert.pem --issuer-key out/ca-key.pem \
+    --out-cert out/cert.pem --out-key out/key.pem \
+    --template server \
+    --subject "/CN=127.0.0.1"
+fi
+
+if [ ! -e "${certs_dir}/out/couch_dist.conf" ]; then
+  cat <<EOF >"${certs_dir}/out/couch_dist.conf"
+[
+  {server, [
+    {cacertfile, "$(pwd)/out/ca-cert.pem"},
+    {certfile,   "$(pwd)/out/cert.pem"},
+    {keyfile,    "$(pwd)/out/key.pem"},
+    {secure_renegotiate, true},
+    {verify, verify_peer},
+    {fail_if_no_peer_cert, true}
+  ]},
+  {client, [
+    {cacertfile, "$(pwd)/out/ca-cert.pem"},
+    {certfile,   "$(pwd)/out/cert.pem"},
+    {keyfile,    "$(pwd)/out/key.pem"},
+    {secure_renegotiate, true},
+    {verify, verify_peer}
+  ]}
+].
+EOF
+fi
diff --git a/src/docs/src/cluster/tls_erlang_distribution.rst b/src/docs/src/cluster/tls_erlang_distribution.rst
index 07e48eab3..5d946fa72 100644
--- a/src/docs/src/cluster/tls_erlang_distribution.rst
+++ b/src/docs/src/cluster/tls_erlang_distribution.rst
@@ -29,28 +29,53 @@ Reference: `Using TLS for Erlang Distribution`_
 
 Generate Certificate
 ====================
-For TLS to work properly, at least one public key and one certificate must be
-specified. In the following example (couch_ssl_dist.conf), the PEM file contains
-the ``certificate`` and its ``private key``.
+To distribute using TLS, appropriate certificates need to be provided.
+In the following example (couch_dist.conf), the cert.pem certificate must be
+trusted by a root certificate known to the server, and the erlserver.pem file
+contains the "certificate" and its "private key".
 
     .. code-block:: text
 
         [{server,
-          [{certfile, "</path/to/erlserver.pem>"},
-           {secure_renegotiate, true}]},
+          [{cacertfile, "</absolute_path/to/ca-cert.pem>"},
+           {certfile,   "</absolute_path/to/erlserver.pem>"},
+           {secure_renegotiate, true},
+           {verify, verify_peer},
+           {fail_if_no_peer_cert, true}]},
          {client,
-          [{secure_renegotiate, true}]}].
+          [{cacertfile, "</absolute_path/to/ca-cert.pem>"},
+           {keyfile,    "</absolute_path/to/key.pem>"},
+           {certfile,   "</absolute_path/to/cert.pem>"},
+           {secure_renegotiate, true},
+           {verify, verify_peer}]}].
 
-The following command is an example of generating a certificate (PEM) file.
+You can use ``{verify, verify_peer}`` to enable verification,
+but it requires appropriate certificates to verify.
+
+This is an example of generating certificates.
 
     .. code-block:: bash
 
-        $ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
-        $ cat key.pem cert.pem > erlserver.pem && rm key.pem cert.pem
+        $ git clone https://github.com/rnewson/elixir-certs
+        $ cd elixir-certs
+        $ ./certs self-signed \
+            --out-cert ca-cert.pem --out-key ca-key.pem \
+            --template root-ca \
+            --subject "/CN=CouchDB Root CA"
+        $./certs create-cert \
+            --issuer-cert ca-cert.pem --issuer-key ca-key.pem \
+            --out-cert cert.pem --out-key key.pem \
+            --template server \
+            --subject "/CN=<hostname>"
+        $ cat key.pem cert.pem >erlserver.pem
 
     .. note::
-       This is **not** an endorsement of a specific expiration limit,
-       key size or algorithm.
+        * The above examples are **not** an endorsement of specific expiration limits, key sizes, or algorithms.
+        * If option ``verify_peer`` is set, the ``server_name_indication`` option should also be specified.
+        * The option ``{fail_if_no_peer_cert, true}`` should only be used on the server side in OTP 26,
+          for previous versions it can be specified both on the server side and client side.
+        * When generating certificates, make sure Common Name (FQDN) should be different in CA certificate and certificate.
+          Also, FQDN in the certificate should be the same as the hostname.
 
 Config Settings
 ===============
@@ -62,7 +87,7 @@ To enable TLS distribution, make sure to set custom parameters in ``vm.args``.
 
         -proto_dist couch
         -couch_dist no_tls \"clouseau@127.0.0.1\"
-        -ssl_dist_optfile <path/to/couch_ssl_dist.conf>
+        -ssl_dist_optfile </absolute_path/to/couch_dist.conf>
 
     .. note::
        * The default value of ``no_tls`` is ``false``. If the user does not
@@ -90,7 +115,7 @@ The ``no_tls`` flag can have these values:
 
         # Specify node1 and node2 to use TCP, others use TLS
 
-        -couch_dist no_tls \"node1@127.0.0.1\"
+        -couch_dist no_tls '"node1@127.0.0.1"'
         -couch_dist no_tls \"node2@127.0.0.1\"
 
     .. code-block:: text
@@ -119,4 +144,4 @@ Start Erlang using a remote shell connected to Node.
 
     .. code-block:: bash
 
-        $ ./remsh -t <path/to/couch_ssl_dist.conf>
+        $ ./remsh -t </absolute_path/to/couch_dist.conf>