Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Complementary code and IAM policy for "You don't need that Bastion host"
<?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...");
}
}
<?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
]
];
<?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!");
}
}
{
"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": "*"
}
]
}
<?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

This comment has been minimized.

Copy link
Owner Author

commented Jul 3, 2018

@obscurerichard

This comment has been minimized.

Copy link

commented May 29, 2019

@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!

@obscurerichard

This comment has been minimized.

Copy link

commented Jun 11, 2019

@HSPDev Pretty please consider my request above. I'm quite persistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.