Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions files/build_templates/sonic_debian_extension.j2
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ sudo cp $IMAGE_CONFIGS/interfaces/*.j2 $FILESYSTEM_ROOT/usr/share/sonic/template
# Copy initial interfaces configuration file, will be overwritten on first boot
sudo cp $IMAGE_CONFIGS/interfaces/init_interfaces $FILESYSTEM_ROOT/etc/network

# Copy hostcfgd files
sudo cp $IMAGE_CONFIGS/hostcfgd/hostcfgd.service $FILESYSTEM_ROOT/etc/systemd/system/
sudo LANG=C chroot $FILESYSTEM_ROOT systemctl enable hostcfgd.service
sudo cp $IMAGE_CONFIGS/hostcfgd/hostcfgd $FILESYSTEM_ROOT/usr/bin/

# Copy hostname configuration scripts
sudo cp $IMAGE_CONFIGS/hostname/hostname-config.service $FILESYSTEM_ROOT/etc/systemd/system/
sudo LANG=C chroot $FILESYSTEM_ROOT systemctl enable hostname-config.service
Expand Down
339 changes: 339 additions & 0 deletions files/image_config/hostcfgd/hostcfgd
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
#!/usr/bin/python -u
# -*- coding: utf-8 -*-

import os
import commands
import sys
import redis
import subprocess
import syslog
from swsssdk import ConfigDBConnector

# FILE
PAM_CONFIG_DIR = "/etc/pam.d/"
PAM_AUTH_FILE = PAM_CONFIG_DIR + "common-auth-sonic"
NSS_TACPLUS_CONF = "/etc/tacplus_nss.conf"
NSS_CONF = "/etc/nsswitch.conf"

# AAA
AAA_TACPLUS = "tacacs+"
AAA_LOCAL = "local"

# TACACS+
TACPLUS_SERVER_TCP_PORT_DEFAULT = "49"
TACPLUS_SERVER_PASSKEY_DEFAULT = ""
TACPLUS_SERVER_TIMEOUT_DEFAULT = "5"
TACPLUS_SERVER_PRI_DEFAULT = "1"
TACPLUS_SERVER_AUTH_TYPE_DEFAULT = "pap"

# PAM
PAM_TACACS_MODULE = "pam_tacplus.so"
PAM_LOCAL_MODULE = "pam_unix.so"

AUTH_FILE_HEADER = "# THIS IS AN AUTO-GENERATED FILE\n" \
"#\n" \
"# /etc/pam.d/common-auth- authentication settings common to all services\n" \
"# This file is included from other service-specific PAM config files,\n" \
"# and should contain a list of the authentication modules that define\n" \
"# the central authentication scheme for use on the system\n" \
"# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the\n" \
"# traditional Unix authentication mechanisms.\n" \
"#\n" \
"# here are the per-package modules (the \"Primary\" block)\n"

AUTH_FILE_FOOTER = "#\n" \
"# here's the fallback if no module succeeds\n" \
"auth requisite pam_deny.so\n" \
"# prime the stack with a positive return value if there isn't one already;\n" \
"# this avoids us returning an error just because nothing sets a success code\n" \
"# since the modules above will each just jump around\n" \
"auth required pam_permit.so\n" \
"# and here are more per-package modules (the \"Additional\" block)\n"


class TacplusServer(object):
def __init__(self,
address,
tcp_port,
passkey,
timeout,
auth_type,
priority):
self.address = address
self.tcp_port = tcp_port
self.passkey = passkey
self.timeout = timeout
self.auth_type = auth_type
self.priority = priority

def get_pam_conf(self, global_conf, debug):
passkey = self.passkey
timeout = self.timeout
auth_type = self.auth_type

if passkey is None:
passkey = global_conf['passkey']
if timeout is None:
timeout = global_conf['timeout']
if auth_type is None:
auth_type = global_conf['auth_type']

pam_auth = PAM_TACACS_MODULE
if debug:
pam_auth += " debug"
pam_auth += (" server=%s:%s secret=%s login=%s timeout=%s try_first_pass\n" % \
(self.address, self.tcp_port, passkey, auth_type, timeout))
return pam_auth

def get_nss_conf(self, global_conf):
passkey = self.passkey
timeout = self.timeout
if passkey is None:
passkey = global_conf['passkey']
if timeout is None:
timeout = global_conf['timeout']

conf = "server=%s:%s,secret=%s,timeout=%s\n" % (self.address, self.tcp_port, passkey, timeout)
return conf


def is_true(val):
if val == 'True' or val == 'true':
return True
else:
return False


class AaaCfg(object):
def __init__(self):
self.auth_login = [AAA_LOCAL]
self.auth_fallback = True
self.auth_failthrough = True
self.auz_fallback = True
self.auz_failthrough = True
self.tacplus_global = {
'auth_type': TACPLUS_SERVER_AUTH_TYPE_DEFAULT,
'timeout': TACPLUS_SERVER_TIMEOUT_DEFAULT,
'passkey': TACPLUS_SERVER_PASSKEY_DEFAULT
}
self.tacplus_servers = []
self.debug = False

# Load conf from ConfigDb
def load(self, aaa_conf, tac_global_conf, tacplus_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)
self.modify_pam_auth_file()
self.modify_nss_conf()

def aaa_update(self, key, data, modify_conf=True):
if key == 'authentication':
self.auth_login = [AAA_LOCAL]
self.auth_fallback = True
self.auth_failthrough = True

if 'fallback' in data:
self.auth_fallback = is_true(data['fallback'])
if 'failthrough' in data:
self.auth_failthrough = is_true(data['failthrough'])
if 'login' in data:
val = data['login']
self.auth_login = val.split(',', 1)
elif key == 'authorization':
self.auz_fallback = True
self.auz_failthrough = True
if 'fallback' in data:
self.auz_fallback = is_true(data['fallback'])
if 'failthrough' in data:
self.auz_failthrough = is_true(data['failthrough'])

if modify_conf:
self.modify_pam_auth_file()
self.modify_nss_conf()

def tacacs_global_update(self, key, data, modify_conf=True):
if key == 'global':
self.tacplus_global = {
'auth_type': TACPLUS_SERVER_AUTH_TYPE_DEFAULT,
'timeout': TACPLUS_SERVER_TIMEOUT_DEFAULT,
'passkey': TACPLUS_SERVER_PASSKEY_DEFAULT
}
if 'auth_type' in data:
self.tacplus_global['auth_type'] = data['auth_type']
if 'timeout' in data:
self.tacplus_global['timeout'] = data['timeout']
if 'passkey' in data:
self.tacplus_global['passkey'] = data['passkey']
if modify_conf:
self.modify_pam_auth_file()
self.modify_nss_conf()

def tacacs_server_update(self, key, data, modify_conf=True):
if data == {}:
for server in self.tacplus_servers:
if key == server.address:
self.tacplus_servers.remove(server)
else:
tcp_port = TACPLUS_SERVER_TCP_PORT_DEFAULT
pri = TACPLUS_SERVER_PRI_DEFAULT
auth_type = None
timeout = None
passkey = None

if 'tcp_port' in data:
tcp_port = data['tcp_port']
if 'priority' in data:
pri = data['priority']
if 'auth_type' in data:
auth_type = data['auth_type']
if 'passkey' in data:
passkey = data['passkey']
if 'timeout' in data:
timeout = data['timeout']

found = False
for server in self.tacplus_servers:
if key == server.address:
server.tcp_port = tcp_port
server.auth_type = auth_type
server.timeout = timeout
server.passkey = passkey
server.priority = pri
found = True
if not found:
server = TacplusServer(key, tcp_port, passkey, timeout, auth_type, pri)
self.tacplus_servers.append(server)

self.tacplus_servers = sorted(self.tacplus_servers,
key=lambda server: server.priority,
reverse=True)
if modify_conf:
self.modify_pam_auth_file()
self.modify_nss_conf()

def get_pam_auth(self, pam_module):
if pam_module == AAA_LOCAL:
pam_auth = PAM_LOCAL_MODULE
if self.debug:
pam_auth += " debug"
pam_auth += " nullok try_first_pass\n"
return pam_auth
else:
return pam_module.get_pam_conf(self.tacplus_global, self.debug)

def modify_pam_auth_file(self):
if self.auth_failthrough:
pam_control = "[success=done new_authtok_reqd=done default=ignore]"
else:
pam_control = "[success=done new_authtok_reqd=done default=ignore auth_err=die]"

pam_modules = []
auth_file_body = ""

# Set local and tacacs+ pam priority
# ['local'] or ['local','tacacs+'] or ['tacacs+', 'local'] or ['tacacs+']
if self.auth_login == [AAA_LOCAL]:
pam_modules = [AAA_LOCAL]
elif self.auth_login == [AAA_LOCAL, AAA_TACPLUS]:
pam_modules = [AAA_LOCAL] + self.tacplus_servers
elif self.auth_login == [AAA_TACPLUS] or self.auth_login == [AAA_TACPLUS, AAA_LOCAL]:
# Don't accept only TACACS+ authentication
# Make sure root will always authentication on local, not TACACS+
pam_modules = self.tacplus_servers + [AAA_LOCAL]
auth_file_body += "auth\t[success=%d new_authtok_reqd=done default=ignore]\t" % (len(pam_modules)-1)
auth_file_body += "pam_succeed_if.so user = root debug\n"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

root is not allowed to login by default, there is also no debug account, only admin is enabled.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This configuration is used to ensure that an administrator user always login via local if TACACS+ authentication hangs or other error occur. But it's not correct to use root. The 'debug' means output debug log, not means debug account.
I have removed this logic because it looks like an impossible scenario.


if len(pam_modules):
for pam_module in pam_modules[:-1]:
line = self.get_pam_auth(pam_module)
if line is not "":
auth_file_body += "auth\t" + pam_control + "\t" + line

pam_module = pam_modules[-1]
line = self.get_pam_auth(pam_module)
auth_file_body += "auth\t[success=1 default=ignore]\t" + line

with open(PAM_AUTH_FILE, "w") as f:
f.write(AUTH_FILE_HEADER + auth_file_body + AUTH_FILE_FOOTER)

# Modify common-auth include file in /etc/pam.d/type and sshd
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/etc/pam.d/type -> /etc/pam.d/login

if os.path.isfile(PAM_AUTH_FILE):
os.system("sed -i -e '/^@include/s/common-auth$/common-auth-sonic/' /etc/pam.d/sshd")
os.system("sed -i -e '/^@include/s/common-auth$/common-auth-sonic/' /etc/pam.d/login")
else:
os.system("sed -i -e '/^@include/s/common-auth-sonic$/common-auth/' /etc/pam.d/sshd")
os.system("sed -i -e '/^@include/s/common-auth-sonic$/common-auth/' /etc/pam.d/login")

# Set tacacs+ server in nss-tacplus conf
def modify_nss_conf(self):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is very difficult to know what the actual conf file generated, it is better to design a template and use jinja2 library in python to generate the conf file. You can refer to the code in sonic-cfggen.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed it with jinja2 template.

if AAA_TACPLUS in self.auth_login:
# Add tacplus in nsswitch.conf if TACACS+ enable
if os.path.isfile(NSS_CONF):
os.system("sed -i -e '/tacplus/b' -e '/^passwd/s/compat/& tacplus/' /etc/nsswitch.conf")
else:
if os.path.isfile(NSS_CONF):
os.system("sed -i -e '/^passwd/s/ tacplus//' /etc/nsswitch.conf")

dbg = ""
servers = ""
if self.debug:
dbg = "debug=on\n"
for server in self.tacplus_servers:
servers += server.get_nss_conf(self.tacplus_global)

contents = ""
with open(NSS_TACPLUS_CONF, 'r') as f:
line = f.readline()
while line:
if '#' is line[0]:
contents += line
elif 'debug=' != line[0:6] and 'server=' != line[0:7]:
contents += line
line = f.readline()
contents += dbg + servers
with open(NSS_TACPLUS_CONF, 'w') as f:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once you change the conf file, how to make the conf effective? do you need to reload anything?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If tacplus is enabled for passwd in nsswitch.conf, the conf is loaded by nss-tacplus plugin when the function getpwnam() is called each time. Don't need to reload it.

f.write(contents)


class HostConfigDaemon:

def __init__(self):
self.config_db = ConfigDBConnector()
self.config_db.connect(wait_for_init=True, retry_on=True)
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
aaa = self.config_db.get_table('AAA')
tacacs_global = self.config_db.get_table('TACPLUS')
tacacs_server = self.config_db.get_table('TACPLUS_SERVER')
self.aaacfg = AaaCfg()
self.aaacfg.load(aaa, tacacs_global, tacacs_server)

def aaa_handler(self, key, data):
syslog.syslog(syslog.LOG_INFO, 'value for {} changed to {}'.format(key, data))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is debug level -> LOG_DEBUG

self.aaacfg.aaa_update(key, data)

def tacacs_server_handler(self, key, data):
syslog.syslog(syslog.LOG_INFO, 'value for {} changed to {}'.format(key, data))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEBUG level

self.aaacfg.tacacs_server_update(key, data)

def tacacs_global_handler(self, key, data):
syslog.syslog(syslog.LOG_INFO, 'value for {} changed to {}'.format(key, data))
self.aaacfg.tacacs_global_update(key, data)

def start(self):
self.config_db.subscribe('AAA', lambda table, key, data: self.aaa_handler(key, data))
self.config_db.subscribe('TACPLUS_SERVER', lambda table, key, data: self.tacacs_server_handler(key, data))
self.config_db.subscribe('TACPLUS', lambda table, key, data: self.tacacs_global_handler(key, data))
self.config_db.listen()


def main():
daemon = HostConfigDaemon()
daemon.start()

if __name__ == "__main__":
main()

11 changes: 11 additions & 0 deletions files/image_config/hostcfgd/hostcfgd.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=Host config enforcer daemon
Requires=database.service
After=database.service

[Service]
Type=simple
ExecStart=/usr/bin/hostcfgd

[Install]
WantedBy=multi-user.target