Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create an egress proxy module #53

Merged
merged 7 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
path: ["cg_space", "clamav", "database", "domain", "redis", "s3"]
path: ["cg_space", "clamav", "database", "domain", "redis", "s3", "egress_proxy"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -28,7 +28,7 @@ jobs:
strategy:
fail-fast: false
matrix:
path: ["cg_space", "clamav", "database", "domain", "redis", "s3"]
path: ["cg_space", "clamav", "database", "domain", "redis", "s3", "egress_proxy"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,17 @@ jobs:
CF_PASSWORD: ${{ secrets.CF_PASSWORD }}
with:
path: ${{ matrix.module }}

test-egress-proxy:
runs-on: ubuntu-latest
name: Egress proxy integration test
env:
TERRAFORM_PRE_RUN: |
apt-get update
apt-get install -y zip
steps:
- uses: actions/checkout@v4
- name: terraform test egress_proxy
uses: dflook/terraform-test@v1
with:
path: egress_proxy
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# terraform-cloudgov

Terraform modules for working with cloud.gov commonly used by [18f/rails-template](https://github.com/18f/rails-template) based apps
Terraform modules for working with cloud.gov commonly used by [GSA-TTS/rails-template](https://github.com/GSA-TTS/rails-template) based apps

## Module Examples

Expand All @@ -10,7 +10,7 @@ Creates an RDS database based on the `rds_plan_name` variable and outputs the `i

```
module "database" {
source = "github.com/18f/terraform-cloudgov//database?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//database?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
Expand All @@ -32,7 +32,7 @@ Creates a Elasticache redis instance and outputs the `instance_id` for use elsew

```
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//redis?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
Expand All @@ -54,7 +54,7 @@ Creates an s3 bucket and outputs the `bucket_id` for use elsewhere.

```
module "s3" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//s3?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
Expand All @@ -79,7 +79,7 @@ Note that the domain must be created in cloud.gov by an OrgManager before this m

```
module "domain" {
source = "github.com/18f/terraform-cloudgov//domain?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//domain?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
Expand All @@ -101,7 +101,7 @@ Notes:

```
module "clamav" {
source = "github.com/18f/terraform-cloudgov//clamav?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//clamav?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
Expand Down Expand Up @@ -129,7 +129,7 @@ Creates a new cloud.gov space, such as when creating an egress space, and output

```
module "egress_space" {
source = "github.com/18f/terraform-cloudgov//cg_space?ref=v1.0.0"
source = "github.com/GSA-TTS/terraform-cloudgov//cg_space?ref=v1.1.0"

cf_org_name = local.cf_org_name
cf_space_name = "${local.cf_space_name}-egress"
Expand All @@ -145,6 +145,30 @@ module "egress_space" {
}
```

### egress_proxy

Creates and configures an instance of cg-egress-proxy to proxy traffic from your apps.

Prerequities:

* existing client_space with already deployed apps
* existing public-egress space to deploy the proxy into

```
module "egress_proxy" {
source = "github.com/GSA-TTS/terraform-cloudgov//egress_proxy?ref=v1.1.0"
zjrgov marked this conversation as resolved.
Show resolved Hide resolved

cf_org_name = local.cf_org_name
cf_space_name = "${local.cf_space_name}-egress"
client_space = local.cf_space_name
name = "egress-proxy"
allowlist = {
"source_app_name" = ["host.com:443", "otherhost.com:443"]
}
# see egress_proxy/variables.tf for full list of optional arguments
}
```

## Testing


Expand Down
4 changes: 4 additions & 0 deletions cg_space/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
output "space_id" {
value = cloudfoundry_space.space.id
}

output "space_name" {
value = cloudfoundry_space.space.name
}
5 changes: 5 additions & 0 deletions cg_space/tests/creation.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ run "test_space_creation" {
condition = cloudfoundry_space.space.name == var.cf_space_name
error_message = "Space name should match the cf_space_name variable"
}

assert {
condition = cloudfoundry_space.space.name == output.space_name
error_message = "Space name output must match the new space"
}
}

run "test_manager_only" {
Expand Down
5 changes: 5 additions & 0 deletions egress_proxy/acl.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
%{ for app, dests in list ~}
%{ for dest in dests ~}
${ split(":", dest)[0] }
%{ endfor ~}
%{ endfor ~}
127 changes: 127 additions & 0 deletions egress_proxy/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
locals {

# Make a clean list of the client apps for iteration purposes
clients = toset(keys(merge(var.allowlist, var.denylist)))

# Generate Caddy-compatible allow and deny ACLs, one target per line.
#
# For now, there's just one consolidated allowlist and denylist, no matter
# what apps they were specified for. Future improvments could improve this,
# but it would mean also changing the proxy to be both more complex (in terms
# of how the Caddyfile is constructed) and more discriminating (in terms of
# recognizing client apps based on GUIDs supplied by Envoy in request headers,
# as well as the destination ports). However, adding these improvements won't
# require modifying the module's interface, since we're already collecting
# that refined information.
allowacl = templatefile("${path.module}/acl.tftpl", { list = var.allowlist })
denyacl = templatefile("${path.module}/acl.tftpl", { list = var.denylist })
}

###
### Set up the authenticated egress application in the target space on apps.internal
###

data "cloudfoundry_domain" "internal" {
name = "apps.internal"
}

resource "cloudfoundry_route" "egress_route" {
space = data.cloudfoundry_space.egress_space.id
domain = data.cloudfoundry_domain.internal.id
hostname = substr("${var.cf_org_name}-${replace(var.cf_space_name, ".", "-")}-${var.name}", -63, -1)
# Yields something like: orgname-spacename-name.apps.internal, limited to the last 63 characters
}

resource "random_uuid" "username" {}
resource "random_password" "password" {
length = 16
special = false
}

data "cloudfoundry_space" "egress_space" {
org_name = var.cf_org_name
name = var.cf_space_name
}

# This zips up just the depoyable files from the specified gitref in the
# cg-egress-proxy repository
data "external" "proxyzip" {
program = ["/bin/sh", "prepare-proxy.sh"]
working_dir = path.module
query = {
gitref = var.gitref
}
}

resource "cloudfoundry_app" "egress_app" {
name = var.name
space = data.cloudfoundry_space.egress_space.id
path = "${path.module}/${data.external.proxyzip.result.path}"
source_code_hash = filesha256("${path.module}/${data.external.proxyzip.result.path}")
buildpack = "binary_buildpack"
command = "./caddy run --config Caddyfile"
memory = var.egress_memory
instances = var.instances
strategy = "rolling"

routes {
route = cloudfoundry_route.egress_route.id
}
environment = {
PROXY_PORTS : join(" ", var.allowports)
PROXY_ALLOW : local.allowacl
PROXY_DENY : local.denyacl
PROXY_USERNAME : random_uuid.username.result
PROXY_PASSWORD : random_password.password.result
}
}

###
### Set up network policies so that the clients can reach the proxy
###

data "cloudfoundry_space" "client_space" {
org_name = var.cf_org_name
name = var.client_space
}

data "cloudfoundry_app" "clients" {
for_each = local.clients
name_or_id = each.key
space = data.cloudfoundry_space.client_space.id
}

resource "cloudfoundry_network_policy" "client_routing" {
for_each = local.clients
policy {
source_app = data.cloudfoundry_app.clients[each.key].id
destination_app = cloudfoundry_app.egress_app.id
port = "61443"
}
}

###
### Create a credential service for bound clients to use when make requests of the proxy
###
locals {
https_proxy = "https://${random_uuid.username.result}:${random_password.password.result}@${cloudfoundry_route.egress_route.endpoint}:61443"
domain = cloudfoundry_route.egress_route.endpoint
username = random_uuid.username.result
password = random_password.password.result
protocol = "https"
port = 61443
app_id = cloudfoundry_app.egress_app.id
}

resource "cloudfoundry_user_provided_service" "credentials" {
name = "${var.name}-creds"
space = data.cloudfoundry_space.client_space.id
credentials = {
"uri" = local.https_proxy
"domain" = local.domain
"username" = local.username
"password" = local.password
"protocol" = local.protocol
"port" = local.port
}
}
29 changes: 29 additions & 0 deletions egress_proxy/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
output "https_proxy" {
value = local.https_proxy
sensitive = true
}

output "domain" {
value = local.domain
}

output "username" {
value = local.username
}

output "password" {
value = local.password
sensitive = true
}

output "protocol" {
value = local.protocol
}

output "app_id" {
value = local.app_id
}

output "port" {
value = local.port
}
25 changes: 25 additions & 0 deletions egress_proxy/prepare-proxy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/sh

# Exit if any step fails
set -e

eval "$(jq -r '@sh "GITREF=\(.gitref)"')"
mogul marked this conversation as resolved.
Show resolved Hide resolved

popdir=$(pwd)

# Portable construct so this will work everywhere
# https://unix.stackexchange.com/a/84980
tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
cd "$tmpdir"

# Grab a copy of the zip file for the specified ref
curl -s -L https://github.com/GSA-TTS/cg-egress-proxy/archive/${GITREF}.zip --output local.zip

# Zip up just the proxy/ subdirectory for pushing
unzip -q -u local.zip \*/proxy/\*
zip -q -j -r ${popdir}/proxy.zip cg-egress-proxy-*/proxy

# Tell Terraform where to find it
cat << EOF
{ "path": "proxy.zip" }
EOF
9 changes: 9 additions & 0 deletions egress_proxy/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
terraform {
required_version = "~> 1.0"
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = ">=0.53.1"
}
}
}
46 changes: 46 additions & 0 deletions egress_proxy/tests/creation.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
mock_provider "cloudfoundry" {}

variables {
cf_org_name = "gsa-tts-devtools-prototyping"
cf_space_name = "terraform-cloudgov-ci-tests-egress"
client_space = "terraform-cloudgov-ci-tests"
name = "terraform-egress-app"
allowlist = { "continuous_monitoring-staging" = ["raw.githubusercontent.com:443"] }
}

run "test_proxy_creation" {
assert {
condition = output.https_proxy == "https://${output.username}:${output.password}@${output.domain}:61443"
error_message = "HTTPS_PROXY output must match the correct form, got ${nonsensitive(output.https_proxy)}"
}

assert {
condition = output.domain == cloudfoundry_route.egress_route.endpoint
error_message = "Output domain must match the route endpoint"
}

assert {
condition = output.username == random_uuid.username.result
error_message = "Output username must come from the random_uuid resource"
}

assert {
condition = output.password == random_password.password.result
error_message = "Output password must come from the random_password resource"
}

assert {
condition = output.protocol == "https"
error_message = "protocol only supports https"
}

assert {
condition = output.app_id == cloudfoundry_app.egress_app.id
error_message = "Output app_id is the egress_app's ID"
}

assert {
condition = output.port == 61443
error_message = "port only supports 61443 internal https listener"
}
}
Loading