symbiosis-firewall-whitelist 6.53 KB
Newer Older
Patrick J Cherry's avatar
Patrick J Cherry committed
1
#! /usr/bin/ruby1.8
2
#
Steve Kemp's avatar
Steve Kemp committed
3
# NAME
4
#   symbiosis-firewall-whitelist -- Automatically whitelist IP addresses.
Steve Kemp's avatar
Steve Kemp committed
5
6
#
# SYNOPSIS
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#  symbiosis-firewall-whitelist [ -h | --help ] [-m | --manual]
#       [ -v | --verbose ] [ -x | --no-exec] [ -d | --no-delete ]
#       [ -e | --expire-after <n> ] [ -w | --wtmp-file <file> ]
#       [ -p | --prefix <dir> ] [ -t | --template-d <dir> ] 
#
# 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 <n>  Number of days after which whitelisted IPs should be
#                          expired. Defaults to 8.
#
#  -w, --wtmp-file <file>  wtmp(5) file to read to find IPs to whitelist.
#                          Defaults to /var/log/wtmp.
Steve Kemp's avatar
Steve Kemp committed
28
#
29
#
30
31
#  -p, --prefix <dir>      Directory where action.d, incoming.d, outgoing.d etc.
#                          are located. Defaults to /etc/symbiosis/firewall.
32
#
33
#  -t, --template-d <dir>  Additional directory to search for templates.
Steve Kemp's avatar
Steve Kemp committed
34
#
35
# USAGE
Steve Kemp's avatar
Steve Kemp committed
36
37
38
39
#
# This script is designed to automatically whitelist IP addresses which
# have been used to successfully login via SSH.
#
40
41
42
43
44
# It does this by opening the wtmp file, and looking for IP addresses. Once it
# has found some, it records them in /etc/symbiosis/firewall/whitelist.d/.
# Each addition is one of the two forms:
#
#   1.2.3.4.auto                The IPv4 address 1.2.3.4
45
#   2001:123:456:789::|64.auto  The IPv6 range 2001:123:456:789::/64
46
47
48
49
50
#
# Once that directory has been written, symbiosis-firewall(1) is called with
# the reload-whitelist action.
#
# Most of the flags above are passed straight on to symbiosis-firewall(1).
Steve Kemp's avatar
Steve Kemp committed
51
52
53
#
# AUTHOR
#
54
#  Steve Kemp <steve@bytemark.co.uk>
Steve Kemp's avatar
Steve Kemp committed
55
56
57
58
59
60
61
#

#
#  Modules we require
#

require 'getoptlong'
62
require 'tempfile'
63
require 'fileutils'
Patrick J Cherry's avatar
Patrick J Cherry committed
64
require 'symbiosis/utmp'
65
66
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
67
require 'symbiosis/firewall/ipaddr'
Steve Kemp's avatar
Steve Kemp committed
68
69


70
71
72
#
#  The options set by the command line.
#
73
74
75
76
77
78
79
help         = false
manual       = false
$VERBOSE     = false
base_dir     = "/etc/symbiosis/firewall/"
wtmp_file    = "/var/log/wtmp"
delete       = false
execute      = false
80
template_dir = nil
81
force        = false
82
83
84
85
86
87
88
89
90
91
92
93
94
95
expire_after = 8

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 ],
         [ '--template-d', '-t', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--wtmp-file',  '-w', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--expire-after', '-e', GetoptLong::REQUIRED_ARGUMENT ]
       )
Steve Kemp's avatar
Steve Kemp committed
96
97
98

opts.each do |opt,arg|
  case opt
99
100
101
102
103
104
105
106
107
108
109
110
111
  when '--help'
    help = true
  when '--manual'
    manual = true
  when '--verbose'
    $VERBOSE = true
  when '--test'
    test = true
  when '--no-execute'
    execute = false
  when '--no-delete'
    delete = false
  when '--force'
112
    force = true
113
114
115
116
117
118
119
120
  when '--prefix'
    base_dir     = File.expand_path(arg)
  when '--template-d'
    template_dir = File.expand_path(arg)
  when '--expire-after'
    expire_after = arg.to_i
  when '--wtmp-file'
    wtmp_file = arg
Steve Kemp's avatar
Steve Kemp committed
121
122
123
124
125
126
  end
end

#
# CAUTION! Here be quality kode.
#
127
if manual or help
Steve Kemp's avatar
Steve Kemp committed
128
  # Open the file, stripping the shebang line
129
130
131
  lines = File.open(__FILE__){|fh| fh.readlines}[1..-1]

  found_synopsis = false
Steve Kemp's avatar
Steve Kemp committed
132
133

  lines.each do |line|
134

Steve Kemp's avatar
Steve Kemp committed
135
136
    line.chomp!
    break if line.empty?
137
138
139
140
141
142

    if help and !found_synopsis
      found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/)
      next
    end

Steve Kemp's avatar
Steve Kemp committed
143
    puts line[2..-1].to_s
