symbiosis-firewall-whitelist 6.79 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      = false
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
159
160
161
162
#
# These requires are here to prevent un-needed dependencies when just making
# manpages.
#
require 'symbiosis/utmp'
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
require 'symbiosis/ipaddr'

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

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

177
# ensure the directory exists.
178
179
unless File.directory?(whitelist_d)
  FileUtils.mkdir_p(whitelist_d)
180
181
end

182
183
184
185
#
# Expiry is measured in days.
#
expire_before = Time.now - ( expire_after * ( 24 * 60 * 60 ) )
186

187
188
189
190
191
#
#  Expire old entries first of all, then add new ones.
#
puts "Expiring old whitelist entries" if ( $VERBOSE )

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

194
  if  File.mtime(entry) < expire_before
195

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

200
201
  end

202
end
203

204
puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSE )
Steve Kemp's avatar
Steve Kemp committed
205
206
207
208
209
210
211
212

#
#  Did we update?
#
updated=false

#
#
213
214
215
216
217
# Fetch the IP addresses
#
Symbiosis::Utmp.read(wtmp_file).each do |entry|

  begin
218
    ip = Symbiosis::IPAddr.new(entry['ip'].to_s)
219
220
221
222
223
224
225
226
  rescue ArgumentError
    #
    # Oops.  Can't interpret the IP.
    #
    next
  end
  at = entry['time']

Patrick J Cherry's avatar
Patrick J Cherry committed
227
  #
228
  # Make sure the record isn't already expired.
Patrick J Cherry's avatar
Patrick J Cherry committed
229
  #
230
  next unless at > expire_before
Patrick J Cherry's avatar
Patrick J Cherry committed
231

232
233
234
235
236
237
238
239
240
  #
  # Mask IPv6 to /64s.
  #
  ip = ip.mask(64) if ip.ipv6?

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

242
243
  #
  # Only include globally routable IPs.
Patrick J Cherry's avatar
Patrick J Cherry committed
244
245
246
  #
  # FIXME: Need better IPv6 conditions.
  #
247
248
  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
249

250
  puts "Found IP address: #{ip}" if ( $VERBOSE )
251

252
253
254
  #
  # Check filename without .auto first.
  #
255
  fn = File.join(whitelist_d,ip.to_s.gsub("/","|"))
Steve Kemp's avatar
Steve Kemp committed
256

257
258
  if ( File.exists?(fn) )
    puts "\tAlready manually whitelisted" if ( $VERBOSE )
259

260
261
262
263
264
  else
    #
    # Automatically whitelist.
    #
    fn += ".auto"
Steve Kemp's avatar
Steve Kemp committed
265

266
    if ! File.exists?(fn)
267
      updated=true
Steve Kemp's avatar
Steve Kemp committed
268
      puts "\tAdding to whitelist" if ( $VERBOSE )
269
270
271
272
273
274
275
276
277
278
279
280

      #
      # 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
281
282
    end

283
  end
Steve Kemp's avatar
Steve Kemp committed
284

285
end
Steve Kemp's avatar
Steve Kemp committed
286

287
288


Steve Kemp's avatar
Steve Kemp committed
289
#
290
# Re-generate the whitelist chain
Steve Kemp's avatar
Steve Kemp committed
291
#
292
if ( updated || expired > 0 || force )
293
  cmd = %w(/usr/sbin/symbiosis-firewall)
294
295
296
297
298
299
  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"
300
301
  puts "Executing #{cmd.join(" ")}" if $VERBOSE
  exec(*cmd)
Steve Kemp's avatar
Steve Kemp committed
302
end