Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Radius Management User Authentication Feature #4220

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions files/build_templates/sonic_debian_extension.j2
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ if [[ $CONFIGURED_ARCH == amd64 ]]; then
sudo DEBIAN_FRONTEND=noninteractive dpkg --root=$FILESYSTEM_ROOT -i $debs_path/kdump-tools_*.deb || \
sudo LANG=C DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true chroot $FILESYSTEM_ROOT apt-get -q --no-install-suggests --no-install-recommends --force-no install
fi
# Install pam-radius-auth and nss-radius
sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/libpam-radius-auth_*.deb || \
sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install -f
sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/libnss-radius_*.deb || \
sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install -f
# Disable radius by default
# radius does not have any profiles
#sudo LANG=C chroot $FILESYSTEM_ROOT pam-auth-update --remove radius tacplus
sudo sed -i -e '/^passwd/s/ radius//' $FILESYSTEM_ROOT/etc/nsswitch.conf

# Install custom-built monit package and SONiC configuration files
sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/monit_*.deb || \
Expand Down
44 changes: 40 additions & 4 deletions files/image_config/hostcfgd/common-auth-sonic.j2
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,57 @@
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass

{% elif auth['login'] == 'local,tacacs+' %}
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die maxtries=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass
{% for server in servers | sub(0, -1) %}
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_tacplus.so server={{ server.ip }}:{{ server.tcp_port }} secret={{ server.passkey }} login={{ server.auth_type }} timeout={{ server.timeout }} {% if server.vrf %} vrf={{ server.vrf }} {% endif %} try_first_pass
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_tacplus.so server={{ server.ip }}:{{ server.tcp_port }} [secret={{ server.passkey }}] login={{ server.auth_type }} timeout={{ server.timeout }} {% if server.vrf %} vrf={{ server.vrf }} {% endif %} {% if debug %} debug {% endif %} try_first_pass
{% endfor %}
{% if servers | count %}
{% set last_server = servers | last %}
auth [success=1 default=ignore] pam_tacplus.so server={{ last_server.ip }}:{{ last_server.tcp_port }} secret={{ last_server.passkey }} login={{ last_server.auth_type }} timeout={{ last_server.timeout }} {% if last_server.vrf %} vrf={{ last_server.vrf }} {% endif %} try_first_pass
auth [success=1 default=ignore] pam_tacplus.so server={{ last_server.ip }}:{{ last_server.tcp_port }} [secret={{ last_server.passkey }}] login={{ last_server.auth_type }} timeout={{ last_server.timeout }} {% if last_server.vrf %} vrf={{ last_server.vrf }} {% endif %} {% if debug %} debug {% endif %} try_first_pass

{% endif %}
{% elif auth['login'] == 'tacacs+' or auth['login'] == 'tacacs+,local' %}
{% for server in servers %}
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_tacplus.so server={{ server.ip }}:{{ server.tcp_port }} secret={{ server.passkey }} login={{ server.auth_type }} timeout={{ server.timeout }} {%if server.vrf %} vrf={{ server.vrf }} {% endif %} try_first_pass
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_tacplus.so server={{ server.ip }}:{{ server.tcp_port }} [secret={{ server.passkey }}] login={{ server.auth_type }} timeout={{ server.timeout }} {%if server.vrf %} vrf={{ server.vrf }} {% endif %} {% if debug %} debug {% endif %} try_first_pass
{% endfor %}
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass

