Skip to content

Commit

Permalink
Feature/custom outbound security group (#62)
Browse files Browse the repository at this point in the history
* make egress and service ports configurable; create example

* fix formatting, obsolete explicit manual dependencies

* fix up documentation for 8.1 release
  • Loading branch information
joshuamkite authored Jul 30, 2022
1 parent 6b5ac21 commit 65bbdff
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 44 deletions.
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
# This Terraform deploys a stateless containerised sshd bastion service on AWS with IAM based authentication:

**This module requires Terraform >/=0.15/1.x.x**

- Terraform 0.13.x was _previously_ supported with module version to ~> v6.1
- Terraform 0.12.x was _previously_ supported with module version to ~> v5.0
- Terraform 0.11.x was _previously_ supported with module version to ~> v4.0
This module requires Terraform >/=1.2.0 Older versions were previously supported going back to Terraform 0.11.x with module version to ~> v4.0

**N.B. If you are using a newer version of this module when you have an older version deployed, please review the changelog!**

Expand All @@ -26,7 +22,9 @@ You may find it more convenient to call it in your plan [directly from the Terra

# Quick start

Ivan Mesic has kindly contributed an example use of this module creating a VPC and a bastion instance within it - see `/examples`
Ivan Mesic has kindly contributed an example use of this module creating a VPC and a bastion instance within it - see `/examples/full-with-public-ip`

`examples/custom-outbound-security-group` is for more specialist use cases demonstrating how to run the service on a different port with an external supplied security group for external ingress and egress. This would not be necessary for most users.

# Custom sections:

Expand All @@ -48,6 +46,12 @@ The variables for these sections are:

If you exclude any section then you must replace it with equivalent functionality, either in your base AMI or `extra_user_data*` for a working service. Especially if you are not replacing all sections then be mindful that the systemd service expects docker to be installed and to be able to call the docker container as `sshd_worker`. The service container in turn references the `ssh_populate` script which calls `iam-authorized-keys` from a specific location.

You can **supply a list of one or more security groups to attach to the host instance launch configuration** within the module if you wish. This can be supplied together with or instead of a whitelisted range of CIDR blocks. Starting with release 8.1 it is possible to use this to also passlist egress ports **exclusively** if `var.custom_outbound_security_group = true` (default false). It may be useful in an enterprise setting to have security groups with rules managed separately from the bastion plan but of course if you do not assign either a suitable security group or whitelist then you may not be able to reach the service!

Starting with release 8.1 it is possible to **assign a custom port for the containerised ssh bastion service**, e.g. port 443. This may be useful for advanced users and must match security group ingress and egress- see `examples/custom-outbound-security-group`

**Load Balancer health check port may be optionally set to either the containerised service port (by default port 22) or port 2222 (EC2 host sshd)**. Port 2222 is the default. If you are deploying a large number of bastion instances, all of them checking into the same parent account for IAM queries in response to load balancer health checks on port 22 causes IAM rate limiting from AWS. Using the modified EC2 host sshd of port 2222 avoids this issue, is recommended for larger deployments and is now default. The host sshd is set to port 2222 as part of the service setup so this healthcheck is not entirely invalid. Security group rules, target groups and load balancer listeners are conditionally created to support any combination of access/healthcheck on port 2222 or not.

# Ability to assume a role in another account

The ability to assume a role to source IAM users from another account has been integrated with conditional logic. If you supply the ARN for a role for the bastion service to assume (typically in another account) ${var.assume_role_arn} then this plan will create an instance profile, role and policy along with each bastion to make use of it. A matching sample policy and trust relationship is given as an output from the plan to assist with application in the other account. If you do not supply this arn then this plan presumes IAM lookups in the same account and creates an appropriate instance profile, role and policies for each bastion in the same AWS account. 'Each bastion' here refers to a combination of environment, AWS account, AWS region and VPCID determined by deployment. This is a high availability service, but if you are making more than one independent deployment using this same module within such a combination then you can specify "service_name" to avoid resource collision.
Expand Down Expand Up @@ -205,12 +209,6 @@ Starting with release 3.8 it is possible to use the output giving the name of th

- ssh keys are called only at login- if an account or ssh public key is deleted from AWS whilst a user is logged in then that session will continue until otherwise terminated.

# Notes for deployment

Load Balancer health check port may be optionally set to either port 22 (containerised service) or port 2222 (EC2 host sshd). Port 2222 is the default. If you are deploying a large number of bastion instances, all of them checking into the same parent account for IAM queries in response to load balancer health checks on port 22 causes IAM rate limiting from AWS. Using the modified EC2 host sshd of port 2222 avoids this issue, is recommended for larger deployments and is now default. The host sshd is set to port 2222 as part of the service setup so this healthcheck is not entirely invalid. Security group rules, target groups and load balancer listeners are conditionally created to support any combination of access/healthcheck on port 2222 or not.

You can supply list of one or more security groups to attach to the host instance launch configuration within the module if you wish. This can be supplied together with or instead of a whitelisted range of CIDR blocks. It may be useful in an enterprise setting to have security groups with rules managed separately from the bastion plan but of course if you do not assign either a suitable security group or whitelist then you may not be able to reach the service!

## Components (using default userdata)

**EC2 Host OS (debian) with:**
Expand Down Expand Up @@ -260,6 +258,7 @@ The DNS entry (if created) for the service is also displayed as an output of the
```terraform
name = "${var.environment_name}-${data.aws_region.current.name}-${var.vpc}-bastion-service.${var.dns_domain}"
```

## Inputs and Outputs

These have been generated with [terraform-docs](https://github.com/segmentio/terraform-docs)
Expand All @@ -268,14 +267,14 @@ These have been generated with [terraform-docs](https://github.com/segmentio/ter

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.15 |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.2.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |
| <a name="provider_cloudinit"></a> [cloudinit](#provider\_cloudinit) | n/a |
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.22.0 |
| <a name="provider_cloudinit"></a> [cloudinit](#provider\_cloudinit) | 2.2.0 |

## Modules

Expand Down Expand Up @@ -333,13 +332,15 @@ No modules.
| <a name="input_bastion_host_name"></a> [bastion\_host\_name](#input\_bastion\_host\_name) | The hostname to give to the bastion instance | `string` | `""` | no |
| <a name="input_bastion_instance_types"></a> [bastion\_instance\_types](#input\_bastion\_instance\_types) | List of ec2 types for the bastion host, used by aws\_launch\_template (first from the list) and in aws\_autoscaling\_group | `list` | <pre>[<br> "t3.small",<br> "t3.medium",<br> "t3.large"<br>]</pre> | no |
| <a name="input_bastion_service_host_key_name"></a> [bastion\_service\_host\_key\_name](#input\_bastion\_service\_host\_key\_name) | AWS ssh key *.pem to be used for ssh access to the bastion service host | `string` | `""` | no |
| <a name="input_bastion_service_port"></a> [bastion\_service\_port](#input\_bastion\_service\_port) | Port for containerised ssh daemon | `number` | `22` | no |
| <a name="input_bastion_vpc_name"></a> [bastion\_vpc\_name](#input\_bastion\_vpc\_name) | define the last part of the hostname, by default this is the vpc ID with magic default value of 'vpc\_id' but you can pass a custom string, or an empty value to omit this | `string` | `"vpc_id"` | no |
| <a name="input_cidr_blocks_whitelist_host"></a> [cidr\_blocks\_whitelist\_host](#input\_cidr\_blocks\_whitelist\_host) | range(s) of incoming IP addresses to whitelist for the HOST | `list(string)` | `[]` | no |
| <a name="input_cidr_blocks_whitelist_service"></a> [cidr\_blocks\_whitelist\_service](#input\_cidr\_blocks\_whitelist\_service) | range(s) of incoming IP addresses to whitelist for the SERVICE | `list(string)` | `[]` | no |
| <a name="input_container_ubuntu_version"></a> [container\_ubuntu\_version](#input\_container\_ubuntu\_version) | ubuntu version to use for service container | `string` | `"22.04"` | no |
| <a name="input_custom_ami_id"></a> [custom\_ami\_id](#input\_custom\_ami\_id) | id for custom ami if used | `string` | `""` | no |
| <a name="input_custom_authorized_keys_command"></a> [custom\_authorized\_keys\_command](#input\_custom\_authorized\_keys\_command) | any value excludes default Go binary iam-authorized-keys built from source from userdata | `string` | `""` | no |
| <a name="input_custom_docker_setup"></a> [custom\_docker\_setup](#input\_custom\_docker\_setup) | any value excludes default docker installation and container build from userdata | `string` | `""` | no |
| <a name="input_custom_outbound_security_group"></a> [custom\_outbound\_security\_group](#input\_custom\_outbound\_security\_group) | don't create default outgoing permissive security group rule - will only work with custom AMI or if security group supplied with ports 53(UDP); 80(TCP); 443(TCP) open for 0.0.0.0/0 egress | `bool` | `false` | no |
| <a name="input_custom_ssh_populate"></a> [custom\_ssh\_populate](#input\_custom\_ssh\_populate) | any value excludes default ssh\_populate script used on container launch from userdata | `string` | `""` | no |
| <a name="input_custom_systemd"></a> [custom\_systemd](#input\_custom\_systemd) | any value excludes default systemd and hostname change from userdata | `string` | `""` | no |
| <a name="input_delete_network_interface_on_termination"></a> [delete\_network\_interface\_on\_termination](#input\_delete\_network\_interface\_on\_termination) | if network interface created for bastion host should be deleted when instance in terminated. Setting propagated to aws\_launch\_template.network\_interfaces.delete\_on\_termination | `bool` | `true` | no |
Expand All @@ -348,7 +349,7 @@ No modules.
| <a name="input_extra_user_data_content"></a> [extra\_user\_data\_content](#input\_extra\_user\_data\_content) | Extra user-data to add to the default built-in | `string` | `""` | no |
| <a name="input_extra_user_data_content_type"></a> [extra\_user\_data\_content\_type](#input\_extra\_user\_data\_content\_type) | What format is content in - eg 'text/cloud-config' or 'text/x-shellscript' | `string` | `"text/x-shellscript"` | no |
| <a name="input_extra_user_data_merge_type"></a> [extra\_user\_data\_merge\_type](#input\_extra\_user\_data\_merge\_type) | Control how cloud-init merges user-data sections | `string` | `"str(append)"` | no |
| <a name="input_lb_healthcheck_port"></a> [lb\_healthcheck\_port](#input\_lb\_healthcheck\_port) | TCP port to conduct lb target group healthchecks. Acceptable values are 22 or 2222 | `string` | `"2222"` | no |
| <a name="input_lb_healthcheck_port"></a> [lb\_healthcheck\_port](#input\_lb\_healthcheck\_port) | TCP port to conduct lb target group healthchecks. Acceptable values are 2222 or the value defined for `bastion_service_port` | `string` | `"2222"` | no |
| <a name="input_lb_healthy_threshold"></a> [lb\_healthy\_threshold](#input\_lb\_healthy\_threshold) | Healthy threshold for lb target group | `string` | `"2"` | no |
| <a name="input_lb_interval"></a> [lb\_interval](#input\_lb\_interval) | interval for lb target group health check | `string` | `"30"` | no |
| <a name="input_lb_is_internal"></a> [lb\_is\_internal](#input\_lb\_is\_internal) | whether the lb will be internal | `string` | `false` | no |
Expand Down
11 changes: 11 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# 8.1

- **Feature:** Make default permissive outbound security group rule creation conditional: `var.custom_outbound_security_group` `type = bool`. Historic behaviour is followed by default
- **Feature:** Make bastion service port configurable: `var.bastion_service_port` `type = number`. Historic behaviour is followed by default
- **Feature:** Add new `examples/custom_outbound_security_group` demonstrating use of above
- **Change:** Increment required terraform version to >= 1.2.0 since we are not testing historic versions
- **Change:** Increment suggested AWS provider to 4.22 (not hard enforce)
- **Change:** Remove obsolete explicit manual dependencies from examples
- **Change:** Remove obsolete quotes from interpolations in locals
- **Change:** Tidy up Readme to include new options sensibly

# 8.0

- **Change:** Defaults to Debian 11 (host) and Ubuntu 22.04 (Container). Alternative combinations, distributions and non-AMD64 platforms not tested at this time. Tested using
Expand Down
85 changes: 85 additions & 0 deletions examples/custom-outbound-security-group/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
This example for more advanced use shows a complete setup for a new `bastion` service with all needed parts using a single AWS account similar to the sibling `examples/full-with-public-ip`.:

* a new VPC,
* private subnet(s) inside the VPC,
* an internet gateway and route tables.

**Additionally** a custom external security group is substituted for that normally created by the module all external IP ingress/egress, permitting only:

VPC egress ports:

- 53(UDP)
- 80(TCP)
- 443(TCP)

open for 0.0.0.0/0

VPC ingress:

- port 443(TCP)

open for 0.0.0.0/0

ssh is configured in the container to run on port 443 so connect with

```bash
ssh -p 443 user@load_balancer_dns_output_value
```

## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.2.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.22.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_ssh-bastion-service"></a> [ssh-bastion-service](#module\_ssh-bastion-service) | joshuamkite/ssh-bastion-service/aws | n/a |

## Resources

| Name | Type |
|------|------|
| [aws_internet_gateway.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource |
| [aws_route.bastion-ipv4-out](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route_table.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource |
| [aws_route_table_association.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource |
| [aws_security_group.custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_security_group_rule.in_443](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.out_443](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.out_53](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.out_80_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_subnet.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource |
| [aws_vpc.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource |
| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | Default AWS region | `string` | `"eu-west-1"` | no |
| <a name="input_bastion_service_port"></a> [bastion\_service\_port](#input\_bastion\_service\_port) | Port for containerised ssh daemon | `number` | `443` | no |
| <a name="input_cidr-start"></a> [cidr-start](#input\_cidr-start) | Default CIDR block | `string` | `"10.50"` | no |
| <a name="input_custom_cidr"></a> [custom\_cidr](#input\_custom\_cidr) | CIDR for custom security gtoup ingress | `list(string)` | <pre>[<br> "0.0.0.0/0"<br>]</pre> | no |
| <a name="input_environment_name"></a> [environment\_name](#input\_environment\_name) | n/a | `string` | `"demo"` | no |
| <a name="input_everyone_cidr"></a> [everyone\_cidr](#input\_everyone\_cidr) | Everyone | `string` | `"0.0.0.0/0"` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | tags aplied to all resources | `map(string)` | `{}` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_bastion_service_role_name"></a> [bastion\_service\_role\_name](#output\_bastion\_service\_role\_name) | role created for service host asg - if created without assume role |
| <a name="output_bastion_sg_id_custom"></a> [bastion\_sg\_id\_custom](#output\_bastion\_sg\_id\_custom) | Custom (external) Security Group id of the bastion host |
| <a name="output_bastion_sg_id_module"></a> [bastion\_sg\_id\_module](#output\_bastion\_sg\_id\_module) | Sbuilt-in security Group id of the bastion host |
| <a name="output_lb_arn"></a> [lb\_arn](#output\_lb\_arn) | aws load balancer arn |
| <a name="output_lb_dns_name"></a> [lb\_dns\_name](#output\_lb\_dns\_name) | aws load balancer dns |
| <a name="output_lb_zone_id"></a> [lb\_zone\_id](#output\_lb\_zone\_id) | n/a |
47 changes: 47 additions & 0 deletions examples/custom-outbound-security-group/custom_security_group.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
resource "aws_security_group" "custom" {
name = "custom"
description = "custom security group"
revoke_rules_on_delete = true
vpc_id = aws_vpc.bastion.id
tags = var.tags
}

resource "aws_security_group_rule" "in_443" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.custom_cidr
security_group_id = aws_security_group.custom.id
description = "custom security group rule"
}

resource "aws_security_group_rule" "out_53" {
type = "egress"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.custom.id
description = "custom security group rule"
}

resource "aws_security_group_rule" "out_80_tcp" {
type = "egress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.custom.id
description = "custom security group rule"
}

resource "aws_security_group_rule" "out_443" {
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.custom.id
description = "custom security group rule"
}
5 changes: 5 additions & 0 deletions examples/custom-outbound-security-group/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
locals {
default_tags = {
Name = "bastion-service-${var.environment_name}"
}
}
Loading

0 comments on commit 65bbdff

Please sign in to comment.