summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/login/pam_radius_auth.conf.tmpl33
-rw-r--r--data/templates/system-login/pam_radius_auth.conf.tmpl16
-rw-r--r--interface-definitions/include/radius-server-ipv4-ipv6.xml.i32
-rw-r--r--interface-definitions/include/source-address-ipv4-ipv6.xml.i1
-rw-r--r--interface-definitions/system-login.xml.in2
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py66
-rwxr-xr-xsrc/conf_mode/system-login.py47
7 files changed, 157 insertions, 40 deletions
diff --git a/data/templates/login/pam_radius_auth.conf.tmpl b/data/templates/login/pam_radius_auth.conf.tmpl
new file mode 100644
index 000000000..56a5e10ee
--- /dev/null
+++ b/data/templates/login/pam_radius_auth.conf.tmpl
@@ -0,0 +1,33 @@
+# Automatically generated by system-login.py
+# RADIUS configuration file
+
+{# RADIUS IPv6 source address must be specified in [] notation #}
+{% set source_address = namespace() %}
+{% if radius_source_address is defined and radius_source_address is not none %}
+{% for address in radius_source_address %}
+{% if address | is_ipv4 %}
+{% set source_address.ipv4 = address %}
+{% elif address | is_ipv6 %}
+{% set source_address.ipv6 = "[" + address + "]" %}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if radius_server is defined and radius_server is not none %}
+# server[:port] shared_secret timeout source_ip
+{% for server in radius_server | sort(attribute='priority') if not server.disabled %}
+{# RADIUS IPv6 servers must be specified in [] notation #}
+{% if server.address | is_ipv4 %}
+{{ server.address }}:{{ server.port }} {{ "%-25s" | format(server.key) }} {{ "%-10s" | format(server.timeout) }} {{ source_address.ipv4 if source_address.ipv4 is defined }}
+{% else %}
+[{{ server.address }}]:{{ server.port }} {{ "%-25s" | format(server.key) }} {{ "%-10s" | format(server.timeout) }} {{ source_address.ipv6 if source_address.ipv6 is defined }}
+{% endif %}
+{% endfor %}
+
+priv-lvl 15
+mapped_priv_user radius_priv_user
+
+{% if radius_vrf %}
+vrf-name {{ radius_vrf }}
+{% endif %}
+{% endif %}
+
diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl
deleted file mode 100644
index ec2d6df95..000000000
--- a/data/templates/system-login/pam_radius_auth.conf.tmpl
+++ /dev/null
@@ -1,16 +0,0 @@
-# Automatically generated by system-login.py
-# RADIUS configuration file
-{% if radius_server %}
-# server[:port] shared_secret timeout source_ip
-{% for s in radius_server|sort(attribute='priority') if not s.disabled %}
-{% set addr_port = s.address + ":" + s.port %}
-{{ "%-22s" | format(addr_port) }} {{ "%-25s" | format(s.key) }} {{ "%-10s" | format(s.timeout) }} {{ radius_source_address if radius_source_address }}
-{% endfor %}
-
-priv-lvl 15
-mapped_priv_user radius_priv_user
-
-{% if radius_vrf %}
-vrf-name {{ radius_vrf }}
-{% endif %}
-{% endif %}
diff --git a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i
new file mode 100644
index 000000000..e947c09e2
--- /dev/null
+++ b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i
@@ -0,0 +1,32 @@
+<!-- included start from radius-server-ipv4-ipv6.xml.i -->
+<node name="radius">
+ <properties>
+ <help>RADIUS based user authentication</help>
+ </properties>
+ <children>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ <tagNode name="server">
+ <properties>
+ <help>RADIUS server configuration</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>RADIUS server IPv4 address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>RADIUS server IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ #include <include/radius-server-key.xml.in>
+ #include <include/radius-server-port.xml.in>
+ </children>
+ </tagNode>
+ </children>
+</node>
+<!-- included end -->
diff --git a/interface-definitions/include/source-address-ipv4-ipv6.xml.i b/interface-definitions/include/source-address-ipv4-ipv6.xml.i
index 004e04f7b..4da4698c2 100644
--- a/interface-definitions/include/source-address-ipv4-ipv6.xml.i
+++ b/interface-definitions/include/source-address-ipv4-ipv6.xml.i
@@ -17,6 +17,7 @@
<validator name="ipv4-address"/>
<validator name="ipv6-address"/>
</constraint>
+ <multi/>
</properties>
</leafNode>
<!-- included end -->
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 0bea6a22d..6c573bf96 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -110,7 +110,7 @@
</leafNode>
</children>
</tagNode>
- #include <include/radius-server-ipv4.xml.i>
+ #include <include/radius-server-ipv4-ipv6.xml.i>
<node name="radius">
<children>
<tagNode name="server">
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 6188cf38b..bb6f57fc2 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -24,8 +24,10 @@ from platform import release as kernel_version
from subprocess import Popen, PIPE
from vyos.configsession import ConfigSession
+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', 'vyos2']
@@ -42,7 +44,7 @@ class TestSystemLogin(unittest.TestCase):
self.session.commit()
del self.session
- def test_local_user(self):
+ def test_system_login_user(self):
# Check if user can be created and we can SSH to localhost
self.session.set(['service', 'ssh', 'port', '22'])
@@ -82,7 +84,7 @@ class TestSystemLogin(unittest.TestCase):
for option in options:
self.assertIn(f'{option}=y', kernel_config)
- def test_radius_config(self):
+ def test_system_login_radius_ipv4(self):
# Verify generated RADIUS configuration files
radius_key = 'VyOSsecretVyOS'
@@ -95,6 +97,12 @@ class TestSystemLogin(unittest.TestCase):
self.session.set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
self.session.set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
self.session.set(base_path + ['radius', 'source-address', radius_source])
+ self.session.set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ # check validate() - Only one IPv4 source-address supported
+ with self.assertRaises(ConfigSessionError):
+ self.session.commit()
+ self.session.delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
self.session.commit()
@@ -130,5 +138,59 @@ class TestSystemLogin(unittest.TestCase):
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.session.set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
+ self.session.set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
+ self.session.set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
+ self.session.set(base_path + ['radius', 'source-address', radius_source])
+ self.session.set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ # check validate() - Only one IPv4 source-address supported
+ with self.assertRaises(ConfigSessionError):
+ self.session.commit()
+ self.session.delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
+
+ self.session.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 39bad717d..92f717df8 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 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 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
@@ -25,6 +25,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
+from vyos.template import is_ipv4
from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755
from vyos import ConfigError
@@ -38,7 +39,7 @@ default_config_data = {
'add_users': [],
'del_users': [],
'radius_server': [],
- 'radius_source_address': '',
+ 'radius_source_address': [],
'radius_vrf': ''
}
@@ -134,7 +135,7 @@ def get_config(config=None):
conf.set_level(base_level + ['radius'])
if conf.exists(['source-address']):
- login['radius_source_address'] = conf.return_value(['source-address'])
+ login['radius_source_address'] = conf.return_values(['source-address'])
# retrieve VRF instance
if conf.exists(['vrf']):
@@ -214,6 +215,17 @@ def verify(login):
if fail:
raise ConfigError('At least one RADIUS server must be active.')
+ 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!')
+
vrf_name = login['radius_vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
@@ -255,13 +267,8 @@ def generate(login):
pass
if len(login['radius_server']) > 0:
- render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl',
- login)
-
- uid = getpwnam('root').pw_uid
- gid = getpwnam('root').pw_gid
- os.chown(radius_config_file, uid, gid)
- chmod_600(radius_config_file)
+ render(radius_config_file, 'login/pam_radius_auth.conf.tmpl', login,
+ permission=0o600, user='root', group='root')
else:
if os.path.isfile(radius_config_file):
os.unlink(radius_config_file)
@@ -284,16 +291,15 @@ def apply(login):
# 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
if user['password_encrypted']:
- command += " -p '{}'".format(user['password_encrypted'])
+ command += " -p '{password_encrypted}'".format(**user)
if user['full_name']:
- command += " -c '{}'".format(user['full_name'])
+ command += " -c '{full_name}'".format(**user)
if user['home_dir']:
- command += " -d '{}'".format(user['home_dir'])
+ command += " -d '{home_dir}'".format(**user)
- command += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
- command += " {}".format(user['name'])
+ command += " -G frrvty,vyattacfg,sudo,adm,dip,disk {name}".format(**user)
try:
cmd(command)
@@ -321,10 +327,9 @@ def apply(login):
for id in user['public_keys']:
line = ''
if id['options']:
- line = '{} '.format(id['options'])
+ line = '{options} '.format(**id)
- line += '{} {} {}\n'.format(id['type'],
- id['key'], id['name'])
+ line += '{type} {key} {name}\n'.format(**id)
f.write(line)
os.chown(ssh_key_file, uid, gid)
@@ -339,8 +344,8 @@ def apply(login):
try:
# Logout user if he is logged in
if user in list(set([tmp[0] for tmp in users()])):
- print('{} is logged in, forcing logout'.format(user))
- call('pkill -HUP -u {}'.format(user))
+ print(f'{user} is logged in, forcing logout')
+ call(f'pkill -HUP -u {user}')
# Remove user account but leave home directory to be safe
call(f'userdel -r {user}', stderr=DEVNULL)
@@ -356,7 +361,7 @@ def apply(login):
env = os.environ.copy()
env['DEBIAN_FRONTEND'] = 'noninteractive'
# Enable RADIUS in PAM
- cmd("pam-auth-update --package --enable radius", env=env)
+ cmd('pam-auth-update --package --enable radius', env=env)
# Make NSS system aware of RADIUS, too
command = "sed -i -e \'/\smapname/b\' \