{% elif auth['login'] == 'local,radius' %}
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die maxtries=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass
# For the RADIUS servers, on success jump to the cacheing the MPL(Privilege)
{% for server in servers %}
auth [success={{ (servers | count) - loop.index0 }} new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_radius_auth.so conf=/etc/pam_radius_auth.d/{{ server.ip }}:{{ server.auth_port }}.conf privilege_level protocol={{ server.auth_type }} retry={{ server.retransmit }} {% if debug %} debug {% endif %} try_first_pass
{% endfor %}
auth requisite pam_deny.so
# Cache MPL(Privilege)
auth [success=1 default=ignore] pam_exec.so /usr/sbin/cache_radius

{% elif auth['login'] == 'radius,local' %}
# root user can only be authenticated locally. Jump to local.
auth [success={{ (servers | count) }} default=ignore] pam_succeed_if.so user = root
# For the RADIUS servers, on success jump to the cache the MPL(Privilege)
{% for server in servers %}
auth [success={{ (servers | count) + 1 - loop.index0 }} new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_radius_auth.so conf=/etc/pam_radius_auth.d/{{ server.ip }}:{{ server.auth_port }}.conf privilege_level protocol={{ server.auth_type }} retry={{ server.retransmit }} {% if debug %} debug {% endif %} try_first_pass
{% endfor %}
# Local
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die maxtries=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass
auth requisite pam_deny.so
# Cache MPL(Privilege)
auth [success=1 default=ignore] pam_exec.so /usr/sbin/cache_radius

{% elif auth['login'] == 'radius' %}
# root user can only be authenticated locally. Jump to local.
auth [success={{ (servers | count) + 2 }} default=ignore] pam_succeed_if.so user = root
# For the RADIUS servers, on success jump to the cache the MPL(Privilege)
{% for server in servers %}
auth [success={{ (servers | count) - loop.index0 }} new_authtok_reqd=done default=ignore{{ ' auth_err=die' if not auth['failthrough'] }}] pam_radius_auth.so conf=/etc/pam_radius_auth.d/{{ server.ip }}:{{ server.auth_port }}.conf privilege_level protocol={{ server.auth_type }} retry={{ server.retransmit }} {% if debug %} debug {% endif %} try_first_pass
{% endfor %}
auth requisite pam_deny.so
# Cache MPL(Privilege)
auth [success=2 default=ignore] pam_exec.so /usr/sbin/cache_radius
# Local
auth [success=done new_authtok_reqd=done default=ignore{{ ' auth_err=die maxtries=die' if not auth['failthrough'] }}] pam_unix.so nullok try_first_pass

{% else %}
auth [success=1 default=ignore] pam_unix.so nullok try_first_pass

Expand Down
126 changes: 121 additions & 5 deletions files/image_config/hostcfgd/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,24 @@ PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
PAM_AUTH_CONF_TEMPLATE = "/usr/share/sonic/templates/common-auth-sonic.j2"
NSS_TACPLUS_CONF = "/etc/tacplus_nss.conf"
NSS_TACPLUS_CONF_TEMPLATE = "/usr/share/sonic/templates/tacplus_nss.conf.j2"
NSS_RADIUS_CONF = "/etc/radius_nss.conf"
NSS_RADIUS_CONF_TEMPLATE = "/usr/share/sonic/templates/radius_nss.conf.j2"
PAM_RADIUS_AUTH_CONF_TEMPLATE = "/usr/share/sonic/templates/pam_radius_auth.conf.j2"
NSS_CONF = "/etc/nsswitch.conf"

# TACACS+
TACPLUS_SERVER_PASSKEY_DEFAULT = ""
TACPLUS_SERVER_TIMEOUT_DEFAULT = "5"
TACPLUS_SERVER_AUTH_TYPE_DEFAULT = "pap"

# RADIUS
RADIUS_SERVER_AUTH_PORT_DEFAULT = "1812"
RADIUS_SERVER_PASSKEY_DEFAULT = ""
RADIUS_SERVER_RETRANSMIT_DEFAULT = "3"
RADIUS_SERVER_TIMEOUT_DEFAULT = "5"
RADIUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"


def is_true(val):
if val == 'True' or val == 'true':
Expand Down Expand Up @@ -123,23 +134,38 @@ class AaaCfg(object):
'login': 'local',
}
self.tacplus_global_default = {
'priority': 0,
'auth_type': TACPLUS_SERVER_AUTH_TYPE_DEFAULT,
'timeout': TACPLUS_SERVER_TIMEOUT_DEFAULT,
'passkey': TACPLUS_SERVER_PASSKEY_DEFAULT
}
self.auth = {}
self.tacplus_global = {}
self.tacplus_servers = {}
self.radius_global_default = {
'priority': 0,
'auth_port': RADIUS_SERVER_AUTH_PORT_DEFAULT,
'auth_type': RADIUS_SERVER_AUTH_TYPE_DEFAULT,
'retransmit': RADIUS_SERVER_RETRANSMIT_DEFAULT,
'timeout': RADIUS_SERVER_TIMEOUT_DEFAULT,
'passkey': RADIUS_SERVER_PASSKEY_DEFAULT
}
self.radius_global = {}
self.radius_servers = {}
self.auth = {}
self.debug = False

# Load conf from ConfigDb
def load(self, aaa_conf, tac_global_conf, tacplus_conf):
def load(self, aaa_conf, tac_global_conf, tacplus_conf, rad_global_conf, radius_conf):
for row in aaa_conf:
self.aaa_update(row, aaa_conf[row], modify_conf=False)
for row in tac_global_conf:
self.tacacs_global_update(row, tac_global_conf[row], modify_conf=False)
for row in tacplus_conf:
self.tacacs_server_update(row, tacplus_conf[row], modify_conf=False)
for row in rad_global_conf:
self.radius_global_update(row, rad_global_conf[row], modify_conf=False)
for row in radius_conf:
self.radius_server_update(row, radius_conf[row], modify_conf=False)
self.modify_conf_file()

def aaa_update(self, key, data, modify_conf=True):
Expand Down Expand Up @@ -173,6 +199,22 @@ class AaaCfg(object):
cmd = "sed -e {0} {1} > {1}.new; mv -f {1} {1}.old; mv -f {1}.new {1}".format(' -e '.join(operations), filename)
os.system(cmd)

def radius_global_update(self, key, data, modify_conf=True):
if key == 'global':
self.radius_global = data
if modify_conf:
self.modify_conf_file()

def radius_server_update(self, key, data, modify_conf=True):
if data == {}:
if key in self.radius_servers:
del self.radius_servers[key]
else:
self.radius_servers[key] = data

if modify_conf:
self.modify_conf_file()

def modify_conf_file(self):
auth = self.auth_default.copy()
auth.update(self.auth)
Expand All @@ -188,11 +230,35 @@ class AaaCfg(object):
servers_conf.append(server)
servers_conf = sorted(servers_conf, key=lambda t: int(t['priority']), reverse=True)

radius_global = self.radius_global_default.copy()
radius_global.update(self.radius_global)

radsrvs_conf = []
if self.radius_servers:
for addr in self.radius_servers:
server = radius_global.copy()
server['ip'] = addr
server.update(self.radius_servers[addr])
radsrvs_conf.append(server)
radsrvs_conf = sorted(radsrvs_conf, key=lambda t: t['priority'], reverse=True)

template_file = os.path.abspath(PAM_AUTH_CONF_TEMPLATE)
env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True)
env.filters['sub'] = sub
template = env.get_template(template_file)
pam_conf = template.render(auth=auth, servers=servers_conf)
if 'radius' in auth['login']:
pam_conf = template.render(debug=self.debug, auth=auth, servers=radsrvs_conf)
else:
# Need to escape some chars.
pat = re.compile(']')
servers_conf4pam = []
for server in servers_conf:
server4pam = server.copy()
server4pam['passkey'] = pat.sub(r'\]', server['passkey'])
servers_conf4pam.append(server4pam)
servers_conf4pam = sorted(servers_conf4pam, key=lambda t: int(t['priority']), reverse=True)
pam_conf = template.render(debug=self.debug, auth=auth, servers=servers_conf4pam)

