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

#
#  Modules we require
#

require 'getoptlong'
60
require 'tempfile'
61
require 'fileutils'
62
require 'syslog'
Steve Kemp's avatar
Steve Kemp committed
63

64
65
66
#
#  The options set by the command line.
#
67
68
help         = false
manual       = false
69
$VERBOSELOCAL     = false
70
base_dir     = "/etc/sympl/firewall/"
71
wtmp_file    = "/var/log/wtmp"
72
delete       = true
73
execute      = true
74
force        = false
75
expire_after = 7
76
77
78
79
80
81
82
83
84
85
86
87

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 ],
         [ '--wtmp-file',  '-w', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--expire-after', '-e', GetoptLong::REQUIRED_ARGUMENT ]
       )
Steve Kemp's avatar
Steve Kemp committed
88

89
90
91
92
93
94
95
96
begin
  opts.each do |opt,arg|
    case opt
    when '--help'
      help = true
    when '--manual'
      manual = true
    when '--verbose'
97
      $VERBOSELOCAL = true
98
99
100
101
102
103
    when '--test'
      test = true
    when '--no-execute'
      execute = false
    when '--no-delete'
      delete = false
104
  when '--force'
105
106
107
108
109
110
111
112
      force = true
    when '--prefix'
      base_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
113
  end
114
115
116
rescue
  # any errors, show the help
  help = true
Steve Kemp's avatar
Steve Kemp committed
117
118
end

119

Steve Kemp's avatar
Steve Kemp committed
120
121
122
#
# CAUTION! Here be quality kode.
#
123
if manual or help
Steve Kemp's avatar
Steve Kemp committed
124
  # Open the file, stripping the shebang line
125
126
127
  lines = File.open(__FILE__){|fh| fh.readlines}[1..-1]

  found_synopsis = false
Steve Kemp's avatar
Steve Kemp committed
128
129

  lines.each do |line|
130

Steve Kemp's avatar
Steve Kemp committed
131
132
    line.chomp!
    break if line.empty?
133
134
135
136
137
138

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

Steve Kemp's avatar
Steve Kemp committed
139
    puts line[2..-1].to_s
140
141
142

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

Steve Kemp's avatar
Steve Kemp committed
143
144
145
146
147
  end

  exit 0
end

148
149
150
151
152
#
# These requires are here to prevent un-needed dependencies when just making
# manpages.
#
require 'symbiosis/utmp'
153
require 'symbiosis/utils'
154
155
156
157
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
require 'symbiosis/ipaddr'

158
#
159
160
# Exit if we've been disabled
#
161
if %w(disabled.whitelist whitelist.d/disabled).any?{|fn| File.exist?(File.join(base_dir, fn))}
162
  puts "Firewall whitelist disabled.  Exiting." if $VERBOSELOCAL
163
164
  exit 0
end
165

166
167
168
#
# Basics.
#
169
expired = 0
170
whitelist_d = File.join(base_dir, "whitelist.d")
171
syslog = Syslog.open( File.basename($0), Syslog::LOG_NDELAY, Syslog::LOG_USER)
172

173
#
174
# Work out which user we're supposed to create the whitelist directory as.
175
#
176
177
178
179
180
begin
  srv = File.stat("/srv")
  admin_uid = srv.uid
  admin_gid = srv.gid
rescue Errno::ENOENT
181
  admin_gid = admin_uid = 1000
182
end
183

184
185
# 
# ensure the directory exists.
186
#
187
188
unless File.directory?( whitelist_d )
  Symbiosis::Utils.mkdir_p(whitelist_d, :user => admin_uid, :group => admin_gid)
189
end
190

Steve Kemp's avatar
Steve Kemp committed
191
192
193
194
195
#
#  Did we update?
#
updated=false

196
197
198
199
200
#
# Time we started this run
#
time_now = Time.now

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

206
207
208
#
# Check to see when we were last run.
#
209
stamp_file = '/var/lib/sympl/sympl-firewall-whitelist.stamp'
210

