Skip to content

Commit 353558c

Browse files
MB-61292: Add /encryptionAtRest API
Adds API to manage encryption-at-rest for configuration. The same API will be used in future to manage encryption for other parts like logs. GET /settings/security/encryptionAtRest/config POST /settings/security/encryptionAtRest/config encryptionMethod = disabled | encryption_service | secret encryptionSecretId = -1 | secret_id Change-Id: Ib752a1f62bff10b0f7d106898d0585b0da904045 Reviewed-on: https://review.couchbase.org/c/ns_server/+/211733 Well-Formed: Build Bot <[email protected]> Reviewed-by: Navdeep S Boparai <[email protected]> Tested-by: Timofey Barmin <[email protected]>
1 parent f92c787 commit 353558c

File tree

5 files changed

+159
-7
lines changed

5 files changed

+159
-7
lines changed

apps/ns_server/src/cb_cluster_secrets.erl

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
rotate/1,
5050
get_secret_by_kek_id_map/1,
5151
ensure_can_encrypt_dek_kind/3,
52-
generate_raw_key/1]).
52+
is_allowed_usage_for_secret/3,
53+
generate_raw_key/1,
54+
sync_with_all_node_monitors/0]).
5355

5456
%% gen_server callbacks
5557
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@@ -349,6 +351,19 @@ ensure_can_encrypt_dek_kind(SecretId, DekKind, Snapshot) ->
349351
{error, not_found} -> {error, not_found}
350352
end.
351353

354+
-spec is_allowed_usage_for_secret(secret_id(), secret_usage(),
355+
chronicle_snapshot()) ->
356+
ok | {error, not_allowed | not_found}.
357+
is_allowed_usage_for_secret(SecretId, Usage, Snapshot) ->
358+
maybe
359+
{ok, #{usage := AllowedUsages}} ?= get_secret(SecretId, Snapshot),
360+
true ?= is_allowed([Usage], AllowedUsages),
361+
ok
362+
else
363+
false -> {error, not_allowed};
364+
{error, not_found} -> {error, not_found}
365+
end.
366+
352367
-spec get_secret_by_kek_id_map(chronicle_snapshot()) ->
353368
#{kek_id() := secret_id()}.
354369
get_secret_by_kek_id_map(Snapshot) ->

apps/ns_server/src/chronicle_local.erl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,8 @@ sync() ->
153153
gen_server2:call(?MODULE, sync, ?CALL_TIMEOUT).
154154

155155
get_encryption(Snapshot) ->
156-
Settings = chronicle_compat:get(Snapshot,
157-
?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY,
158-
#{default => #{}}),
159-
ConfigEncryptionSettings = maps:get(config_encryption, Settings,
160-
#{encryption => disabled,
161-
secret_id => ?SECRET_ID_NOT_SET}),
156+
#{config_encryption := ConfigEncryptionSettings} =
157+
menelaus_web_encr_at_rest:get_settings(Snapshot),
162158
{ok, case ConfigEncryptionSettings of
163159
#{encryption := disabled} -> disabled;
164160
#{encryption := encryption_service} -> encryption_service;

apps/ns_server/src/menelaus_web.erl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,9 @@ get_action(Req, {AppRoot, IsSSL, Plugins}, Path, PathTokens) ->
509509
["settings", "passwordPolicy"] ->
510510
{{[admin, security], read},
511511
fun menelaus_web_rbac:handle_get_password_policy/1};
512+
["settings", "security", "encryptionAtRest" | PathRest] ->
513+
{{[admin, security], read},
514+
fun menelaus_web_encr_at_rest:handle_get/2, [PathRest]};
512515
["settings", "security" | Keys] ->
513516
{{[admin, security], read},
514517
fun menelaus_web_settings:handle_get/3, [security, Keys]};
@@ -794,6 +797,9 @@ get_action(Req, {AppRoot, IsSSL, Plugins}, Path, PathTokens) ->
794797
["settings", "passwordPolicy"] ->
795798
{{[admin, security], write},
796799
fun menelaus_web_rbac:handle_post_password_policy/1};
800+
["settings", "security", "encryptionAtRest" | PathRest] ->
801+
{{[admin, security], write},
802+
fun menelaus_web_encr_at_rest:handle_post/2, [PathRest]};
797803
["settings", "security" | Keys] ->
798804
{{[admin, security], write},
799805
fun menelaus_web_settings:handle_post/3, [security, Keys]};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
%% @author Couchbase <[email protected]>
2+
%% @copyright 2024-Present Couchbase, Inc.
3+
%%
4+
%% Use of this software is governed by the Business Source License included in
5+
%% the file licenses/BSL-Couchbase.txt. As of the Change Date specified in that
6+
%% file, in accordance with the Business Source License, use of this software
7+
%% will be governed by the Apache License, Version 2.0, included in the file
8+
%% licenses/APL2.txt.
9+
%%
10+
-module(menelaus_web_encr_at_rest).
11+
12+
-include("ns_common.hrl").
13+
-include_lib("ns_common/include/cut.hrl").
14+
-include("cb_cluster_secrets.hrl").
15+
16+
-export([handle_get/2, handle_post/2, get_settings/1]).
17+
18+
params() ->
19+
[{"config.encryptionMethod",
20+
#{cfg_key => [config_encryption, encryption],
21+
type => {one_of, existing_atom,
22+
[disabled, encryption_service, secret]}}},
23+
{"config.encryptionSecretId",
24+
#{cfg_key => [config_encryption, secret_id],
25+
type => {int, -1, infinity}}}].
26+
27+
handle_get(Path, Req) ->
28+
Settings = get_settings(direct),
29+
List = maps:to_list(maps:map(fun (_, V) -> maps:to_list(V) end, Settings)),
30+
menelaus_web_settings2:handle_get(Path, params(), undefined, List, Req).
31+
32+
handle_post(Path, Req) ->
33+
menelaus_web_settings2:handle_post(
34+
fun (Params, Req2) ->
35+
NewSettings = maps:map(fun (_, V) -> maps:from_list(V) end,
36+
maps:groups_from_list(
37+
fun ({[K1, _K2], _V}) -> K1 end,
38+
fun ({[_K1, K2], V}) -> {K2, V} end,
39+
Params)),
40+
RV = chronicle_kv:transaction(
41+
kv, [?CHRONICLE_SECRETS_KEY,
42+
?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY],
43+
fun (Snapshot) ->
44+
#{config_encryption := Cfg} = ToApply =
45+
get_settings(Snapshot, NewSettings),
46+
case validate_sec_settings(config_encryption,
47+
Cfg, Snapshot) of
48+
ok ->
49+
{commit, [{set,
50+
?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY,
51+
ToApply}]};
52+
{error, _} = Error ->
53+
{abort, Error}
54+
end
55+
end),
56+
case RV of
57+
{ok, _} ->
58+
cb_cluster_secrets:sync_with_all_node_monitors(),
59+
handle_get(Path, Req2);
60+
{error, Msg} ->
61+
menelaus_util:reply_global_error(Req2, Msg)
62+
end
63+
end, Path, params(), undefined, Req).
64+
65+
get_settings(Snapshot) -> get_settings(Snapshot, #{}).
66+
get_settings(Snapshot, ExtraSettings) ->
67+
Merge = fun (Settings1, Settings2) ->
68+
maps:merge_with(fun (_K, V1, V2) -> maps:merge(V1, V2) end,
69+
Settings1, Settings2)
70+
end,
71+
Settings = chronicle_compat:get(Snapshot,
72+
?CHRONICLE_ENCR_AT_REST_SETTINGS_KEY,
73+
#{default => #{}}),
74+
Merge(Merge(defaults(), Settings), ExtraSettings).
75+
76+
defaults() ->
77+
#{config_encryption => #{encryption => disabled,
78+
secret_id => ?SECRET_ID_NOT_SET}}.
79+
80+
validate_sec_settings(_, #{encryption := disabled,
81+
secret_id := ?SECRET_ID_NOT_SET}, _) ->
82+
ok;
83+
validate_sec_settings(_, #{encryption := disabled,
84+
secret_id := _}, _) ->
85+
{error, "Secret id must not be set when encryption is disabled"};
86+
validate_sec_settings(_, #{encryption := encryption_service,
87+
secret_id := ?SECRET_ID_NOT_SET}, _) ->
88+
ok;
89+
validate_sec_settings(_, #{encryption := encryption_service,
90+
secret_id := _}, _) ->
91+
{error, "Secret id must not be set when encryption_service is used"};
92+
validate_sec_settings(_, #{encryption := secret,
93+
secret_id := ?SECRET_ID_NOT_SET}, _) ->
94+
{error, "Secret id must be set"};
95+
validate_sec_settings(Name, #{encryption := secret,
96+
secret_id := Id}, Snapshot) ->
97+
case cb_cluster_secrets:is_allowed_usage_for_secret(Id, Name, Snapshot) of
98+
ok -> ok;
99+
{error, not_found} -> {error, "Secret not found"};
100+
{error, not_allowed} -> {error, "Secret not allowed"}
101+
end.

