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 79b07478b..5bbfdeb88 100644
--- a/data/templates/ssh/sshd_config.j2
+++ b/data/templates/ssh/sshd_config.j2
@@ -1,102 +1,102 @@
 ### Autogenerated by ssh.py ###
 
 # https://linux.die.net/man/5/sshd_config
 
 #
 # Non-configurable defaults
 #
 Protocol 2
 HostKey /etc/ssh/ssh_host_rsa_key
 HostKey /etc/ssh/ssh_host_dsa_key
 HostKey /etc/ssh/ssh_host_ecdsa_key
 HostKey /etc/ssh/ssh_host_ed25519_key
 SyslogFacility AUTH
 LoginGraceTime 120
 StrictModes yes
 PubkeyAuthentication yes
 IgnoreRhosts yes
 HostbasedAuthentication no
 PermitEmptyPasswords no
-ChallengeResponseAuthentication no
 X11Forwarding yes
 X11DisplayOffset 10
 PrintMotd no
 PrintLastLog yes
 TCPKeepAlive yes
 Banner /etc/issue.net
 Subsystem sftp /usr/lib/openssh/sftp-server
 UsePAM yes
 PermitRootLogin no
 PidFile /run/sshd/sshd.pid
 AddressFamily any
 DebianBanner no
+PasswordAuthentication no
 
 #
 # User configurable section
 #
 
 # Look up remote host name and check that the resolved host name for the remote IP
 # address maps back to the very same IP address.
 UseDNS {{ "no" if disable_host_validation is vyos_defined else "yes" }}
 
 # Specifies the port number that sshd(8) listens on
 {% for value in port %}
 Port {{ value }}
 {% endfor %}
 
 # Gives the verbosity level that is used when logging messages from sshd
 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
 {%     for address in listen_address %}
 ListenAddress {{ address }}
 {%     endfor %}
 {% endif %}
 
 {% if ciphers is vyos_defined %}
 # Specifies the ciphers allowed for protocol version 2
 Ciphers {{ ciphers | join(',') }}
 {% endif %}
 
 {% if mac is vyos_defined %}
 # Specifies the available MAC (message authentication code) algorithms
 MACs {{ mac | join(',') }}
 {% endif %}
 
 {% if key_exchange is vyos_defined %}
 # Specifies the available Key Exchange algorithms
 KexAlgorithms {{ key_exchange | join(',') }}
 {% endif %}
 
 {% if access_control is vyos_defined %}
 {%     if access_control.allow.user is vyos_defined %}
 # If specified, login is allowed only for user names that match
 AllowUsers {{ access_control.allow.user | join(' ') }}
 {%     endif %}
 {%     if access_control.allow.group is vyos_defined %}
 # If specified, login is allowed only for users whose primary group or supplementary group list matches
 AllowGroups {{ access_control.allow.group | join(' ') }}
 {%     endif %}
 {%     if access_control.deny.user is vyos_defined %}
 # Login is disallowed for user names that match
 DenyUsers {{ access_control.deny.user | join(' ') }}
 {%     endif %}
 {%     if access_control.deny.group is vyos_defined %}
 # Login is disallowed for users whose primary group or supplementary group list matches
 DenyGroups {{ access_control.deny.group | join(' ') }}
 {%     endif %}
 {% endif %}
 
 {% if client_keepalive_interval is vyos_defined %}
 # Sets a timeout interval in seconds after which if no data has been received from the client,
 # sshd(8) will send a message through the encrypted channel to request a response from the client
 ClientAliveInterval {{ client_keepalive_interval }}
 {% endif %}
 
 {% if rekey.data is vyos_defined  %}
 RekeyLimit {{ rekey.data }}M {{ rekey.time + 'M' if rekey.time is vyos_defined }}
 {% endif %}
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
@@ -1,107 +1,117 @@
 #!/bin/sh -e
 
 # Turn off Debian default for %sudo
 sed -i -e '/^%sudo/d' /etc/sudoers || true
 
 # Add minion user for salt-minion
 if ! grep -q '^minion' /etc/passwd; then
     adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg \
         --gecos "salt minion user" --shell /bin/vbash minion
     adduser --quiet minion frrvty
     adduser --quiet minion sudo
     adduser --quiet minion adm
     adduser --quiet minion dip
     adduser --quiet minion disk
     adduser --quiet minion users
     adduser --quiet minion frr
 fi
 
 # OpenVPN should get its own user
 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 \
         --no-create-home --gecos "radius user" \
         --shell /sbin/radius_shell radius_user
     adduser --quiet radius_user frrvty
     adduser --quiet radius_user vyattaop
     adduser --quiet radius_user operator
     adduser --quiet radius_user adm
     adduser --quiet radius_user dip
     adduser --quiet radius_user users
 fi
 
 # Add RADIUS admin user for RADIUS authenticated users to map to
 if ! grep -q '^radius_priv_user' /etc/passwd; then
     adduser --quiet --firstuid 1000 --disabled-login --ingroup vyattacfg \
         --no-create-home --gecos "radius privileged user" \
         --shell /sbin/radius_shell radius_priv_user
     adduser --quiet radius_priv_user frrvty
     adduser --quiet radius_priv_user vyattacfg
     adduser --quiet radius_priv_user sudo
     adduser --quiet radius_priv_user adm
     adduser --quiet radius_priv_user dip
     adduser --quiet radius_priv_user disk
     adduser --quiet radius_priv_user users
     adduser --quiet radius_priv_user frr
 fi
 
 # add hostsd group for vyos-hostsd
 if ! grep -q '^hostsd' /etc/group; then
     addgroup --quiet --system hostsd
 fi
 
 # add dhcpd user for dhcp-server
 if ! grep -q '^dhcpd' /etc/passwd; then
     adduser --quiet --system --disabled-login --no-create-home --home /run/dhcp-server dhcpd
     adduser --quiet dhcpd hostsd
 fi
 
 # ensure hte proxy user has a proper shell
 chsh -s /bin/sh proxy
 
 # create /opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 POSTCONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
 if [ ! -x $POSTCONFIG_SCRIPT ]; then
     mkdir -p $(dirname $POSTCONFIG_SCRIPT)
     touch $POSTCONFIG_SCRIPT
     chmod 755 $POSTCONFIG_SCRIPT
     cat <<EOF >>$POSTCONFIG_SCRIPT
 #!/bin/sh
 # This script is executed at boot time after VyOS configuration is fully applied.
 # Any modifications required to work around unfixed bugs
 # or use services not available through the VyOS CLI system can be placed here.
 
 EOF
 fi
 
 # symlink destination is deleted during ISO assembly - this generates some noise
 # when the system boots: systemd-sysv-generator[1881]: stat() failed on
 # /etc/init.d/README, ignoring: No such file or directory. Thus we simply drop
 # the file.
 if [ -L /etc/init.d/README ]; then
     rm -f /etc/init.d/README
 fi
 
 # Remove unwanted daemon files from /etc
 # conntackd
 # pmacct
 # fastnetmon
 # ntp
 DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
         /etc/default/pmacctd /etc/pmacct
         /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf
         /etc/ntp.conf /etc/default/ssh
         /etc/powerdns /etc/default/pdns-recursor"
 for tmp in $DELETE; do
     if [ -e ${tmp} ]; then
         rm -rf ${tmp}
     fi
 done
 
 # Remove logrotate items controlled via CLI and VyOS defaults
 sed -i '/^\/var\/log\/messages$/d' /etc/logrotate.d/rsyslog
 sed -i '/^\/var\/log\/auth.log$/d' /etc/logrotate.d/rsyslog
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index d189be3f8..7dd045e6c 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -1,171 +1,247 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="system">
     <children>
       <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py">
         <properties>
           <help>System User Login Configuration</help>
           <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>
               <constraint>
                 <regex>[-_a-zA-Z0-9.]{1,100}</regex>
               </constraint>
               <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
             </properties>
             <children>
               <node name="authentication">
                 <properties>
                   <help>Password authentication</help>
                 </properties>
                 <children>
                   <leafNode name="encrypted-password">
                     <properties>
                       <help>Encrypted password</help>
                       <constraint>
                         <regex>(\*|\!)</regex>
                         <regex>[a-zA-Z0-9\.\/]{13}</regex>
                         <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex>
                         <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex>
                         <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage>
                     </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>
                     </properties>
                   </leafNode>
                   <tagNode name="public-keys">
                     <properties>
                       <help>Remote access public keys</help>
                       <valueHelp>
                         <format>txt</format>
                         <description>Key identifier used by ssh-keygen (usually of form user@host)</description>
                       </valueHelp>
                     </properties>
                     <children>
                       <leafNode name="key">
                         <properties>
                           <help>Public key value (Base64 encoded)</help>
                           <constraint>
                             <validator name="base64"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="options">
                         <properties>
                           <help>Optional public key options</help>
                         </properties>
                       </leafNode>
                       <leafNode name="type">
                         <properties>
                           <help>Public key type</help>
                           <completionHelp>
                             <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list>
                           </completionHelp>
                           <valueHelp>
                             <format>ssh-dss</format>
                             <description/>
                           </valueHelp>
                           <valueHelp>
                             <format>ssh-rsa</format>
                             <description/>
                           </valueHelp>
                           <valueHelp>
                             <format>ecdsa-sha2-nistp256</format>
                             <description/>
                           </valueHelp>
                           <valueHelp>
                             <format>ecdsa-sha2-nistp384</format>
                             <description/>
                           </valueHelp>
                           <valueHelp>
                             <format>ssh-ed25519</format>
                             <description/>
                           </valueHelp>
                           <constraint>
                             <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)</regex>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                 </children>
               </node>
               <leafNode name="full-name">
                 <properties>
                   <help>Full name of the user (use quotes for names with spaces)</help>
                   <constraint>
                     <regex>[^:]*</regex>
                   </constraint>
                   <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="home-directory">
                 <properties>
                   <help>Home directory</help>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
           #include <include/radius-server-ipv4-ipv6.xml.i>
           <node name="radius">
             <children>
               <tagNode name="server">
                 <children>
                   <leafNode name="timeout">
                     <properties>
                       <help>Session timeout</help>
                       <valueHelp>
                         <format>u32:1-30</format>
                         <description>Session timeout in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-30"/>
                       </constraint>
                       <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage>
                     </properties>
                     <defaultValue>2</defaultValue>
                   </leafNode>
                   <leafNode name="priority">
                     <properties>
                       <help>Server priority</help>
                       <valueHelp>
                         <format>u32:1-255</format>
                         <description>Server priority</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-255"/>
                       </constraint>
                     </properties>
                     <defaultValue>255</defaultValue>
                   </leafNode>
                 </children>
               </tagNode>
               #include <include/interface/vrf.xml.i>
             </children>
           </node>
           <leafNode name="timeout">
             <properties>
               <help>Session timeout</help>
               <valueHelp>
                 <format>u32:5-604800</format>
                 <description>Session timeout in seconds</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 5-604800"/>
               </constraint>
               <constraintErrorMessage>Timeout must be between 5 and 604800 seconds</constraintErrorMessage>
             </properties>
           </leafNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
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
@@ -1,244 +1,260 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
 import platform
 import unittest
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from distutils.version import LooseVersion
 from platform import release as kernel_version
 from subprocess import Popen, PIPE
 from pwd import getpwall
 
 from vyos.configsession import ConfigSessionError
 from vyos.util import cmd
 from vyos.util import read_file
 from vyos.template import inc_ip
 
 base_path = ['system', 'login']
 users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
 
 ssh_pubkey = """
 AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF
 TlXYfVt6Ls2NZZsUSwHbnhK4BKDrPvVZMW/LycjQPzWW6TGtk6UbZP1WqdviQ9hP
 jsEeKJSTKciMSvQpjBWyEQQPXSKYQC7ryQQilZDqnJgzqwzejKEe+nhhOdBvjuZc
 uukxjT69E0UmWAwLxzvfiurwiQaC7tG+PwqvtfHOPL3i6yRO2C5ORpFarx8PeGDS
 IfIXJCr3LoUbLHeuE7T2KaOKQcX0UsWJ4CoCapRLpTVYPDB32BYfgq7cW1Sal1re
 EGH2PzuXBklinTBgCHA87lHjpwDIAqdmvMj7SXIW9LxazLtP+e37sexE7xEs0cpN
 l68txdDbY2P2Kbz5mqGFfCvBYKv9V2clM5vyWNy/Xp5TsCis89nn83KJmgFS7sMx
 pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr
 TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk=
 """
 
 class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
     def tearDown(self):
         # Delete individual users from configuration
         for user in users:
             self.cli_delete(base_path + ['user', user])
 
         self.cli_commit()
 
         # After deletion, a user is not allowed to remain in /etc/passwd
         usernames = [x[0] for x in getpwall()]
         for user in users:
             self.assertNotIn(user, usernames)
 
     def test_add_linux_system_user(self):
         # We are not allowed to re-use a username already taken by the Linux
         # base system
         system_user = 'backup'
         self.cli_set(base_path + ['user', system_user, 'authentication', 'plaintext-password', system_user])
 
         # check validate() - can not add username which exists on the Debian
         # base system (UID < 1000)
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
 
         self.cli_delete(base_path + ['user', system_user])
 
     def test_system_login_user(self):
         # Check if user can be created and we can SSH to localhost
         self.cli_set(['service', 'ssh', 'port', '22'])
 
         for user in users:
             name = "VyOS Roxx " + user
             home_dir = "/tmp/" + user
 
             self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user])
             self.cli_set(base_path + ['user', user, 'full-name', 'VyOS Roxx'])
             self.cli_set(base_path + ['user', user, 'home-directory', home_dir])
 
         self.cli_commit()
 
         for user in users:
             cmd = ['su','-', user]
             proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
             tmp = "{}\nuname -a".format(user)
             proc.stdin.write(tmp.encode())
             proc.stdin.flush()
             (stdout, stderr) = proc.communicate()
 
             # stdout is something like this:
             # 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'
         type = 'ssh-rsa'
 
         self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'key', ssh_pubkey.replace('\n','')])
 
         # check validate() - missing type for public-key
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_set(base_path + ['user', ssh_user, 'authentication', 'public-keys', public_keys, 'type', type])
 
         self.cli_commit()
 
         # Check that SSH key was written properly
         tmp = cmd(f'sudo cat /home/{ssh_user}/.ssh/authorized_keys')
         key = f'{type} ' + ssh_pubkey.replace('\n','')
         self.assertIn(key, tmp)
 
         self.cli_delete(base_path + ['user', ssh_user])
 
     def test_radius_kernel_features(self):
         # T2886: RADIUS requires some Kernel options to be present
         kernel = platform.release()
         kernel_config = read_file(f'/boot/config-{kernel}')
 
         # T2886 - RADIUS authentication - check for statically compiled options
         options = ['CONFIG_AUDIT', 'CONFIG_AUDITSYSCALL', 'CONFIG_AUDIT_ARCH']
         if LooseVersion(kernel_version()) < LooseVersion('5.0'):
             options.append('CONFIG_AUDIT_WATCH')
             options.append('CONFIG_AUDIT_TREE')
 
         for option in options:
             self.assertIn(f'{option}=y', kernel_config)
 
     def test_system_login_radius_ipv4(self):
         # Verify generated RADIUS configuration files
 
         radius_key = 'VyOSsecretVyOS'
         radius_server = '172.16.100.10'
         radius_source = '127.0.0.1'
         radius_port = '2000'
         radius_timeout = '1'
 
         self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
         self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
         self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
         self.cli_set(base_path + ['radius', 'source-address', radius_source])
         self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
 
         # check validate() - Only one IPv4 source-address supported
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
 
         self.cli_commit()
 
         # this file must be read with higher permissions
         pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
         tmp = re.findall(r'\n?{}:{}\s+{}\s+{}\s+{}'.format(radius_server,
                         radius_port, radius_key, radius_timeout,
                         radius_source), pam_radius_auth_conf)
         self.assertTrue(tmp)
 
         # required, static options
         self.assertIn('priv-lvl 15', pam_radius_auth_conf)
         self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
 
         # PAM
         pam_common_account = read_file('/etc/pam.d/common-account')
         self.assertIn('pam_radius_auth.so', pam_common_account)
 
         pam_common_auth = read_file('/etc/pam.d/common-auth')
         self.assertIn('pam_radius_auth.so', pam_common_auth)
 
         pam_common_session = read_file('/etc/pam.d/common-session')
         self.assertIn('pam_radius_auth.so', pam_common_session)
 
         pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
         self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
 
         # NSS
         nsswitch_conf = read_file('/etc/nsswitch.conf')
         tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
         self.assertTrue(tmp)
 
         tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
         self.assertTrue(tmp)
 
     def test_system_login_radius_ipv6(self):
         # Verify generated RADIUS configuration files
 
         radius_key = 'VyOS-VyOS'
         radius_server = '2001:db8::1'
         radius_source = '::1'
         radius_port = '4000'
         radius_timeout = '4'
 
         self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
         self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
         self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
         self.cli_set(base_path + ['radius', 'source-address', radius_source])
         self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
 
         # check validate() - Only one IPv4 source-address supported
         with self.assertRaises(ConfigSessionError):
             self.cli_commit()
         self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
 
         self.cli_commit()
 
         # this file must be read with higher permissions
         pam_radius_auth_conf = cmd('sudo cat /etc/pam_radius_auth.conf')
         tmp = re.findall(r'\n?\[{}\]:{}\s+{}\s+{}\s+\[{}\]'.format(radius_server,
                         radius_port, radius_key, radius_timeout,
                         radius_source), pam_radius_auth_conf)
         self.assertTrue(tmp)
 
         # required, static options
         self.assertIn('priv-lvl 15', pam_radius_auth_conf)
         self.assertIn('mapped_priv_user radius_priv_user', pam_radius_auth_conf)
 
         # PAM
         pam_common_account = read_file('/etc/pam.d/common-account')
         self.assertIn('pam_radius_auth.so', pam_common_account)
 
         pam_common_auth = read_file('/etc/pam.d/common-auth')
         self.assertIn('pam_radius_auth.so', pam_common_auth)
 
         pam_common_session = read_file('/etc/pam.d/common-session')
         self.assertIn('pam_radius_auth.so', pam_common_session)
 
         pam_common_session_noninteractive = read_file('/etc/pam.d/common-session-noninteractive')
         self.assertIn('pam_radius_auth.so', pam_common_session_noninteractive)
 
         # NSS
         nsswitch_conf = read_file('/etc/nsswitch.conf')
         tmp = re.findall(r'passwd:\s+mapuid\s+files\s+mapname', nsswitch_conf)
         self.assertTrue(tmp)
 
         tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
         self.assertTrue(tmp)
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index dbd346fe4..bd9cc3b89 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,327 +1,340 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020-2022 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
 
 from crypt import crypt
 from crypt import METHOD_SHA512
 from psutil import users
 from pwd import getpwall
 from pwd import getpwnam
 from spwd import getspnam
 from sys import exit
 from time import sleep
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.configverify import verify_vrf
 from vyos.template import render
 from vyos.template import is_ipv4
 from vyos.util import cmd
 from vyos.util import call
 from vyos.util import run
 from vyos.util import DEVNULL
 from vyos.util import dict_search
 from vyos.xml import defaults
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 autologout_file = "/etc/profile.d/autologout.sh"
 radius_config_file = "/etc/pam_radius_auth.conf"
 
 def get_local_users():
     """Return list of dynamically allocated users (see Debian Policy Manual)"""
     local_users = []
     for s_user in getpwall():
         uid = getpwnam(s_user.pw_name).pw_uid
         if uid in range(1000, 29999):
             if s_user.pw_name not in ['radius_user', 'radius_priv_user']:
                 local_users.append(s_user.pw_name)
 
     return local_users
 
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['system', 'login']
     login = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  no_tag_node_value_mangle=True, get_first_key=True)
 
     # users no longer existing in the running configuration need to be deleted
     local_users = get_local_users()
     cli_users = []
     if 'user' in login:
         cli_users = list(login['user'])
 
         # XXX: T2665: we can not safely rely on the defaults() when there are
         # tagNodes in place, it is better to blend in the defaults manually.
         default_values = defaults(base + ['user'])
         for user in login['user']:
             login['user'][user] = dict_merge(default_values, login['user'][user])
 
     # XXX: T2665: we can not safely rely on the defaults() when there are
     # tagNodes in place, it is better to blend in the defaults manually.
     default_values = defaults(base + ['radius', 'server'])
     for server in dict_search('radius.server', login) or []:
         login['radius']['server'][server] = dict_merge(default_values,
             login['radius']['server'][server])
 
     # create a list of all users, cli and users
     all_users = list(set(local_users + cli_users))
     # We will remove any normal users that dos not exist in the current
     # configuration. This can happen if user is added but configuration was not
     # saved and system is rebooted.
     rm_users = [tmp for tmp in all_users if tmp not in cli_users]
     if rm_users: login.update({'rm_users' : rm_users})
 
     return login
 
 def verify(login):
     if 'rm_users' in login:
         cur_user = os.environ['SUDO_USER']
         if cur_user in login['rm_users']:
             raise ConfigError(f'Attempting to delete current user: {cur_user}')
 
     if 'user' in login:
         system_users = getpwall()
         for user, user_config in login['user'].items():
             # Linux system users range up until UID 1000, we can not create a
             # VyOS CLI user which already exists as system user
             for s_user in system_users:
                 if s_user.pw_name == user and s_user.pw_uid < 1000:
                     raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
 
             for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
                 if 'type' not in pubkey_options:
                     raise ConfigError(f'Missing type for public-key "{pubkey}"!')
                 if 'key' not in pubkey_options:
                     raise ConfigError(f'Missing key for public-key "{pubkey}"!')
 
     # At lease one RADIUS server must not be disabled
     if 'radius' in login:
         if 'server' not in login['radius']:
             raise ConfigError('No RADIUS server defined!')
 
         fail = True
         for server, server_config in dict_search('radius.server', login).items():
             if 'key' not in server_config:
                 raise ConfigError(f'RADIUS server "{server}" requires key!')
 
             if 'disabled' not in server_config:
                 fail = False
                 continue
         if fail:
             raise ConfigError('All RADIUS servers are disabled')
 
         verify_vrf(login['radius'])
 
         if 'source_address' in login['radius']:
             ipv4_count = 0
             ipv6_count = 0
             for address in login['radius']['source_address']:
                 if is_ipv4(address): ipv4_count += 1
                 else:                ipv6_count += 1
 
             if ipv4_count > 1:
                 raise ConfigError('Only one IPv4 source-address can be set!')
             if ipv6_count > 1:
                 raise ConfigError('Only one IPv6 source-address can be set!')
 
     return None
 
 
 def generate(login):
     # calculate users encrypted password
     if 'user' in login:
         for user, user_config in login['user'].items():
             tmp = dict_search('authentication.plaintext_password', user_config)
             if tmp:
                 encrypted_password = crypt(tmp, METHOD_SHA512)
                 login['user'][user]['authentication']['encrypted_password'] = encrypted_password
                 del login['user'][user]['authentication']['plaintext_password']
 
                 # remove old plaintext password and set new encrypted password
                 env = os.environ.copy()
                 env['vyos_libexec_dir'] = '/usr/libexec/vyos'
 
                 # Set default commands for re-adding user with encrypted password
                 del_user_plain = f"system login user '{user}' authentication plaintext-password"
                 add_user_encrypt = f"system login user '{user}' authentication encrypted-password '{encrypted_password}'"
 
                 lvl = env['VYATTA_EDIT_LEVEL']
                 # We're in config edit level, for example "edit system login"
                 # Change default commands for re-adding user with encrypted password
                 if lvl != '/':
                     # Replace '/system/login' to 'system login'
                     lvl = lvl.strip('/').split('/')
                     # Convert command str to list
                     del_user_plain = del_user_plain.split()
                     # New command exclude level, for example "edit system login"
                     del_user_plain = del_user_plain[len(lvl):]
                     # Convert string to list
                     del_user_plain = " ".join(del_user_plain)
 
                     add_user_encrypt = add_user_encrypt.split()
                     add_user_encrypt = add_user_encrypt[len(lvl):]
                     add_user_encrypt = " ".join(add_user_encrypt)
 
                 call(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
                 call(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
             else:
                 try:
                     if getspnam(user).sp_pwdp == dict_search('authentication.encrypted_password', user_config):
                         # If the current encrypted bassword matches the encrypted password
                         # from the config - do not update it. This will remove the encrypted
                         # value from the system logs.
                         #
                         # The encrypted password will be set only once during the first boot
                         # after an image upgrade.
                         del login['user'][user]['authentication']['encrypted_password']
                 except:
                     pass
 
     if 'radius' in login:
         render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
                    permission=0o600, user='root', group='root')
     else:
         if os.path.isfile(radius_config_file):
             os.unlink(radius_config_file)
 
     if 'timeout' in login:
         render(autologout_file, 'login/autologout.j2', login,
                    permission=0o755, user='root', group='root')
     else:
         if os.path.isfile(autologout_file):
             os.unlink(autologout_file)
 
     return None
 
 
 def apply(login):
     if 'user' in login:
         for user, user_config in login['user'].items():
             # make new user using vyatta shell and make home directory (-m),
             # default group of 100 (users)
             command = 'useradd --create-home --no-user-group'
             # check if user already exists:
             if user in get_local_users():
                 # update existing account
                 command = 'usermod'
 
             # all accounts use /bin/vbash
             command += ' --shell /bin/vbash'
             # we need to use '' quotes when passing formatted data to the shell
             # else it will not work as some data parts are lost in translation
             tmp = dict_search('authentication.encrypted_password', user_config)
             if tmp: command += f" --password '{tmp}'"
 
             tmp = dict_search('full_name', user_config)
             if tmp: command += f" --comment '{tmp}'"
 
             tmp = dict_search('home_directory', user_config)
             if tmp: command += f" --home '{tmp}'"
             else: command += f" --home '/home/{user}'"
 
             command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}'
             try:
                 cmd(command)
 
                 # we should not rely on the value stored in
                 # user_config['home_directory'], as a crazy user will choose
                 # username root or any other system user which will fail.
                 #
                 # XXX: Should we deny using root at all?
                 home_dir = getpwnam(user).pw_dir
                 render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.j2',
                        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}"')
 
     if 'rm_users' in login:
         for user in login['rm_users']:
             try:
                 # Disable user to prevent re-login
                 call(f'usermod -s /sbin/nologin {user}')
 
                 # Logout user if he is still logged in
                 if user in list(set([tmp[0] for tmp in users()])):
                     print(f'{user} is logged in, forcing logout!')
                     # re-run command until user is logged out
                     while run(f'pkill -HUP -u {user}'):
                         sleep(0.250)
 
                 # Remove user account but leave home directory in place. Re-run
                 # command until user is removed - userdel might return 8 as
                 # SSH sessions are not all yet properly cleaned away, thus we
                 # simply re-run the command until the account wen't away
                 while run(f'userdel --remove {user}', stderr=DEVNULL):
                     sleep(0.250)
 
             except Exception as e:
                 raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
 
     #
     # RADIUS configuration
     #
     env = os.environ.copy()
     env['DEBIAN_FRONTEND'] = 'noninteractive'
     try:
         if 'radius' in login:
             # Enable RADIUS in PAM
             cmd('pam-auth-update --package --enable radius', env=env)
             # Make NSS system aware of RADIUS
             # This fancy snipped was copied from old Vyatta code
             command = "sed -i -e \'/\smapname/b\' \
                           -e \'/^passwd:/s/\s\s*/&mapuid /\' \
                           -e \'/^passwd:.*#/s/#.*/mapname &/\' \
                           -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
                           -e \'/^group:.*#/s/#.*/ mapname &/\' \
                           -e \'/^group:[^#]*$/s/: */&mapname /\' \
                           /etc/nsswitch.conf"
         else:
             # Disable RADIUS in PAM
             cmd('pam-auth-update --package --remove radius', env=env)
             # Drop RADIUS from NSS NSS system
             # This fancy snipped was copied from old Vyatta code
             command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
                    -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
                    -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
                    -e \'s/[ \t]*$//\' \
                    /etc/nsswitch.conf"
 
         cmd(command)
     except Exception as e:
         raise ConfigError(f'RADIUS configuration failed: {e}')
 
     return None
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)