Skip to content

Commit 65bbdff

Browse files
authored
Feature/custom outbound security group (#62)
* make egress and service ports configurable; create example * fix formatting, obsolete explicit manual dependencies * fix up documentation for 8.1 release
1 parent 6b5ac21 commit 65bbdff

File tree

20 files changed

+333
-44
lines changed

20 files changed

+333
-44
lines changed

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
# This Terraform deploys a stateless containerised sshd bastion service on AWS with IAM based authentication:
22

3-
**This module requires Terraform >/=0.15/1.x.x**
4-
5-
- Terraform 0.13.x was _previously_ supported with module version to ~> v6.1
6-
- Terraform 0.12.x was _previously_ supported with module version to ~> v5.0
7-
- Terraform 0.11.x was _previously_ supported with module version to ~> v4.0
3+
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
84

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

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

2723
# Quick start
2824

29-
Ivan Mesic has kindly contributed an example use of this module creating a VPC and a bastion instance within it - see `/examples`
25+
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`
26+
27+
`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.
3028

3129
# Custom sections:
3230

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

4947
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.
5048

49+
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!
50+
51+
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`
52+
53+
**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.
54+
5155
# Ability to assume a role in another account
5256

5357
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.
@@ -205,12 +209,6 @@ Starting with release 3.8 it is possible to use the output giving the name of th
205209

206210
- 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.
207211

208-
# Notes for deployment
209-
210-
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.
211-
212-
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!
213-
214212
## Components (using default userdata)
215213

216214
**EC2 Host OS (debian) with:**
@@ -260,6 +258,7 @@ The DNS entry (if created) for the service is also displayed as an output of the
260258
```terraform
261259
name = "${var.environment_name}-${data.aws_region.current.name}-${var.vpc}-bastion-service.${var.dns_domain}"
262260
```
261+
263262
## Inputs and Outputs
264263

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

269268
| Name | Version |
270269
|------|---------|
271-
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.15 |
270+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.2.0 |
272271

273272
## Providers
274273

275274
| Name | Version |
276275
|------|---------|
277-
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |
278-
| <a name="provider_cloudinit"></a> [cloudinit](#provider\_cloudinit) | n/a |
276+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.22.0 |
277+
| <a name="provider_cloudinit"></a> [cloudinit](#provider\_cloudinit) | 2.2.0 |
279278

280279
## Modules
281280

@@ -333,13 +332,15 @@ No modules.
333332
| <a name="input_bastion_host_name"></a> [bastion\_host\_name](#input\_bastion\_host\_name) | The hostname to give to the bastion instance | `string` | `""` | no |
334333
| <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 |
335334
| <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 |
335+
| <a name="input_bastion_service_port"></a> [bastion\_service\_port](#input\_bastion\_service\_port) | Port for containerised ssh daemon | `number` | `22` | no |
336336
| <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 |
337337
| <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 |
338338
| <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 |
339339
| <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 |
340340
| <a name="input_custom_ami_id"></a> [custom\_ami\_id](#input\_custom\_ami\_id) | id for custom ami if used | `string` | `""` | no |
341341
| <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 |
342342
| <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 |
343+
| <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 |
343344
| <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 |
344345
| <a name="input_custom_systemd"></a> [custom\_systemd](#input\_custom\_systemd) | any value excludes default systemd and hostname change from userdata | `string` | `""` | no |
345346
| <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 |
@@ -348,7 +349,7 @@ No modules.
348349
| <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 |
349350
| <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 |
350351
| <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 |
351-
| <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 |
352+
| <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 |
352353
| <a name="input_lb_healthy_threshold"></a> [lb\_healthy\_threshold](#input\_lb\_healthy\_threshold) | Healthy threshold for lb target group | `string` | `"2"` | no |
353354
| <a name="input_lb_interval"></a> [lb\_interval](#input\_lb\_interval) | interval for lb target group health check | `string` | `"30"` | no |
354355
| <a name="input_lb_is_internal"></a> [lb\_is\_internal](#input\_lb\_is\_internal) | whether the lb will be internal | `string` | `false` | no |

changelog.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# 8.1
2+
3+
- **Feature:** Make default permissive outbound security group rule creation conditional: `var.custom_outbound_security_group` `type = bool`. Historic behaviour is followed by default
4+
- **Feature:** Make bastion service port configurable: `var.bastion_service_port` `type = number`. Historic behaviour is followed by default
5+
- **Feature:** Add new `examples/custom_outbound_security_group` demonstrating use of above
6+
- **Change:** Increment required terraform version to >= 1.2.0 since we are not testing historic versions
7+
- **Change:** Increment suggested AWS provider to 4.22 (not hard enforce)
8+
- **Change:** Remove obsolete explicit manual dependencies from examples
9+
- **Change:** Remove obsolete quotes from interpolations in locals
10+
- **Change:** Tidy up Readme to include new options sensibly
11+
112
# 8.0
213

314
- **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
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
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`.:
2+
3+
* a new VPC,
4+
* private subnet(s) inside the VPC,
5+
* an internet gateway and route tables.
6+
7+
**Additionally** a custom external security group is substituted for that normally created by the module all external IP ingress/egress, permitting only:
8+
9+
VPC egress ports:
10+
11+
- 53(UDP)
12+
- 80(TCP)
13+
- 443(TCP)
14+
15+
open for 0.0.0.0/0
16+
17+
VPC ingress:
18+
19+
- port 443(TCP)
20+
21+
open for 0.0.0.0/0
22+
23+
ssh is configured in the container to run on port 443 so connect with
24+
25+
```bash
26+
ssh -p 443 user@load_balancer_dns_output_value
27+
```
28+
29+
## Requirements
30+
31+
| Name | Version |
32+
|------|---------|
33+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.2.0 |
34+
35+
## Providers
36+
37+
| Name | Version |
38+
|------|---------|
39+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.22.0 |
40+
41+
## Modules
42+
43+
| Name | Source | Version |
44+
|------|--------|---------|
45+
| <a name="module_ssh-bastion-service"></a> [ssh-bastion-service](#module\_ssh-bastion-service) | joshuamkite/ssh-bastion-service/aws | n/a |
46+
47+
## Resources
48+
49+
| Name | Type |
50+
|------|------|
51+
| [aws_internet_gateway.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource |
52+
| [aws_route.bastion-ipv4-out](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
53+
| [aws_route_table.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table) | resource |
54+
| [aws_route_table_association.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association) | resource |
55+
| [aws_security_group.custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
56+
| [aws_security_group_rule.in_443](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
57+
| [aws_security_group_rule.out_443](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
58+
| [aws_security_group_rule.out_53](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
59+
| [aws_security_group_rule.out_80_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
60+
| [aws_subnet.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource |
61+
| [aws_vpc.bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource |
62+
| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source |
63+
64+
## Inputs
65+
66+
| Name | Description | Type | Default | Required |
67+
|------|-------------|------|---------|:--------:|
68+
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | Default AWS region | `string` | `"eu-west-1"` | no |
69+
| <a name="input_bastion_service_port"></a> [bastion\_service\_port](#input\_bastion\_service\_port) | Port for containerised ssh daemon | `number` | `443` | no |
70+
| <a name="input_cidr-start"></a> [cidr-start](#input\_cidr-start) | Default CIDR block | `string` | `"10.50"` | no |
71+
| <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 |
72+
| <a name="input_environment_name"></a> [environment\_name](#input\_environment\_name) | n/a | `string` | `"demo"` | no |
73+
| <a name="input_everyone_cidr"></a> [everyone\_cidr](#input\_everyone\_cidr) | Everyone | `string` | `"0.0.0.0/0"` | no |
74+
| <a name="input_tags"></a> [tags](#input\_tags) | tags aplied to all resources | `map(string)` | `{}` | no |
75+
76+
## Outputs
77+
78+
| Name | Description |
79+
|------|-------------|
80+
| <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 |
81+
| <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 |
82+
| <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 |
83+
| <a name="output_lb_arn"></a> [lb\_arn](#output\_lb\_arn) | aws load balancer arn |
84+
| <a name="output_lb_dns_name"></a> [lb\_dns\_name](#output\_lb\_dns\_name) | aws load balancer dns |
85+
| <a name="output_lb_zone_id"></a> [lb\_zone\_id](#output\_lb\_zone\_id) | n/a |
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
resource "aws_security_group" "custom" {
2+
name = "custom"
3+
description = "custom security group"
4+
revoke_rules_on_delete = true
5+
vpc_id = aws_vpc.bastion.id
6+
tags = var.tags
7+
}
8+
9+
resource "aws_security_group_rule" "in_443" {
10+
type = "ingress"
11+
from_port = 443
12+
to_port = 443
13+
protocol = "tcp"
14+
cidr_blocks = var.custom_cidr
15+
security_group_id = aws_security_group.custom.id
16+
description = "custom security group rule"
17+
}
18+
19+
resource "aws_security_group_rule" "out_53" {
20+
type = "egress"
21+
from_port = 53
22+
to_port = 53
23+
protocol = "udp"
24+
cidr_blocks = ["0.0.0.0/0"]
25+
security_group_id = aws_security_group.custom.id
26+
description = "custom security group rule"
27+
}
28+
29+
resource "aws_security_group_rule" "out_80_tcp" {
30+
type = "egress"
31+
from_port = 80
32+
to_port = 80
33+
protocol = "tcp"
34+
cidr_blocks = ["0.0.0.0/0"]
35+
security_group_id = aws_security_group.custom.id
36+
description = "custom security group rule"
37+
}
38+
39+
resource "aws_security_group_rule" "out_443" {
40+
type = "egress"
41+
from_port = 443
42+
to_port = 443
43+
protocol = "tcp"
44+
cidr_blocks = ["0.0.0.0/0"]
45+
security_group_id = aws_security_group.custom.id
46+
description = "custom security group rule"
47+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
locals {
2+
default_tags = {
3+
Name = "bastion-service-${var.environment_name}"
4+
}
5+
}

0 commit comments

Comments
 (0)