Skip to content

Commit

Permalink
smoketest: T7038: add freeradius container to live validate login via…
Browse files Browse the repository at this point in the history
… RADIUS

RADIUS is pretty sensible to its configuration. Instead of manual testing,
extend the smoketest platform to ship a freeradius container and perform logins
against a locally running freeradius server in a container.
  • Loading branch information
c-po committed Jan 10, 2025
1 parent 9f6a986 commit e67853b
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 62 deletions.
6 changes: 6 additions & 0 deletions debian/vyos-1x-smoketest.postinst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ TACPLUS_PATH="/usr/share/vyos/tacplus-alpine.tar"
if [[ ! -f $TACPLUS_PATH ]]; then
skopeo copy --additional-tag "$TACPLUS_TAG" "docker://$TACPLUS_TAG" "docker-archive:/$TACPLUS_PATH"
fi

RADIUS_TAG="docker.io/dchidell/radius-web:latest"
RADIUS_PATH="/usr/share/vyos/radius-latest.tar"
if [[ ! -f $RADIUS_PATH ]]; then
skopeo copy --additional-tag "$RADIUS_TAG" "docker://$RADIUS_TAG" "docker-archive:/$RADIUS_PATH"
fi
183 changes: 121 additions & 62 deletions smoketest/scripts/cli/test_system_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
from vyos.utils.file import read_file
from vyos.utils.file import write_file
from vyos.template import inc_ip
from vyos.template import is_ipv6
from vyos.xml_ref import default_value

base_path = ['system', 'login']
users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']

SSH_PROCESS_NAME = 'sshd'

ssh_pubkey = """
AAAAB3NzaC1yc2EAAAADAQABAAABgQD0NuhUOEtMIKnUVFIHoFatqX/c4mjerXyF
TlXYfVt6Ls2NZZsUSwHbnhK4BKDrPvVZMW/LycjQPzWW6TGtk6UbZP1WqdviQ9hP
Expand All @@ -54,10 +54,10 @@
pHJz8umqkxy3hfw0K7BRFtjWd63sbOP8Q/SDV7LPaIfIxenA9zv2rY7y+AIqTmSr
TTSb0X1zPGxPIRFy5GoGtO9Mm5h4OZk=
"""
SSH_PROCESS_NAME = 'sshd'

tac_image = 'docker.io/lfkeitel/tacacs_plus:alpine'
tac_image_path = '/usr/share/vyos/tacplus-alpine.tar'

TAC_PLUS_TMPL_SRC = """
id = spawnd {
debug redirect = /dev/stdout
Expand Down Expand Up @@ -100,6 +100,25 @@
member = admin
}
}
"""

radius_image = 'docker.io/dchidell/radius-web:latest'
radius_image_path = '/usr/share/vyos/radius-latest.tar'
RADIUS_CLIENTS_TMPL_SRC = """
client SMOKETEST {
secret = {{ radius_key }}
nastype = other
ipaddr = {{ source_address }}/32
}
"""
RADIUS_USERS_TMPL_SRC = """
# User configuration
{{ username }} Cleartext-Password := "{{ password }}"
Service-Type = NAS-Prompt-User,
Cisco-AVPair = "shell:priv-lvl=15"
"""

class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
Expand All @@ -112,16 +131,21 @@ def setUpClass(cls):
cls.cli_delete(cls, base_path + ['radius'])
cls.cli_delete(cls, base_path + ['tacacs'])

# Load image for smoketest provided in vyos-1x-smoketest
# Load images for smoketest provided in vyos-1x-smoketest
if not os.path.exists(tac_image_path):
cls.fail(cls, f'{tac_image} image not available')
cmd(f'sudo podman load -i {tac_image_path}')

if not os.path.exists(radius_image_path):
cls.fail(cls, f'{radius_image} image not available')
cmd(f'sudo podman load -i {radius_image_path}')

@classmethod
def tearDownClass(cls):
super(TestSystemLogin, cls).tearDownClass()
# Cleanup podman image
# Cleanup container images
cmd(f'sudo podman image rm -f {tac_image}')
cmd(f'sudo podman image rm -f {radius_image}')

def tearDown(self):
# Delete individual users from configuration
Expand Down Expand Up @@ -240,71 +264,77 @@ def test_radius_kernel_features(self):
self.assertIn(f'{option}=y', kernel_config)

def test_system_login_radius_ipv4(self):
# Verify generated RADIUS configuration files
radius_servers = ['100.64.0.4', '100.64.0.5']
radius_source = '100.64.0.1'
self._system_login_radius_test_helper(radius_servers, radius_source)

radius_key = 'VyOSsecretVyOS'
radius_server = '172.16.100.10'
radius_source = '127.0.0.1'
radius_port = '2000'
radius_timeout = '1'
def test_system_login_radius_ipv6(self):
radius_servers = ['2001:db8::4', '2001:db8::5']
radius_source = '2001:db8::1'
self._system_login_radius_test_helper(radius_servers, radius_source)

self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
self.cli_set(base_path + ['radius', 'source-address', radius_source])
self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
def _system_login_radius_test_helper(self, radius_servers: list, radius_source: str):
# Verify generated RADIUS configuration files
radius_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))

