Erlang ssh server #1

Merged
Simponic merged 7 commits from erlang_ssh_server into main 2022-12-29 20:37:51 -05:00
7 changed files with 32 additions and 109 deletions
Showing only changes of commit 0aab3c2027 - Show all commits

View File

@ -8,7 +8,7 @@ config :chessh,
config :chessh, RateLimits, config :chessh, RateLimits,
jail_timeout_ms: 5 * 60 * 1000, jail_timeout_ms: 5 * 60 * 1000,
jail_threshold: 15, jail_attempt_threshold: 15,
max_concurrent_user_sessions: 5 max_concurrent_user_sessions: 5
config :hammer, config :hammer,

View File

@ -2,7 +2,7 @@ import Config
config :chessh, RateLimits, config :chessh, RateLimits,
jail_timeout_ms: 1000, jail_timeout_ms: 1000,
jail_threshold: 2 jail_attempt_threshold: 3
config :chessh, Chessh.Repo, config :chessh, Chessh.Repo,
database: "chessh-test", database: "chessh-test",

View File

@ -22,26 +22,26 @@ defmodule Chessh.SSH.Daemon do
end end
def pwd_authenticate(username, password, inet) do def pwd_authenticate(username, password, inet) do
[jail_timeout_ms, jail_threshold] = [jail_timeout_ms, jail_attempt_threshold] =
Application.get_env(:chessh, RateLimits) Application.get_env(:chessh, RateLimits)
|> Keyword.take([:jail_timeout_ms, :jail_threshold]) |> Keyword.take([:jail_timeout_ms, :jail_attempt_threshold])
|> Keyword.values() |> Keyword.values()
{ip, _port} = inet {ip, _port} = inet
rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}" rateId = "failed_password_attempts:#{Enum.join(Tuple.to_list(ip), ".")}"
case Hammer.check_rate(rateId, jail_timeout_ms, jail_threshold) do if pwd_authenticate(username, password) do
true
else
case Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_attempt_threshold, 1) do
{:allow, _count} -> {:allow, _count} ->
pwd_authenticate(username, password) ||
(fn ->
Hammer.check_rate_inc(rateId, jail_timeout_ms, jail_threshold, 1)
false false
end).()
{:deny, _limit} -> {:deny, _limit} ->
:disconnect :disconnect
end end
end end
end
def pwd_authenticate(username, password, inet, _address), def pwd_authenticate(username, password, inet, _address),
do: pwd_authenticate(username, password, inet) do: pwd_authenticate(username, password, inet)

View File

@ -5,7 +5,8 @@ defmodule Chessh.Auth.PasswordAuthenticatorTest do
@valid_user %{username: "logan", password: "password"} @valid_user %{username: "logan", password: "password"}
setup_all do setup_all do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) Ecto.Adapters.SQL.Sandbox.checkout(Repo)
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
{:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) {:ok, _user} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))

View File

@ -9,7 +9,8 @@ defmodule Chessh.Auth.PublicKeyAuthenticatorTest do
} }
setup_all do setup_all do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Chessh.Repo) Ecto.Adapters.SQL.Sandbox.checkout(Repo)
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))

View File

@ -1,81 +0,0 @@
defmodule Chessh.SSH.AuthTest do
use ExUnit.Case
alias Chessh.{Player, Repo, Key}
@localhost '127.0.0.1'
@key_name "The Gamer Machine"
@valid_user %{username: "logan", password: "password"}
@client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
@client_pub_key 'id_ed25519.pub'
setup_all do
case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do
:ok -> nil
{:already, :owner} -> nil
end
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
{:ok, key_text} = File.read(Path.join(@client_test_keys_dir, @client_pub_key))
{:ok, _key} =
Repo.insert(
Key.changeset(%Key{}, %{key: key_text, name: @key_name})
|> Ecto.Changeset.put_assoc(:player, player)
)
:ok
end
test "Password attempts are rate limited" do
assert :disconnect ==
Enum.reduce(
1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold),
fn _, _ ->
Chessh.SSH.Daemon.pwd_authenticate(
@valid_user.username,
'wrong_password',
@localhost
) do
end
)
end
test "INTEGRATION - Can ssh into daemon with password or public key" do
{:ok, sup} = Task.Supervisor.start_link()
test_pid = self()
Task.Supervisor.start_child(sup, fn ->
{:ok, _pid} =
:ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
user: String.to_charlist(@valid_user.username),
password: String.to_charlist(@valid_user.password),
auth_methods: 'password',
silently_accept_hosts: true
)
send(test_pid, :connected_via_password)
end)
Task.Supervisor.start_child(sup, fn ->
{:ok, _pid} =
:ssh.connect(@localhost, Application.fetch_env!(:chessh, :port),
user: String.to_charlist(@valid_user.username),
auth_methods: 'publickey',
silently_accept_hosts: true,
user_dir: String.to_charlist(@client_test_keys_dir)
)
send(test_pid, :connected_via_public_key)
end)
assert_receive(:connected_via_password, 500)
assert_receive(:connected_via_public_key, 500)
end
test "INTEGRATION - User cannot have more than specified concurrent sessions" do
:ok
end
end

View File

@ -3,17 +3,14 @@ defmodule Chessh.SSH.AuthTest do
alias Chessh.{Player, Repo, Key} alias Chessh.{Player, Repo, Key}
@localhost '127.0.0.1' @localhost '127.0.0.1'
@localhost_inet {{127, 0, 0, 1}, 1}
@key_name "The Gamer Machine" @key_name "The Gamer Machine"
@valid_user %{username: "logan", password: "password"} @valid_user %{username: "logan", password: "password"}
@client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys") @client_test_keys_dir Path.join(Application.compile_env!(:chessh, :key_dir), "client_keys")
@client_pub_key 'id_ed25519.pub' @client_pub_key 'id_ed25519.pub'
setup_all do setup_all do
case Ecto.Adapters.SQL.Sandbox.checkout(Repo) do Ecto.Adapters.SQL.Sandbox.checkout(Repo)
:ok -> nil
{:already, :owner} -> nil
end
Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
{:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user)) {:ok, player} = Repo.insert(Player.registration_changeset(%Player{}, @valid_user))
@ -30,15 +27,19 @@ defmodule Chessh.SSH.AuthTest do
end end
test "Password attempts are rate limited" do test "Password attempts are rate limited" do
jail_attempt_threshold =
Application.get_env(:chessh, RateLimits)
|> Keyword.get(:jail_attempt_threshold)
assert :disconnect == assert :disconnect ==
Enum.reduce( Enum.reduce(
1..Application.fetch_env!(:chessh, RateLimits, :jail_threshold), 0..(jail_attempt_threshold + 1),
fn _, _ -> fn _, _ ->
Chessh.SSH.Daemon.pwd_authenticate( Chessh.SSH.Daemon.pwd_authenticate(
@valid_user.username, @valid_user.username,
'wrong_password', "wrong_password",
@localhost @localhost_inet
) do )
end end
) )
end end
@ -75,7 +76,8 @@ defmodule Chessh.SSH.AuthTest do
assert_receive(:connected_via_public_key, 500) assert_receive(:connected_via_public_key, 500)
end end
test "INTEGRATION - User cannot have more than specified concurrent sessions" do # TODO
:ok # test "INTEGRATION - User cannot have more than specified concurrent sessions" do
end # :ok
# end
end end