cluster_tests/testsets/native_encryption_tests.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def teardown(self):
3434
pass
3535

3636
def test_teardown(self):
37+
set_cfg_encryption(self.cluster, 'disabled', -1)
3738
self.cluster.delete_bucket(self.bucket_name)
3839
for s in get_secrets(self.cluster):
3940
delete_secret(self.cluster, s['id'])
@@ -559,6 +560,39 @@ def rotation_happened(key_num, expected_rotation_time_iso):
559560
lambda: rotation_happened(3, next_rotation),
560561
sleep_time=0.3, timeout=10)
561562

563+
def cfg_encryption_api_test(self):
564+
secret = auto_generated_secret(usage=['bucket-encryption-*'])
565+
bad_id = create_secret(self.random_node(), secret)
566+
secret['usage'] = ['bucket-encryption-*', 'configuration-encryption']
567+
good_id = create_secret(self.random_node(), secret)
568+
node = self.random_node()
569+
set_cfg_encryption(node, 'disabled', -1)
570+
set_cfg_encryption(node, 'encryption_service', -1)
571+
set_cfg_encryption(node, 'secret', -1, expected_code=400)
572+
set_cfg_encryption(node, 'secret', bad_id, expected_code=400)
573+
set_cfg_encryption(node, 'secret', good_id)
574+
575+
secret['usage'] = ['bucket-encryption-*']
576+
errors = update_secret(node, good_id, secret, expected_code=400)
577+
assert errors['_'] == 'can\'t modify usage as this secret is in use', \
578+
f'unexpected error: {errors}'
579+
580+
set_cfg_encryption(node, 'encryption_service', -1)
581+
582+
update_secret(node, good_id, secret)
583+
584+
585+
def set_cfg_encryption(cluster, mode, secret, expected_code=200):
586+
testlib.post_succ(cluster, '/settings/security/encryptionAtRest/config',
587+
json={'encryptionMethod': mode,
588+
'encryptionSecretId': secret},
589+
expected_code=expected_code)
590+
if expected_code == 200:
591+
r = testlib.get_succ(cluster, '/settings/security/encryptionAtRest')
592+
r = r.json()
593+
testlib.assert_eq(r['config']['encryptionMethod'], mode)
594+
testlib.assert_eq(r['config']['encryptionSecretId'], secret)
595+
562596

563597
def auto_generated_secret(name=None,
564598
usage=None,

0 commit comments

Comments
 (0)