symbiosis-firewall-whitelist 6.57 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
173
174
#
# Expiry is measured in days.
#
expire_before = Time.now - ( expire_after * ( 24 * 60 * 60 ) )
175

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

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

183
  if  File.mtime(entry) < expire_before
184

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

189
190
  end

191
end
192

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

#
#  Did we update?
#
updated=false

#
#
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 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
216
  #
217
  # Make sure the record isn't already expired.
Patrick J Cherry's avatar
Patrick J Cherry committed
218
  #
219
  next unless at > expire_before
Patrick J Cherry's avatar
Patrick J Cherry committed
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?
230

231
232
  #
  # Only include globally routable IPs.
Patrick J Cherry's avatar
Patrick J Cherry committed
233
234
235
  #
  # FIXME: Need better IPv6 conditions.
  #
236
237
  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
238

239
  puts "Found IP address: #{ip}" if ( $VERBOSE )
240

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

246
247
  if ( File.exists?(fn) )
    puts "\tAlready manually whitelisted" if ( $VERBOSE )
248

249
250
251
252
253
  else
    #
    # Automatically whitelist.
    #
    fn += ".auto"
Steve Kemp's avatar
Steve Kemp committed
254

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

      #
      # 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
270
271
    end

272
  end
Steve Kemp's avatar
Steve Kemp committed
273

274
end
Steve Kemp's avatar
Steve Kemp committed
275

276
277


Steve Kemp's avatar
Steve Kemp committed
278
#
279
# Re-generate the whitelist chain
Steve Kemp's avatar
Steve Kemp committed
280
#
281
if ( updated || expired > 0 || force )
282
  cmd = %w(/usr/sbin/symbiosis-firewall)
283
284
285
286
287
288
  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"
289
290
  puts "Executing #{cmd.join(" ")}" if $VERBOSE
  exec(*cmd)
Steve Kemp's avatar
Steve Kemp committed
291
end