From cd11ba957a33933742304b4efbba77461e803cb3 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Tue, 9 Dec 2025 23:14:50 +0100 Subject: login: T8086: replace getpwall() user enumeration to avoid NSS/TACACS timeouts The previous implementation of "system login" relied on Python's pwd.getpwall() to enumerate user accounts. This forces a full walk through the NSS stack, which is acceptable in general but problematic for our use-case. VyOS only needs information about locally created accounts and not remote accounts provided via AAA backends such as TACACS or RADIUS. When TACACS servers are unreachable, NSS lookups become extremely slow due to repeated timeouts. As a result, any operation triggering pwd.getpwall() (including configuration commits) can stall for several minutes. This change introduces a dedicated helper, get_local_passwd_entries(), which reads /etc/passwd directly and avoids NSS entirely. Since only local UIDs are relevant, this provides all required data with no external dependencies. Performance improvement on VyOS 1.4.3 with two unreachable TACACS servers: # set system login tacacs server 192.168.1.50 key test123 # set system login tacacs server 192.168.1.51 key test123 # time commit Before: real 3m29.825s user 0m0.329s sys 0m0.246s After: real 0m1.464s user 0m0.337s sys 0m0.195s This significantly improves commit performance and removes sensitivity to AAA server outages. --- python/vyos/utils/auth.py | 59 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) (limited to 'python') diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py index 462332332..4aa446455 100644 --- a/python/vyos/utils/auth.py +++ b/python/vyos/utils/auth.py @@ -18,10 +18,13 @@ import math import re import string -from enum import StrEnum +from dataclasses import dataclass from decimal import Decimal -from pwd import getpwall from pwd import getpwnam +from enum import StrEnum +from typing import List +from typing import Optional + from vyos.utils.process import cmd # Minimum UID used when adding system users @@ -146,12 +149,62 @@ def get_current_user() -> str: current_user = os.environ['USER'] return current_user +@dataclass +class PasswdEntry: + pw_name: str + pw_passwd: str + pw_uid: int + pw_gid: int + pw_gecos: str + pw_dir: str + pw_shell: str + +def get_local_passwd_entries(uid: Optional[int] = None) -> PasswdEntry | List[PasswdEntry] | None: + """ + If uid is None: return a list of all passwd entries. + If uid is given: return the matching entry or None. + """ + entries = [] + with open('/etc/passwd', 'r') as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split(":") + if len(parts) != 7: + continue + + try: + entry = PasswdEntry( + pw_name=parts[0], + pw_passwd=parts[1], + pw_uid=int(parts[2]), + pw_gid=int(parts[3]), + pw_gecos=parts[4], + pw_dir=parts[5], + pw_shell=parts[6], + ) + except ValueError: + # Skip entries with non-numeric UID or GID + continue + + # If searching for a specific UID, return immediately if found + if uid is not None and entry.pw_uid == uid: + return entry + + entries.append(entry) + + # uid given but not found + if uid is not None: + return None + + return entries def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID) -> list: """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] - for s_user in getpwall(): + for s_user in get_local_passwd_entries(): if s_user.pw_uid < min_uid: continue if s_user.pw_uid > max_uid: -- cgit v1.2.3 From 197809bf77d841fe57bdcbaeeb3b078145aab140 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Tue, 9 Dec 2025 23:15:39 +0100 Subject: login: T8086: replace getpwnam()/getpwuid() with get_local_passwd_entries() Switch to our custom implementation to avoid NSS/TACACS timeouts as explained in commit 4c9eaaa96e06 ("login: replace getpwall() user enumeration to avoid NSS/TACACS timeouts"). --- python/vyos/utils/auth.py | 7 ++++--- src/conf_mode/system_login.py | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'python') diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py index 4aa446455..f9551419f 100644 --- a/python/vyos/utils/auth.py +++ b/python/vyos/utils/auth.py @@ -20,7 +20,6 @@ import string from dataclasses import dataclass from decimal import Decimal -from pwd import getpwnam from enum import StrEnum from typing import List from typing import Optional @@ -215,7 +214,9 @@ def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID) -> list: return local_users - def get_user_home_dir(user: str) -> str: """Return user's home directory""" - return getpwnam(user).pw_dir + for u in get_local_passwd_entries(): + if u.pw_name == user: + return u.pw_dir + raise KeyError(f"User '{user}' not found in /etc/passwd") diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py index 4da1d10ae..3cff7a806 100755 --- a/src/conf_mode/system_login.py +++ b/src/conf_mode/system_login.py @@ -21,7 +21,6 @@ import json from copy import deepcopy from passlib.hosts import linux_context from psutil import users -from pwd import getpwuid from sys import exit from time import sleep @@ -432,7 +431,7 @@ def apply(login): # retrieve current owner of home directory and adjust on demand dir_owner = None try: - dir_owner = getpwuid(os.stat(home_dir).st_uid).pw_name + dir_owner = get_local_passwd_entries(os.stat(home_dir).st_uid).pw_name except: pass -- cgit v1.2.3