Posted by hamal on Tue 4 Dec 2007 at 12:48
I use Xen to create multiple locked down virtual machines that to run services which I want to present to the internet I do not allow direct connections from the internet to my firewall but sometimes there's a need to do remote administration (via ssh) so I can temporarily open up one or more ports. This I do with a webpage where an OPIE (one time password) challenge should be entered.
The idea of using OPIE challenges to open up firewall ports is not new. I saw
this option with Checkpoint FW1 firewalls. I implemented this myself with a
couple of perl scripts. The system is presented in the following image:

I connect to a CGI page which presents me with an OPIE challenge and a box to
fill in an IP address. The latter will default to the connecting IP address,
but the option is provided for when your ISP uses a transparent proxy or your
browser is configured to use a web proxy. If the verification succeeds, the IP
address is sent to a dedicated tcp port on the firewall. Behind that port
another script waits, parses the IP address and adds one or more rules for the
INPUT table to the iptables rulebase. The ports that are added to the rulebase
are hardcoded in the listening script on the firewall. The script will then
sleep for a limited time (I use 5 minutes) and then remove all added rules
from the firewall rulebase. Sessions that have been established during that
time can be kept alive by using state checking in your iptables rulebase.
#!/usr/bin/perl -w
# vim:ai:filetype=perl:sta:sw=4:et:
#
# This CGI script will use the S/Key One-Time
# Password mechanism for verification and if
# sucessfull, will pass an IP address (default:
# $ENV{REMOTE_ADDR}) to a tcp socket for inclusion
# in a firewall rulebase
#
use strict;
use CGI qw /:standard/;
use CGI::Carp 'fatalsToBrowser';
use HTML::Template;
use IO::Socket;
use Authen::OPIE qw(opie_challenge opie_verify);
# var declarations
our ($cgi, $template);
our $opie_user="opie";
our $fwhost="192.168.1.1";
our $fwport="54321";
$cgi=CGI->new();
print $cgi->header();
if (not defined $cgi->param(-name=>"login") or
$cgi->param(-name=>"login") ne "Open Sesame") {
#
# print challenge screen
#
my $opiechalstr=&opie_challenge($opie_user);
&Barf2Browser("Unknown OPIE user: $opie_user")
if (not defined $opiechalstr);
my @opiechalarr=split(/ /, $opiechalstr);
if ($opiechalarr[0] ne "otp-md5") {
&Barf2Browser("Crazy challenge: $opiechalstr");
}
my $response=$cgi->textfield(-name=>"response",
-value=>"",
-size=>40);
my $ipaddr=$cgi->textfield(-name=>"ipaddr",
-value=>"",
-size=>16);
my $submitbutton=$cgi->submit(-name=>"login",
-value=>"Open Sesame");
$template=HTML::Template->new(filename =>"sesame.tmpl",
path => "/home/$opie_user/templates");
$template->param(chalbool => 1);
$template->param(formstart => $cgi->start_form);
$template->param(sequence => $opiechalarr[1]);
$template->param(seed => $opiechalarr[2]);
$template->param(response => $response);
$template->param(ipaddr => $ipaddr);
$template->param(curraddr => $ENV{REMOTE_ADDR});
$template->param(submit => $submitbutton);
print $template->output;
}
else {
#
# Verify the challenge
#
my $response=$cgi->param(-name=>"response");
my $ipaddr=$cgi->param(-name=>"ipaddr");
&Barf2Browser("Empty response")
if (not defined $response or $response eq "");
my $verifyval=&opie_verify($opie_user,$response);
if (not defined $verifyval or $verifyval != 0) {
&Barf2Browser("<span class=red>Athentication attempt FAILED</span>");
}
else {
#
# OTP challenge succeeded, send IP address to firewall
#
$ipaddr=$ENV{REMOTE_ADDR} if (not defined $ipaddr or $ipaddr eq "");
my $socket= new IO::Socket::INET (PeerAddr => $fwhost,
PeerPort => $fwport,
Proto => "tcp",
Type => SOCK_STREAM)
or &Barf2Browser("Authentication succeeded but "
."<span class=red>network connection failed</span>");
print $socket "$ipaddr";
close $socket;
my $submitbutton=$cgi->submit(-name=>"ok",
-value=>"OK");
$template=HTML::Template->new(filename =>"sesame.tmpl",
path => "/home/$opie_user/templates");
$template->param(msgbool => 1);
$template->param(formstart => $cgi->start_form);
$template->param(msghdr => "<span class=blue>Authentication succeeded!</span>");
$template->param(message => "Sent $ipaddr to the firewall");
$template->param(submit => $submitbutton);
print $template->output;
}
}
exit 0;
sub Barf2Browser() {
# output error
my ($string)=@_;
my $submitbutton=$cgi->submit(-name=>"ok",
-value=>"OK");
$string="undefined" if (not defined $string);
$template=HTML::Template->new(filename =>"sesame.tmpl",
path => "/home/$opie_user/templates");
$template->param(msgbool => 1);
$template->param(formstart => $cgi->start_form);
$template->param(msghdr => "Sesame error:");
$template->param(message => $string);
$template->param(submit => $submitbutton);
print $template->output;
exit 0;
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<style type="text/css">
BODY {background-color: #b0c4ef; color: black}
A:link {color: #000040}
A:external {color: #000040}
A:active {color: #000040}
A:visited {color: #000040}
SPAN.blue {color: #0000c0}
SPAN.red {color: #c00000}
</style>
</head>
<body>
<tmpl_var name="formstart">
<h1>Sesame Verifyer</h1>
<tmpl_if name="chalbool">
<h3>Challenge:<br/>
<tmpl_var name="sequence"> <tmpl_var name="seed"></h3>
<br/>
<strong>Response:</strong><br/>
<tmpl_var name="response"><br/><br/>
<table><tr><td>
Current IP address:
</td><td>
<tt><strong><tmpl_var name="curraddr"></strong></tt>
</td></tr><tr><td>
Optionally override with:
</td><td>
<tmpl_var name="ipaddr">
</td></tr></table>
<tmpl_else>
<tmpl_if name="msgbool">
<h3><tmpl_var name=msghdr></h3>
<strong><tmpl_var name="message"></strong>
</tmpl_if>
</tmpl_if>
<br/><br/><tmpl_var name="submit">
</form>
</body>
</html>
#!/usr/bin/perl
# vim:ai:filetype=perl:sta:sw=4:et:
#
# This script will read a line from STDIN, expecting
# expecting an IP address and will use that address
# as a source for a firewall rule that temporarily
# opens one or more ports in the INPUT chain
#
# ports must be specified as /^[tu]\d+$/ (the first
# character specifies the tcp or udp protocol
use strict;
use Sys::Syslog qw(:standard :macros);
our @ports = ("t22","u5000");
our $timeslot = 300; #5 minutes
my $addr=<STDIN>;
chomp $addr;
# check if we received a correct IP address
if ($addr !~ /^(\d{1,3}\.){3}\d{1,3}$/) {
&LogText(LOG_WARNING, "WARNING: someone tried something nasty!");
exit 0;
}
foreach my $port (@ports) {
&IPTrule("-I",$port,$addr);
}
sleep $timeslot;
foreach my $port (@ports) {
&IPTrule("-D",$port,$addr);
}
exit 0;
sub LogText() {
my ($level, $text)=@_;
openlog("sesamed","ndelay,pid",LOG_DAEMON);
syslog($level,$text);
closelog;
}
sub IPTrule() {
my ($act,$protoport,$addr)=@_;
my ($proto,$port)=();
if ($protoport =~ /^([tu])(\d+)$/) {
$port=$2;
$proto = ($1 eq "t") ? "tcp" : "udp";
}
if (not defined $proto) {
&LogText(LOG_NOTICE, "BUG: wrong protoport specified");
return;
}
my @cmdline=("/sbin/iptables",$act,"INPUT","-p",$proto,"--dport");
push @cmdline, ($port,"-s",$addr,"-j","ACCEPT");
&LogText(LOG_INFO, join(" ", @cmdline));
system(@cmdline);
}
This article can be found online at the Debian Administration website at the following bookmarkable URL (along with associated comments):
This article is copyright 2007 hamal - please ask for permission to republish or translate.