Fail-fast secrets in stage/prod via EVENTHUB_ENV. Refs EventHub/EventHubBack#25
This commit is contained in:
+23
-22
@@ -1,23 +1,24 @@
|
||||
[
|
||||
{eventhub, [
|
||||
{http_port, "${HTTP_PORT:-8080}"},
|
||||
{ws_port, "${WS_PORT:-8081}"},
|
||||
{admin_http_port, "${ADMIN_HTTP_PORT:-8445}"},
|
||||
{admin_ws_port, "${ADMIN_WS_PORT:-8446}"},
|
||||
{jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>}
|
||||
]},
|
||||
{kernel, [
|
||||
{logger_level, info},
|
||||
{logger, [
|
||||
{handler, default, logger_std_h,
|
||||
#{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}}
|
||||
}
|
||||
]}
|
||||
]},
|
||||
{mnesia, [
|
||||
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
||||
]},
|
||||
{ cowboy_swagger, [
|
||||
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
||||
]}
|
||||
[
|
||||
{eventhub, [
|
||||
{http_port, "${HTTP_PORT:-8080}"},
|
||||
{ws_port, "${WS_PORT:-8081}"},
|
||||
{admin_http_port, "${ADMIN_HTTP_PORT:-8445}"},
|
||||
{admin_ws_port, "${ADMIN_WS_PORT:-8446}"},
|
||||
{jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>},
|
||||
{admin_jwt_secret, <<"${ADMIN_JWT_SECRET:-change_me_in_production}">>}
|
||||
]},
|
||||
{kernel, [
|
||||
{logger_level, info},
|
||||
{logger, [
|
||||
{handler, default, logger_std_h,
|
||||
#{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}}
|
||||
}
|
||||
]}
|
||||
]},
|
||||
{mnesia, [
|
||||
{dir, "data/Mnesia.eventhub@${NODE_NAME}"}
|
||||
]},
|
||||
{ cowboy_swagger, [
|
||||
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
|
||||
]}
|
||||
].
|
||||
+55
-26
@@ -3,6 +3,15 @@
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
case infra_secrets:validate_startup() of
|
||||
ok ->
|
||||
start_application();
|
||||
{error, Reason} ->
|
||||
io:format("~nFATAL: secret validation failed: ~p~n", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
start_application() ->
|
||||
case infra_sup:start_link() of
|
||||
{ok, Pid} ->
|
||||
% Определяем список узлов кластера, если режим CLUSTER_MODE=true
|
||||
@@ -48,8 +57,10 @@ start(_StartType, _StartArgs) ->
|
||||
start_swagger_http(), % Swagger UI и спецификация (8447)
|
||||
application:ensure_all_started(prometheus),
|
||||
application:ensure_all_started(prometheus_cowboy),
|
||||
init_default_admins(),
|
||||
{ok, Pid};
|
||||
case init_default_admins() of
|
||||
ok -> {ok, Pid};
|
||||
{error, Reason} -> {error, Reason}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
@@ -199,31 +210,49 @@ start_swagger_http() ->
|
||||
init_default_admins() ->
|
||||
case core_admin:list_all() of
|
||||
[] ->
|
||||
% Суперадмин
|
||||
SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")),
|
||||
SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")),
|
||||
{ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin),
|
||||
io:format("Default superadmin created: ~s~n", [SuperEmail]),
|
||||
|
||||
% Админ
|
||||
AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")),
|
||||
AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")),
|
||||
{ok, _} = core_admin:create(AdminEmail, AdminPass, admin),
|
||||
io:format("Default admin created: ~s~n", [AdminEmail]),
|
||||
|
||||
% Модератор
|
||||
ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")),
|
||||
ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")),
|
||||
{ok, _} = core_admin:create(ModerEmail, ModerPass, moderator),
|
||||
io:format("Default moderator created: ~s~n", [ModerEmail]),
|
||||
|
||||
% Поддержка
|
||||
SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")),
|
||||
SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")),
|
||||
{ok, _} = core_admin:create(SupportEmail, SupportPass, support),
|
||||
io:format("Default support created: ~s~n", [SupportEmail]);
|
||||
seed_default_admins();
|
||||
_ ->
|
||||
io:format("Admins already exist. Skipping creation.~n")
|
||||
io:format("Admins already exist. Skipping creation.~n"),
|
||||
ok
|
||||
end.
|
||||
|
||||
seed_default_admins() ->
|
||||
Admins = [
|
||||
{superadmin, "ADMIN_SUPER_EMAIL", "superadmin@eventhub.local",
|
||||
"ADMIN_SUPER_PASSWORD", "123456"},
|
||||
{admin, "ADMIN_EMAIL", "admin@eventhub.local",
|
||||
"ADMIN_PASSWORD", "123456"},
|
||||
{moderator, "ADMIN_MODER_EMAIL", "moderator@eventhub.local",
|
||||
"ADMIN_MODER_PASSWORD", "123456"},
|
||||
{support, "ADMIN_SUPPORT_EMAIL", "support@eventhub.local",
|
||||
"ADMIN_SUPPORT_PASSWORD", "123456"}
|
||||
],
|
||||
lists:foldl(fun seed_admin/2, ok, Admins).
|
||||
|
||||
seed_admin({Role, EmailKey, DefaultEmail, PassKey, DefaultPass}, ok) ->
|
||||
case admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) of
|
||||
{error, _} = Err -> Err;
|
||||
{ok, {Email, Pass}} ->
|
||||
{ok, _} = core_admin:create(Email, Pass, Role),
|
||||
io:format("Default ~p created: ~s~n", [Role, Email]),
|
||||
ok
|
||||
end;
|
||||
seed_admin(_, Err) -> Err.
|
||||
|
||||
admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) ->
|
||||
case infra_secrets:is_dev() of
|
||||
true ->
|
||||
{ok, {list_to_binary(os:getenv(EmailKey, DefaultEmail)),
|
||||
list_to_binary(os:getenv(PassKey, DefaultPass))}};
|
||||
false ->
|
||||
case infra_secrets:require_admin_seed_email(EmailKey) of
|
||||
{error, _} = Err -> Err;
|
||||
{ok, Email} ->
|
||||
case infra_secrets:require_admin_seed_password(PassKey) of
|
||||
{error, _} = Err -> Err;
|
||||
{ok, Pass} -> {ok, {Email, Pass}}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
get_env_int(Key, Default) ->
|
||||
|
||||
@@ -24,7 +24,11 @@ get_user_secret() ->
|
||||
|
||||
get_user_secret_from_env() ->
|
||||
case os:getenv("JWT_SECRET") of
|
||||
false -> <<"user-secret-key-32-bytes-minimum!">>;
|
||||
false ->
|
||||
case infra_secrets:is_dev() of
|
||||
true -> <<"user-secret-key-32-bytes-minimum!">>;
|
||||
false -> error({missing_secret, jwt_secret})
|
||||
end;
|
||||
S -> list_to_binary(S)
|
||||
end.
|
||||
|
||||
@@ -37,7 +41,11 @@ get_admin_secret() ->
|
||||
|
||||
get_admin_secret_from_env() ->
|
||||
case os:getenv("ADMIN_JWT_SECRET") of
|
||||
false -> <<"admin-secret-key-32-bytes-minimum!">>;
|
||||
false ->
|
||||
case infra_secrets:is_dev() of
|
||||
true -> <<"admin-secret-key-32-bytes-minimum!">>;
|
||||
false -> error({missing_secret, admin_jwt_secret})
|
||||
end;
|
||||
S -> list_to_binary(S)
|
||||
end.
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Валидация секретов и паролей при старте (EVENTHUB_ENV).
|
||||
%%% dev — допускаются documented fallbacks; stage/prod — fail-fast.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(infra_secrets).
|
||||
-export([mode/0, is_dev/0, validate_startup/0,
|
||||
require_admin_seed_password/1, require_admin_seed_email/1]).
|
||||
|
||||
-define(MIN_SECRET_LEN, 32).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% API
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-spec mode() -> dev | stage | prod.
|
||||
mode() ->
|
||||
case os:getenv("EVENTHUB_ENV") of
|
||||
false -> dev;
|
||||
Env ->
|
||||
case string:lowercase(Env) of
|
||||
"dev" -> dev;
|
||||
"stage" -> stage;
|
||||
"prod" -> prod;
|
||||
"production" -> prod;
|
||||
_ -> stage
|
||||
end
|
||||
end.
|
||||
|
||||
-spec is_dev() -> boolean().
|
||||
is_dev() -> mode() =:= dev.
|
||||
|
||||
%% @doc Проверка JWT-секретов до поднятия Mnesia/HTTP.
|
||||
-spec validate_startup() -> ok | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
|
||||
validate_startup() ->
|
||||
case mode() of
|
||||
dev ->
|
||||
dev_warn_if_weak(jwt_secret, effective_jwt_secret()),
|
||||
dev_warn_if_weak(admin_jwt_secret, effective_admin_jwt_secret()),
|
||||
ok;
|
||||
_ ->
|
||||
case require_strong_secret(jwt_secret, effective_jwt_secret()) of
|
||||
ok -> require_strong_secret(admin_jwt_secret, effective_admin_jwt_secret());
|
||||
Err -> Err
|
||||
end
|
||||
end.
|
||||
|
||||
-spec require_admin_seed_email(string()) ->
|
||||
{ok, binary()} | {error, {missing_secret, atom()}}.
|
||||
require_admin_seed_email(EnvKey) ->
|
||||
case os:getenv(EnvKey) of
|
||||
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||
Email -> {ok, list_to_binary(Email)}
|
||||
end.
|
||||
|
||||
-spec require_admin_seed_password(string()) ->
|
||||
{ok, binary()} | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
|
||||
require_admin_seed_password(EnvKey) ->
|
||||
case os:getenv(EnvKey) of
|
||||
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
|
||||
Pass ->
|
||||
Bin = list_to_binary(Pass),
|
||||
case is_strong_secret(Bin) of
|
||||
true -> {ok, Bin};
|
||||
false -> {error, {weak_secret, list_to_atom(EnvKey)}}
|
||||
end
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% Effective values (порядок как в eventhub_auth)
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
effective_jwt_secret() ->
|
||||
effective_secret(jwt_secret, "JWT_SECRET").
|
||||
|
||||
effective_admin_jwt_secret() ->
|
||||
effective_secret(admin_jwt_secret, "ADMIN_JWT_SECRET").
|
||||
|
||||
effective_secret(AppKey, OsKey) ->
|
||||
case application:get_env(eventhub, AppKey) of
|
||||
{ok, Secret} when is_binary(Secret), byte_size(Secret) > 0 ->
|
||||
case is_placeholder(Secret) of
|
||||
true -> os_getenv_binary(OsKey);
|
||||
false -> Secret
|
||||
end;
|
||||
_ ->
|
||||
os_getenv_binary(OsKey)
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% Internal
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
require_strong_secret(Name, undefined) ->
|
||||
{error, {missing_secret, Name}};
|
||||
require_strong_secret(Name, Secret) ->
|
||||
case is_strong_secret(Secret) of
|
||||
true -> ok;
|
||||
false -> {error, {weak_secret, Name}}
|
||||
end.
|
||||
|
||||
is_strong_secret(Secret) when is_binary(Secret) ->
|
||||
byte_size(Secret) >= ?MIN_SECRET_LEN andalso not lists:member(Secret, denylist()).
|
||||
|
||||
is_placeholder(Secret) ->
|
||||
lists:member(Secret, [
|
||||
<<"change_me_in_production">>,
|
||||
<<"${JWT_SECRET:-change_me_in_production}">>,
|
||||
<<"${ADMIN_JWT_SECRET:-change_me_in_production}">>
|
||||
]).
|
||||
|
||||
denylist() -> [
|
||||
<<"123456">>,
|
||||
<<"change_me_in_production">>,
|
||||
<<"eventhub_top_secret">>,
|
||||
<<"user-secret-key-32-bytes-minimum!">>,
|
||||
<<"admin-secret-key-32-bytes-minimum!">>,
|
||||
<<"botpass123">>
|
||||
].
|
||||
|
||||
os_getenv_binary(Key) ->
|
||||
case os:getenv(Key) of
|
||||
false -> undefined;
|
||||
"" -> undefined;
|
||||
Val -> list_to_binary(Val)
|
||||
end.
|
||||
|
||||
dev_warn_if_weak(_Name, undefined) -> ok;
|
||||
dev_warn_if_weak(Name, Secret) ->
|
||||
case is_strong_secret(Secret) of
|
||||
true -> ok;
|
||||
false ->
|
||||
io:format("~n[infra_secrets] WARNING: ~p is weak or missing in dev mode; "
|
||||
"runtime fallbacks may apply.~n", [Name])
|
||||
end.
|
||||
Reference in New Issue
Block a user