riak_test/tests/replication2_ssl.erl

391 lines
15 KiB
Erlang
Raw Normal View History

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2012-2015 Basho Technologies, Inc.
%%
%% This file is provided 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.
%%
%% -------------------------------------------------------------------
-module(replication2_ssl).
-behavior(riak_test).
-export([confirm/0]).
-include_lib("eunit/include/eunit.hrl").
%% Certificate Names
-define(DEF_DOM, ".basho.com").
-define(DOM_WC, "*" ++ ?DEF_DOM).
-define(BAD_WC, "*.bahso.com").
-define(CERTN(S), S ++ ?DEF_DOM).
-define(SITEN(N), ?CERTN("site" ++ ??N)).
-define(CERTP(S), filename:join(CertDir, S)).
%% Certificate Information
-record(ci, {
cn, %% common name of the certificate
rd = 0, %% required ssl_depth
wc = ?DOM_WC, %% acceptable *.domain wildcard
ssl %% options returned from ssl_paths
}).
%%
%% @doc Tests various TLS (SSL) connection scenarios for MDC.
%% The following configiration options are recognized:
%%
%% num_nodes [default 6]
%% How many nodes to use to build two clusters.
%%
%% cluster_a_size [default (num_nodes div 2)]
%% How many nodes to use in cluster "A". The remainder is used in cluster "B".
%%
%% conn_fail_time [default rt_max_wait_time]
%% A (presumably shortened) timout to use in tests where the connection is
%% expected to be rejected due to invalid TLS configurations. Something around
%% one minute is appropriate. Using the default ten-minute timeout, this
%% test will take more than an hour and a half to run successfully.
%%
confirm() ->
2014-01-08 17:07:32 +00:00
%% test requires allow_mult=false
2014-01-19 18:00:21 +00:00
rt:set_conf(all, [{"buckets.default.allow_mult", "false"}]),
2014-01-08 17:07:32 +00:00
NumNodes = rt_config:get(num_nodes, 6),
ClusterASize = rt_config:get(cluster_a_size, (NumNodes div 2)),
CertDir = rt_config:get(rt_scratch_dir) ++ "/certs",
%% make some CAs
make_certs:rootCA(CertDir, "CA_0"),
make_certs:intermediateCA(CertDir, "CA_1", "CA_0"),
make_certs:intermediateCA(CertDir, "CA_2", "CA_1"),
%% make a bunch of certificates and matching ci records
S1Name = ?SITEN(1),
S2Name = ?SITEN(2),
S3Name = ?SITEN(3),
S4Name = ?SITEN(4),
S5Name = ?SITEN(5),
S6Name = ?SITEN(6),
W1Name = ?CERTN("wildcard1"),
W2Name = ?CERTN("wildcard2"),
make_certs:endusers(CertDir, "CA_0", [S1Name, S2Name]),
CIdep0s1 = #ci{cn = S1Name, rd = 0, ssl = ssl_paths(?CERTP(S1Name))},
CIdep0s2 = #ci{cn = S2Name, rd = 0, ssl = ssl_paths(?CERTP(S2Name))},
make_certs:endusers(CertDir, "CA_1", [S3Name, S4Name]),
CIdep1s1 = #ci{cn = S3Name, rd = 1, ssl = ssl_paths(?CERTP(S3Name))},
CIdep1s2 = #ci{cn = S4Name, rd = 1, ssl = ssl_paths(?CERTP(S4Name))},
make_certs:endusers(CertDir, "CA_2", [S5Name, S6Name]),
CIdep2s1 = #ci{cn = S5Name, rd = 2, ssl = ssl_paths(?CERTP(S5Name))},
CIdep2s2 = #ci{cn = S6Name, rd = 2, ssl = ssl_paths(?CERTP(S6Name))},
make_certs:enduser(CertDir, "CA_1", ?DOM_WC, W1Name),
CIdep1wc = #ci{cn = ?DOM_WC, rd = 1, ssl = ssl_paths(?CERTP(W1Name))},
make_certs:enduser(CertDir, "CA_2", ?DOM_WC, W2Name),
CIdep2wc = #ci{cn = ?DOM_WC, rd = 2, ssl = ssl_paths(?CERTP(W2Name))},
% crufty old certs really need to be replaced
CIexpired = #ci{cn = "ny.cataclysm-software.net", rd = 0,
wc = "*.cataclysm-software.net", ssl = ssl_paths(
filename:join([rt:priv_dir(), "certs", "cacert.org"]),
"ny-cert-old.pem", "ny-key.pem", "ca")},
lager:info("Deploy ~p nodes", [NumNodes]),
ConfRepl = {riak_repl,
[{fullsync_on_connect, false}, {fullsync_interval, disabled}]},
ConfTcpBasic = [ConfRepl, {riak_core, [{ssl_enabled, false}]}],
%%
%% !!! IMPORTANT !!!
%% Properties added to node configurations CANNOT currently be removed,
%% only overwritten. As such, configurations that include ACLs MUST come
%% after ALL non-ACL configurations! This has been learned the hard way :(
%% The same applies to the ssl_depth option, though it's much easier to
%% contend with - make sure it works, then always use a valid depth.
%%
%%
%% Connection Test descriptors
%% Each is a tuple: {Description, Node1Config, Node2Config, Should Pass}
%%
SslConnTests = [
%%
%% basic tests
%%
{"non-SSL peer fails",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
ConfTcpBasic,
false},
{"non-SSL local fails",
ConfTcpBasic,
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s2#ci.ssl }],
false},
{"basic SSL connectivity",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s2#ci.ssl }],
true},
{"expired peer certificate fails",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIexpired#ci.ssl }],
false},
{"expired local certificate fails",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIexpired#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s2#ci.ssl }],
false},
{"identical certificate CN is disallowed",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
false},
{"identical wildcard certificate CN is allowed",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1wc#ci.ssl }],
true},
{"SSL connectivity with one intermediate CA is allowed by default",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1s2#ci.ssl }],
true},
{"SSL connectivity with two intermediate CAs is disallowed by default",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep2s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep2s2#ci.ssl }],
false},
{"wildcard certificates on both ends",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1wc#ci.ssl }],
true},
{"wildcard certificate on one end",
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
] ++ CIdep0s1#ci.ssl }],
true},
%%
%% first use of ssl_depth, all subsequent tests must specify
%%
{"disallowing intermediate CA setting allows direct-signed certs",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, 0}
] ++ CIdep0s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, 0}
] ++ CIdep0s2#ci.ssl }],
true},
{"disallowing intermediate CA disallows intermediate-signed peer",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, 0}
] ++ CIdep0s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep0s1#ci.rd}
] ++ CIdep1s2#ci.ssl }],
false},
{"disallowing intermediate CA disallows intermediate-signed local",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep0s2#ci.rd}
] ++ CIdep1s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, 0}
] ++ CIdep0s2#ci.ssl }],
false},
{"allow arbitrary-depth intermediate CAs",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep2s2#ci.rd}
] ++ CIdep2s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep2s1#ci.rd}
] ++ CIdep2s2#ci.ssl }],
true},
%%
%% first use of peer_common_name_acl, all subsequent tests must specify
%%
{"wildcard certificate on one end with matching ACL",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s1#ci.rd}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1wc#ci.rd}
, {peer_common_name_acl, [?DOM_WC]}
] ++ CIdep1s1#ci.ssl }],
true},
{"wildcard certificate on one end with mismatched ACL",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s1#ci.rd}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1wc#ci.rd}
, {peer_common_name_acl, [?BAD_WC]}
] ++ CIdep1s1#ci.ssl }],
false},
{"one wildcard ACL and one strict ACL",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s2#ci.rd}
, {peer_common_name_acl, [?DOM_WC]}
] ++ CIdep1s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s1#ci.rd}
, {peer_common_name_acl, [CIdep1s1#ci.cn]}
] ++ CIdep1s2#ci.ssl }],
true},
{"wildcard certificates on both ends with ACLs",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep2wc#ci.rd}
, {peer_common_name_acl, [CIdep2wc#ci.wc]}
] ++ CIdep1wc#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1wc#ci.rd}
, {peer_common_name_acl, [CIdep1wc#ci.wc]}
] ++ CIdep2wc#ci.ssl }],
true},
{"explicit certificates with strict ACLs",
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep2s2#ci.rd}
, {peer_common_name_acl, [CIdep2s2#ci.cn]}
] ++ CIdep1s1#ci.ssl }],
[ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s1#ci.rd}
, {peer_common_name_acl, [CIdep1s1#ci.cn]}
] ++ CIdep2s2#ci.ssl }],
true}
],
lager:info("Deploying 2 nodes for connectivity tests"),
[Node1, Node2] = rt:deploy_nodes(2, ConfTcpBasic, [riak_kv, riak_repl]),
repl_util:name_cluster(Node1, "A"),
repl_util:name_cluster(Node2, "B"),
%% we'll need to wait for cluster names before continuing
rt:wait_until_ring_converged([Node1]),
rt:wait_until_ring_converged([Node2]),
rt:wait_for_service(Node1, [riak_kv, riak_repl]),
rt:wait_for_service(Node2, [riak_kv, riak_repl]),
lager:info("=== Testing basic connectivity"),
rt:log_to_nodes([Node1, Node2], "Testing basic connectivity"),
{ok, {_IP, Port}} = rpc:call(Node2, application, get_env,
[riak_core, cluster_mgr]),
lager:info("connect cluster A:~p to B on port ~p", [Node1, Port]),
rt:log_to_nodes([Node1, Node2], "connect A to B"),
repl_util:connect_cluster(Node1, "127.0.0.1", Port),
lager:info("Waiting for connection to B"),
?assertEqual(ok, repl_util:wait_for_connection(Node1, "B")),
%% run each of the SSL connectivity tests
lists:foreach(fun({Desc, Conf1, Conf2, ShouldPass}) ->
test_connection(Desc, {Node1, Conf1}, {Node2, Conf2}, ShouldPass)
end, SslConnTests),
lager:info("Connectivity tests passed"),
2013-04-02 03:29:08 +00:00
repl_util:disconnect_cluster(Node1, "B"),
lager:info("Re-deploying 6 nodes"),
Nodes = rt:deploy_nodes(6, ConfTcpBasic, [riak_kv, riak_repl]),
[rt:wait_until_pingable(N) || N <- Nodes],
{ANodes, BNodes} = lists:split(ClusterASize, Nodes),
lager:info("Reconfiguring nodes with SSL options"),
ConfANodes = [ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s2#ci.rd}
, {peer_common_name_acl, [CIdep1s2#ci.cn]}
] ++ CIdep1s1#ci.ssl }],
ConfBNodes = [ConfRepl, {riak_core, [{ssl_enabled, true}
, {ssl_depth, CIdep1s1#ci.rd}
, {peer_common_name_acl, [CIdep1s1#ci.cn]}
] ++ CIdep1s2#ci.ssl }],
[rt:update_app_config(N, ConfANodes) || N <- ANodes],
[rt:update_app_config(N, ConfBNodes) || N <- BNodes],
[rt:wait_until_pingable(N) || N <- Nodes],
lager:info("Build cluster A"),
repl_util:make_cluster(ANodes),
lager:info("Build cluster B"),
repl_util:make_cluster(BNodes),
2013-04-02 03:29:08 +00:00
repl_util:disconnect_cluster(Node1, "B"),
replication2:replication(ANodes, BNodes, false),
pass.
test_connection(Desc, {N1, C1}, {N2, C2}, ShouldPass) ->
lager:info("=== Testing " ++ Desc),
rt:log_to_nodes([N1, N2], "Testing " ++ Desc),
test_connection({N1, C1}, {N2, C2}, ShouldPass).
test_connection(Left, Right, true) ->
?assertEqual(ok, test_connection(Left, Right)),
lager:info("Connection succeeded");
test_connection(Left, Right, false) ->
DefaultTimeout = rt_config:get(rt_max_wait_time),
ConnFailTimeout = rt_config:get(conn_fail_time, DefaultTimeout),
rt_config:set(rt_max_wait_time, ConnFailTimeout),
?assertMatch({fail, _}, test_connection(Left, Right)),
rt_config:set(rt_max_wait_time, DefaultTimeout),
lager:info("Connection rejected").
test_connection({Node1, Config1}, {Node2, Config2}) ->
repl_util:disconnect_cluster(Node1, "B"),
repl_util:wait_for_disconnect(Node1, "B"),
rt:update_app_config(Node2, Config2),
rt:wait_until_pingable(Node2),
rt:update_app_config(Node1, Config1),
rt:wait_until_pingable(Node1),
rt:wait_for_service(Node1, [riak_kv, riak_repl]),
rt:wait_for_service(Node2, [riak_kv, riak_repl]),
{ok, {_IP, Port}} = rpc:call(Node2, application, get_env,
[riak_core, cluster_mgr]),
lager:info("connect cluster A:~p to B on port ~p", [Node1, Port]),
rt:log_to_nodes([Node1, Node2], "connect A to B"),
repl_util:connect_cluster(Node1, "127.0.0.1", Port),
repl_util:wait_for_connection(Node1, "B").
ssl_paths(Dir) ->
ssl_paths(Dir, "cert.pem", "key.pem", "cacerts.pem").
ssl_paths(Dir, Cert, Key, CaCerts) ->
[{certfile, filename:join(Dir, Cert)}
,{keyfile, filename:join(Dir, Key)}
,{cacertdir, filename:join(Dir, CaCerts)}].