Created
July 2, 2018 22:56
-
-
Save HSPDev/74ad755060880b2c30ae9e9a6ed20eda to your computer and use it in GitHub Desktop.
Complementary code and IAM policy for "You don't need that Bastion host"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// For laravel 5 based systems | |
// /path/to/project/app/Console/Commands/AllowSSHFromIP.php | |
namespace App\Console\Commands; | |
use Aws\Ec2\Ec2Client; | |
use Carbon\Carbon; | |
use Illuminate\Console\Command; | |
class AllowSSHFromIP extends Command | |
{ | |
/** | |
* The name and signature of the console command. | |
* | |
* @var string | |
*/ | |
protected $signature = 'ssh:allow {ip?}'; | |
/** | |
* The console command description. | |
* | |
* @var string | |
*/ | |
protected $description = 'Allows SSH access from the specified IP'; | |
/** | |
* Create a new command instance. | |
* | |
*/ | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
/** | |
* Execute the console command. | |
* | |
* @return mixed | |
*/ | |
public function handle() | |
{ | |
$ip = $this->argument('ip'); | |
if(empty($ip)) | |
{ | |
$this->info('No IP Specified, grabbing the current one from api.ipify.org...'); | |
$ip = trim(file_get_contents('https://api.ipify.org/')); | |
$this->info("Current IP is: {$ip}"); | |
} | |
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 & FILTER_FLAG_NO_PRIV_RANGE & FILTER_FLAG_NO_RES_RANGE)) | |
{ | |
$this->error("The specified IP was invalid: {$ip}"); | |
return false; | |
} | |
$ip = $ip.'/32'; //This specific IP. | |
$ec2Client = Ec2Client::factory(array( | |
'version' => '2016-11-15', | |
'region' => config('aws.ssh.region'), | |
'credentials' => [ | |
'key' => config('aws.ssh.key'), | |
'secret' => config('aws.ssh.secret'), | |
] | |
)); | |
$securityGroupDescription = $ec2Client->describeSecurityGroups([ | |
'GroupIds' => [config('aws.ssh.group_id')] | |
]); | |
$permissions = $securityGroupDescription->get('SecurityGroups'); | |
if(count($permissions) != 1) | |
{ | |
$this->error("Expected precisely 1 security group, got : ".count($permissions)); | |
return false; | |
} | |
$permissions = $permissions[0] ?? []; | |
$permissions = $permissions['IpPermissions'] ?? []; | |
if(count($permissions) > 1) | |
{ | |
$this->error("Expected precisely 1 or 0 permission on group, got : ".count($permissions)); | |
return false; | |
} | |
$ipRules = $permissions[0] ?? []; | |
if(!empty($ipRules) && ($ipRules['FromPort'] !== 22 || $ipRules['ToPort'] !== 22)) | |
{ | |
$this->error("Rule has not been set up correctly and is allowing to other than port 22."); | |
return false; | |
} | |
//The actual CIDR blocks being allowed. | |
$ipRules = $ipRules['IpRanges'] ?? []; | |
$ipRulesCount = count($ipRules); | |
$this->info("Current source rules: {$ipRulesCount}."); | |
$doesCurrentIpExist = false; | |
foreach($ipRules as $rule) | |
{ | |
$loopSource = $rule['CidrIp'] ?? null; | |
$loopDescription = $rule['Description'] ?? ''; | |
$loopDate = ''; | |
//Parse the date | |
preg_match('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/m', $loopDescription, $matches, PREG_OFFSET_CAPTURE, 0); | |
$date = ($matches[0] ?? [])[0] ?? 'nope-format'; | |
try { | |
$date = Carbon::createFromFormat('Y-m-d H:i:s', $date); | |
} catch (\Exception $ex) { | |
$date = now()->subYears(5); | |
} | |
$loopDate = $date->format('Y-m-d H:i'); | |
//Check if older than a week. | |
$deleteRule = ($date < now()->subWeek()); | |
$this->info("\tSource: {$loopSource} created at: ${loopDate}. Delete: ".($deleteRule ? 'Yes':'No')); | |
if($loopSource == $ip && !$deleteRule) | |
{ | |
$this->info("\t\tCurrent IP being asked for found. We won't reinstate it."); | |
$doesCurrentIpExist = true; | |
} | |
if($deleteRule) | |
{ | |
try { | |
$ec2Client->revokeSecurityGroupIngress([ | |
'GroupId' => config('aws.ssh.group_id'), | |
'IpPermissions' => [ | |
[ | |
'IpProtocol' => 'tcp', | |
'FromPort' => config('aws.ssh.ssh_port'), | |
'ToPort' => config('aws.ssh.ssh_port'), | |
'IpRanges' => [ | |
[ | |
'CidrIp' => $loopSource, | |
'Description' => $loopDescription, | |
] | |
], | |
] | |
] | |
]); | |
$this->warn("\t\tDeleted {$loopSource} OK."); | |
} catch (\Exception $exception) | |
{ | |
$this->error("Trying to delete rule: {$loopSource} resulted in error: ".$exception->getMessage()); | |
} | |
} | |
} | |
if($doesCurrentIpExist) | |
{ | |
$this->info("The IP block {$ip} was found as a rule, we don't need to create it again."); | |
return true; | |
} | |
$result = null; | |
try { | |
$result = $ec2Client->authorizeSecurityGroupIngress([ | |
'GroupId' => config('aws.ssh.group_id'), | |
'IpPermissions' => [ | |
[ | |
'IpProtocol' => 'tcp', | |
'FromPort' => config('aws.ssh.ssh_port'), | |
'ToPort' => config('aws.ssh.ssh_port'), | |
'IpRanges' => [ | |
[ | |
'CidrIp' => $ip, | |
'Description' => 'Generated from CLI by: '.get_current_user(). ' at '.now()->format('Y-m-d H:i:s'), | |
] | |
], | |
] | |
] | |
]); | |
} catch (\Exception $ex) | |
{ | |
$this->error("We got an error from AWS: ".$ex->getMessage()); | |
return false; | |
} | |
if($result->get('@metadata')['statusCode'] !== 200) | |
{ | |
$this->error("Something went wrong... Check AWS Web Management..."); | |
return false; | |
} | |
$this->info("SSH access to AWS from {$ip} approved. Remember to delete the rule sometime..."); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// For laravel 5 based systems | |
// /path/to/project/config/aws.php | |
return [ | |
'ssh' => [ | |
'region' => 'YOUR_REGION', //e.g. eu-west-1 | |
'key' => 'YOUR_KEY', | |
'secret' => 'YOUR_SECRET', | |
'group_id' => 'sg-number_from_security_group_here', //e.g. sg-124532 | |
'ssh_port' => 22, //Be sure it matches IAM policy | |
] | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// For laravel 5 based systems | |
// /path/to/project/app/Console/Commands/ClearSSHAllowances.php | |
namespace App\Console\Commands; | |
use Aws\Ec2\Ec2Client; | |
use Carbon\Carbon; | |
use Illuminate\Console\Command; | |
class ClearSSHAllowances extends Command | |
{ | |
/** | |
* The name and signature of the console command. | |
* | |
* @var string | |
*/ | |
protected $signature = 'ssh:clear'; | |
/** | |
* The console command description. | |
* | |
* @var string | |
*/ | |
protected $description = 'Clears ALL allowed SSH rules.'; | |
/** | |
* Create a new command instance. | |
* | |
*/ | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
/** | |
* Execute the console command. | |
* | |
* @return mixed | |
*/ | |
public function handle() | |
{ | |
$ec2Client = Ec2Client::factory(array( | |
'version' => '2016-11-15', | |
'region' => config('aws.ssh.region'), | |
'credentials' => [ | |
'key' => config('aws.ssh.key'), | |
'secret' => config('aws.ssh.secret'), | |
] | |
)); | |
$securityGroupDescription = $ec2Client->describeSecurityGroups([ | |
'GroupIds' => [config('aws.ssh.group_id')] | |
]); | |
$permissions = $securityGroupDescription->get('SecurityGroups'); | |
if(count($permissions) != 1) | |
{ | |
$this->error("Expected precisely 1 security group, got : ".count($permissions)); | |
return false; | |
} | |
$permissions = $permissions[0] ?? []; | |
$permissions = $permissions['IpPermissions'] ?? []; | |
if(count($permissions) > 1) | |
{ | |
$this->error("Expected precisely 1 or 0 permission on group, got : ".count($permissions)); | |
return false; | |
} | |
$ipRules = $permissions[0] ?? []; | |
if(!empty($ipRules) && ($ipRules['FromPort'] !== 22 || $ipRules['ToPort'] !== 22)) | |
{ | |
$this->error("Rule has not been set up correctly and is allowing to other than port 22."); | |
return false; | |
} | |
//The actual CIDR blocks being allowed. | |
$ipRules = $ipRules['IpRanges'] ?? []; | |
$ipRulesCount = count($ipRules); | |
$this->info("Current source rules: {$ipRulesCount}."); | |
foreach($ipRules as $rule) | |
{ | |
$loopSource = $rule['CidrIp'] ?? null; | |
$loopDescription = $rule['Description'] ?? ''; | |
$this->info("\tSource: {$loopSource}, shall be deleted."); | |
try { | |
$ec2Client->revokeSecurityGroupIngress([ | |
'GroupId' => config('aws.ssh.group_id'), | |
'IpPermissions' => [ | |
[ | |
'IpProtocol' => 'tcp', | |
'FromPort' => config('aws.ssh.ssh_port'), | |
'ToPort' => config('aws.ssh.ssh_port'), | |
'IpRanges' => [ | |
[ | |
'CidrIp' => $loopSource, | |
'Description' => $loopDescription, | |
] | |
], | |
] | |
] | |
]); | |
$this->warn("\t\tDeleted {$loopSource} OK."); | |
} catch (\Exception $exception) | |
{ | |
$this->error("Trying to delete rule: {$loopSource} resulted in error: ".$exception->getMessage()); | |
return false; | |
} | |
} | |
$this->info("All SSH access rules deleted OK!"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Sid": "VisualEditor0", | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:RevokeSecurityGroupIngress", | |
"ec2:AuthorizeSecurityGroupIngress", | |
"ec2:UpdateSecurityGroupRuleDescriptionsIngress" | |
], | |
"Resource": "arn:aws:ec2:*:*:security-group/sg-number_from_security_group_here" | |
}, | |
{ | |
"Sid": "VisualEditor1", | |
"Effect": "Allow", | |
"Action": "ec2:DescribeSecurityGroups", | |
"Resource": "*" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// For laravel 5 based systems | |
// /path/to/project/app/Console/Commands/ListAllSSHAllowances.php | |
namespace App\Console\Commands; | |
use Aws\Ec2\Ec2Client; | |
use Carbon\Carbon; | |
use Illuminate\Console\Command; | |
class ListAllSSHAllowances extends Command | |
{ | |
/** | |
* The name and signature of the console command. | |
* | |
* @var string | |
*/ | |
protected $signature = 'ssh:list'; | |
/** | |
* The console command description. | |
* | |
* @var string | |
*/ | |
protected $description = 'Lists ALL allowed SSH rules.'; | |
/** | |
* Create a new command instance. | |
* | |
*/ | |
public function __construct() | |
{ | |
parent::__construct(); | |
} | |
/** | |
* Execute the console command. | |
* | |
* @return mixed | |
*/ | |
public function handle() | |
{ | |
$ec2Client = Ec2Client::factory(array( | |
'version' => '2016-11-15', | |
'region' => config('aws.ssh.region'), | |
'credentials' => [ | |
'key' => config('aws.ssh.key'), | |
'secret' => config('aws.ssh.secret'), | |
] | |
)); | |
$securityGroupDescription = $ec2Client->describeSecurityGroups([ | |
'GroupIds' => [config('aws.ssh.group_id')] | |
]); | |
$permissions = $securityGroupDescription->get('SecurityGroups'); | |
if(count($permissions) != 1) | |
{ | |
$this->error("Expected precisely 1 security group, got : ".count($permissions)); | |
return false; | |
} | |
$permissions = $permissions[0] ?? []; | |
$permissions = $permissions['IpPermissions'] ?? []; | |
if(count($permissions) > 1) | |
{ | |
$this->error("Expected precisely 1 or 0 permission on group, got : ".count($permissions)); | |
return false; | |
} | |
$ipRules = $permissions[0] ?? []; | |
if(!empty($ipRules) && ($ipRules['FromPort'] !== 22 || $ipRules['ToPort'] !== 22)) | |
{ | |
$this->error("Rule has not been set up correctly and is allowing to other than port 22."); | |
return false; | |
} | |
//The actual CIDR blocks being allowed. | |
$ipRules = $ipRules['IpRanges'] ?? []; | |
$ipRulesCount = count($ipRules); | |
$this->info("Current source rules: {$ipRulesCount}."); | |
foreach($ipRules as $rule) | |
{ | |
$loopSource = $rule['CidrIp'] ?? null; | |
$loopDescription = $rule['Description'] ?? ''; | |
$loopDate = ''; | |
//Parse the date | |
preg_match('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/m', $loopDescription, $matches, PREG_OFFSET_CAPTURE, 0); | |
$date = ($matches[0] ?? [])[0] ?? 'nope-format'; | |
try { | |
$date = Carbon::createFromFormat('Y-m-d H:i:s', $date); | |
$loopDate = $date->diffForHumans(); | |
} catch (\Exception $ex) { | |
$date = null; | |
$loopDate = '(unknown)'; | |
} | |
$this->info("\tSource: {$loopSource}, created: {$loopDate}."); | |
$this->info("\t\tDescription: \"{$loopDescription}\""); | |
} | |
} | |
} |
@HSPDev Would you mind adding an explicit copyright statement and license on the gist with the security group management code, please? The gist says it is “Complementary” but that won’t satisfy people who are sticklers for tracking the provenance of code they use.
I’d suggest an MIT license or the Creative Commons CC0 public domain dedication.
Thank you!
@HSPDev Pretty please consider my request above. I'm quite persistent.
Does it work with Window Machines for port 3389?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For people coming in the wrong way around:
https://medium.com/@henriksylvesterpedersen/you-dont-need-that-bastion-host-cd1b1717a9e7