#!/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; use Term::ANSIColor; use Getopt::Long; use Net::DBus qw(:typing); # 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"; exit 1; } use IO::Prompt qw( prompt ); use User::pwent; my $NAME = "AWIT-SSH-Client"; our $VERSION = "0.3.3"; 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; } # 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 my ($userBit,$hostBit) = split('@',$loginHost); if (defined($hostBit)) { $loginUsername = $userBit; $loginHost = $hostBit; } } else { logger('ERROR',color('magenta')."No hostname provided".color('reset')); exit 1; } my %iniSetup; # 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} = {}; print STDERR "Your LDAP URI : "; chomp($iniSetup{server}{uri} = <STDIN>); $iniSetup{server}{uri} =~ s/^uri=//; print STDERR "Your LDAP Base : "; chomp($iniSetup{server}{base} = <STDIN>); $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')); exit 1; } 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; } } # Check if we should be doing port knocking if (defined(my $knock = $optctl{'knock'})) { # If so, split off the host and the port my ($host,$port) = split(':',$knock); if (!defined($port)) { logger('ERROR',color('magenta')."Port knock specifications should be in the format of HOST:PORT".color('reset')); exit 1; } print STDERR "Port knocking '$host' on port '$port'..."; # Do the port knock my $sock = IO::Socket::INET6->new( PeerAddr => $host, PeerPort => $port, Proto => 'tcp', Timeout => 3 ); # We should get a failure of "Connection refused", if not ERR if (defined($sock) || $! ne "Connection refused") { print STDERR "FAILED\n"; exit 1; } print STDERR "success\n"; } 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 = <STDIN>; } else { 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); KWALLET_END: } # If kwallet returned nothing, try read from terminal if (!defined($password) || $password eq "") { $password = prompt("Your LDAP Password: ", '-echo' => "*"); } print STDERR "\n"; my $ldap = Net::LDAP->new($ldapURI, # 'debug' => 15 ); if (!defined($ldap)) { logger('ERROR',color('magenta')."Failed to setup LDAP object '%s'".color('reset'),$@); exit 2; } # Bind my $mesg = $ldap->bind("cn=$username,ou=Users,$ldapBase",password => $password); # Search $mesg = $ldap->search( base => "ou=Servers,$ldapBase", filter => "(|(cn=$loginHost)(awitLoginHost=$loginHost)(awitLoginHostAlias=$loginHost))", ); # Check for error 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); } else { logger('ERROR',color('magenta')."LDAP returned '%s'".color('reset'),$mesgCode); } exit 2; } # 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) { my $counter = 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"; exit 3; } $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 host $loginHost = $ldapEntryName; if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) { logger('INFO'," - Host ".color('green')."%s".color('reset')." (awitLoginHost)",$ldapLoginHost); $loginHost = $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); $loginPort = $ldapLoginPort if (!defined($loginPort)); } # Check if we need to set the username if (my $ldapLoginUsername = $ldapEntry->get_value('awitLoginUsername')) { logger('INFO'," - Username ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername); $loginUsername = $ldapLoginUsername if (!defined($loginUsername)); } # 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); } # 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',"Wiki Page"); logger('INFO'," ".color('green')."%s".color('reset'),$ldapLoginWikiPage); } print STDERR "\n"; } # TODO for forwarding my @sshArgs = (); if (defined($pkcsProvider) && $pkcsProvider ne "") { push(@sshArgs,'-I',$pkcsProvider); } # Check if we have a port defined, if so specify it if (defined($loginPort)) { push(@sshArgs,'-p',$loginPort); } # Check if we have a different username defined to login as if (defined($loginUsername)) { push(@sshArgs,'-l',$loginUsername); } # 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); } # If the server is ancient, we need to enable DSS if (defined($needDSS)) { logger('WARNING',color('red')."Host needs ssh-dss".color('reset')); push(@sshArgs,'-o','PubkeyAcceptedKeyTypes=+ssh-dss'); push(@sshArgs,'-o','HostbasedKeyTypes=+ssh-dss'); push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss'); } logger('NOTICE',"Connecting to host '".color('green')."$loginHost".color('reset')."'" . (defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . "...\n\n\n"); # Fixup environment $ENV{'LANG'} = "en_US.UTF-8"; delete($ENV{'LC_ALL'}); delete($ENV{'LC_TIME'}); delete($ENV{'LC_CTYPE'}); # 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); } exec('/usr/bin/ssh', # Try our key only, we should never need to fall back to password '-o','PreferredAuthentications=publickey', '-o','StrictHostKeyChecking=ask', # Use TCP keepalive '-o','TCPKeepAlive=yes', '-o','ServerAliveInterval=5', '-o','ServerAliveCountMax=24', # 120s # Timeout for our connect '-o','ConnectTimeout=30', # Use basic compression '-o','Compression=yes', '-o','CompressionLevel=1', # Fail if we cannot forward ports '-o','ExitOnForwardFailure=yes', '-o','ControlMaster=autoask', '-o','ControlPath=~/.ssh/awit-ssh-master-%C', @sshArgs, $loginHost ); exit 0; # # Internal Functions # # Log something sub logger { my ($level,$arg1,@args) = @_; printf(STDERR '%-7s: '.$arg1.color('reset')."\n",$level,@args); return; } # Display version sub displayVersion { print("Version: $VERSION\n"); return; } # Display usage sub displayHelp { print(STDERR<<EOF); Usage: $0 <options> [USER@]HOST 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 return; }