Newer
Older
#!/usr/bin/perl
# awit-ssh - SSH initiator which searches LDAP for host details
# Copyright (c) 2016, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
use strict;
use warnings;
# Check Config::IniFiles
if (!eval {require Config::IniFiles; 1;}) {
print STDERR "You're missing Config::IniFiles, try 'apt-get install libconfig-inifiles-perl'\n";
exit 1;
}
# Check IO::Socket::INET6
if (!eval {require IO::Socket::INET6; 1;}) {
print STDERR "You're missing IO::Socket::INET6, try 'apt-get install libio-socket-inet6-perl'\n";
exit 1;
}
# Check Net::LDAP
if (!eval {require Net::LDAP; 1;}) {
print STDERR "You're missing Net::LDAP, try 'apt-get install libnet-ldap-perl'\n";
exit 1;
}
# Check IO::Prompt
if (!eval {require IO::Prompt; 1;}) {
print STDERR "You're missing IO::Prompt, try 'apt-get install libio-prompt-perl'\n";
print(STDERR "$NAME v$VERSION - Copyright (c) 2016, AllWorldIT\n\n");
# Grab options
my %optctl = ();
GetOptions(\%optctl,
"help|?",
"version",
"debug",
"knock=s",
) or exit 1;
# Check for help
if (defined($optctl{'help'})) {
displayHelp();
exit 0;
}
# Check for version
if (defined($optctl{'version'})) {
displayVersion();
exit 0;
}
# Check if we should be doing port knocking
my ($knockHost,$knockPort);
if (defined(my $knock = $optctl{'knock'})) {
# If so, split off the host and the port
($knockHost,$knockPort) = split(':',$knock);
if (!defined($knockPort)) {
logger('ERROR',color('magenta')."Port knock specifications should be in the format of HOST:PORT".color('reset'));
exit 1;
}
}
# Variables we may set below
my $loginUsername;
# Pull in hostname
my $hostSpec = shift(@ARGV) // "";
my ($loginHost,$loginPort) = split(':',$hostSpec);
if (defined($loginHost)) {
# Suck in username if specified
if (defined($hostBit)) {
$loginUsername = $userBit;
$loginHost = $hostBit;
}
} else {
logger('ERROR',color('magenta')."No hostname provided".color('reset'));
exit 1;
}
# Make sure we save the hostname
my $realLoginHost = $loginHost;
# Port forwarding/bouncing
my ($forwardHost,$forwardPort,$forwardUsername);
# Check for config and read
my $configFile = $ENV{"HOME"}.'/.awit-ssh.conf';
if (! -f $configFile) {
print STDERR "No configuration file found. Please answer the questions below to generate it.\n\n";
tie %iniSetup, 'Config::IniFiles';
$iniSetup{server} = {};
$iniSetup{server}{uri} = prompt("Your LDAP URI : ");
$iniSetup{server}{uri} =~ s/^uri=//;
$iniSetup{server}{base} = prompt("Your LDAP Base : ");
$iniSetup{server}{base} =~ s/^base=//;
tied(%iniSetup)->WriteConfig($configFile) or die "Could not write settings to new configuration file.";
untie %iniSetup;
} else {
# Flag, so we don't echo the same output lower down immediately after a setup.
$iniSetup{server} = 'installed';
}
my $config = Config::IniFiles->new(-file => $configFile);
my $ldapURI = $config->val("server","uri");
if (!defined($ldapURI) || $ldapURI eq "") {
logger('ERROR',color('magenta')."Server URI not defined in config file".color('reset'));
exit 1;
}
my $ldapBase = $config->val("server","base");
if (!defined($ldapBase) || $ldapBase eq "") {
logger('ERROR',color('magenta')."Server base DN not defined in config file".color('reset'));
my $pkcsProvider = $config->val("pkcs11","provider");
if (defined($pkcsProvider) && $pkcsProvider ne "") {
if (! -f $pkcsProvider) {
logger('ERROR',color('magenta')."PKCS11 provider '$pkcsProvider' does not exist".color('reset'));
exit 1;
}
}
if (%iniSetup) {
print STDERR "LDAP server URI : $ldapURI\n";
print STDERR "LDAP server base : $ldapBase\n";
}
# Try get name automatically
my $pwent = getpwnam($ENV{'USER'});
(my $username) = split(/,/,$pwent->gecos);
if (!defined($username) || $username eq "") {
print STDERR "WARNING: Cannot determine your name, set your gecos field.\n\n";
print STDERR "Your LDAP CN : $username (passwd->gecos)\n";
# Sort out LDAP password
my $password;
# Lets try kwallet
my ($kwalletObject,$kwalletHandle);
# IF removed, lets rather run this in its own scope...
{
my $dbus = Net::DBus->find();
# Grab the kwallet service off DBus
my $kwalletService = $dbus->get_service('org.kde.kwalletd');
if (!defined($kwalletService)) {
logger('WARNING',color('magenta')."Kwallet not found on DBus".color('reset'));
goto KWALLET_END;
}
$kwalletObject = $kwalletService->get_object('/modules/kwalletd','org.kde.KWallet');
# Grab a handle to the network wallet
my $networkWalletName = $kwalletObject->networkWallet();
$kwalletHandle = $kwalletObject->open($networkWalletName,0,$NAME);
$password = $kwalletObject->readPassword($kwalletHandle,"ldap","password",$NAME);
# If kwallet returned nothing, try read from terminal
$password = prompt("Your LDAP Password: ", '-echo' => "*");
my $ldap = Net::LDAP->new($ldapURI,
logger('ERROR',color('magenta')."Failed to setup LDAP object '%s'".color('reset'),$@);
my $mesg = $ldap->bind("cn=$username,ou=Users,$ldapBase",password => $password);
base => "ou=Servers,$ldapBase",
filter => "(|(cn=$loginHost)(awitLoginHost=$loginHost)(awitLoginHostAlias=$loginHost))",
if (my $mesgCode = $mesg->code()) {
if ($mesgCode eq "No such object") {
logger('ERROR',color('magenta')."LDAP returned '%s', this more than likely means a Username/Password error or your BaseDN is wrong.".color('reset'),$mesgCode);
logger('ERROR',color('magenta')."LDAP returned '%s'".color('reset'),$mesgCode);

Nigel Kukard
committed
# Some flags we may need
my $needDSS;
# If no matches
my @ldapResults = $mesg->entries();
my $ldapNumResults = @ldapResults;
my $ldapEntry;
if ($ldapNumResults < 1) {
logger('NOTICE',color('bold red')."No LDAP results, using the hostname provided on the commandline".color('reset'));
} elsif ($ldapNumResults == 1) {
$ldapEntry = $ldapResults[0];
} elsif ($ldapNumResults > 1) {
logger('WARNING',color('red')."Found multiple entries!".color('reset'));
print STDERR "\n";
foreach my $key (sort(keys %{$mesg->as_struct()})) {
logger('MENU '.$counter," ".color('red')."%s".color('reset'),$key);
$counter++;
my $menuSelection = prompt("Your selection [1-$ldapNumResults,q]: ",
'-onechar',
'-require' => {
"Invalid Value - Your selection [1-$ldapNumResults,q]: " => sub {
my $val = $_;
return (
# Check if is numeric and its within range
$val =~ /^\d$/ &&
$val > 0 &&
$val <= $ldapNumResults
) || (
# Else our only other option we accept is 'q'
$val eq "q"
);
}
}
);
if ($menuSelection eq "q") {
print STDERR "Exiting...\n";
$menuSelection--;
$ldapEntry = $ldapResults[$menuSelection];
}
print STDERR "\n";
# Check if we got a result and modify our connection details
if ($ldapEntry) {
my $ldapEntryName = $ldapEntry->get_value('cn');
logger('INFO',"Found server entry '".color('green')."$ldapEntryName".color('reset')."'");
# Check if we need to set the port knocking host & port
if (my $ldapLoginKnockHost = $ldapEntry->get_value('awitLoginKnockHost')) {
logger('INFO'," - Knock host ".color('green')."%s".color('reset')." (awitLoginKnockHost)",$ldapLoginKnockHost);
$knockHost //= $ldapLoginKnockHost;
}
if (my $ldapLoginKnockPort = $ldapEntry->get_value('awitLoginKnockPort')) {
logger('INFO'," - Knock port ".color('green')."%s".color('reset')." (awitLoginKnockPort)",$ldapLoginKnockPort);
$knockPort //= $ldapLoginKnockPort;
}
# Check if we need to set the port forwarding info
if (my $ldapLoginForwardHost = $ldapEntry->get_value('awitLoginForwardHost')) {
logger('INFO'," - Forward host ".color('green')."%s".color('reset')." (awitLoginForwardHost)",$ldapLoginForwardHost);
$forwardHost //= $ldapLoginForwardHost;
}
if (my $ldapLoginForwardPort = $ldapEntry->get_value('awitLoginForwardPort')) {
logger('INFO'," - Forward port ".color('green')."%s".color('reset')." (awitLoginForwardPort)",$ldapLoginForwardPort);
$forwardPort //= $ldapLoginForwardPort;
}
if (my $ldapLoginForwardUsername = $ldapEntry->get_value('awitLoginForwardUsername')) {
logger('INFO'," - Forward user ".color('green')."%s".color('reset')." (awitLoginForwardUsername)",$ldapLoginForwardUsername);
$forwardUsername //= $ldapLoginForwardUsername;
}
if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) {
logger('INFO'," - Host ".color('green')."%s".color('reset')." (awitLoginHost)",$ldapLoginHost);
}
# Check if we need to set the port
if (my $ldapLoginPort = $ldapEntry->get_value('awitLoginPort')) {
logger('INFO'," - Port ".color('green')."%s".color('reset')." (awitLoginPort)",$ldapLoginPort);
# Check if we need to set the username
if (my $ldapLoginUsername = $ldapEntry->get_value('awitLoginUsername')) {
logger('INFO'," - User ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername);
# Check if we have a description
if (my $ldapDescription = $ldapEntry->get_value('description')) {
logger('INFO',"Description");
foreach my $line (split(/\n/,$ldapDescription)) {
logger('INFO'," ".color('green')."%s".color('reset'),$line);

Nigel Kukard
committed
# Hack'ish ... look if the description mentions dss is required...
if ($ldapDescription =~ /needs ssh-dss/i) {
$needDSS = 1;
}
}
# Check if we have a wiki page
if (my $ldapLoginWikiPage = $ldapEntry->get_value('awitLoginWikiPage')) {
logger('INFO'," ".color('green')."%s".color('reset'),$ldapLoginWikiPage);
# If we have kwalletObject and kwalletHandle defined, store the password as we've given awit-ssh-client permission to access
# kwallet
if (defined($kwalletObject) && defined($kwalletHandle)) {
$kwalletObject->writePassword($kwalletHandle,"ldap","password",$password,$NAME);
}
# Check if we need to do port knocking
if (defined($knockHost)) {
# Make sure we have a port knocking port
if (!defined($knockPort)) {
logger('ERROR',color('bold red')."No port knocking port defined!".color('reset'));
logger('NOTICE',"Port knocking '".color('green')."%s".color('reset')."' on port '".color('green')."%s".color('reset')."'...",
$knockHost,$knockPort);
# Do the port knock
my $sock = IO::Socket::INET6->new(
PeerAddr => $knockHost,
PeerPort => $knockPort,
Proto => 'tcp',
Timeout => 3
);
# We should get a failure of "Connection refused", if not ERR
if (defined($sock) || $! ne "Connection refused") {
logger('ERROR',color('bold red')."Port knocking failed!".color('reset'));
if (defined($pkcsProvider) && $pkcsProvider ne "") {
logger('NOTICE',color('blue')."Enabling smartcard/token authentication.".color('reset'));
# Only push the config file override to SSH if the config file exists in the users homedir\
if (-f (my $sshConfigFile = $ENV{"HOME"}.'/.ssh/config')) {
push(@sshArgs,'-F',$sshConfigFile);
}

Nigel Kukard
committed
# If the server is ancient, we need to enable DSS
if (defined($needDSS)) {
logger('WARNING',color('red')."Host needs ssh-dss".color('reset'));

Nigel Kukard
committed
push(@sshArgs,'-o','PubkeyAcceptedKeyTypes=+ssh-dss');
push(@sshArgs,'-o','HostbasedKeyTypes=+ssh-dss');
push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss');
}
# Try our key only, we should never need to fall back to password
push(@sshArgs,'-o','PreferredAuthentications=publickey');
push(@sshArgs,'-o','StrictHostKeyChecking=ask');
# Use TCP keepalive
push(@sshArgs,'-o','TCPKeepAlive=yes');
push(@sshArgs,'-o','ServerAliveInterval=5');
push(@sshArgs,'-o','ServerAliveCountMax=24'); # 120s
# Timeout for our connect
push(@sshArgs,'-o','ConnectTimeout=30');
# Fail if we cannot forward ports
push(@sshArgs,'-o','ExitOnForwardFailure=yes');
# Fixup environment before we start to run SSH
local $ENV{'LANG'} = "en_US.UTF-8";
delete($ENV{'LC_ALL'});
delete($ENV{'LC_TIME'});
delete($ENV{'LC_CTYPE'});
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# Setup TMPDIR, we prefer XDG_RUNTIME_DIR as its protected in /run/user/$UID/
my $TMPDIR = $ENV{'XDG_RUNTIME_DIR'} // $ENV{'TMPDIR'} // '/tmp';
# Setup our forward port name
our $forwardSocket;
# Forward child PID
our $forwardChild;
# Check if we're forwarding, we need to work a few things out...
if (defined($forwardHost)) {
logger('NOTICE',"Forwarding '".color('green').$realLoginHost.color('reset')."' via host '".color('green').$loginHost.
color('reset')."'" .(defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") .
"...\n\n\n");
# Default to port 22 if the login port is not defined
my $destPort = $forwardPort // 22;
# Add forward socket name
$forwardSocket = "$TMPDIR/awit-ssh-forward-".sha1_hex("$forwardHost:$destPort $$").".sock";
# Build up our forwarding process args into this...
my @forwardArgs = ();
# Add on port we're forwarding
push(@forwardArgs,'-L',"$forwardSocket:$forwardHost:$forwardPort");
# Check if we have a port defined, if so specify it
if (defined($loginPort)) {
push(@forwardArgs,'-p',$loginPort);
}
# Check if we have a different username defined to login as
if (defined($loginUsername)) {
push(@forwardArgs,'-l',$loginUsername);
}
# Explicitly disable control master for the main forwarding process
push(@forwardArgs,'-o','ControlMaster=no');
# Fork off child to establish the main connection
$forwardChild = fork();
if (!$forwardChild) {
# Exec ssh
if (!exec('/usr/bin/ssh',
@sshArgs,
@forwardArgs,
# Use basic compression
'-o','Compression=yes',
'-o','CompressionLevel=1',
# All we're doing here is forwarding the port...
'-N',
$loginHost
)) {
logger('ERROR',color('magenta')."Forwarding SSH process failed to start".color('reset'));
exit 1;
}
}
};
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# Install signal handlers to cleanup if we get a TERM or INT
local $SIG{TERM} = local $SIG{INT} = \&cleanup;
# Check if we're forwarding to a socket...
if (defined($forwardSocket)) {
# Loop waiting for the socket to be created
my $delay = 30;
while (! -e $forwardSocket && $delay > 0) {
$delay--;
sleep 1;
}
if ($delay) {
# Check if we need to specify the username
push(@sshArgs,'-l',$forwardUsername) if (defined($forwardUsername));
logger('NOTICE',"Connecting to host '".color('green')."$forwardHost".color('reset')."'" .
(defined($forwardPort) ? " on port '".color('green')."$forwardPort".color('reset')."'" : "") . "...\n\n\n");
# Fire up ssh
system('/usr/bin/ssh',
@sshArgs,
# Override where we connecting to
'-o',"ProxyCommand=nc -U $forwardSocket",
# Explicitly disable control master
'-o','ControlMaster=no',
$realLoginHost
);
# Unlink socket and unset it to designate we exited normally
unlink($forwardSocket);
undef($forwardSocket);
} else {
logger('ERROR',color('magenta')."Socket not connected, aborting!".color('reset'));
}
# Normal SSH connection
} else {
logger('NOTICE',"Connecting to host '".color('green')."$loginHost".color('reset')."'" .
(defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . "...\n\n\n");
# Make sure we get asked for control master connections...
push(@sshArgs,'-o','ControlMaster=autoask');
push(@sshArgs,'-o',"ControlPath=$TMPDIR/awit-ssh-master-%C");
# Check if we have a different username defined to login as
if (defined($loginUsername)) {
push(@sshArgs,'-l',$loginUsername);
}
# Check if we have a port defined, if so specify it
if (defined($loginPort)) {
push(@sshArgs,'-p',$loginPort);
}
system('/usr/bin/ssh',
@sshArgs,
exit 0;
#
# Internal Functions
#
# Cleanup function
sub cleanup
{
# Kill the child
if ($forwardChild && kill(-1,$forwardChild)) {
kill('TERM',$forwardChild);
# Wait for it to die
waitpid($forwardChild,-1);
}
# Unlink the socket
if ($forwardSocket) {
unlink($forwardSocket);
print STDERR "\nExiting...\n";
exit 1;
}
exit 0;
}
# Log something
sub logger
{
my ($level,$arg1,@args) = @_;
printf(STDERR '%-7s: '.$arg1.color('reset')."\n",$level,@args);
}
# Display version
sub displayVersion
{
print("Version: $VERSION\n");
}
# Display usage
sub displayHelp
{
print(STDERR<<EOF);
General Options:
--help What you're seeing now.
--version Display version.
--debug Enable debugging.
Port Knocking:
--knock HOST:PORT Port knock a host to get access.
EOF