summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/login/pam_otp_ga.conf.j27
-rw-r--r--data/templates/ssh/sshd_config.j24
-rw-r--r--debian/vyos-1x.postinst10
-rw-r--r--interface-definitions/system-login.xml.in76
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py16
-rwxr-xr-xsrc/conf_mode/system-login.py15
6 files changed, 125 insertions, 3 deletions
diff --git a/data/templates/login/pam_otp_ga.conf.j2 b/data/templates/login/pam_otp_ga.conf.j2
new file mode 100644
index 000000000..4c1f411d1
--- /dev/null
+++ b/data/templates/login/pam_otp_ga.conf.j2
@@ -0,0 +1,7 @@
+{% if authentication.otp.key is vyos_defined %}
+{{ authentication.otp.key }}
+" RATE_LIMIT {{ authentication.otp.rate_limit }} {{ authentication.otp.rate_time }}
+" WINDOW_SIZE {{ authentication.otp.window_size }}
+" DISALLOW_REUSE
+" TOTP_AUTH
+{% endif %}
diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2
index e7dbca581..93c6735dd 100644
--- a/data/templates/ssh/sshd_config.j2
+++ b/data/templates/ssh/sshd_config.j2
@@ -17,7 +17,6 @@ PubkeyAuthentication yes
IgnoreRhosts yes
HostbasedAuthentication no
PermitEmptyPasswords no
-ChallengeResponseAuthentication no
X11Forwarding yes
X11DisplayOffset 10
PrintMotd no
@@ -30,6 +29,7 @@ PermitRootLogin no
PidFile /run/sshd/sshd.pid
AddressFamily any
DebianBanner no
+PasswordAuthentication no
#
# User configurable section
@@ -48,7 +48,7 @@ Port {{ value }}
LogLevel {{ loglevel | upper }}
# Specifies whether password authentication is allowed
-PasswordAuthentication {{ "no" if disable_password_authentication is vyos_defined else "yes" }}
+ChallengeResponseAuthentication {{ "no" if disable_password_authentication is vyos_defined else "yes" }}
{% if listen_address is vyos_defined %}
# Specifies the local addresses sshd should listen on
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 6879b6e4f..dc64e7a42 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -21,6 +21,16 @@ if ! grep -q '^openvpn' /etc/passwd; then
adduser --quiet --firstuid 100 --system --group --shell /usr/sbin/nologin openvpn
fi
+# Add 2FA support for SSH
+sudo grep -qF -- "auth required pam_google_authenticator.so nullok" "/etc/pam.d/sshd" || \
+sudo sed -i '/^@include common-auth/a # Check OTP 2FA, if configured for the user\nauth required pam_google_authenticator.so nullok' /etc/pam.d/sshd \
+/
+
+# Add 2FA support for local authentication
+sudo grep -qF -- "auth required pam_google_authenticator.so nullok" "/etc/pam.d/login" || \
+sudo sed -i '/^@include common-auth/a # Check OTP 2FA, if configured for the user\nauth required pam_google_authenticator.so nullok' /etc/pam.d/login \
+/
+
# Add RADIUS operator user for RADIUS authenticated users to map to
if ! grep -q '^radius_user' /etc/passwd; then
adduser --quiet --firstuid 1000 --disabled-login --ingroup vyattaop \
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 24eeee355..79c7c4791 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -8,6 +8,62 @@
<priority>400</priority>
</properties>
<children>
+ <node name="authentication">
+ <properties>
+ <help>Global authentication settings</help>
+ </properties>
+ <children>
+ <node name="otp">
+ <properties>
+ <help>2FA OTP authentication parameters</help>
+ </properties>
+ <children>
+ <leafNode name="rate-limit">
+ <properties>
+ <help>Number of attempts. Limit logins to N per every M seconds</help>
+ <valueHelp>
+ <format>u32:1-10</format>
+ <description>Number of attempts. Limit logins to N per every M seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-10"/>
+ </constraint>
+ <constraintErrorMessage>Number of login attempts must me between 1 and 10</constraintErrorMessage>
+ </properties>
+ <defaultValue>3</defaultValue>
+ </leafNode>
+ <leafNode name="rate-time">
+ <properties>
+ <help>Time interval. Limit logins to N per every M seconds</help>
+ <valueHelp>
+ <format>u32:15-600</format>
+ <description>Time interval. Limit logins to N per every M seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 15-600"/>
+ </constraint>
+ <constraintErrorMessage>Rate limit time interval must be between 15 and 600 seconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>30</defaultValue>
+ </leafNode>
+ <leafNode name="window-size">
+ <properties>
+ <help>Set window of concurrently valid codes</help>
+ <valueHelp>
+ <format>u32:1-21</format>
+ <description>Set window of concurrently valid codes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-21"/>
+ </constraint>
+ <constraintErrorMessage>Window of concurrently valid codes must be between 1 and 21</constraintErrorMessage>
+ </properties>
+ <defaultValue>3</defaultValue>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
<tagNode name="user">
<properties>
<help>Local user account information</help>
@@ -36,6 +92,26 @@
</properties>
<defaultValue>!</defaultValue>
</leafNode>
+ <node name="otp">
+ <properties>
+ <help>2FA OTP authentication parameters</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>Token Key Secret key for the token algorithm (see RFC 4226)</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>OTP key (base32 encoded secret)</description>
+ </valueHelp>
+ <constraint>
+ <regex>[a-zA-Z2-7]{20,10000}</regex>
+ </constraint>
+ <constraintErrorMessage>Key must only include base32 characters and be at least 26 characters long</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
<leafNode name="plaintext-password">
<properties>
<help>Plaintext password used for encryption</help>
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 1131b6f93..a99721d66 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -97,6 +97,22 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# b'Linux LR1.wue3 5.10.61-amd64-vyos #1 SMP Fri Aug 27 08:55:46 UTC 2021 x86_64 GNU/Linux\n'
self.assertTrue(len(stdout) > 40)
+ def test_system_login_otp(self):
+ otp_user = 'otp-test_user'
+ otp_password = 'SuperTestPassword'
+ otp_key = '76A3ZS6HFHBTOK2H4NDHTIVFPQ'
+
+ self.cli_set(base_path + ['user', otp_user, 'authentication', 'plaintext-password', otp_password])
+ self.cli_set(base_path + ['user', otp_user, 'authentication', 'otp', 'key', otp_key])
+
+ self.cli_commit()
+
+ # Check if OTP key was written properly
+ tmp = cmd(f'sudo head -1 /home/{otp_user}/.google_authenticator')
+ self.assertIn(otp_key, tmp)
+
+ self.cli_delete(base_path + ['user', otp_user])
+
def test_system_user_ssh_key(self):
ssh_user = 'ssh-test_user'
public_keys = 'vyos_test@domain-foo.com'
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 3dcbc995c..fc2723ece 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -245,7 +245,20 @@ def apply(login):
user_config, permission=0o600,
formater=lambda _: _.replace("&quot;", '"'),
user=user, group='users')
-
+ #OTP 2FA key file generation
+ if dict_search('authentication.otp.key', user_config):
+ user_config['authentication']['otp']['key'] = user_config['authentication']['otp']['key'].upper()
+ user_config['authentication']['otp']['rate_limit'] = login['authentication']['otp']['rate_limit']
+ user_config['authentication']['otp']['rate_time'] = login['authentication']['otp']['rate_time']
+ user_config['authentication']['otp']['window_size'] = login['authentication']['otp']['window_size']
+ render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2',
+ user_config, permission=0o600,
+ formater=lambda _: _.replace("&quot;", '"'),
+ user=user, group='users')
+ #OTP 2FA key file deletion
+ elif os.path.exists(f'{home_dir}/.google_authenticator'):
+ os.remove(f'{home_dir}/.google_authenticator')
+
except Exception as e:
raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')