with open(PAM_AUTH_CONF, 'w') as f:
f.write(pam_conf)

Expand All @@ -204,13 +270,22 @@ class AaaCfg(object):
self.modify_single_file('/etc/pam.d/sshd', [ "'/^@include/s/common-auth-sonic$/common-auth/'" ])
self.modify_single_file('/etc/pam.d/login', [ "'/^@include/s/common-auth-sonic$/common-auth/'" ])

# Add tacplus in nsswitch.conf if TACACS+ enable
# Add tacplus/radius in nsswitch.conf if TACACS+/RADIUS enable
if 'tacacs+' in auth['login']:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/tacplus/b'", "'/^passwd/s/compat/tacplus &/'"])
else:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/tacplus //'" ])
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/ radius//'" ])
elif 'radius' in auth['login']:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/tacplus //'" ])
self.modify_single_file(NSS_CONF, [ "'/radius/b'", "'/^passwd/s/compat/& radius/'"])
else:
if os.path.isfile(NSS_CONF):
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/tacplus //'" ])
self.modify_single_file(NSS_CONF, [ "'/^passwd/s/ radius//'" ])

# Set tacacs+ server in nss-tacplus conf
template_file = os.path.abspath(NSS_TACPLUS_CONF_TEMPLATE)
Expand All @@ -219,6 +294,28 @@ class AaaCfg(object):
with open(NSS_TACPLUS_CONF, 'w') as f:
f.write(nss_tacplus_conf)

