Skip to content

Instantly share code, notes, and snippets.

@HSPDev
Created July 2, 2018 22:56
Show Gist options
  • Save HSPDev/74ad755060880b2c30ae9e9a6ed20eda to your computer and use it in GitHub Desktop.
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"
<?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}\"");
}
}
}
@obscurerichard
Copy link

@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
Copy link

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

@srikantgawai
Copy link

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