diff --git a/build_debian.sh b/build_debian.sh index 2e0f7f152b51..e1c3745ae40f 100755 --- a/build_debian.sh +++ b/build_debian.sh @@ -224,6 +224,9 @@ sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y do sudo mv $FILESYSTEM_ROOT/grub-pc-bin*.deb $FILESYSTEM_ROOT/$PLATFORM_DIR/x86_64-grub +sudo dpkg --root=$FILESYSTEM_ROOT -i target/debs/libwrap0_*.deb || \ + sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install -f + ## Disable kexec supported reboot which was installed by default sudo sed -i 's/LOAD_KEXEC=true/LOAD_KEXEC=false/' $FILESYSTEM_ROOT/etc/default/kexec diff --git a/dockers/docker-snmp-sv2/Dockerfile.j2 b/dockers/docker-snmp-sv2/Dockerfile.j2 index 0e83b230746a..da46cb9b1cfb 100644 --- a/dockers/docker-snmp-sv2/Dockerfile.j2 +++ b/dockers/docker-snmp-sv2/Dockerfile.j2 @@ -43,6 +43,8 @@ RUN apt-get update && apt-get install -y libperl5.20 libpci3 libwrap0 \ COPY ["start.sh", "/usr/bin/"] COPY ["supervisord.conf", "/etc/supervisor/conf.d/"] COPY ["*.j2", "/usr/share/sonic/templates/"] +COPY ["snmpd-config-updater", "/usr/bin/snmpd-config-updater"] +RUN chmod +x /usr/bin/snmpd-config-updater ## Although exposing ports is not needed for host net mode, keep it for possible bridge mode EXPOSE 161/udp 162/udp diff --git a/dockers/docker-snmp-sv2/snmpd-config-updater b/dockers/docker-snmp-sv2/snmpd-config-updater new file mode 100755 index 000000000000..9f3e858bdb04 --- /dev/null +++ b/dockers/docker-snmp-sv2/snmpd-config-updater @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +# Daemon that listens to updates about the source IP prefixes from which snmp access +# is allowed. In case of change, it will update the snmp configuration file accordingly. +# Also, after a change, it will notify snmpd to re-read its config file (service reload). + +import os +import re +import sys +import time +import redis + +service="snmpd" +config_file_path="/etc/snmp" +redis_key="SNMP_ALLOW_LIST" # the redis list we listen to +subscription='__keyspace@0__:%s' % redis_key +temporization_duration = 3 # how long we wait for changes to settle (ride out a bursts of changes in redis_key) +fake_infinite = 9999 # How often we wake up when nothing is going on --get_message()'s timeout has no 'infinite' value +# after these operations we may need to revisit existing ssh connections because they removed or modified existing entries +delete_operations = ["lrem", "lpop", "rpop", "blpop", "brpop", "brpoplpush", "rpoplpush", "ltrim", "del", "lset"] + +r = redis.StrictRedis(host='localhost') +p = r.pubsub() + +# If redis is not up yet, this can fail, so wait for redis to be available +while True: + try: + p.subscribe(subscription) + break + except redis.exceptions.ConnectionError: + time.sleep(3) + +# We could loose contact with redis at a later stage, in which case we will exit with +# return code -2 and supervisor will restart us, at which point we are back in the +# while loop above waiting for redis to be ready. +try: + + # By default redis does enable events, so enable them + r.config_set("notify-keyspace-events", "KAE") + + + # To update the configuration file + # + # Example config file for reference: + # root@sonic:/# cat /etc/snmp/snmpd.conf + # <...some snmp config, like udp port to use etc...> + # rocommunity public 172.20.61.0/24 + # rocommunity public 172.20.60.0/24 + # rocommunity public 127.00.00.0/8 + # <...some more snmp config...> + # root@sonic:/# + # + # snmpd.conf supports include file, like so: + # includeFile /etc/snmp/community.conf + # includeDir /etc/snmp/config.d + # which could make file massaging simpler, but even then we still deal with lines + # that have shared "masters", since some other entity controls the community strings + # part of that line. + # If other database attributes need to be written to the snmp config file, then + # it should be done by this daemon as well (sure, we could inotify on the file + # and correct it back, but that's glitchy). + + def write_configuration_file(v): + filename="%s/%s.conf" % (config_file_path, service) + filename_tmp = filename + ".tmp" + f=open(filename, "r") + snmpd_config = f.read() + f.close() + f=open(filename_tmp, "w") + this_community = "not_a_community" + for l in snmpd_config.split('\n'): + m = re.match("^(..)community (\S+)", l) + if not m: + f.write(l) + f.write("\n") + else: + if not l.startswith(this_community): # already handled community (each community is duplicated per allow entry) + this_community="%scommunity %s" % (m.group(1), m.group(2)) + if len(v): + for value in v: + f.write("%s %s\n" % (this_community, value)) + else: + f.write("%s\n" % this_community) + f.close() + os.rename(filename_tmp, filename) + os.system("kill -HUP $(pgrep snmpd) > /dev/null 2> /dev/null || :") + + # write initial configuration + write_configuration_file(r.lrange(redis_key, 0, -1)) + + # listen for changes and rewrite configuration file if needed, after some temporization + # + # How those subscribed to messages look like, for reference: + # {'pattern': None, 'type': 'subscribe', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 1L} + # {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'rpush'} + # {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lpush'} + # {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lrem'} + # {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lset'} + # {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'del'} + + select_timeout = fake_infinite + config_changed = False + while True: + try: + m = p.get_message(timeout=select_timeout) + except Exception: + sys.exit(-2) + # temporization: no change after 'timeout' seconds -> commit any accumulated changes + if not m and config_changed: + write_configuration_file(r.lrange(redis_key, 0, -1)) + config_changed = False + select_timeout = fake_infinite + if m and m['type'] == "message": + if m['channel'] != subscription: + print "WTF: unexpected case" + continue + config_changed = True + select_timeout = temporization_duration + # some debugs for now + print "-------------------- config change: ", + if m["data"] in delete_operations: + print "DELETE" + else: + print "" + v = r.lrange(redis_key, 0, -1) + for value in v: + print value + +except redis.exceptions.ConnectionError as e: + sys.exit(-2) + + diff --git a/dockers/docker-snmp-sv2/supervisord.conf b/dockers/docker-snmp-sv2/supervisord.conf index d80579506100..b760d5c0453e 100644 --- a/dockers/docker-snmp-sv2/supervisord.conf +++ b/dockers/docker-snmp-sv2/supervisord.conf @@ -11,6 +11,15 @@ autorestart=false stdout_logfile=syslog stderr_logfile=syslog +[program:snmpd-config-updater] +command=/usr/bin/snmpd-config-updater +priority=1 +autostart=true +autorestart=unexpected +startsecs=0 +stdout_logfile=syslog +stderr_logfile=syslog + [program:rsyslogd] command=/usr/sbin/rsyslogd -n priority=2 diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2 index a2bea7a6baa6..218386baa1af 100644 --- a/files/build_templates/sonic_debian_extension.j2 +++ b/files/build_templates/sonic_debian_extension.j2 @@ -224,6 +224,18 @@ if [ "$image_type" = "aboot" ]; then sudo sed -i 's/udevadm settle/udevadm settle -E \/sys\/class\/net\/eth0/' $FILESYSTEM_ROOT/etc/init.d/networking fi +# Service to update the sshd config file based on database changes +sudo cp $IMAGE_CONFIGS/ssh/sshd-config-updater.service $FILESYSTEM_ROOT/etc/systemd/system +sudo mkdir -p $FILESYSTEM_ROOT/etc/systemd/system/multi-user.target.wants +cd $FILESYSTEM_ROOT/etc/systemd/system/multi-user.target.wants/ +sudo ln -s ../sshd-config-updater.service sshd-config-updater.service +cd - +sudo cp $IMAGE_CONFIGS/ssh/sshd-config-updater $FILESYSTEM_ROOT/usr/bin/ +sudo chmod +x $FILESYSTEM_ROOT/usr/bin/sshd-config-updater +sudo cp $IMAGE_CONFIGS/ssh/sshd-clear-denied-sessions $FILESYSTEM_ROOT/usr/bin +sudo chmod +x $FILESYSTEM_ROOT/usr/bin/sshd-clear-denied-sessions +sudo cp src/libwrap/tcp-wrappers-7.6.q/tcpdmatch $FILESYSTEM_ROOT/usr/bin + ## copy platform rc.local sudo cp $IMAGE_CONFIGS/platform/rc.local $FILESYSTEM_ROOT/etc/ diff --git a/files/image_config/ssh/sshd-clear-denied-sessions b/files/image_config/ssh/sshd-clear-denied-sessions new file mode 100755 index 000000000000..76226e7fc699 --- /dev/null +++ b/files/image_config/ssh/sshd-clear-denied-sessions @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +""" +This utility will find the ip addresses of all hosts that have connected to +this device via ssh, then validate they are still in the list of allowed prefixes, +and if not kill the ssh session with a SIGHUP. +""" + +import os +import re +import subprocess + +# Run utmpdump, capture and return its output +def run_utmpdump(_utmpFilename): + devnull = file("/dev/null", "w" ) + p = subprocess.Popen(args=["utmpdump", _utmpFilename], stdout=subprocess.PIPE, stderr=devnull) + (stdout, stderr) = p.communicate() + rc = p.returncode + assert rc is not None # because p.communicate() should wait. + out = (stdout or '') + (stderr or '') + if rc: + e = SystemCommandError("%r: error code %d" % (" ".join(argv), rc)) + e.error = rc + e.output = out + raise e + return stdout + +# Run utmpdump and parse its output into a list of dicts and return that +def get_utmp_data(utmpFileName=None): + """Reads the specified utmp file. + Returns a list of dictionaries, one for each utmp entry. + All dictionary keys and values are strings + Values are right padded with spaces and may contain all + spaces if that utmp field is empty. + Dictionary keys: + "type": See UTMP_TYPE_* above + "pid": Process ID as a string + "tty": TTY (line) name - device name of tty w/o "/dev/" + "tty4": 4 char abbreivated TTY (line) name + "user": User ID + "host": Hostname for remote login, + kernel release for Run Level and Boot Time + "ipAddr": IP Address + "time": Time and date entry was made + See linux docs on utmp and utmpdemp for more info. + Example output from utmpdump: + pid tty4 user tty host ipAddr time + [7] [22953] [/238] [myname ] [pts/238 ] [example.com] [253.122.98.159 ] [Mon Dec 18 21:08:09 2017 PST] + """ + if not utmpFileName: + utmpFileName = os.environ.get( "DEFAULT_UTMP_FILE", "/var/run/utmp" ) + if not os.path.exists(utmpFileName): + return [] + output = run_utmpdump(utmpFileName) + lines = re.split("\n", output) + regExp = re.compile( + r"\[(?P" r"[^\]]*?)\s*\] \[(?P" r"[^\]]*?)\s*\] " \ + r"\[(?P" r"[^\]]*?)\s*\] \[(?P" r"[^\]]*?)\s*\] " \ + r"\[(?P" r"[^\]]*?)\s*\] \[(?P" r"[^\]]*?)\s*\] " \ + r"\[(?P" r"[^\]]*?)\s*\] \[(?P