# Set debug in nss-radius conf
template_file = os.path.abspath(NSS_RADIUS_CONF_TEMPLATE)
template = env.get_template(template_file)
nss_radius_conf = template.render(debug=self.debug, servers=radsrvs_conf)
with open(NSS_RADIUS_CONF, 'w') as f:
f.write(nss_radius_conf)

# Create the per server pam_radius_auth.conf
if self.radius_servers:
for addr in self.radius_servers:
srv = radius_global.copy()
srv['ip'] = addr
srv.update(self.radius_servers[addr])
pam_radius_auth_file = RADIUS_PAM_AUTH_CONF_DIR + addr + ":" + srv['auth_port'] + ".conf"
template_file = os.path.abspath(PAM_RADIUS_AUTH_CONF_TEMPLATE)
template = env.get_template(template_file)
pam_radius_auth_conf = template.render(server=srv)

with open(pam_radius_auth_file, 'w') as f:
f.write(pam_radius_auth_conf)



class HostConfigDaemon:
def __init__(self):
Expand All @@ -228,8 +325,10 @@ class HostConfigDaemon:
aaa = self.config_db.get_table('AAA')
tacacs_global = self.config_db.get_table('TACPLUS')
tacacs_server = self.config_db.get_table('TACPLUS_SERVER')
radius_global = self.config_db.get_table('RADIUS')
radius_server = self.config_db.get_table('RADIUS_SERVER')
self.aaacfg = AaaCfg()
self.aaacfg.load(aaa, tacacs_global, tacacs_server)
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
lpbk_table = self.config_db.get_table('LOOPBACK_INTERFACE')
self.iptables = Iptables()
self.iptables.load(lpbk_table)
Expand Down Expand Up @@ -262,6 +361,21 @@ class HostConfigDaemon:

self.iptables.iptables_handler(key, data, add)


def radius_server_handler(self, key, data):
self.aaacfg.radius_server_update(key, data)
log_data = copy.deepcopy(data)
if log_data.has_key('passkey'):
log_data['passkey'] = obfuscate(log_data['passkey'])
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))

def radius_global_handler(self, key, data):
self.aaacfg.radius_global_update(key, data)
log_data = copy.deepcopy(data)
if log_data.has_key('passkey'):
log_data['passkey'] = obfuscate(log_data['passkey'])
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))

def feature_status_handler(self, key, data):
status_data = self.config_db.get_table('FEATURE')
for key in status_data.keys():
Expand Down Expand Up @@ -307,6 +421,8 @@ class HostConfigDaemon:
self.config_db.subscribe('TACPLUS', lambda table, key, data: self.tacacs_global_handler(key, data))
self.config_db.subscribe('LOOPBACK_INTERFACE', lambda table, key, data: self.lpbk_handler(key, data))
self.config_db.subscribe('FEATURE', lambda table, key, data: self.feature_status_handler(key, data))
self.config_db.subscribe('RADIUS_SERVER', lambda table, key, data: self.radius_server_handler(key, data))
self.config_db.subscribe('RADIUS', lambda table, key, data: self.radius_global_handler(key, data))
self.config_db.listen()


