diff options
Diffstat (limited to 'src/conf_mode/system_login.py')
| -rwxr-xr-x | src/conf_mode/system_login.py | 73 |
1 files changed, 64 insertions, 9 deletions
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 439fa645b..00cacccd1 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2024 VyOS maintainers and contributors +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> # # 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 @@ -24,10 +24,15 @@ from pwd import getpwuid from sys import exit from time import sleep +from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configverify import verify_vrf from vyos.template import render from vyos.template import is_ipv4 +from vyos.utils.auth import EPasswdStrength +from vyos.utils.auth import evaluate_strength from vyos.utils.auth import get_current_user from vyos.utils.configfs import delete_cli_node from vyos.utils.configfs import add_cli_node @@ -58,20 +63,21 @@ MAX_RADIUS_TIMEOUT: int = 50 MAX_RADIUS_COUNT: int = 8 # Maximum number of supported TACACS servers MAX_TACACS_COUNT: int = 8 - +# Minimum USER id for TACACS users +MIN_TACACS_UID = 900 # List of local user accounts that must be preserved SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1', 'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6', 'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11', 'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15'] -def get_local_users(): +def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] for s_user in getpwall(): - if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID: + if getpwnam(s_user.pw_name).pw_uid < min_uid: continue - if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID: + if getpwnam(s_user.pw_name).pw_uid > max_uid: continue if s_user.pw_name in SYSTEM_USER_SKIP_LIST: continue @@ -119,6 +125,13 @@ def get_config(config=None): rm_users = [tmp for tmp in all_users if tmp not in cli_users] if rm_users: login.update({'rm_users' : rm_users}) + # Build TACACS user mapping + if 'tacacs' in login: + login['exclude_users'] = get_local_users(min_uid=0, + max_uid=MIN_TACACS_UID) + cli_users + login['tacacs_min_uid'] = MIN_TACACS_UID + + set_dependents('ssh', conf) return login def verify(login): @@ -139,6 +152,19 @@ def verify(login): if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID: raise ConfigError(f'User "{user}" can not be created, conflict with local system account!') + # T6353: Check password for complexity using cracklib. + # A user password should be sufficiently complex + plaintext_password = dict_search( + path='authentication.plaintext_password', + dict_object=user_config + ) or None + + failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR] + if plaintext_password is not None: + result = evaluate_strength(plaintext_password) + if result['strength'] in failed_check_status: + Warning(result['error']) + 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}"!') @@ -322,6 +348,17 @@ def apply(login): user_config, permission=0o600, formater=lambda _: _.replace(""", '"'), user=user, group='users') + + principals_file = f'{home_dir}/.ssh/authorized_principals' + if dict_search('authentication.principal', user_config): + render(principals_file, 'login/authorized_principals.j2', + user_config, permission=0o600, + formater=lambda _: _.replace(""", '"'), + user=user, group='users') + else: + if os.path.exists(principals_file): + os.unlink(principals_file) + except Exception as e: raise ConfigError(f'Adding user "{user}" raised exception: "{e}"') @@ -338,14 +375,15 @@ def apply(login): chown(home_dir, user=user, recursive=True) # Generate 2FA/MFA One-Time-Pad configuration + google_auth_file = f'{home_dir}/.google_authenticator' if dict_search('authentication.otp.key', user_config): enable_otp = True - render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2', + render(google_auth_file, 'login/pam_otp_ga.conf.j2', user_config, permission=0o400, user=user, group='users') else: # delete configuration as it's not enabled for the user - if os.path.exists(f'{home_dir}/.google_authenticator'): - os.remove(f'{home_dir}/.google_authenticator') + if os.path.exists(google_auth_file): + os.unlink(google_auth_file) # Lock/Unlock local user account lock_unlock = '--unlock' @@ -359,6 +397,22 @@ def apply(login): # Disable user to prevent re-login call(f'usermod -s /sbin/nologin {user}') + home_dir = getpwnam(user).pw_dir + # Remove SSH authorized keys file + authorized_keys_file = f'{home_dir}/.ssh/authorized_keys' + if os.path.exists(authorized_keys_file): + os.unlink(authorized_keys_file) + + # Remove SSH authorized principals file + principals_file = f'{home_dir}/.ssh/authorized_principals' + if os.path.exists(principals_file): + os.unlink(principals_file) + + # Remove Google Authenticator file + google_auth_file = f'{home_dir}/.google_authenticator' + if os.path.exists(google_auth_file): + os.unlink(google_auth_file) + # 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!') @@ -397,8 +451,9 @@ def apply(login): # Enable/disable Google authenticator cmd('pam-auth-update --disable mfa-google-authenticator') if enable_otp: - cmd(f'pam-auth-update --enable mfa-google-authenticator') + cmd('pam-auth-update --enable mfa-google-authenticator') + call_dependents() return None |
