diff options
-rw-r--r-- | data/templates/login/pam_otp_ga.conf.j2 | 7 | ||||
-rw-r--r-- | data/templates/ssh/sshd_config.j2 | 4 | ||||
-rw-r--r-- | debian/vyos-1x.postinst | 10 | ||||
-rw-r--r-- | interface-definitions/system-login.xml.in | 76 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_system_login.py | 16 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py | 15 |
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(""", '"'), 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(""", '"'), + 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}"') |