Expand Down
3 changes: 3 additions & 0 deletions files/image_config/hostcfgd/pam_radius_auth.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# server[:port] shared_secret timeout(s) source_ip vrf
[{{ server.ip }}]:{{ server.auth_port }} {{ server.passkey }} {{ server.timeout }} {% if server.src_ip %} {{ server.src_ip }} {% endif %} {% if server.vrf %} {% if not server.src_ip %} - {% endif %} {{ server.vrf }}{% endif %}

37 changes: 37 additions & 0 deletions files/image_config/hostcfgd/radius_nss.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# RADIUS NSS Configuration File
#
# Debug: on|off|trace
# Default: off
#
# debug=on
{% if debug %}
debug=on
{% endif %}

#
# User Privilege:
# Default:
# user_priv=15;pw_info=remote_user_su;uid=1000;gid=1000;group=sudo,docker;dir=/home/admin;shell=/bin/bash
# user_priv=1;pw_info=remote_user;uid=65534;gid=65534;group=users;dir=/var/tmp;shell=/bin/bash

# Eg:
# First need to create netops, operator using:
# useradd netops -G users -u 2007 -g 100 -c "netops" -m -s /bin/bash
# useradd operator -G users -u 2001 -g 100 -c "operator" -m -s /bin/rbash
#
# Then uncomment the lines below.
#
# user_priv=15;pw_info=remote_user_su;uid=1000;gid=1000;group=sudo,docker;dir=/home/admin;shell=/bin/bash
# user_priv=7;pw_info=netops;uid=2007;gid=100;group=users;dir=/home/netops;shell=/bin/bash
# user_priv=1;pw_info=operator;uid=2001;gid=100;group=users;dir=/home/operator;shell=/bin/rbash

# many_to_one:
# y: Map TACACS+ users to one local user per privilege.
# n: Create local user account on first successful authentication.
# a: Anonymous: Return least privileged(user_priv=1) user info for unknown.
# Default: n
#

# Eg:
# many_to_one=y
#
24 changes: 24 additions & 0 deletions rules/radius.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# libpam-radius-auth packages

PAM_RADIUS_VERSION = 1.4.1-1

export PAM_RADIUS_VERSION

LIBPAM_RADIUS = libpam-radius-auth_$(PAM_RADIUS_VERSION)_amd64.deb
$(LIBPAM_RADIUS)_SRC_PATH = $(SRC_PATH)/radius/pam
SONIC_MAKE_DEBS += $(LIBPAM_RADIUS)

SONIC_STRETCH_DEBS += $(LIBPAM_RADIUS)

# libnss-radius packages

NSS_RADIUS_VERSION = 1.0.1-1

export NSS_RADIUS_VERSION

LIBNSS_RADIUS = libnss-radius_$(NSS_RADIUS_VERSION)_amd64.deb
$(LIBNSS_RADIUS)_SRC_PATH = $(SRC_PATH)/radius/nss
SONIC_MAKE_DEBS += $(LIBNSS_RADIUS)

SONIC_STRETCH_DEBS += $(LIBNSS_RADIUS)

2 changes: 2 additions & 0 deletions slave.mk
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,8 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \
$(PYTHON_CLICK) \
$(IFUPDOWN2) \
$(KDUMP_TOOLS) \
$(LIBPAM_RADIUS) \
$(LIBNSS_RADIUS) \
$(LIBPAM_TACPLUS) \
$(LIBNSS_TACPLUS) \
$(MONIT)) \
Expand Down
23 changes: 23 additions & 0 deletions src/radius/nss/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.ONESHELL:
SHELL = /bin/bash
.SHELLFLAGS += -e

MAIN_TARGET = libnss-radius_$(NSS_RADIUS_VERSION)_amd64.deb

$(addprefix $(DEST)/, $(MAIN_TARGET)): $(DEST)/% :
pushd ./libnss-radius

make clean
-rm -rf debian
-rm -rf patches
cp -r ../debian .
cp -r ../patches .

# Apply patch (if any)

dpkg-buildpackage -rfakeroot -b -us -uc
popd

mv $(DERIVED_TARGETS) $* $(DEST)/

$(addprefix $(DEST)/, $(DERIVED_TARGETS)): $(DEST)/% : $(DEST)/$(MAIN_TARGET)
Loading