-
-
Save 40a/c2257e31abf1b7c0b8ecba6dc35f4f3c 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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}\""); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment