Fail-fast secrets in stage/prod via EVENTHUB_ENV. Refs EventHub/EventHubBack#25

This commit is contained in:
2026-07-07 19:52:04 +03:00
parent 018bf85b87
commit cf8e6bcc07
10 changed files with 855 additions and 583 deletions
+10 -2
View File
@@ -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.
+137
View File
@@ -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.