diff --git a/docker/DockerFile b/docker/DockerFile index 778f294..9c8f0cd 100644 --- a/docker/DockerFile +++ b/docker/DockerFile @@ -7,6 +7,8 @@ COPY --chown=sshnet:sshnet user/sshnet /home/sshnet/.ssh RUN apk add --no-cache syslog-ng && \ # install and configure sshd apk add --no-cache openssh && \ + # install openssh-server-pam to allow for keyboard-interactive authentication + apk add --no-cache openssh-server-pam && \ chmod 400 /etc/ssh/ssh*key && \ sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ sed -i 's/#LogLevel\s*INFO/LogLevel DEBUG3/' /etc/ssh/sshd_config && \ @@ -29,7 +31,9 @@ RUN apk add --no-cache syslog-ng && \ passwd -u sshnetadm && \ echo 'sshnetadm:ssh4ever' | chpasswd && \ addgroup sshnetadm sudo && \ - dos2unix /opt/sshnet/* + dos2unix /opt/sshnet/* && \ + # install shadow package; we use chage command in this package to expire/unexpire password of the sshnet user + apk add --no-cache shadow EXPOSE 22 22 diff --git a/docker/build.cmd b/docker/build.cmd index b0b5836..9b823df 100644 --- a/docker/build.cmd +++ b/docker/build.cmd @@ -1,2 +1,4 @@ @echo off -docker build -t sshnet -f DockerFile . \ No newline at end of file + +rem Build new image +docker build -t sshnet -f DockerFile . diff --git a/docker/prune.cmd b/docker/prune.cmd new file mode 100644 index 0000000..262d66c --- /dev/null +++ b/docker/prune.cmd @@ -0,0 +1,4 @@ +@echo off + +rem Remove dangling images +docker image prune -f diff --git a/docker/run.cmd b/docker/run.cmd index 398c267..299e7a4 100644 --- a/docker/run.cmd +++ b/docker/run.cmd @@ -1,2 +1,2 @@ @echo off -docker run --rm -p 22:22 --name sshnet -d sshnet \ No newline at end of file +docker run --rm -p 22:22 --name sshnet -d sshnet diff --git a/docker/server/script/start.sh b/docker/server/script/start.sh index baa6464..e7e9f75 100644 --- a/docker/server/script/start.sh +++ b/docker/server/script/start.sh @@ -1,4 +1,10 @@ #!/bin/ash /usr/sbin/syslog-ng -/usr/sbin/sshd -tail -f < /var/log/auth.log \ No newline at end of file + +# allow us to make changes to /etc/hosts; we need this for the port forwarding tests +chmod 777 /etc/hosts + +# start PAM-enabled ssh daemon as we also want keyboard-interactive authentication to work +/usr/sbin/sshd.pam + +tail -f < /var/log/auth.log diff --git a/src/SshNetTests/AuthenticationTests.cs b/src/SshNetTests/AuthenticationTests.cs index 4978e32..5dae02b 100644 --- a/src/SshNetTests/AuthenticationTests.cs +++ b/src/SshNetTests/AuthenticationTests.cs @@ -26,9 +26,23 @@ public void SetUp() [TestCleanup] public void TearDown() { - if (_remoteSshdConfig != null) + _remoteSshdConfig?.Reset(); + + using (var client = new SshClient(_adminConnectionInfoFactory.Create())) { - _remoteSshdConfig.Reset(); + client.Connect(); + + // Reset the password back to the "regular" password. + using (var cmd = client.RunCommand($"echo \"{Users.Regular.Password}\n{Users.Regular.Password}\" | sudo passwd " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + + // Remove password expiration + using (var cmd = client.RunCommand($"sudo chage --expiredate -1 " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } } } @@ -37,6 +51,8 @@ public void Multifactor_KeyboardInteractiveAndPublicKey() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive,publickey") .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) .Update() .Restart(); @@ -95,7 +111,9 @@ public void Multifactor_Password_MatchPartialSuccessLimit() public void Multifactor_Password_Or_PublicKeyAndKeyboardInteractive() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,keyboard-interactive") - .WithChallengeResponseAuthentication(false) + .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) .Update() .Restart(); @@ -111,7 +129,6 @@ public void Multifactor_Password_Or_PublicKeyAndKeyboardInteractive() public void Multifactor_Password_Or_PublicKeyAndPassword_BadPassword() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,password") - .WithChallengeResponseAuthentication(false) .Update() .Restart(); @@ -136,7 +153,6 @@ public void Multifactor_Password_Or_PublicKeyAndPassword_BadPassword() public void Multifactor_PasswordAndPublicKey_Or_PasswordAndPassword() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,publickey password,password") - .WithChallengeResponseAuthentication(false) .Update() .Restart(); @@ -169,7 +185,6 @@ public void Multifactor_PasswordAndPublicKey_Or_PasswordAndPassword() public void Multifactor_PasswordAndPassword_Or_PublicKey() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password publickey") - .WithChallengeResponseAuthentication(false) .Update() .Restart(); @@ -192,7 +207,6 @@ public void Multifactor_PasswordAndPassword_Or_PublicKey() public void Multifactor_Password_Or_Password() { _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password password") - .WithChallengeResponseAuthentication(false) .Update() .Restart(); @@ -219,7 +233,7 @@ public void KeyboardInteractive_PasswordExpired() { client.Connect(); - // Temporarity modify password so that when a expire this password, we change reset the password back to + // Temporarity modify password so that when we expire this password, we change reset the password back to // the "regular" password. using (var cmd = client.RunCommand($"echo \"{temporaryPassword}\n{temporaryPassword}\" | sudo passwd " + Users.Regular.UserName)) { @@ -227,7 +241,7 @@ public void KeyboardInteractive_PasswordExpired() } // Force the password to expire immediately - using (var cmd = client.RunCommand($"sudo passwd --expire " + Users.Regular.UserName)) + using (var cmd = client.RunCommand($"sudo chage -d 0 " + Users.Regular.UserName)) { Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); } @@ -235,6 +249,8 @@ public void KeyboardInteractive_PasswordExpired() _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive") .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) .Update() .Restart(); @@ -274,14 +290,6 @@ public void KeyboardInteractive_PasswordExpired() client.Connect(); Assert.AreEqual(4, authenticationPromptCount); } - - _remoteSshdConfig.Reset(); - - connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); - using (var client = new SftpClient(connectionInfo)) - { - client.Connect(); - } } } } diff --git a/src/SshNetTests/Common/RemoteSshdConfigExtensions.cs b/src/SshNetTests/Common/RemoteSshdConfigExtensions.cs index c058d8b..9c66efb 100644 --- a/src/SshNetTests/Common/RemoteSshdConfigExtensions.cs +++ b/src/SshNetTests/Common/RemoteSshdConfigExtensions.cs @@ -10,11 +10,12 @@ public static void Reset(this RemoteSshdConfig remoteSshdConfig) { remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, DefaultAuthenticationMethods) .WithChallengeResponseAuthentication(false) + .WithKeyboardInteractiveAuthentication(false) .WithLogLevel(LogLevel.Debug3) .ClearHostKeyFiles() .AddHostKeyFile(HostKeyFile.Rsa.FilePath) .ClearSubsystems() - .AddSubsystem(new Subsystem("sftp", "/usr/lib/openssh/sftp-server")) + .AddSubsystem(new Subsystem("sftp", "/usr/lib/ssh/sftp-server")) .ClearCiphers() .ClearKeyExchangeAlgorithms() .ClearHostKeyAlgorithms() diff --git a/src/SshNetTests/RemoteSshd.cs b/src/SshNetTests/RemoteSshd.cs index 1b5904a..7652f65 100644 --- a/src/SshNetTests/RemoteSshd.cs +++ b/src/SshNetTests/RemoteSshd.cs @@ -29,14 +29,14 @@ public RemoteSshd Restart() client.Connect(); // Kill all processes that start with 'sshd' and that run as root - var stopCommand = client.CreateCommand("sudo pkill -9 -U 0 sshd"); + var stopCommand = client.CreateCommand("sudo pkill -9 -U 0 sshd.pam"); var stopOutput = stopCommand.Execute(); if (stopCommand.ExitStatus != 0) { throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}"); } - var resetFailedCommand = client.CreateCommand("sudo /usr/sbin/sshd"); + var resetFailedCommand = client.CreateCommand("sudo /usr/sbin/sshd.pam"); var resetFailedOutput = resetFailedCommand.Execute(); if (resetFailedCommand.ExitStatus != 0) { @@ -76,12 +76,32 @@ public RemoteSshdConfig(RemoteSshd remoteSshd, IConnectionInfoFactory connection } } - public RemoteSshdConfig WithChallengeResponseAuthentication(bool value) + /// + /// Specifies whether challenge-response authentication is allowed. + /// + /// to allow challenge-response authentication. + /// + /// The current instance. + /// + public RemoteSshdConfig WithChallengeResponseAuthentication(bool? value) { _config.ChallengeResponseAuthentication = value; return this; } + /// + /// Specifies whether to allow keyboard-interactive authentication. + /// + /// to allow keyboard-interactive authentication. + /// + /// The current instance. + /// + public RemoteSshdConfig WithKeyboardInteractiveAuthentication(bool value) + { + _config.KeyboardInteractiveAuthentication = value; + return this; + } + public RemoteSshdConfig WithAuthenticationMethods(string user, string authenticationMethods) { var sshNetMatch = _config.Matches.FirstOrDefault(m => m.Users.Contains(user)); @@ -164,7 +184,7 @@ public RemoteSshdConfig WithLogLevel(LogLevel logLevel) public RemoteSshdConfig WithUsePAM(bool usePAM) { - _config.UsePAM = true; + _config.UsePAM = usePAM; return this; } diff --git a/src/SshNetTests/SshTests.cs b/src/SshNetTests/SshTests.cs index d9b7974..c3af625 100644 --- a/src/SshNetTests/SshTests.cs +++ b/src/SshNetTests/SshTests.cs @@ -63,7 +63,7 @@ public void Ssh_ShellStream_Exit() shellStream.ReadLine(); Assert.Fail(); } - catch (NullReferenceException ex) + catch (NullReferenceException) { }