211
if File.exist?(stamp_file)
212
213
214
215
216
217
218
  last_run = File.stat(stamp_file).mtime
else
  last_run = nil
end

FileUtils.touch(stamp_file)

Steve Kemp's avatar
Steve Kemp committed
219
220
#
#
221
222
223
# Fetch the IP addresses
#
Symbiosis::Utmp.read(wtmp_file).each do |entry|
224
225
226
227
228
  #
  # Only interested in USER_PROCESS types.
  #
  next unless entry['type'] == 7

229
230
231
232
233
  #
  # Fetch the time the entry was logged at.
  #
  at = entry['time']

234
235
236
237
238
239
240
241
242
  #
  # Make sure the entry isn't in the future
  #
  next unless at < time_now

  #
  # Make sure the record isn't already expired.
  #
  next unless at > expire_before
243

244
245
246
  #
  # Fetch the IP
  #
247
  begin
248
    ip = Symbiosis::IPAddr.new(entry['ip'].to_s)
249
250
251
252
253
254
  rescue ArgumentError
    #
    # Oops.  Can't interpret the IP.
    #
    next
  end
Patrick J Cherry's avatar
Patrick J Cherry committed
255

256
  #
257
  # Mask IPv6 to /128s.
258
  #
259
  ip = ip.mask(128) if ip.ipv6?
260
261
262
263
264

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

266
267
  #
  # Only include globally routable IPs.
Patrick J Cherry's avatar
Patrick J Cherry committed
268
269
270
  #
  # FIXME: Need better IPv6 conditions.
  #
271
272
  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
273

274
  puts "Found IP address: #{ip}" if ( $VERBOSELOCAL )
275

276
277
  setting = ip.to_s.gsub("/","|")

278
279
280
  #
  # Check filename without .auto first.
  #
281
  if !Symbiosis::Utils.get_param(setting, whitelist_d)
282
283
284
    #
    # Automatically whitelist.
    #
285
    setting += ".auto"
286
    value = !!Symbiosis::Utils.get_param(setting, whitelist_d)
287
288

    if false == value
289
      puts "\tAdding whitelist entry" if  $VERBOSELOCAL
290
291
      syslog.info("adding #{ip} to whitelist")

292
      value = "22"
293

294
    elsif last_run.nil? or at > last_run
295
      puts "\tUpdating whitelist entry" if  $VERBOSELOCAL
296
297
298
299
      syslog.info("updating #{ip} in whitelist")
  
    else
      next
300

Steve Kemp's avatar
Steve Kemp committed
301
    end
302
303
304
305
306
307
308
    #
    # Yes, we're updating.
    #
    updated = true

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

311
  end
Steve Kemp's avatar
Steve Kemp committed
312

313
end
Steve Kemp's avatar
Steve Kemp committed
314

315

316
317
318
#
# Now expire old entries
#
319
puts "Expiring old whitelist entries" if ( $VERBOSELOCAL )
320
321
322
323
324

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

  if  File.mtime(entry) < expire_before

325
    puts "Removing #{entry}" if ( $VERBOSELOCAL )
326
327
    syslog.info("expiring #{File.basename(entry,".auto")} from whitelist")

328
329
330
331
332
333
334
    File.unlink(entry)
    expired += 1

  end

end

335
puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSELOCAL )
336

Steve Kemp's avatar
Steve Kemp committed
337
#
338
339
340
341
342
343
344
345
346
347
348
349
# Re-generate the whitelist chain
#
if ( updated || expired > 0 || force )
  cmd = %w(/usr/sbin/sympl-firewall)
  cmd << "--verbose" if $VERBOSELOCAL
  cmd << "--no-execute" unless execute
  cmd << "--no-delete"  unless delete
  cmd += ["--prefix", base_dir]
  cmd << "reload-whitelist"
  puts "Executing #{cmd.join(" ")}" if $VERBOSELOCAL
  exec(*cmd)
end