Page MenuHomeVyOS Platform

login: replace getpwall() based user enumeration to avoid NSS/TACACS timeouts
In progress, NormalPublicFEATURE REQUEST

Description

In VyOS we use a Python function to list all user accounts on the device (pwd.getpwall()). However, this function can become very slow if external authentication services like RADIUS or TACACS are not responding, because the system waits for them to time out. Since we really only need information about the local user accounts, I’m considering skipping that slow function entirely and instead reading the local user list directly from the /etc/passwd file.

Would this be a better and faster approach? This is what this POC task is about.

POC snipped which needs to be integrated:

#!/usr/bin/env python3

from dataclasses import dataclass
from typing import List

@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(path: str = "/etc/passwd") -> List[PasswdEntry]:
    entries = []
    with open(path, "r") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue

            parts = line.split(":")
            if len(parts) != 7:
                # malformed line (rare but possible)
                continue

            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],
            )
            entries.append(entry)

    return entries

if __name__ == "__main__":
    for e in get_local_passwd_entries():
        print(e)

Will result in

lnx01:~ # /tmp/poc.py
PasswdEntry(pw_name='root', pw_passwd='x', pw_uid=0, pw_gid=0, pw_gecos='root', pw_dir='/root', pw_shell='/bin/bash')
PasswdEntry(pw_name='daemon', pw_passwd='x', pw_uid=1, pw_gid=1, pw_gecos='daemon', pw_dir='/usr/sbin', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='bin', pw_passwd='x', pw_uid=2, pw_gid=2, pw_gecos='bin', pw_dir='/bin', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='sys', pw_passwd='x', pw_uid=3, pw_gid=3, pw_gecos='sys', pw_dir='/dev', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='sync', pw_passwd='x', pw_uid=4, pw_gid=65534, pw_gecos='sync', pw_dir='/bin', pw_shell='/bin/sync')
PasswdEntry(pw_name='games', pw_passwd='x', pw_uid=5, pw_gid=60, pw_gecos='games', pw_dir='/usr/games', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='man', pw_passwd='x', pw_uid=6, pw_gid=12, pw_gecos='man', pw_dir='/var/cache/man', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='lp', pw_passwd='x', pw_uid=7, pw_gid=7, pw_gecos='lp', pw_dir='/var/spool/lpd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='mail', pw_passwd='x', pw_uid=8, pw_gid=8, pw_gecos='mail', pw_dir='/var/mail', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='news', pw_passwd='x', pw_uid=9, pw_gid=9, pw_gecos='news', pw_dir='/var/spool/news', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='uucp', pw_passwd='x', pw_uid=10, pw_gid=10, pw_gecos='uucp', pw_dir='/var/spool/uucp', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='proxy', pw_passwd='x', pw_uid=13, pw_gid=13, pw_gecos='proxy', pw_dir='/bin', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='www-data', pw_passwd='x', pw_uid=33, pw_gid=33, pw_gecos='www-data', pw_dir='/var/www', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='backup', pw_passwd='x', pw_uid=34, pw_gid=34, pw_gecos='backup', pw_dir='/var/backups', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='list', pw_passwd='x', pw_uid=38, pw_gid=38, pw_gecos='Mailing List Manager', pw_dir='/var/list', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='irc', pw_passwd='x', pw_uid=39, pw_gid=39, pw_gecos='ircd', pw_dir='/run/ircd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='gnats', pw_passwd='x', pw_uid=41, pw_gid=41, pw_gecos='Gnats Bug-Reporting System (admin)', pw_dir='/var/lib/gnats', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='nobody', pw_passwd='x', pw_uid=65534, pw_gid=65534, pw_gecos='nobody', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='_apt', pw_passwd='x', pw_uid=100, pw_gid=65534, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='systemd-timesync', pw_passwd='x', pw_uid=101, pw_gid=102, pw_gecos='systemd Time Synchronization,,,', pw_dir='/run/systemd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='systemd-network', pw_passwd='x', pw_uid=102, pw_gid=103, pw_gecos='systemd Network Management,,,', pw_dir='/run/systemd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='systemd-resolve', pw_passwd='x', pw_uid=103, pw_gid=104, pw_gecos='systemd Resolver,,,', pw_dir='/run/systemd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='messagebus', pw_passwd='x', pw_uid=104, pw_gid=110, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='sshd', pw_passwd='x', pw_uid=105, pw_gid=65534, pw_gecos='', pw_dir='/run/sshd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='systemd-coredump', pw_passwd='x', pw_uid=999, pw_gid=999, pw_gecos='systemd Core Dumper', pw_dir='/', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='ntp', pw_passwd='x', pw_uid=106, pw_gid=112, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='sssd', pw_passwd='x', pw_uid=107, pw_gid=115, pw_gecos='SSSD system user,,,', pw_dir='/var/lib/sss', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='Debian-exim', pw_passwd='x', pw_uid=108, pw_gid=116, pw_gecos='', pw_dir='/var/spool/exim4', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='geoclue', pw_passwd='x', pw_uid=109, pw_gid=117, pw_gecos='', pw_dir='/var/lib/geoclue', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='avahi', pw_passwd='x', pw_uid=110, pw_gid=118, pw_gecos='Avahi mDNS daemon,,,', pw_dir='/var/run/avahi-daemon', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='Debian-ow', pw_passwd='x', pw_uid=111, pw_gid=119, pw_gecos='Debian OWFS system account,,,', pw_dir='/var/lib/owfs', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='freerad', pw_passwd='x', pw_uid=112, pw_gid=121, pw_gecos='', pw_dir='/etc/freeradius', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='uuidd', pw_passwd='x', pw_uid=113, pw_gid=122, pw_gecos='', pw_dir='/run/uuidd', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='swift', pw_passwd='x', pw_uid=114, pw_gid=123, pw_gecos='', pw_dir='/var/lib/swift', pw_shell='/bin/false')
PasswdEntry(pw_name='tcpdump', pw_passwd='x', pw_uid=115, pw_gid=124, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='arpwatch', pw_passwd='x', pw_uid=116, pw_gid=126, pw_gecos='ARP Watcher,,,', pw_dir='/var/lib/arpwatch', pw_shell='/bin/sh')
PasswdEntry(pw_name='iperf3', pw_passwd='x', pw_uid=117, pw_gid=128, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='ntpsec', pw_passwd='x', pw_uid=118, pw_gid=129, pw_gecos='', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')
PasswdEntry(pw_name='polkitd', pw_passwd='x', pw_uid=996, pw_gid=996, pw_gecos='polkit', pw_dir='/nonexistent', pw_shell='/usr/sbin/nologin')

My local user-account cpo does not appear as it comes from sssd - so local only name resolution works

cpo lnx01:~ # id
uid=1776001104(cpo) gid=1776001674(gr_linux) groups=1776001674(gr_linux),997(clab_admins),998(docker),1776000513(domain users),1776001110(a_rdp),1776001111(a_vpn)

Implementation

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.

Details

Version
-
Is it a breaking change?
Unspecified (possibly destroys the router)
Issue type
Feature (new functionality)

Event Timeline

c-po changed the task status from Open to In progress.
c-po claimed this task.
c-po triaged this task as Normal priority.
c-po renamed this task from Evaluate direct read of /etc/passwd over Python getpwall() to login: replace getpwall() based user enumeration to avoid NSS/TACACS timeouts.Wed, Dec 10, 2:11 PM
c-po updated the task description. (Show Details)