summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-06-22 22:41:48 +0200
committerGitHub <noreply@github.com>2023-06-22 22:41:48 +0200
commitb5976afd68b850523335ec64db7e1e59efb8f9fd (patch)
treec1a2932dbec7dc3e498ad5bd9db2831249c4b1fa
parent2128dc0ddf6b921f440067d7b862f30d9fad0cb2 (diff)
parent3ec727670de02cac06321719a0323650046d54a1 (diff)
downloadvyos-1x-b5976afd68b850523335ec64db7e1e59efb8f9fd.tar.gz
vyos-1x-b5976afd68b850523335ec64db7e1e59efb8f9fd.zip
Merge pull request #2038 from c-po/t141-tacacs
T141: add TACACS support
-rw-r--r--data/templates/login/nsswitch.conf.j221
-rw-r--r--data/templates/login/tacplus_nss.conf.j274
-rw-r--r--data/templates/login/tacplus_servers.j259
-rw-r--r--debian/control5
-rw-r--r--debian/vyos-1x.postinst56
-rw-r--r--debian/vyos-1x.preinst1
-rw-r--r--interface-definitions/include/radius-server-auth-port.xml.i11
-rw-r--r--interface-definitions/system-login.xml.in59
-rw-r--r--op-mode-definitions/monitor-log.xml.in4
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py14
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py16
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py46
-rwxr-xr-xsrc/conf_mode/system-login.py141
-rwxr-xr-xsrc/op_mode/container.py2
-rw-r--r--src/pam-configs/radius11
-rw-r--r--src/pam-configs/tacplus17
16 files changed, 432 insertions, 105 deletions
diff --git a/data/templates/login/nsswitch.conf.j2 b/data/templates/login/nsswitch.conf.j2
new file mode 100644
index 000000000..65dc88291
--- /dev/null
+++ b/data/templates/login/nsswitch.conf.j2
@@ -0,0 +1,21 @@
+# Automatically generated by system-login.py
+# /etc/nsswitch.conf
+#
+# Example configuration of GNU Name Service Switch functionality.
+
+passwd: {{ 'mapuid ' if radius is vyos_defined }}{{ 'tacplus ' if tacacs is vyos_defined }}files{{ ' mapname' if radius is vyos_defined }}
+group: {{ 'mapname ' if radius is vyos_defined }}{{ 'tacplus ' if tacacs is vyos_defined }}files
+shadow: files
+gshadow: files
+
+# Per T2678, commenting out myhostname
+hosts: files dns #myhostname
+networks: files
+
+protocols: db files
+services: db files
+ethers: db files
+rpc: db files
+
+netgroup: nis
+
diff --git a/data/templates/login/tacplus_nss.conf.j2 b/data/templates/login/tacplus_nss.conf.j2
new file mode 100644
index 000000000..2a30b1710
--- /dev/null
+++ b/data/templates/login/tacplus_nss.conf.j2
@@ -0,0 +1,74 @@
+#%NSS_TACPLUS-1.0
+# Install this file as /etc/tacplus_nss.conf
+# Edit /etc/nsswitch.conf to add tacplus to the passwd lookup, similar to this
+# where tacplus precede compat (or files), and depending on local policy can
+# follow or precede ldap, nis, etc.
+# passwd: tacplus compat
+#
+# Servers are tried in the order listed, and once a server
+# replies, no other servers are attempted in a given process instantiation
+#
+# This configuration is similar to the libpam_tacplus configuration, but
+# is maintained as a configuration file, since nsswitch.conf doesn't
+# support passing parameters. Parameters must start in the first
+# column, and parsing stops at the first whitespace
+
+# if set, errors and other issues are logged with syslog
+#debug=1
+
+# min_uid is the minimum uid to lookup via tacacs. Setting this to 0
+# means uid 0 (root) is never looked up, good for robustness and performance
+# Cumulus Linux ships with it set to 1001, so we never lookup our standard
+# local users, including the cumulus uid of 1000. Should not be greater
+# than the local tacacs{0..15} uids
+min_uid=900
+
+# This is a comma separated list of usernames that are never sent to
+# a tacacs server, they cause an early not found return.
+#
+# "*" is not a wild card. While it's not a legal username, it turns out
+# that during pathname completion, bash can do an NSS lookup on "*"
+# To avoid server round trip delays, or worse, unreachable server delays
+# on filename completion, we include "*" in the exclusion list.
+exclude_users=root,telegraf,radvd,strongswan,tftp,conservr,frr,ocserv,pdns,_chrony,_lldpd,sshd,openvpn,radius_user,radius_priv_user,*{{ ',' + user | join(',') if user is vyos_defined }}
+
+# The include keyword allows centralizing the tacacs+ server information
+# including the IP address and shared secret
+# include=/etc/tacplus_servers
+
+# The server IP address can be optionally followed by a ':' and a port
+# number (server=1.1.1.1:49). It is strongly recommended that you NOT
+# add secret keys to this file, because it is world readable.
+{% if tacacs.server is vyos_defined %}
+{% for server, server_config in tacacs.server.items() %}
+secret={{ server_config.key }}
+server={{ server }}:{{ server_config.port }}
+
+{% endfor %}
+{% endif %}
+
+{% if tacacs.vrf is vyos_defined %}
+# If the management network is in a vrf, set this variable to the vrf name.
+# This would usually be "mgmt". When this variable is set, the connection to the
+# TACACS+ accounting servers will be made through the named vrf.
+vrf={{ tacacs.vrf }}
+{% endif %}
+
+{% if tacacs.source_address is vyos_defined %}
+# Sets the IPv4 address used as the source IP address when communicating with
+# the TACACS+ server. IPv6 addresses are not supported, nor are hostnames.
+# The address must work when passsed to the bind() system call, that is, it must
+# be valid for the interface being used.
+source_ip={{ tacacs.source_address }}
+{% endif %}
+
+# The connection timeout for an NSS library should be short, since it is
+# invoked for many programs and daemons, and a failure is usually not
+# catastrophic. Not set or set to a negative value disables use of poll().
+# This follows the include of tacplus_servers, so it can override any
+# timeout value set in that file.
+# It's important to have this set in this file, even if the same value
+# as in tacplus_servers, since tacplus_servers should not be readable
+# by users other than root.
+timeout={{ tacacs.timeout }}
+
diff --git a/data/templates/login/tacplus_servers.j2 b/data/templates/login/tacplus_servers.j2
new file mode 100644
index 000000000..5a65d6e68
--- /dev/null
+++ b/data/templates/login/tacplus_servers.j2
@@ -0,0 +1,59 @@
+# Automatically generated by system-login.py
+# TACACS+ configuration file
+
+# This is a common file used by audisp-tacplus, libpam_tacplus, and
+# libtacplus_map config files as shipped.
+#
+# Any tac_plus client config can go here that is common to all users of this
+# file, but typically it's just the TACACS+ server IP address(es) and shared
+# secret(s)
+#
+# This file should normally be mode 600, if you care about the security of your
+# secret key. When set to mode 600 NSS lookups for TACACS users will only work
+# for tacacs users that are logged in, via the local mapping. For root, lookups
+# will work for any tacacs users, logged in or not.
+
+# Set a per-connection timeout of 10 seconds, and enable the use of poll() when
+# trying to read from tacacs servers. Otherwise standard TCP timeouts apply.
+# Not set or set to a negative value disables use of poll(). There are usually
+# multiple connection attempts per login.
+timeout={{ tacacs.timeout }}
+
+{% if tacacs.server is vyos_defined %}
+{% for server, server_config in tacacs.server.items() %}
+secret={{ server_config.key }}
+server={{ server }}:{{ server_config.port }}
+{% endfor %}
+{% endif %}
+
+# If set, login/logout accounting records are sent to all servers in
+# the list, otherwise only to the first responding server
+# Also used by audisp-tacplus per-command accounting, if it sources this file.
+acct_all=1
+
+{% if tacacs.vrf is vyos_defined %}
+# If the management network is in a vrf, set this variable to the vrf name.
+# This would usually be "mgmt". When this variable is set, the connection to the
+# TACACS+ accounting servers will be made through the named vrf.
+vrf={{ tacacs.vrf }}
+{% endif %}
+
+{% if tacacs.source_address is vyos_defined %}
+# Sets the IPv4 address used as the source IP address when communicating with
+# the TACACS+ server. IPv6 addresses are not supported, nor are hostnames.
+# The address must work when passsed to the bind() system call, that is, it must
+# be valid for the interface being used.
+source_ip={{ tacacs.source_address }}
+{% endif %}
+
+# If user_homedir=1, then tacacs users will be set to have a home directory
+# based on their login name, rather than the mapped tacacsN home directory.
+# mkhomedir_helper is used to create the directory if it does not exist (similar
+# to use of pam_mkhomedir.so). This flag is ignored for users with restricted
+# shells, e.g., users mapped to a tacacs privilege level that has enforced
+# per-command authorization (see the tacplus-restrict man page).
+user_homedir=1
+
+service=shell
+protocol=ssh
+
diff --git a/debian/control b/debian/control
index 8cbad99d9..dcce8036a 100644
--- a/debian/control
+++ b/debian/control
@@ -26,6 +26,10 @@ Standards-Version: 3.9.6
Package: vyos-1x
Architecture: amd64 arm64
+Pre-Depends:
+ libnss-tacplus (>= 1.0.4),
+ libpam-tacplus (>= 1.4.3),
+ libpam-radius-auth (>= 1.5.0)
Depends:
${python3:Depends} (>= 3.10),
aardvark-dns,
@@ -83,7 +87,6 @@ Depends:
libndp-tools,
libnetfilter-conntrack3,
libnfnetlink0,
- libpam-radius-auth (>= 1.5.0),
libqmi-utils,
libstrongswan-extra-plugins (>=5.9),
libstrongswan-standard-plugins (>=5.9),
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 6653cd585..9822ce286 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -29,10 +29,60 @@ do
sed -i "/^# Standard Un\*x authentication\./i${PAM_CONFIG}" $file
done
+# Remove TACACS user added by base package - we use our own UID range and group
+# assignments - see below
+if grep -q '^tacacs' /etc/passwd; then
+ if [ $(id -u tacacs0) -ge 1000 ]; then
+ level=0
+ vyos_group=vyattaop
+ while [ $level -lt 16 ]; do
+ userdel tacacs${level} || true
+ level=$(( level+1 ))
+ done 2>&1
+ fi
+fi
+
+# Add TACACS system users required for TACACS based system authentication
+if ! grep -q '^tacacs' /etc/passwd; then
+ # Add the tacacs group and all 16 possible tacacs privilege-level users to
+ # the password file, home directories, etc. The accounts are not enabled
+ # for local login, since they are only used to provide uid/gid/homedir for
+ # the mapped TACACS+ logins (and lookups against them). The tacacs15 user
+ # is also added to the sudo group, and vyattacfg group rather than vyattaop
+ # (used for tacacs0-14).
+ level=0
+ vyos_group=vyattaop
+ while [ $level -lt 16 ]; do
+ adduser --quiet --system --firstuid 900 --disabled-login --ingroup ${vyos_group} \
+ --no-create-home --gecos "TACACS+ mapped user at privilege level ${level}" \
+ --shell /bin/vbash tacacs${level}
+ adduser --quiet tacacs${level} frrvty
+ adduser --quiet tacacs${level} adm
+ adduser --quiet tacacs${level} dip
+ adduser --quiet tacacs${level} users
+ adduser --quiet tacacs${level} aaa
+ if [ $level -lt 15 ]; then
+ adduser --quiet tacacs${level} vyattaop
+ adduser --quiet tacacs${level} operator
+ else
+ adduser --quiet tacacs${level} vyattacfg
+ adduser --quiet tacacs${level} sudo
+ adduser --quiet tacacs${level} disk
+ adduser --quiet tacacs${level} frr
+ fi
+ level=$(( level+1 ))
+ done 2>&1 | grep -v 'User tacacs${level} already exists'
+fi
+
+
+if ! grep -q '^aaa' /etc/group; then
+ addgroup --firstgid 1000 --quiet aaa
+fi
+
# 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" \
+ --no-create-home --gecos "RADIUS mapped user at privilege level operator" \
--shell /sbin/radius_shell radius_user
adduser --quiet radius_user frrvty
adduser --quiet radius_user vyattaop
@@ -40,12 +90,13 @@ if ! grep -q '^radius_user' /etc/passwd; then
adduser --quiet radius_user adm
adduser --quiet radius_user dip
adduser --quiet radius_user users
+ adduser --quiet radius_user aaa
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" \
+ --no-create-home --gecos "RADIUS mapped user at privilege level admin" \
--shell /sbin/radius_shell radius_priv_user
adduser --quiet radius_priv_user frrvty
adduser --quiet radius_priv_user vyattacfg
@@ -55,6 +106,7 @@ if ! grep -q '^radius_priv_user' /etc/passwd; then
adduser --quiet radius_priv_user disk
adduser --quiet radius_priv_user users
adduser --quiet radius_priv_user frr
+ adduser --quiet radius_priv_user aaa
fi
# add hostsd group for vyos-hostsd
diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst
index 949ffcbc4..bfbeb112c 100644
--- a/debian/vyos-1x.preinst
+++ b/debian/vyos-1x.preinst
@@ -3,6 +3,7 @@ dpkg-divert --package vyos-1x --add --no-rename /etc/security/capability.conf
dpkg-divert --package vyos-1x --add --no-rename /lib/systemd/system/lcdproc.service
dpkg-divert --package vyos-1x --add --no-rename /etc/logrotate.d/conntrackd
dpkg-divert --package vyos-1x --add --no-rename /usr/share/pam-configs/radius
+dpkg-divert --package vyos-1x --add --no-rename /usr/share/pam-configs/tacplus
dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.conf
dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.bashrc
dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.profile
diff --git a/interface-definitions/include/radius-server-auth-port.xml.i b/interface-definitions/include/radius-server-auth-port.xml.i
index 660fa540f..d9ea1d445 100644
--- a/interface-definitions/include/radius-server-auth-port.xml.i
+++ b/interface-definitions/include/radius-server-auth-port.xml.i
@@ -1,15 +1,6 @@
<!-- include start from radius-server-auth-port.xml.i -->
+#include <include/port-number.xml.i>
<leafNode name="port">
- <properties>
- <help>Authentication port</help>
- <valueHelp>
- <format>u32:1-65535</format>
- <description>Numeric IP port</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- </properties>
<defaultValue>1812</defaultValue>
</leafNode>
<!-- include end -->
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index be4f53c3b..d772c7821 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -193,20 +193,7 @@
<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>
+ #include <include/radius-timeout.xml.i>
<leafNode name="priority">
<properties>
<help>Server priority</help>
@@ -225,6 +212,50 @@
#include <include/interface/vrf.xml.i>
</children>
</node>
+ <node name="tacacs">
+ <properties>
+ <help>TACACS+ based user authentication</help>
+ </properties>
+ <children>
+ <tagNode name="server">
+ <properties>
+ <help>TACACS+ server configuration</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>TACACS+ server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ #include <include/radius-server-key.xml.i>
+ #include <include/port-number.xml.i>
+ <leafNode name="port">
+ <defaultValue>49</defaultValue>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="source-address">
+ <properties>
+ <help>Source IP used to communicate with TACACS+ server</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 source address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/radius-timeout.xml.i>
+ #include <include/interface/vrf.xml.i>
+ </children>
+ </node>
<leafNode name="max-login-session">
<properties>
<help>Maximum number of all login sessions</help>
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in
index 06b1cf129..c577c5a39 100644
--- a/op-mode-definitions/monitor-log.xml.in
+++ b/op-mode-definitions/monitor-log.xml.in
@@ -6,13 +6,13 @@
<properties>
<help>Monitor last lines of messages file</help>
</properties>
- <command>journalctl --no-hostname --follow --boot</command>
+ <command>SYSTEMD_LOG_COLOR=false journalctl --no-hostname --follow --boot</command>
<children>
<node name="color">
<properties>
<help>Output log in a colored fashion</help>
</properties>
- <command>grc journalctl --no-hostname --follow --boot</command>
+ <command>SYSTEMD_LOG_COLOR=false grc journalctl --no-hostname --follow --boot</command>
</node>
<node name="ids">
<properties>
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index 7cfb53045..d56a8eb33 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -14,6 +14,7 @@
import os
import unittest
+import paramiko
from time import sleep
from typing import Type
@@ -87,6 +88,19 @@ class VyOSUnitTestSHIM:
pprint.pprint(out)
return out
+ def ssh_send_cmd(command, username, password, hostname='localhost'):
+ """ SSH command execution helper """
+ # Try to login via SSH
+ ssh_client = paramiko.SSHClient()
+ ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_client.connect(hostname=hostname, username=username, password=password)
+ print(host, username, password)
+ _, stdout, stderr = ssh_client.exec_command(command)
+ output = stdout.read().decode().strip()
+ error = stderr.read().decode().strip()
+ ssh_client.close()
+ return output, error
+
# standard construction; typing suggestion: https://stackoverflow.com/a/70292317
def ignore_warning(warning: Type[Warning]):
import warnings
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index 8de98f34f..e03907dd8 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -174,18 +174,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
#
# We also try to login as an invalid user - this is not allowed to work.
- def ssh_send_cmd(command, username, password, host='localhost'):
- """ SSH command execution helper """
- # Try to login via SSH
- ssh_client = paramiko.SSHClient()
- ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
- ssh_client.connect(hostname='localhost', username=username, password=password)
- _, stdout, stderr = ssh_client.exec_command(command)
- output = stdout.read().decode().strip()
- error = stderr.read().decode().strip()
- ssh_client.close()
- return output, error
-
test_user = 'ssh_test'
test_pass = 'v2i57DZs8idUwMN3VC92'
test_command = 'uname -a'
@@ -197,14 +185,14 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Login with proper credentials
- output, error = ssh_send_cmd(test_command, test_user, test_pass)
+ output, error = self.ssh_send_cmd(test_command, test_user, test_pass)
# verify login
self.assertFalse(error)
self.assertEqual(output, cmd(test_command))
# Login with invalid credentials
with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
- output, error = ssh_send_cmd(test_command, 'invalid_user', 'invalid_password')
+ output, error = self.ssh_send_cmd(test_command, 'invalid_user', 'invalid_password')
self.cli_delete(['system', 'login', 'user', test_user])
self.cli_commit()
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index a1d2ba2ad..8a4f5fdd1 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -17,13 +17,13 @@
import re
import platform
import unittest
+import paramiko
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 time import sleep
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
@@ -53,12 +53,16 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# ensure we can also run this test on a live system - so lets clean
# out the current configuration which will break this test
cls.cli_delete(cls, base_path + ['radius'])
+ cls.cli_delete(cls, base_path + ['tacacs'])
def tearDown(self):
# Delete individual users from configuration
for user in users:
self.cli_delete(base_path + ['user', user])
+ self.cli_delete(base_path + ['radius'])
+ self.cli_delete(base_path + ['tacacs'])
+
self.cli_commit()
# After deletion, a user is not allowed to remain in /etc/passwd
@@ -149,9 +153,6 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
# 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)
@@ -284,6 +285,41 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path + ['timeout'])
self.cli_delete(base_path + ['max-login-session'])
+ def test_system_login_tacacs(self):
+ tacacs_secret = 'tac_plus_key'
+ tacacs_servers = ['100.64.0.11', '100.64.0.12']
+
+ # Enable TACACS
+ for server in tacacs_servers:
+ self.cli_set(base_path + ['tacacs', 'server', server, 'key', tacacs_secret])
+
+ self.cli_commit()
+
+ # NSS
+ nsswitch_conf = read_file('/etc/nsswitch.conf')
+ tmp = re.findall(r'passwd:\s+tacplus\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ tmp = re.findall(r'group:\s+tacplus\s+files', nsswitch_conf)
+ self.assertTrue(tmp)
+
+ # PAM TACACS configuration
+ pam_tacacs_conf = read_file('/etc/tacplus_servers')
+ # NSS TACACS configuration
+ nss_tacacs_conf = read_file('/etc/tacplus_nss.conf')
+ # Users have individual home directories
+ self.assertIn('user_homedir=1', pam_tacacs_conf)
+
+ # specify services
+ self.assertIn('service=shell', pam_tacacs_conf)
+ self.assertIn('protocol=ssh', pam_tacacs_conf)
+
+ for server in tacacs_servers:
+ self.assertIn(f'secret={tacacs_secret}', pam_tacacs_conf)
+ self.assertIn(f'server={server}', pam_tacacs_conf)
+
+ self.assertIn(f'secret={tacacs_secret}', nss_tacacs_conf)
+ self.assertIn(f'server={server}', nss_tacacs_conf)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index aefab8401..5f8dd17cd 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -30,7 +30,8 @@ from vyos.defaults import directories
from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
-from vyos.util import call, rc_cmd
+from vyos.util import call
+from vyos.util import rc_cmd
from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
@@ -42,20 +43,34 @@ airbag.enable()
autologout_file = "/etc/profile.d/autologout.sh"
limits_file = "/etc/security/limits.d/10-vyos.conf"
radius_config_file = "/etc/pam_radius_auth.conf"
+tacacs_pam_config_file = "/etc/tacplus_servers"
+tacacs_nss_config_file = "/etc/tacplus_nss.conf"
+nss_config_file = "/etc/nsswitch.conf"
+# Minimum UID used when adding system users
+MIN_USER_UID: int = 1000
# LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec
MAX_RADIUS_TIMEOUT: int = 50
# MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout)
MAX_RADIUS_COUNT: int = 25
+# Maximum number of supported TACACS servers
+MAX_TACACS_COUNT: int = 8
+
+# 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():
"""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)
+ if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID:
+ continue
+ if s_user.pw_name in SYSTEM_USER_SKIP_LIST:
+ continue
+ local_users.append(s_user.pw_name)
return local_users
@@ -88,12 +103,21 @@ def get_config(config=None):
for user in login['user']:
login['user'][user] = dict_merge(default_values, login['user'][user])
+ # Add TACACS global defaults
+ if 'tacacs' in login:
+ default_values = defaults(base + ['tacacs'])
+ if 'server' in default_values:
+ del default_values['server']
+ login['tacacs'] = dict_merge(default_values, login['tacacs'])
+
# 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])
+ for backend in ['radius', 'tacacs']:
+ default_values = defaults(base + [backend, 'server'])
+ for server in dict_search(f'{backend}.server', login) or []:
+ login[backend]['server'][server] = dict_merge(default_values,
+ login[backend]['server'][server])
+
# create a list of all users, cli and users
all_users = list(set(local_users + cli_users))
@@ -121,7 +145,7 @@ def verify(login):
# 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:
+ 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!')
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
@@ -130,6 +154,9 @@ def verify(login):
if 'key' not in pubkey_options:
raise ConfigError(f'Missing key for public-key "{pubkey}"!')
+ if {'radius', 'tacacs'} <= set(login):
+ raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!')
+
# At lease one RADIUS server must not be disabled
if 'radius' in login:
if 'server' not in login['radius']:
@@ -149,7 +176,7 @@ def verify(login):
raise ConfigError('All RADIUS servers are disabled')
if radius_servers_count > MAX_RADIUS_COUNT:
- raise ConfigError('Number of RADIUS servers more than 25 ')
+ raise ConfigError(f'Number of RADIUS servers exceeded maximum of {MAX_RADIUS_COUNT}!')
if sum_timeout > MAX_RADIUS_TIMEOUT:
raise ConfigError('Sum of RADIUS servers timeouts '
@@ -169,6 +196,24 @@ def verify(login):
if ipv6_count > 1:
raise ConfigError('Only one IPv6 source-address can be set!')
+ if 'tacacs' in login:
+ tacacs_servers_count: int = 0
+ fail = True
+ for server, server_config in dict_search('tacacs.server', login).items():
+ if 'key' not in server_config:
+ raise ConfigError(f'TACACS server "{server}" requires key!')
+ if 'disable' not in server_config:
+ tacacs_servers_count += 1
+ fail = False
+
+ if fail:
+ raise ConfigError('All RADIUS servers are disabled')
+
+ if tacacs_servers_count > MAX_TACACS_COUNT:
+ raise ConfigError(f'Number of TACACS servers exceeded maximum of {MAX_TACACS_COUNT}!')
+
+ verify_vrf(login['tacacs'])
+
if 'max_login_session' in login and 'timeout' not in login:
raise ConfigError('"login timeout" must be configured!')
@@ -190,8 +235,8 @@ def generate(login):
env['vyos_libexec_dir'] = directories['base']
# 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}'"
+ 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"
@@ -210,10 +255,10 @@ def generate(login):
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)
+ ret, out = rc_cmd(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
+ if ret: raise ConfigError(out)
ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
- if ret:
- raise ConfigError(out)
+ if ret: raise ConfigError(out)
else:
try:
if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config):
@@ -227,6 +272,7 @@ def generate(login):
except:
pass
+ ### RADIUS based user authentication
if 'radius' in login:
render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
permission=0o600, user='root', group='root')
@@ -234,6 +280,24 @@ def generate(login):
if os.path.isfile(radius_config_file):
os.unlink(radius_config_file)
+ ### TACACS+ based user authentication
+ if 'tacacs' in login:
+ render(tacacs_pam_config_file, 'login/tacplus_servers.j2', login,
+ permission=0o644, user='root', group='root')
+ render(tacacs_nss_config_file, 'login/tacplus_nss.conf.j2', login,
+ permission=0o644, user='root', group='root')
+ else:
+ if os.path.isfile(tacacs_pam_config_file):
+ os.unlink(tacacs_pam_config_file)
+ if os.path.isfile(tacacs_nss_config_file):
+ os.unlink(tacacs_nss_config_file)
+
+
+
+ # NSS must always be present on the system
+ render(nss_config_file, 'login/nsswitch.conf.j2', login,
+ permission=0o644, user='root', group='root')
+
# /etc/security/limits.d/10-vyos.conf
if 'max_login_session' in login:
render(limits_file, 'login/limits.j2', login,
@@ -257,7 +321,7 @@ def apply(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'
+ command = 'useradd --create-home --no-user-group '
# check if user already exists:
if user in get_local_users():
# update existing account
@@ -327,38 +391,17 @@ def apply(login):
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}')
+ # Enable RADIUS in PAM configuration
+ pam_cmd = '--remove'
+ if 'radius' in login:
+ pam_cmd = '--enable'
+ cmd(f'pam-auth-update --package {pam_cmd} radius')
+
+ # Enable/Disable TACACS in PAM configuration
+ pam_cmd = '--remove'
+ if 'tacacs' in login:
+ pam_cmd = '--enable'
+ cmd(f'pam-auth-update --package {pam_cmd} tacplus')
return None
diff --git a/src/op_mode/container.py b/src/op_mode/container.py
index d48766a0c..7f726f076 100755
--- a/src/op_mode/container.py
+++ b/src/op_mode/container.py
@@ -83,7 +83,7 @@ def restart(name: str):
if rc != 0:
print(output)
return None
- print(f'Container name "{name}" restarted!')
+ print(f'Container "{name}" restarted!')
return output
diff --git a/src/pam-configs/radius b/src/pam-configs/radius
index aaae6aeb0..08247f77c 100644
--- a/src/pam-configs/radius
+++ b/src/pam-configs/radius
@@ -1,20 +1,17 @@
Name: RADIUS authentication
-Default: yes
+Default: no
Priority: 257
Auth-Type: Primary
Auth:
- [default=ignore success=1] pam_succeed_if.so uid eq 1000 quiet
- [default=ignore success=ignore] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
[authinfo_unavail=ignore success=end default=ignore] pam_radius_auth.so
Account-Type: Primary
Account:
- [default=ignore success=1] pam_succeed_if.so uid eq 1000 quiet
- [default=ignore success=ignore] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
[authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so
Session-Type: Additional
Session:
- [default=ignore success=1] pam_succeed_if.so uid eq 1000 quiet
- [default=ignore success=ignore] pam_succeed_if.so uid eq 1001 quiet
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
[authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so
diff --git a/src/pam-configs/tacplus b/src/pam-configs/tacplus
new file mode 100644
index 000000000..66a1eaa4c
--- /dev/null
+++ b/src/pam-configs/tacplus
@@ -0,0 +1,17 @@
+Name: TACACS+ authentication
+Default: no
+Priority: 257
+Auth-Type: Primary
+Auth:
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
+ [authinfo_unavail=ignore success=end auth_err=bad default=ignore] pam_tacplus.so include=/etc/tacplus_servers login=login
+
+Account-Type: Primary
+Account:
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
+ [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_tacplus.so include=/etc/tacplus_servers login=login
+
+Session-Type: Additional
+Session:
+ [default=ignore success=ignore] pam_succeed_if.so user ingroup aaa quiet
+ [authinfo_unavail=ignore success=ok default=ignore] pam_tacplus.so include=/etc/tacplus_servers login=login