symbiosis-firewall-whitelist 6.7 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/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
help         = false
manual       = false
$VERBOSE     = false
base_dir     = "/etc/symbiosis/firewall/"
wtmp_file    = "/var/log/wtmp"
78
delete       = true
79
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
99
100
101
102
103
104
105
106
107
108
109
110
111
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
112
  when '--force'
113
114
115
116
117
118
119
120
121
122
      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
123
  end
124
125
126
rescue
  # any errors, show the help
  help = true
Steve Kemp's avatar
Steve Kemp committed
127
128
end

129

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

  found_synopsis = false
Steve Kemp's avatar
Steve Kemp committed
138
139

  lines.each do |line|
140

Steve Kemp's avatar
Steve Kemp committed
141
142
    line.chomp!
    break if line.empty?
143
144
145
146
147
148

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

Steve Kemp's avatar
Steve Kemp committed
149
    puts line[2..-1].to_s
150
151
152

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

Steve Kemp's avatar
Steve Kemp committed
153
154
155
156
157
  end

  exit 0
end

158
#
159
160
# Exit if we've been disabled
#
161
if File.exists?(File.join(base_dir, "disabled.whitelist"))
162
163
164
  puts "Firewall whitelist disabled.  Exiting." if $VERBOSE
  exit 0
end
165

166
167
168
#
# Basics.
#
169
expired = 0
170
whitelist_d = File.join(base_dir, "whitelist.d")
171

172
# ensure the directory exists.
173
174
unless File.directory?(whitelist_d)
  FileUtils.mkdir_p(whitelist_d)
175
176
end

177
178
179
180
#
# Expiry is measured in days.
#
expire_before = Time.now - ( expire_after * ( 24 * 60 * 60 ) )
181

182
183
184
185
186
#
#  Expire old entries first of all, then add new ones.
#
puts "Expiring old whitelist entries" if ( $VERBOSE )

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

189
  if  File.mtime(entry) < expire_before
190

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

195
196
  end

197
end
198

199
puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSE )
Steve Kemp's avatar
Steve Kemp committed
200
201
202
203
204
205
206
207

#
#  Did we update?
#
updated=false

#
#
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
249
  #
  # Check filename without .auto first.
  #
250
  fn = File.join(whitelist_d,ip.to_s.gsub("/","|"))
Steve Kemp's avatar
Steve Kemp committed
251

252
253
  if ( File.exists?(fn) )
    puts "\tAlready manually whitelisted" if ( $VERBOSE )
254

255
256
257
258
259
  else
    #
    # Automatically whitelist.
    #
    fn += ".auto"
Steve Kemp's avatar
Steve Kemp committed
260

261
    if ! File.exists?(fn)
262
      updated=true
Steve Kemp's avatar
Steve Kemp committed
263
      puts "\tAdding to whitelist" if ( $VERBOSE )
264
265
266
267
268
269
270
271
272
273
274
275

      #
      # 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
276
277
    end

278
  end
Steve Kemp's avatar
Steve Kemp committed
279

280
end
Steve Kemp's avatar
Steve Kemp committed
281

282
283


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