symbiosis-firewall-whitelist 7.03 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'
Steve Kemp's avatar
Steve Kemp committed
64
65


66
67
68
#
#  The options set by the command line.
#
69
70
71
72
73
help         = false
manual       = false
$VERBOSE     = false
base_dir     = "/etc/symbiosis/firewall/"
wtmp_file    = "/var/log/wtmp"
74
delete       = true
75
execute      = true
76
template_dir = nil
77
force        = false
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
92

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
begin
  opts.each do |opt,arg|
    case opt
    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
108
  when '--force'
109
110
111
112
113
114
115
116
117
118
      force = true
    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
    end
Steve Kemp's avatar
Steve Kemp committed
119
  end
120
121
122
rescue
  # any errors, show the help
  help = true
Steve Kemp's avatar
Steve Kemp committed
123
124
end

125

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

  found_synopsis = false
Steve Kemp's avatar
Steve Kemp committed
134
135

  lines.each do |line|
136

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

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

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

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

Steve Kemp's avatar
Steve Kemp committed
149
150
151
152
153
  end

  exit 0
end

154
155
156
157
158
#
# These requires are here to prevent un-needed dependencies when just making
# manpages.
#
require 'symbiosis/utmp'
159
require 'symbiosis/utils'
160
161
162
163
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
require 'symbiosis/ipaddr'

164
#
165
166
# Exit if we've been disabled
#
167
if File.exists?(File.join(base_dir, "disabled.whitelist"))
168
169
170
  puts "Firewall whitelist disabled.  Exiting." if $VERBOSE
  exit 0
end
171

172
173
174
#
# Basics.
#
175
expired = 0
176
whitelist_d = File.join(base_dir, "whitelist.d")
177

178
#
179
# Work out which user we're supposed to create the whitelist directory as.
180
#
181
182
183
184
185
186
187
begin
  srv = File.stat("/srv")
  admin_uid = srv.uid
  admin_gid = srv.gid
rescue Errno::ENOENT
  admin_gid = admin_uid = 0
end
188

189
190
# 
# ensure the directory exists.
191
#
192
193
unless File.directory?( whitelist_d )
  Symbiosis::Utils.mkdir_p(whitelist_d, :user => admin_uid, :group => admin_gid)
194
end
195

Steve Kemp's avatar
Steve Kemp committed
196
197
198
199
200
#
#  Did we update?
#
updated=false

201
202
203
204
205
#
# Expiry is measured in days.
#
expire_before = Time.now - ( expire_after * ( 24 * 60 * 60 ) )

Steve Kemp's avatar
Steve Kemp committed
206
207
#
#
208
209
210
211
212
# Fetch the IP addresses
#
Symbiosis::Utmp.read(wtmp_file).each do |entry|

  begin
213
    ip = Symbiosis::IPAddr.new(entry['ip'].to_s)
214
215
216
217
218
219
220
221
  rescue ArgumentError
    #
    # Oops.  Can't interpret the IP.
    #
    next
  end
  at = entry['time']

Patrick J Cherry's avatar
Patrick J Cherry committed
222
  #
223
  # Make sure the record isn't already expired.
Patrick J Cherry's avatar
Patrick J Cherry committed
224
  #
225
  next unless at > expire_before
Patrick J Cherry's avatar
Patrick J Cherry committed
226

227
228
229
230
231
232
233
234
235
  #
  # Mask IPv6 to /64s.
  #
  ip = ip.mask(64) if ip.ipv6?

  #
  # Mask IPv4 to /32s.
  #
  ip = ip.mask(32) if ip.ipv4?
236

237
238
  #
  # Only include globally routable IPs.
Patrick J Cherry's avatar
Patrick J Cherry committed
239
240
241
  #
  # FIXME: Need better IPv6 conditions.
  #
242
243
  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)
Steve Kemp's avatar
Steve Kemp committed
244

245
  puts "Found IP address: #{ip}" if ( $VERBOSE )
246

247
248
  setting = ip.to_s.gsub("/","|")

249
250
251
  #
  # Check filename without .auto first.
  #
252
  if Symbiosis::Utils.get_param(setting, whitelist_d) == false
253
254
255
    #
    # Automatically whitelist.
    #
256
257
258
259
260
261
262
263
264
265
    setting += ".auto"
    value = Symbiosis::Utils.get_param(setting, whitelist_d)

    if false == value
      puts "\tAdding whitelist entry" if  $VERBOSE
      value = "all"

    else
      puts "\tUpdating whitelist entry" if  $VERBOSE

Steve Kemp's avatar
Steve Kemp committed
266
    end
267
268
269
270
271
272
273
274
    #
    # Yes, we're updating.
    #
    updated = true

    Symbiosis::Utils.set_param(setting, value, whitelist_d)
  else
    puts "\tAlready manually whitelisted" if ( $VERBOSE )
Steve Kemp's avatar
Steve Kemp committed
275

276
  end
Steve Kemp's avatar
Steve Kemp committed
277

278
end
Steve Kemp's avatar
Steve Kemp committed
279

280

281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#
# Now expire old entries
#
puts "Expiring old whitelist entries" if ( $VERBOSE )

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

  if  File.mtime(entry) < expire_before

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

  end

end

puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSE )
299

Steve Kemp's avatar
Steve Kemp committed
300
#
301
# Re-generate the whitelist chain
Steve Kemp's avatar
Steve Kemp committed
302
#
303
if ( updated || expired > 0 || force )
304
  cmd = %w(/usr/sbin/symbiosis-firewall)
305
306
307
308
309
310
  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"
311
312
  puts "Executing #{cmd.join(" ")}" if $VERBOSE
  exec(*cmd)
Steve Kemp's avatar
Steve Kemp committed
313
end