# check validate() - Only one IPv4 source-address supported
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])
default_port = default_value(base_path + ['radius', 'server', radius_servers[0], 'port'])
default_timeout = default_value(base_path + ['radius', 'server', radius_servers[0], 'timeout'])

self.cli_commit()
dummy_if = 'dum12760'

# 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)
# Load container image for FreeRADIUS server
radius_config = '/tmp/smoketest-radius-server'
radius_container_path = ['container', 'name', 'radius-1']

# 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)
# Generate random string with 10 digits
username = 'radius-admin'
password = ''.join(secrets.choice(string.ascii_letters + string.digits) for i in range(10))
radius_test_user = {
'username' : username,
'password' : password,
'radius_key' : radius_key,
'source_address' : radius_source,
}

pam_common_auth = read_file('/etc/pam.d/common-auth')
self.assertIn('pam_radius_auth.so', pam_common_auth)
tmpl = jinja2.Template(RADIUS_CLIENTS_TMPL_SRC)
write_file(f'{radius_config}/clients.cfg', tmpl.render(radius_test_user))

pam_common_session = read_file('/etc/pam.d/common-session')
self.assertIn('pam_radius_auth.so', pam_common_session)
tmpl = jinja2.Template(RADIUS_USERS_TMPL_SRC)
write_file(f'{radius_config}/users', tmpl.render(radius_test_user))

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)
# Check if SSH service is running
ssh_running = process_named_running(SSH_PROCESS_NAME)
if not ssh_running:
# Start SSH service
self.cli_set(['service', 'ssh'])

def test_system_login_radius_ipv6(self):
# Verify generated RADIUS configuration files
# Start tac_plus container
self.cli_set(radius_container_path + ['allow-host-networks'])
self.cli_set(radius_container_path + ['image', radius_image])
self.cli_set(radius_container_path + ['volume', 'clients', 'destination', '/etc/raddb/clients.conf'])
self.cli_set(radius_container_path + ['volume', 'clients', 'mode', 'ro'])
self.cli_set(radius_container_path + ['volume', 'clients', 'source', f'{radius_config}/clients.cfg'])
self.cli_set(radius_container_path + ['volume', 'users', 'destination', '/etc/raddb/users'])
self.cli_set(radius_container_path + ['volume', 'users', 'mode', 'ro'])
self.cli_set(radius_container_path + ['volume', 'users', 'source', f'{radius_config}/users'])

radius_key = 'VyOS-VyOS'
radius_server = '2001:db8::1'
radius_source = '::1'
radius_port = '4000'
radius_timeout = '4'
# Start container
self.cli_commit()

self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])
self.cli_set(base_path + ['radius', 'server', radius_server, 'port', radius_port])
self.cli_set(base_path + ['radius', 'server', radius_server, 'timeout', radius_timeout])
# Deinfine RADIUS servers
for radius_server in radius_servers:
# Use this system as "remote" RADIUS server
mask = '32'
if is_ipv6(radius_server):
mask = '128'
self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_server}/{mask}'])
self.cli_set(base_path + ['radius', 'server', radius_server, 'key', radius_key])

# Define RADIUS traffic source address
mask = '32'
if is_ipv6(radius_source):
mask = '128'
self.cli_set(['interfaces', 'dummy', dummy_if, 'address', f'{radius_source}/{mask}'])
self.cli_set(base_path + ['radius', 'source-address', radius_source])
self.cli_set(base_path + ['radius', 'source-address', inc_ip(radius_source, 1)])

Expand All @@ -317,10 +347,13 @@ def test_system_login_radius_ipv6(self):

# 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)

for radius_server in radius_servers:
if is_ipv6(radius_server):
tmp = re.findall(rf'\n?\[{radius_server}\]:{default_port}\s+{radius_key}\s+{default_timeout}\s+\[{radius_source}\]', pam_radius_auth_conf)
else:
tmp = re.findall(rf'\n?{radius_server}:{default_port}\s+{radius_key}\s+{default_timeout}\s+{radius_source}', pam_radius_auth_conf)
self.assertTrue(tmp)

# required, static options
self.assertIn('priv-lvl 15', pam_radius_auth_conf)
Expand All @@ -347,6 +380,32 @@ def test_system_login_radius_ipv6(self):
tmp = re.findall(r'group:\s+mapname\s+files', nsswitch_conf)
self.assertTrue(tmp)

# Login with proper credentials
test_command = 'uname -a'
out, err = self.ssh_send_cmd(test_command, username, password)
# verify login
self.assertFalse(err)
self.assertEqual(out, cmd(test_command))

# Login with invalid credentials
with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
_, _ = self.ssh_send_cmd(test_command, username, f'{password}1')

# Remove RADIUS configuration
self.cli_delete(base_path + ['radius'])
# Remove RADIUS container
self.cli_delete(radius_container_path)
# Remove dummy interface
self.cli_delete(['interfaces', 'dummy', dummy_if])
self.cli_commit()

# Remove rendered tac_plus daemon configuration
shutil.rmtree(radius_config)

# Stop SSH service if it was not running before
if not ssh_running:
self.cli_delete(['service', 'ssh'])

def test_system_login_max_login_session(self):
max_logins = '2'
timeout = '600'
Expand Down

0 comments on commit e67853b

Please sign in to comment.