#! /usr/bin/ruby # # NAME # sympl-firewall-whitelist - Automatically whitelist IP addresses. # # SYNOPSIS # sympl-firewall-whitelist [ -h | --help ] [-m | --manual] # [ -v | --verbose ] [ -x | --no-exec] [ -d | --no-delete ] # [ -e | --expire-after ] [ -w | --wtmp-file ] # [ -p | --prefix ] # # OPTIONS # -h, --help Show a help message, and exit. # # -m, --manual Show this manual, and exit. # # -v, --verbose Show verbose errors. # # -x, --no-exec Do not execute the generated firewall rules. # # -d, --no-delete Do not delete the generated script. # # -e, --expire-after Number of days after which whitelisted IPs should be # expired. Defaults to 7. # # -w, --wtmp-file wtmp(5) file to read to find IPs to whitelist. # Defaults to /var/log/wtmp. # # # -p, --prefix Directory where action.d, incoming.d, outgoing.d etc. # are located. Defaults to /etc/sympl/firewall. # # USAGE # # This script is designed to automatically whitelist IP addresses for SSH which # have been used to successfully log in already. # # It does this by opening the wtmp file, and looking for IP addresses. Once it # has found some, it records them in /etc/sympl/firewall/whitelist.d/. # Each addition is one of the two forms: # # 1.2.3.4.auto The IPv4 address 1.2.3.4 # 2001:123:456:789::1.auto The IPv6 address 2001:123:456:789::1 # # Once that directory has been written, sympl-firewall(1) is called with # the reload-whitelist action. # # Most of the flags above are passed straight on to sympl-firewall(1). # # AUTHOR # # Steve Kemp # # # Modules we require # require 'getoptlong' require 'tempfile' require 'fileutils' require 'syslog' # # The options set by the command line. # help = false manual = false $VERBOSELOCAL = false base_dir = "/etc/sympl/firewall/" wtmp_file = "/var/log/wtmp" delete = true execute = true force = false expire_after = 7 opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--manual', '-m', GetoptLong::NO_ARGUMENT ], [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], [ '--no-execute', '-x', GetoptLong::NO_ARGUMENT ], [ '--no-delete', '-d', GetoptLong::NO_ARGUMENT ], [ '--force', '-f', GetoptLong::NO_ARGUMENT ], [ '--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT ], [ '--wtmp-file', '-w', GetoptLong::REQUIRED_ARGUMENT ], [ '--expire-after', '-e', GetoptLong::REQUIRED_ARGUMENT ] ) begin opts.each do |opt,arg| case opt when '--help' help = true when '--manual' manual = true when '--verbose' $VERBOSELOCAL = true when '--test' test = true when '--no-execute' execute = false when '--no-delete' delete = false when '--force' force = true when '--prefix' base_dir = File.expand_path(arg) when '--expire-after' expire_after = arg.to_i when '--wtmp-file' wtmp_file = arg end end rescue # any errors, show the help help = true end # # CAUTION! Here be quality kode. # if manual or help # Open the file, stripping the shebang line lines = File.open(__FILE__){|fh| fh.readlines}[1..-1] found_synopsis = false lines.each do |line| line.chomp! break if line.empty? if help and !found_synopsis found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/) next end puts line[2..-1].to_s break if help and found_synopsis and line =~ /^#\s*$/ end exit 0 end # # These requires are here to prevent un-needed dependencies when just making # manpages. # require 'symbiosis/utmp' require 'symbiosis/utils' require 'symbiosis/firewall/directory' require 'symbiosis/firewall/template' require 'symbiosis/ipaddr' # # Exit if we've been disabled # if %w(disabled.whitelist whitelist.d/disabled).any?{|fn| File.exist?(File.join(base_dir, fn))} puts "Firewall whitelist disabled. Exiting." if $VERBOSELOCAL exit 0 end # # Basics. # expired = 0 whitelist_d = File.join(base_dir, "whitelist.d") syslog = Syslog.open( File.basename($0), Syslog::LOG_NDELAY, Syslog::LOG_USER) # # Work out which user we're supposed to create the whitelist directory as. # begin srv = File.stat("/srv") admin_uid = srv.uid admin_gid = srv.gid rescue Errno::ENOENT admin_gid = admin_uid = 1000 end # # ensure the directory exists. # unless File.directory?( whitelist_d ) Symbiosis::Utils.mkdir_p(whitelist_d, :user => admin_uid, :group => admin_gid) end # # Did we update? # updated=false # # Time we started this run # time_now = Time.now # # Expiry is measured in days. # expire_before = time_now - ( expire_after * ( 24 * 60 * 60 ) ) # # Check to see when we were last run. # stamp_file = '/var/lib/sympl/sympl-firewall-whitelist.stamp' if File.exist?(stamp_file) last_run = File.stat(stamp_file).mtime else last_run = nil end FileUtils.touch(stamp_file) # # # Fetch the IP addresses # Symbiosis::Utmp.read(wtmp_file).each do |entry| # # Only interested in USER_PROCESS types. # next unless entry['type'] == 7 # # Fetch the time the entry was logged at. # at = entry['time'] # # Make sure the entry isn't in the future # next unless at < time_now # # Make sure the record isn't already expired. # next unless at > expire_before # # Fetch the IP # begin ip = Symbiosis::IPAddr.new(entry['ip'].to_s) rescue ArgumentError # # Oops. Can't interpret the IP. # next end # # Mask IPv6 to /128s. # ip = ip.mask(128) if ip.ipv6? # # Mask IPv4 to /32s. # ip = ip.mask(32) if ip.ipv4? # # Only include globally routable IPs. # # FIXME: Need better IPv6 conditions. # next if ip.ipv4? and (Symbiosis::IPAddr.new("127.0.0.1/8").include?(ip) or Symbiosis::IPAddr.new("0.0.0.0") == ip ) next if ip.ipv6? and !Symbiosis::IPAddr.new("2000::/3").include?(ip) puts "Found IP address: #{ip}" if ( $VERBOSELOCAL ) setting = ip.to_s.gsub("/","|") # # Check filename without .auto first. # if !Symbiosis::Utils.get_param(setting, whitelist_d) # # Automatically whitelist. # setting += ".auto" value = !!Symbiosis::Utils.get_param(setting, whitelist_d) if false == value puts "\tAdding whitelist entry" if $VERBOSELOCAL syslog.info("adding #{ip} to whitelist") value = "22" elsif last_run.nil? or at > last_run puts "\tUpdating whitelist entry" if $VERBOSELOCAL syslog.info("updating #{ip} in whitelist") else next end # # Yes, we're updating. # updated = true Symbiosis::Utils.set_param(setting, value, whitelist_d) else puts "\tAlready manually whitelisted" if ( $VERBOSELOCAL ) end end # # Now expire old entries # puts "Expiring old whitelist entries" if ( $VERBOSELOCAL ) Dir.glob( File.join(whitelist_d,"*.auto" ) ).each do |entry| if File.mtime(entry) < expire_before puts "Removing #{entry}" if ( $VERBOSELOCAL ) syslog.info("expiring #{File.basename(entry,".auto")} from whitelist") File.unlink(entry) expired += 1 end end puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSELOCAL ) # # Updating the firewall is now done by the inotify cronjob. #