Revision $Id: webjob-access-list.base,v 1.3 2006/03/31 18:03:45 klm Exp $ Purpose This recipe demonstrates how to patch nph-webjob.cgi to employ the use of an address-based access list. This list will associate client IDs with one or more allowed IP addresses. If the client passes all other authentication tests, a lookup will be performed to determine if the client's IP address is authorized. If it is, processing continues as usual. Otherwise, the CGI script will return a 403 status code to the client, log an error message, and abort. Motivation This recipe grew out of the need to restrict access for a given client ID to a single IP address or set of IP addresses. Requirements Cooking with this recipe requires an operational WebJob server. If you do not have one of those, refer to the instructions provided in the README.INSTALL file that comes with the source distribution. The latest source distribution is available here: http://sourceforge.net/project/showfiles.php?group_id=40788 Each client must be running UNIX and have basic system utilities and WebJob (1.5.0 or higher) installed. The server must be running UNIX and have basic system utilities, Apache, and WebJob (1.5.0 or higher) installed with nph-webjob.cgi 1.42. A copy of revision 1.42 is available here: http://cvs.sourceforge.net/viewcvs.py/webjob/webjob/cgi The commands presented throughout this recipe were designed to be executed within a Bourne shell (i.e., sh or bash). Time to Implement Assuming that you have satisfied all the requirements/prerequisites, this recipe should take less than one hour to implement. Solution The solution is to prepare an access list, install it on your WebJob server, and patch the specified revision of nph-webjob.cgi using the diff provided in Appendix 1. 1. To configure your WebJob server to utilize an access list, you need to upgrade nph-webjob.cgi to the specified revision. If your revision of nph-webjob.cgi is newer, you can still use this recipe, but you may need to patch the script manually. If your server already has WebJob 1.5.0 or higher installed, you should be able to simply replace the existing CGI script with the newer revision. Older installs will require more planning and care. Once the new script is in place, you must patch it with the diff provided in Appendix 1. This can be done as follows: # WEBJOB_CGI_DIR=/usr/local/webjob/cgi/cgi-client # sed -e '1,/^--- nph-webjob.cgi.diff ---$/d; /^--- nph-webjob.cgi.diff ---$/,$d' webjob-access-list.txt > nph-webjob.cgi.diff # patch ${WEBJOB_CGI_DIR}/nph-webjob.cgi nph-webjob.cgi.diff 2. Next, you must create an access list. This is most easily done by using the script provided in Appendix 2. # WEBJOB_BASE_DIR=/var/webjob # sed -e '1,/^--- wjss_create_access_list ---$/d; /^--- wjss_create_access_list ---$/,$d' webjob-access-list.txt > wjss_create_access_list # perl -T wjss_create_access_list -f ${WEBJOB_BASE_DIR}/logfiles/nph-webjob.log > nph-webjob.access Inspect the list. Edit it to suit your requirements. Then, install it on the WebJob server. # WEBJOB_CGI_CONFIG_DIR=/var/webjob/config/nph-webjob # cp nph-webjob.access ${WEBJOB_CGI_CONFIG_DIR}/ # chown root:wheel ${WEBJOB_CGI_CONFIG_DIR}/nph-webjob.access # chmod 644 ${WEBJOB_CGI_CONFIG_DIR}/nph-webjob.access Note: The format of the access list is as follows: = [,] For example, client_host1=192.168.1.1/32 client_host_multihomed=192.168.1.3/32,192.168.10.3/32 client_wjsetup=192.168.1.0/24,192.168.10.0/24 Closing Remarks The main drawback with the access list is that it must be maintained. It would be nice if name resolution was supported so that FQDNs could be used in addition to IP addresses. Credits This recipe was brought to you by Klayton Monroe. References Appendix 1 --- nph-webjob.cgi.diff --- --- nph-webjob.cgi Thu Mar 30 20:31:25 2006 +++ nph-webjob.cgi.new Fri Mar 31 01:35:00 2006 @@ -24,6 +24,7 @@ ( '200' => "OK", '251' => "Link Test OK", + '403' => "Forbidden", '404' => "Not Found", '405' => "Method Not Allowed", '450' => "Invalid Query", @@ -344,6 +345,14 @@ #################################################################### # + # Initialize access list. + # + #################################################################### + + SetupAccessList($phProperties, \$sLocalError); + + #################################################################### + # # Verify run time environment. # #################################################################### @@ -1692,6 +1701,18 @@ ################################################################## # + # Do access list checks. + # + ################################################################## + + if (!CheckAccessList($$phProperties{'AccessList'}{$$phProperties{'ClientId'}}, $$phProperties{'RemoteAddress'})) + { + $$psError = "The ClientId/Address key/value pair ($$phProperties{'ClientId'}=$$phProperties{'RemoteAddress'}/32) is not on the access list"; + return 403; + } + + ################################################################## + # # Do content length checks. # ################################################################## @@ -1830,6 +1851,18 @@ ################################################################## # + # Do access list checks. + # + ################################################################## + + if (!CheckAccessList($$phProperties{'AccessList'}{$$phProperties{'ClientId'}}, $$phProperties{'RemoteAddress'})) + { + $$psError = "The ClientId/Address key/value pair ($$phProperties{'ClientId'}=$$phProperties{'RemoteAddress'}/32) is not on the access list"; + return 403; + } + + ################################################################## + # # Do content length checks. # ################################################################## @@ -2463,4 +2496,107 @@ } 1; +} + + +###################################################################### +# +# CheckAccessList +# +###################################################################### + +sub CheckAccessList +{ + my ($sCidrList, $sIp) = @_; + + my $sCidrRegex = qq(((?:\\d{1,3}[.]){3}(?:\\d{1,3}))\/(\\d{1,2})); + + my $sIpRegex = qq((?:\\d{1,3}[.]){3}(?:\\d{1,3})); + + if (!defined($sCidrList) || !defined($sIp)) + { + return 0; + } + + if ($sIp !~ /^$sIpRegex$/) + { + return 0; + } + + $sCidrList =~ s/\s+//g; # Remove whitespace. + + foreach my $sCidr (split(/,/, $sCidrList)) + { + next if ($sCidr !~ /^$sCidrRegex$/); + my ($sNetwork, $sMaskBits) = ($1, $2); + my $sBits2Clear = 32 - $sMaskBits - 1; + my $sNetMask = 0xffffffff; + foreach my $sBit (0..$sBits2Clear) + { + $sNetMask ^= 1 << $sBit; + } + my $sBinNetwork = hex(sprintf("%02x%02x%02x%02x", split(/\./, $sNetwork))); + my $sBinNetmask = hex(sprintf("%08x", $sNetMask)); + $sBinNetwork &= $sBinNetmask; # Cleanup the network address -- in case the user did not. + my $sBinIp = hex(sprintf("%02x%02x%02x%02x", split(/\./, $sIp))); + if (($sBinIp & $sBinNetmask) == $sBinNetwork) + { + return 1; # We have a winner! + } + } + + return 0; +} + + +###################################################################### +# +# SetupAccessList +# +###################################################################### + +sub SetupAccessList +{ + my ($phProperties, $psError) = @_; + + #################################################################### + # + # Initialize the access list. This list is a hash that contains + # client ID to IP address mappings. If the client passes all other + # authentication tests, a lookup will be performed to determine if + # the client's IP address is authorized. If it is, processing + # continues as usual. Otherwise, the CGI script will return a 403 + # status code to the client, log an error message, and abort. If the + # access list is empty or there is an error processing its file, all + # access will be denied (i.e., this mechanism fails closed). + # + # The format of the list is key/value pair as follows: + # + # = [,] + # + ##################################################################### + + %{$$phProperties{'AccessList'}} = (); # Initialize an empty list. + + if (open(AH, "< /var/webjob/config/nph-webjob/nph-webjob.access")) + { + while (my $sLine = ) + { + $sLine =~ s/[\r\n]+$//; # Remove CRs and LFs. + $sLine =~ s/#.*$//; # Remove comments. + if ($sLine !~ /^\s*$/) + { + my ($sKey, $sValue) = ($sLine =~ /^([^=]*)=(.*)$/); + $sKey =~ s/^\s+//; # Remove leading whitespace. + $sKey =~ s/\s+$//; # Remove trailing whitespace. + $sValue =~ s/^\s+//; # Remove leading whitespace. + $sValue =~ s/\s+$//; # Remove trailing whitespace. + if ($sKey =~ /^$$phProperties{'CommonRegexes'}{'ClientId'}$/) + { + $$phProperties{'AccessList'}{$sKey} = $sValue; + } + } + } + close(AH); + } } --- nph-webjob.cgi.diff --- Appendix 2 --- wjss_create_access_list --- #!/usr/bin/perl -wT ###################################################################### # # $Id: wjss_create_access_list,v 1.2 2006/03/31 04:38:59 klm Exp $ # ###################################################################### # # Copyright 2006-2006 The WebJob Project, All Rights Reserved. # ###################################################################### use strict; use File::Basename; use Getopt::Std; ###################################################################### # # Main Routine # ###################################################################### #################################################################### # # Punch in and go to work. # #################################################################### my ($sProgram); $sProgram = basename(__FILE__); #################################################################### # # Validation expressions. # #################################################################### my $sClientIdRegex = qq((?:[A-Za-z](?:(?:[0-9A-Za-z]|[_-](?=[^.]))){0,62})(?:[.][A-Za-z](?:(?:[0-9A-Za-z]|[_-](?=[^.]))){0,62}){0,127}); my $sIpRegex = qq((?:\\d{1,3}\\.){3}\\d{1,3}); my $sStatusRegex = qq(2\\d{2}); # Match any status code in the range [200-299]. #################################################################### # # Get Options. # #################################################################### my (%hOptions); if (!getopts('f:', \%hOptions)) { Usage($sProgram); } #################################################################### # # A filename, '-f', is required, and can be '-' or a regular file. # #################################################################### my ($sFileHandle, $sFilename); if (!exists($hOptions{'f'})) { Usage($sProgram); } else { $sFilename = $hOptions{'f'}; if (!defined($sFilename) || length($sFilename) < 1) { Usage($sProgram); } if (-f $sFilename) { if (!open(FH, "< $sFilename")) { print STDERR "$sProgram: File='$sFilename' Error='$!'\n"; exit(2); } $sFileHandle = \*FH; } else { if ($sFilename ne '-') { print STDERR "$sProgram: File='$sFilename' Error='File must be regular.'\n"; exit(2); } $sFileHandle = \*STDIN; } } #################################################################### # # If there's any arguments left, it's an error. # #################################################################### if (scalar(@ARGV) > 0) { Usage($sProgram); } #################################################################### # # Loop over the input. Skip records that don't match the criteria. # #################################################################### my (%hAccessList); while (my $sLine = <$sFileHandle>) { $sLine =~ s/[\r\n]+$//; my ($sClientId, $sIp, $sStatus) = (split(/\s+/, $sLine))[3,4,11]; next unless ($sClientId =~ /^$sClientIdRegex$/ && $sIp =~ /^$sIpRegex$/ && $sStatus =~ /^$sStatusRegex$/); $hAccessList{$sClientId}{$sIp}++; } foreach my $sClientId (sort(keys(%hAccessList))) { my (@aIps) = (); foreach my $sIp (sort(keys(%{$hAccessList{$sClientId}}))) { my $sCidr = $sIp . "/32"; push(@aIps, $sCidr); } print "$sClientId=", join(",", @aIps), "\n"; } #################################################################### # # Cleanup and go home. # #################################################################### close($sFileHandle); 1; ###################################################################### # # Usage # ###################################################################### sub Usage { my ($sProgram) = @_; print STDERR "\n"; print STDERR "Usage: $sProgram -f {file|-}\n"; print STDERR "\n"; exit(1); } --- wjss_create_access_list ---