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",
# TODO: debug is not implemented, make sure displayHelp is updated
) 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 using rsync instead of SSH
my $useRsync = 0;
my @rsyncParams;
if (defined(my $rsyncHost = $optctl{'rsync'})) {
$useRsync = $rsyncHost;
}
# 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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
my $hostSpec;
if ($useRsync) {
foreach my $param (@ARGV) {
# Look for the remote:// param
if ($param =~ /remote:\/\//) {
# Remove it and set the hostSpec
my $removedTag = substr($param,9);
# Assing hostSpec to the first part of the tag
($hostSpec) = split(/[\/:]/,$removedTag);
# Change first / to a :/
$removedTag =~ s,/,:/,;
push(@rsyncParams,$removedTag);
# Else just add it
} else {
push(@rsyncParams,$param);
}
}
# Make sure we got a hostSpec
if (!defined($hostSpec)) {
logger('ERROR',color('magenta')."awit-ssh --rsync needs a remote://SERVER.... to be specified on the command line".
color('reset'));
exit 1;
}
} else {
$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") {
$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')."'");
# TODO: Ability to select between mulitple awitLoginHost's separated by ,'s
# 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'});
# 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 = ();
# TODO: Allow the use of multiple forwarded ports by separating them with ,'s
# The first port will be assumed as the SSH port, all other ports will be forwarded via TCP/IP and reported in terminal
500
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
# 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;
}
}
};
# 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");
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
# Check what operation we're doing
if ($useRsync) {
# Build SSH command
my $sshCmd = join(' ','/usr/bin/ssh',
@sshArgs,
# Override where we connecting to
'-o',"ProxyCommand=\"nc -U $forwardSocket\"",
# Explicitly disable control master
'-o','ControlMaster=no',
);
# Run rsync
system('/usr/bin/rsync',
'-e',$sshCmd,
@rsyncParams
);
# Normal SSH
} else {
# 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
);
}
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
# 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);
}
# Check what operation we're doing
if ($useRsync) {
# Build SSH command
my $sshCmd = join(' ','/usr/bin/ssh',
@sshArgs,
# Use basic compression
'-o','Compression=yes',
'-o','CompressionLevel=1'
);
# Run rsync
system('/usr/bin/rsync',
'-e',$sshCmd,
@rsyncParams
);
# Normal SSH
} else {
system('/usr/bin/ssh',
@sshArgs,
# Use basic compression
'-o','Compression=yes',
'-o','CompressionLevel=1',
$loginHost
);
}
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);
$0 <options> --rsync -- <rsync options> remote://[USER@]HOST/file.name /tmp
General Options:
--help What you're seeing now.
--version Display version.
Secure Copy: (using rsync)
--rsync Run rsync instead of ssh, passing all
command line parameters after the host
to it. HOST is used for searching
LDAP.
Port Knocking:
--knock HOST:PORT Port knock a host to get access.
EOF