144
145
146

    break if help and found_synopsis and line =~ /^#\s*$/

Steve Kemp's avatar
Steve Kemp committed
147
148
149
150
151
  end

  exit 0
end

152
#
153
154
# Exit if we've been disabled
#
155
if File.exists?(File.join(base_dir, "disabled.whitelist"))
156
157
158
  puts "Firewall whitelist disabled.  Exiting." if $VERBOSE
  exit 0
end
159

160
161
162
#
# Basics.
#
163
expired = 0
164
whitelist_d = File.join(base_dir, "whitelist.d")
165

166
# ensure the directory exists.
167
168
unless File.directory?(whitelist_d)
  FileUtils.mkdir_p(whitelist_d)
169
170
end

171
172
expire_before = Time.now - ( expire_after * 24 * 60 * 60 )

173
174
175
176
177
#
#  Expire old entries first of all, then add new ones.
#
puts "Expiring old whitelist entries" if ( $VERBOSE )

178
Dir.glob( File.join(whitelist_d,"*.auto" ) ).each do |entry|
179

180
  if  File.mtime(entry) < expire_before
181

182
183
184
    puts "Removing #{entry}" if ( $VERBOSE )
    File.unlink(entry)
    expired += 1
185

186
187
  end

188
end
189

190
puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSE )
Steve Kemp's avatar
Steve Kemp committed
191
192
193
194
195
196
197
198

#
#  Did we update?
#
updated=false

#
#
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# Fetch the IP addresses
#
Symbiosis::Utmp.read(wtmp_file).each do |entry|

  begin
    ip = Symbiosis::Firewall::IPAddr.new(entry['ip'].to_s)
  rescue ArgumentError
    #
    # Oops.  Can't interpret the IP.
    #
    next
  end
  at = entry['time']

Patrick J Cherry's avatar
Patrick J Cherry committed
213
  #
214
  # Make sure the record isn't already expired.
Patrick J Cherry's avatar
Patrick J Cherry committed
215
  #
216
  next unless at > expire_before
Patrick J Cherry's avatar
Patrick J Cherry committed
217

218
219
220
221
222
223
224
225
226
227
228
229
  #
  # Mask IPv6 to /64s.
  #
  ip = ip.mask(64) if ip.ipv6?

  #
  # Mask IPv4 to /32s.
  #
  ip = ip.mask(32) if ip.ipv4?
  
  #
  # Only include globally routable IPs.
Patrick J Cherry's avatar
Patrick J Cherry committed
230
231
232
  #
  # FIXME: Need better IPv6 conditions.
  #
233
234
  next if ip.ipv4? and (IPAddr.new("127.0.0.1/8").include?(ip) or IPAddr.new("0.0.0.0") == ip )
  next if ip.ipv6? and !IPAddr.new("2000::/3").include?(ip)
Steve Kemp's avatar
Steve Kemp committed
235

236
  puts "Found IP address: #{ip}" if ( $VERBOSE )
237

238
239
240
  #
  # Check filename without .auto first.
  #
241
  fn = File.join(whitelist_d,ip.to_s.gsub("/","|"))
Steve Kemp's avatar
Steve Kemp committed
242

243
244
  if ( File.exists?(fn) )
    puts "\tAlready manually whitelisted" if ( $VERBOSE )
245

246
247
248
249
250
  else
    #
    # Automatically whitelist.
    #
    fn += ".auto"
Steve Kemp's avatar
Steve Kemp committed
251

252
253
    if ! File.exists?(fn)
      updated=true 
Steve Kemp's avatar
Steve Kemp committed
254
      puts "\tAdding to whitelist" if ( $VERBOSE )
255
256
257
258
259
260
261
262
263
264
265
266

      #
      # Create a new file.
      #
      FileUtils.touch(fn, :mtime => at)

    elsif File.mtime(fn) < at
      #
      # Update the mtime, if this entry is newer.
      #
      puts "\tUpdating whitelist entry" if ( $VERBOSE )
      FileUtils.touch(fn, :mtime => at)
Steve Kemp's avatar
Steve Kemp committed
267
268
    end

269
  end
Steve Kemp's avatar
Steve Kemp committed
270

271
end
Steve Kemp's avatar
Steve Kemp committed
272
273

#
274
# Re-generate the whitelist chain
Steve Kemp's avatar
Steve Kemp committed
275
#
276
if ( updated || expired > 0 || force )
277
278
279
280
281
282
283
  cmd = %w(symbiosis-firewall)
  cmd << "--verbose" if $VERBOSE
  cmd << "--no-execute" unless execute
  cmd << "--no-delete"  unless delete
  cmd += ["--prefix", base_dir]
  cmd += ["--template-d", template_dir] unless template_dir.nil?
  cmd << "reload-whitelist"
284
285
  puts "Executing #{cmd.join(" ")}" if $VERBOSE
  exec(*cmd)
Steve Kemp's avatar
Steve Kemp committed
286
end