c626dc921f
* [caclmgrd] Also ignore IP protocol if found in rule; we will only use our predefined protocols
186 lines
5.9 KiB
Python
Executable File
186 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Daemon that listens to updates from ConfigDB about the source IP prefixes from which
|
|
# SSH connections are allowed. In case of change, it will update the SSHD configuration
|
|
# file accordingly. SSHD will notice the file has changed next time a connection comes in.
|
|
# Future enhancement: if an entry it modified/removed, go through all existing ssh
|
|
# connections and recompute their permission, and in case one is now denied, kill it.
|
|
#
|
|
# This daemon is meant to be run on Arista platforms only. Service ACLs on all other
|
|
# platforms will be managed by caclmgrd.
|
|
#
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import syslog
|
|
import time
|
|
from swsssdk import ConfigDBConnector
|
|
|
|
VERSION = "1.0"
|
|
|
|
SYSLOG_IDENTIFIER = "sshd-config-updater"
|
|
|
|
|
|
# ============================== Classes ==============================
|
|
|
|
class ConfigUpdater(object):
|
|
SERVICE = "sshd"
|
|
CONFIG_FILE_PATH = "/etc"
|
|
|
|
ACL_TABLE = "ACL_TABLE"
|
|
ACL_RULE = "ACL_RULE"
|
|
|
|
ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"
|
|
|
|
ACL_SERVICE_SSH = "SSH"
|
|
|
|
def get_src_ip_allow_list(self):
|
|
src_ip_allow_list = []
|
|
|
|
# Get current ACL tables and rules from Config DB
|
|
tables_db_info = self.config_db.get_table(self.ACL_TABLE)
|
|
rules_db_info = self.config_db.get_table(self.ACL_RULE)
|
|
|
|
# Walk the ACL tables
|
|
for (table_name, table_data) in tables_db_info.iteritems():
|
|
# Ignore non-control-plane ACL tables
|
|
if table_data["type"] != self.ACL_TABLE_TYPE_CTRLPLANE:
|
|
continue
|
|
|
|
# Ignore non-SSH service ACLs
|
|
if self.ACL_SERVICE_SSH not in table_data["services"]:
|
|
continue
|
|
|
|
acl_rules = {}
|
|
|
|
for ((rule_table_name, rule_id), rule_props) in rules_db_info.iteritems():
|
|
if rule_table_name == table_name:
|
|
acl_rules[rule_props["PRIORITY"]] = rule_props
|
|
|
|
# For each ACL rule in this table (in descending order of priority)
|
|
for priority in sorted(acl_rules.iterkeys(), reverse=True):
|
|
rule_props = acl_rules[priority]
|
|
|
|
if "PACKET_ACTION" not in rule_props:
|
|
log_error("ACL rule does not contain PACKET_ACTION property")
|
|
continue
|
|
|
|
# We're only interested in ACCEPT rules
|
|
if rule_props["PACKET_ACTION"] != "ACCEPT":
|
|
continue
|
|
|
|
if "SRC_IP" in rule_props and rule_props["SRC_IP"]:
|
|
src_ip_allow_list.append(rule_props["SRC_IP"])
|
|
|
|
return src_ip_allow_list
|
|
|
|
# To update the configuration file
|
|
#
|
|
# Example config file for reference:
|
|
# root@sonic:/# cat /etc/snmp/snmpd.conf
|
|
# bash# cat /etc/sshd.allow
|
|
# sshd: [fd7a:629f:52a4:b0c3:ec4:7aff:fe99:201e]/128
|
|
# sshd: 172.17.0.1/32
|
|
# sshd: 172.18.1.0/24
|
|
# Note that any matches are 'permits', and the default action is 'denied'
|
|
# We assume the database contains valid ip addresses/hostnames.
|
|
#
|
|
# src_ip_allow_list may contain individual IP addresses or blocks of
|
|
# IP addresses using CIDR notation.
|
|
def write_configuration_file(self, src_ip_allow_list):
|
|
filename = "%s/%s.allow" % (self.CONFIG_FILE_PATH, self.SERVICE)
|
|
|
|
if len(src_ip_allow_list) == 0:
|
|
if os.path.exists(filename):
|
|
os.remove(filename)
|
|
return
|
|
|
|
filename_tmp = filename + ".tmp"
|
|
|
|
f = open(filename_tmp, "w")
|
|
|
|
for value in src_ip_allow_list:
|
|
f.write("%s: %s\n" % (self.SERVICE, value))
|
|
f.close()
|
|
|
|
os.rename(filename_tmp, filename)
|
|
|
|
# some previously accepted sessions might no longer be allowed: clear them
|
|
os.system("/usr/bin/sshd-clear-denied-sessions")
|
|
|
|
def notification_handler(self, key, data):
|
|
log_info("ACL configuration changed. Updating {} config accordingly...".format(self.SERVICE))
|
|
self.write_configuration_file(self.get_src_ip_allow_list())
|
|
|
|
def run(self):
|
|
# Open a handle to the Config database
|
|
self.config_db = ConfigDBConnector()
|
|
self.config_db.connect()
|
|
|
|
# Write initial configuration
|
|
self.write_configuration_file(self.get_src_ip_allow_list())
|
|
|
|
# Subscribe to notifications when ACL tables or rules change
|
|
self.config_db.subscribe(self.ACL_TABLE,
|
|
lambda table, key, data: self.notification_handler(key, data))
|
|
self.config_db.subscribe(self.ACL_RULE,
|
|
lambda table, key, data: self.notification_handler(key, data))
|
|
|
|
# Indefinitely listen for Config DB notifications
|
|
self.config_db.listen()
|
|
|
|
|
|
# ========================== Syslog wrappers ==========================
|
|
|
|
def log_info(msg):
|
|
syslog.openlog(SYSLOG_IDENTIFIER)
|
|
syslog.syslog(syslog.LOG_INFO, msg)
|
|
syslog.closelog()
|
|
|
|
|
|
def log_warning(msg):
|
|
syslog.openlog(SYSLOG_IDENTIFIER)
|
|
syslog.syslog(syslog.LOG_WARNING, msg)
|
|
syslog.closelog()
|
|
|
|
|
|
def log_error(msg):
|
|
syslog.openlog(SYSLOG_IDENTIFIER)
|
|
syslog.syslog(syslog.LOG_ERR, msg)
|
|
syslog.closelog()
|
|
|
|
|
|
# Determine whether we are running on an Arista platform
|
|
def is_platform_arista():
|
|
proc = subprocess.Popen(["sonic-cfggen", "-H", "-v", "DEVICE_METADATA.localhost.platform"],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
(stdout, stderr) = proc.communicate()
|
|
|
|
if proc.returncode != 0:
|
|
log_error("Failed to retrieve platform string")
|
|
return false
|
|
|
|
return "arista" in stdout
|
|
|
|
|
|
def main():
|
|
log_info("Starting up...")
|
|
|
|
if not os.geteuid() == 0:
|
|
log_error("Must be root to run this daemon")
|
|
print "Error: Must be root to run this daemon"
|
|
sys.exit(1)
|
|
|
|
if not is_platform_arista():
|
|
log_info("Platform is not an Arista platform. Exiting...")
|
|
sys.exit(0)
|
|
|
|
# Instantiate a ConfigUpdater object
|
|
config_updater = ConfigUpdater()
|
|
config_updater.run()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|