├── .gitignore ├── README.md ├── gigaproxy.py └── terraform ├── api-gateway.tf ├── eventbridge.tf ├── forwarder-lambda.tf ├── optional-proxy-terraform ├── README.md ├── main.tf ├── outputs.tf ├── proxy-instance-bootstrap.sh └── variables.tf ├── outputs.tf ├── provider.tf ├── proxy-instance.tf ├── src ├── dependencies.zip └── forwarder-function │ └── lambda_function.py ├── terraform.tfvars.example └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .venv 3 | .vscode 4 | .idea 5 | .DS_Store 6 | .terraform/ 7 | *.tfstate 8 | *.tfstate.backup 9 | *.tfvars 10 | builds/ 11 | 12 | # GET YOUR OWN TF PROVIDER HASHES, FREELOADER 13 | .terraform.lock.hcl 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

gigaproxy

2 | 3 |

The Giga Proxy

4 | 5 | ## Why? 6 | fireprox is great but has one major downside. You can only target a single host at a time. 7 | 8 | Gigaproxy solves this. Check out the blog post [One Proxy to Rule Them All](https://www.sprocketsecurity.com/resources/gigaproxy) for more details on how it works. 9 | 10 | ## Getting Started 11 | 12 | To use this project and the built-in `gigaproxy.py` script, you will need the following: 13 | 14 | - An AWS account with AWS credentials stored locally (AWS SSO session, static access keys, etc.) 15 | - HashiCorp [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) installed 16 | - Python and [pipx](https://github.com/pypa/pipx) installed 17 | - [mitmproxy](https://docs.mitmproxy.org/stable/overview-installation/) installed and in your path 18 | - mitmproxy [certificates](https://docs.mitmproxy.org/stable/concepts-certificates/) installed and trusted on your system 19 | 20 | 21 | ## Build the Infrastructure 22 | 23 | First, optionally update the `terraform/variables.tf` with an API key that will be required to authenticate to the generated API gateway endpoint. **If you don't set this yourself, you must go into the AWS console and get it. We recommend specifying your own!** 24 | 25 | To build the infrastructure, you can use the following commands: 26 | 27 | ```bash 28 | cd terraform/ 29 | terraform init 30 | terraform plan # optional: if you want to see what's going to be built before running, apply 31 | terraform apply 32 | ``` 33 | 34 | Look for the output `api-endpoint` in your terminal after applying. 35 | 36 | 37 | ## Starting the Proxy 38 | 39 | The proxy is started via the command line with arguments to specify the API endpoint and an API key. 40 | 41 | ```bash 42 | mitmdump -s gigaproxy.py --set auth_token= --set proxy_endpoint= 43 | ``` 44 | 45 | There are also a couple of secret options that you can use if you read the code. 46 | 47 | If you run this on a VPS somewhere, we recommend tossing it in a tmux or screen session because it will take over your terminal. 48 | 49 | Note that you can specify a custom port and host to listen on. By default, mitmdump will listen on 127.0.0.1:8080. 50 | 51 | For example: 52 | 53 | ```bash 54 | mitmdump -s gigaproxy.py --set auth_token= --set proxy_endpoint= --listen-host 0.0.0.0 --listen-port 8888 55 | ``` 56 | 57 | ## Optional Proxy Instance 58 | 59 | If you run into issues with installing/configuring mitmproxy on a host, we provide the option to deploy an EC2 instance along with the rest of the Gigaproxy Terraform build that will install and run mitmproxy automatically. All you need to do is point to the public IP address of the EC2 host instead of `localhost` when proxying requests. 60 | 61 | To deploy this host, edit the `terraform/terraform.tfvars.example` file with the following changes: 62 | - remove the `.example` extension from the end of the filename i.e. `terraform.tfvars.example` -> `terraform.tfvars` 63 | - change `optional_proxy_instance` to `true` 64 | - put your own public IP address in for the value of `proxy_inbound_ip_allowed`, including the netmask (e.g. `"x.x.x.x/32"`) 65 | - this value is very important as it will control the security group that gives access to your proxy instance -- **IF YOU LEAVE THIS OPEN TO 0.0.0.0/0 AND SOMEONE FIGURES OUT IT'S A PROXY, THEY CAN ROUTE THEIR TRAFFIC THROUGH YOUR GIGAPROXY INFRA** 66 | - put the **public** key of an SSH key pair for the value of `proxy_public_ssh_key` (e.g. `ssh-rsa AAAA...`) 67 | - although if everything works as desired you *shouldn't* need to SSH into the proxy instance, this gives you the ability to troubleshoot/modify the host as you want 68 | 69 | After editing the above values appropriately, you can re-run `terraform apply` as in the above *Build The Infrastructure* section. The public IP address of the proxy EC2 instance will be displayed in your terminal output. 70 | 71 | **You will still need to install the mitmproxy certificate on client devices, or disable certificate/ssl/tls verification on your tooling.** 72 | 73 | example command run locally with cert validation disabled: `curl -x http://PUBLIC_IP_OF_PROXY_INSTANCE:8888 -k https://ipv4.rawrify.com/ip` 74 | 75 | example command run locally with normal parameters (cert successfully installed on client device): `curl -x http://PUBLIC_IP_OF_PROXY_INSTANCE:8888 https://ipv4.rawrify.com/ip` 76 | 77 | Some notes on the EC2 instance: 78 | - As mentioned above, access is controlled to the EC2 instance via a security group. While SSH access is secured by the SSH public key, proxy access is not. 79 | - The instance will run off the latest Ubuntu 22.04 LTS ARM-based AMI available in AWS *at the time of deployment* 80 | - After deployment, patching and maintenance of the instance is your responsibility and is not automatically handled. 81 | - To save cost, the instance runs as a `t4g.micro` EC2 instance type, which has about 2 vCPU and 1 GB of memory. 82 | - Launching this host will incur more cost since it's a persistently running server (until you shut it down or terminate it). As of this day (07/24/2024), t4g.micro instances cost about $0.0084USD/hour to run. 83 | 84 | You are free to inspect all of the proxy host's Terraform code in the `terraform/optional-proxy-terraform/` directory and the `terraform/proxy-instance.tf` Terraform file. 85 | 86 | 87 | ## Testing 88 | 89 | With mitmdump running, you can test if everything is working properly. First, make a file containing multiple public IP retrieval endpoints. 90 | 91 | ```txt 92 | https://ifconfig.me 93 | https://api.ipify.org 94 | https://ipv4.rawrify.com/ip 95 | ``` 96 | 97 | Then run the following for loop: 98 | 99 | ```bash 100 | while true; do for i in $(cat endpoints.txt); do curl -s $i -x http://127.0.0.1:8080; done; done 101 | ``` 102 | 103 | Every minute, your public IP should change. 104 | 105 | 106 | ## Examples 107 | Run nuclei with the following command: 108 | 109 | ```bash 110 | nuclei -l endpoints.txt -t /path/to/nuclei-templates/ -p http://127.0.0.1:8888 111 | ``` 112 | 113 | Run ffuf with the following command: 114 | 115 | ```bash 116 | ffuf -u https://example.com/FUZZ -w ~/.ffufw/wordlists/misc/raft-large-words.txt -ac -x http://127.0.0.1:8888 117 | ``` 118 | 119 | Proxy a specific CLI tool with exported environment variables. 120 | 121 | ```bash 122 | export http_proxy=http://127.0.0.1:8888 123 | export https_proxy=http://127.0.0.1:8888 124 | 125 | # Now run the command you want to proxy 126 | curl https://example.com 127 | ``` 128 | 129 | ## TODO 130 | 131 | * Add multi-cloud support 132 | * Convert gigaproxy.py to a more portable CLI tool 133 | * Add more examples 134 | * Organize terraform code 135 | 136 | ## References & Thanks 137 | 138 | * [fireprox](https://github.com/ustayready/fireprox) 139 | * [This guy](https://github.com/Hogman-the-Intruder) 140 | -------------------------------------------------------------------------------- /gigaproxy.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import random 5 | from urllib.parse import urlparse 6 | 7 | from mitmproxy import ctx, http 8 | 9 | # Configure logging 10 | logging.basicConfig(level=logging.INFO) 11 | 12 | 13 | def load(l): 14 | l.add_option("auth_token", str, "", "Authorization token for the proxy") 15 | l.add_option( 16 | "proxy_endpoint", str, "", "Target AWS API gateway endpoint to forward to" 17 | ) 18 | l.add_option("rotate_user_agent", bool, False, "Rotate the user agent") 19 | l.add_option("debug", bool, False, "Enable debug mode") 20 | 21 | 22 | user_agents = [ 23 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 24 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15", 25 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0", 26 | "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", 27 | "Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36", 28 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.59", 29 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", 30 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 31 | "Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", 32 | "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36", 33 | ] 34 | 35 | 36 | def request(flow: http.HTTPFlow) -> None: 37 | 38 | # Access options using ctx.options 39 | auth_token = ctx.options.auth_token 40 | proxy_endpoint = ctx.options.proxy_endpoint 41 | rotate_user_agent = ctx.options.rotate_user_agent 42 | debug = ctx.options.debug 43 | 44 | try: 45 | # Adding new required headers for the proxy 46 | flow.request.headers["x-api-key"] = auth_token 47 | flow.request.headers["x-forward-me-to"] = flow.request.pretty_url 48 | 49 | # Optional rotating user agent 50 | if rotate_user_agent: 51 | flow.request.headers["User-Agent"] = random.choice(user_agents) 52 | 53 | # Debug logs in AWS will be generated 54 | if debug: 55 | flow.request.headers["X-DEBUG"] = "SPRKTWASHERE" 56 | 57 | # Setting the target proxy 58 | flow.request.host = urlparse(proxy_endpoint).hostname 59 | flow.request.path = urlparse(proxy_endpoint).path 60 | flow.request.port = 443 61 | flow.request.scheme = "https" 62 | 63 | except Exception as e: 64 | logging.error(f"Error processing request: {e}") 65 | pass 66 | -------------------------------------------------------------------------------- /terraform/api-gateway.tf: -------------------------------------------------------------------------------- 1 | ##################################### 2 | # REST API - SEND YOUR TRAFFIC HERE # 3 | ##################################### 4 | 5 | resource "aws_api_gateway_rest_api" "gigaproxy-api" { 6 | name = "${var.project_identifier}-api" 7 | 8 | description = "${var.project_identifier} REST API" 9 | 10 | # Body defined as OpenAPI v3 JSON -- can use Swagger too 11 | body = jsonencode( 12 | { 13 | "openapi" : "3.0.1", 14 | "paths" : { 15 | "/gigaproxy-forwarder-function" : { 16 | "x-amazon-apigateway-any-method" : { 17 | "responses" : { 18 | "200" : { 19 | "description" : "200 response", 20 | "content" : {} 21 | } 22 | }, 23 | "security" : [{ 24 | "api_key" : [] 25 | }], 26 | "x-amazon-apigateway-integration" : { 27 | "httpMethod" : "POST", 28 | "uri" : "arn:aws:apigateway:${data.aws_region.current-region.name}:lambda:path/2015-03-31/functions/arn:aws:lambda:${data.aws_region.current-region.name}:${data.aws_caller_identity.current-account.account_id}:function:${var.project_identifier}-forwarder-function/invocations", 29 | "responses" : { 30 | "default" : { 31 | "statusCode" : "200" 32 | } 33 | }, 34 | "passthroughBehavior" : "when_no_templates", 35 | "contentHandling" : "CONVERT_TO_TEXT", 36 | "type" : "aws_proxy" 37 | } 38 | } 39 | } 40 | }, 41 | "components" : { 42 | "securitySchemes" : { 43 | "api_key" : { 44 | "type" : "apiKey", 45 | "name" : "x-api-key", 46 | "in" : "header" 47 | } 48 | } 49 | }, 50 | "x-amazon-apigateway-binary-media-types" : ["image/jpg", "image/gif", "image/png"] 51 | } 52 | ) 53 | 54 | endpoint_configuration { 55 | types = ["REGIONAL"] # Can set to edge-optimized if you want, but only really affects inbound latency to API Gateway from clients geographically dispersed across the world. Doesn't really affect IP rotation 56 | } 57 | } 58 | 59 | resource "aws_api_gateway_stage" "v1-stage" { 60 | stage_name = "v1" 61 | rest_api_id = aws_api_gateway_rest_api.gigaproxy-api.id 62 | deployment_id = aws_api_gateway_deployment.v1-deployment.id 63 | } 64 | 65 | resource "aws_api_gateway_deployment" "v1-deployment" { 66 | rest_api_id = aws_api_gateway_rest_api.gigaproxy-api.id 67 | 68 | triggers = { 69 | redeployment = sha1(jsonencode(aws_api_gateway_rest_api.gigaproxy-api.body)) 70 | } 71 | 72 | lifecycle { 73 | create_before_destroy = true 74 | } 75 | } 76 | 77 | # Create an AWS API Gateway REST API API key for auth and usage control 78 | resource "aws_api_gateway_api_key" "proxy-api-key" { 79 | name = "${var.project_identifier}-api-key" 80 | value = var.api_key == "" ? null : var.api_key 81 | } 82 | 83 | resource "aws_api_gateway_usage_plan" "proxy-usage-plan" { 84 | name = "${var.project_identifier}-usage-plan" 85 | description = "Controls access to and usage for the ${var.project_identifier} proxy" 86 | 87 | api_stages { 88 | api_id = aws_api_gateway_rest_api.gigaproxy-api.id 89 | stage = aws_api_gateway_stage.v1-stage.stage_name 90 | } 91 | 92 | quota_settings { 93 | limit = var.api_monthly_quota # By default, limit to 10 million requests per month. Can be modified or fully removed for no cap 94 | period = "MONTH" 95 | } 96 | } 97 | 98 | resource "aws_api_gateway_usage_plan_key" "link-proxy-key" { 99 | key_id = aws_api_gateway_api_key.proxy-api-key.id 100 | key_type = "API_KEY" 101 | usage_plan_id = aws_api_gateway_usage_plan.proxy-usage-plan.id 102 | } -------------------------------------------------------------------------------- /terraform/eventbridge.tf: -------------------------------------------------------------------------------- 1 | # We need to do some re-parsing since jsonencode will escape < and > 2 | locals { 3 | schedule_input = jsonencode({ 4 | "FunctionName" : aws_lambda_function.forwarder-function.function_name 5 | "Description" : "redeployed by eventbridge at " 6 | }) 7 | 8 | corrected_schedule_input = replace(replace(local.schedule_input, "\\u003c", "<"), "\\u003e", ">") 9 | } 10 | 11 | ##################################### 12 | # EVENTBRIDGE SCHEDULE TO ROTATE IP # 13 | ##################################### 14 | 15 | resource "aws_scheduler_schedule" "cycle-lambda-ips" { 16 | name = "${var.project_identifier}-redeploy-ip-rotation" 17 | group_name = "default" 18 | 19 | flexible_time_window { 20 | mode = "OFF" 21 | } 22 | 23 | schedule_expression = "rate(1 minutes)" 24 | 25 | target { 26 | arn = "arn:aws:scheduler:::aws-sdk:lambda:updateFunctionConfiguration" 27 | role_arn = aws_iam_role.eventbridge-role.arn 28 | 29 | input = local.corrected_schedule_input 30 | } 31 | } 32 | 33 | 34 | ####################################### 35 | # IAM POLICY FOR EVENTBRIDGE SCHEDULE # 36 | ####################################### 37 | data "aws_iam_policy_document" "eventbridge-assume-role" { 38 | statement { 39 | effect = "Allow" 40 | principals { 41 | type = "Service" 42 | identifiers = [ 43 | "scheduler.amazonaws.com" 44 | ] 45 | } 46 | actions = [ 47 | "sts:AssumeRole" 48 | ] 49 | } 50 | } 51 | 52 | resource "aws_iam_role" "eventbridge-role" { 53 | name = "${var.project_identifier}-eventbridge-cron-role" 54 | assume_role_policy = data.aws_iam_policy_document.eventbridge-assume-role.json 55 | } 56 | 57 | data "aws_iam_policy_document" "eventbridge-permissions" { 58 | statement { 59 | sid = "AllowUpdateLambda" 60 | effect = "Allow" 61 | actions = [ 62 | "lambda:UpdateFunctionCode", 63 | "lambda:UpdateFunctionConfiguration" 64 | ] 65 | resources = [ 66 | "arn:aws:lambda:*:${data.aws_caller_identity.current-account.account_id}:function:${var.project_identifier}-*" 67 | ] 68 | } 69 | } 70 | 71 | resource "aws_iam_policy" "eventbridge-policy" { 72 | name = "${var.project_identifier}-eventbridge-cron-policy" 73 | policy = data.aws_iam_policy_document.eventbridge-permissions.json 74 | } 75 | 76 | resource "aws_iam_role_policy_attachment" "eventbridge-policy-attach" { 77 | role = aws_iam_role.eventbridge-role.name 78 | policy_arn = aws_iam_policy.eventbridge-policy.arn 79 | } -------------------------------------------------------------------------------- /terraform/forwarder-lambda.tf: -------------------------------------------------------------------------------- 1 | data "archive_file" "lambda-function-zip" { 2 | type = "zip" 3 | source_file = "${path.module}/src/forwarder-function/lambda_function.py" 4 | output_path = "${path.module}/builds/lambda_function_payload.zip" 5 | } 6 | 7 | ########################################################################## 8 | # LAMBDA FUNCTION THAT FORWARDS YOUR TRAFFIC - THIS IS WHERE IT GOES OUT # 9 | ########################################################################## 10 | 11 | resource "aws_lambda_function" "forwarder-function" { 12 | function_name = "${var.project_identifier}-forwarder-function" 13 | description = "Forwarding function for the ${var.project_identifier} API" 14 | 15 | role = aws_iam_role.forwarder-function-role.arn 16 | 17 | handler = "lambda_function.lambda_handler" 18 | 19 | filename = "${path.module}/builds/lambda_function_payload.zip" 20 | source_code_hash = data.archive_file.lambda-function-zip.output_base64sha256 21 | runtime = "python3.12" 22 | 23 | timeout = 10 24 | memory_size = 128 25 | 26 | layers = [ 27 | aws_lambda_layer_version.dependencies-layer.arn 28 | ] 29 | } 30 | 31 | ######################################## 32 | # IAM POLICY STUFF FOR LAMBDA FUNCTION # 33 | ######################################## 34 | 35 | data "aws_iam_policy_document" "lambda-assume-role" { 36 | statement { 37 | effect = "Allow" 38 | 39 | principals { 40 | type = "Service" 41 | identifiers = ["lambda.amazonaws.com"] 42 | } 43 | 44 | actions = ["sts:AssumeRole"] 45 | } 46 | } 47 | 48 | resource "aws_iam_role" "forwarder-function-role" { 49 | name = "${var.project_identifier}-function-role" 50 | assume_role_policy = data.aws_iam_policy_document.lambda-assume-role.json 51 | } 52 | 53 | # Attach the basic AWS Lambda execution role, which allows the Lambda to log to Cloudwatch 54 | resource "aws_iam_role_policy_attachment" "attach-basic-lambda-policy" { 55 | count = var.enable_function_logging ? 1 : 0 56 | 57 | role = aws_iam_role.forwarder-function-role.name 58 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 59 | } 60 | 61 | resource "aws_lambda_permission" "invoke-from-api" { 62 | statement_id = "AllowAPIInvoke" 63 | action = "lambda:InvokeFunction" 64 | function_name = aws_lambda_function.forwarder-function.function_name 65 | principal = "apigateway.amazonaws.com" 66 | source_arn = "${aws_api_gateway_rest_api.gigaproxy-api.execution_arn}/*" 67 | } 68 | 69 | # Package dependencies for the forwarder function 70 | resource "aws_lambda_layer_version" "dependencies-layer" { 71 | layer_name = "${var.project_identifier}-dependencies-layer" 72 | filename = "${path.module}/src/dependencies.zip" 73 | 74 | source_code_hash = filebase64sha256("${path.module}/src/dependencies.zip") 75 | 76 | compatible_architectures = ["arm64"] 77 | compatible_runtimes = ["python3.12"] 78 | } 79 | 80 | ##################################### 81 | # CLOUDWATCH LOG GROUP FOR FUNCTION # 82 | ##################################### 83 | 84 | # Create a log group and set default retention to 14 days so we don't get a crapload of logs just sitting there 85 | resource "aws_cloudwatch_log_group" "lambda-function-logs" { 86 | count = var.enable_function_logging ? 1 : 0 87 | 88 | name = "/aws/lambda/${aws_lambda_function.forwarder-function.function_name}" 89 | 90 | retention_in_days = var.log_retention_period 91 | } -------------------------------------------------------------------------------- /terraform/optional-proxy-terraform/README.md: -------------------------------------------------------------------------------- 1 | ### HEY 2 | This should only be considered as a temporary, optional launching off point to help you in getting started with Gigaproxy. Don't rely on this proxy instance for long-term or production testing! One issue with it is it saves your API Gateway endpoint and auth token in the userdata of the EC2 instance, which can easily be extracted if someone gets access to your AWS account or dumps them from the instance metadata service on the instance (the 169.254.169.254 endpoint). You've been warned. 3 | 4 | Also, you will still need to install the mitmproxy cert locally unless you're fine with disabling cert validation on your tools. (e.g. passing `-k` with curl commands) -------------------------------------------------------------------------------- /terraform/optional-proxy-terraform/main.tf: -------------------------------------------------------------------------------- 1 | ########################### 2 | # OPTIONAL PROXY INSTANCE # 3 | ########################### 4 | 5 | /* 6 | Can't get the mitmproxy stuff working? You can optionally use this to launch a persistent EC2 instance that'll 7 | */ 8 | 9 | # Use latest ARM-based Ubuntu 22.04 10 | data "aws_ami" "latest-ubuntu-arm" { 11 | most_recent = true 12 | filter { 13 | name = "name" 14 | values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server-*"] 15 | } 16 | 17 | owners = ["099720109477"] 18 | } 19 | 20 | resource "aws_key_pair" "ssh-keypair" { 21 | key_name = "${var.project_identifier}-temp-proxy-instance-key" 22 | public_key = var.ssh_public_key 23 | } 24 | 25 | 26 | resource "aws_instance" "proxy-instance" { 27 | 28 | ami = data.aws_ami.latest-ubuntu-arm.id 29 | instance_type = "t4g.micro" 30 | 31 | source_dest_check = false 32 | 33 | vpc_security_group_ids = [aws_security_group.proxy-sg.id] 34 | subnet_id = module.proxy-vpc.public_subnets[0] 35 | 36 | key_name = aws_key_pair.ssh-keypair.id 37 | 38 | user_data_base64 = base64encode( 39 | templatefile("${path.module}/proxy-instance-bootstrap.sh", 40 | { 41 | "AUTH_TOKEN" = var.gigaproxy_api_token, 42 | "API_ENDPOINT" = var.gigaproxy_endpoint 43 | } 44 | ) 45 | ) 46 | 47 | root_block_device { 48 | encrypted = true 49 | } 50 | 51 | tags = { 52 | Name = "${var.project_identifier}-temporary-proxy-instance" 53 | } 54 | } 55 | 56 | 57 | ################################# 58 | # NETWORKING FOR OPTIONAL PROXY # 59 | ################################# 60 | 61 | module "proxy-vpc" { 62 | source = "terraform-aws-modules/vpc/aws" 63 | version = "5.9.0" 64 | 65 | name = "${var.project_identifier}-vpc" 66 | cidr = var.vpc_cidr 67 | 68 | azs = ["${var.aws_region}a", "${var.aws_region}b"] 69 | public_subnets = ["10.99.0.0/24", "10.99.1.0/24"] 70 | 71 | map_public_ip_on_launch = true 72 | } 73 | 74 | resource "aws_security_group" "proxy-sg" { 75 | name = "${var.project_identifier}-sg" 76 | description = "For the ${var.project_identifier} proxy EC2 instance" 77 | vpc_id = module.proxy-vpc.vpc_id 78 | } 79 | 80 | resource "aws_vpc_security_group_egress_rule" "proxy-sg-egress" { 81 | security_group_id = aws_security_group.proxy-sg.id 82 | description = "Allow outbound" 83 | 84 | ip_protocol = "-1" 85 | cidr_ipv4 = "0.0.0.0/0" 86 | } 87 | 88 | resource "aws_vpc_security_group_ingress_rule" "proxy-sg-inbound-ssh" { 89 | security_group_id = aws_security_group.proxy-sg.id 90 | description = "Allow inbound ssh" 91 | 92 | ip_protocol = "tcp" 93 | from_port = 22 94 | to_port = 22 95 | cidr_ipv4 = var.proxy_inbound_ip_allowed 96 | } 97 | 98 | resource "aws_vpc_security_group_ingress_rule" "proxy-sg-inbound" { 99 | security_group_id = aws_security_group.proxy-sg.id 100 | description = "Allow inbound traffic to proxy" 101 | 102 | ip_protocol = "tcp" 103 | from_port = 8888 104 | to_port = 8888 105 | cidr_ipv4 = var.proxy_inbound_ip_allowed 106 | } -------------------------------------------------------------------------------- /terraform/optional-proxy-terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "proxy-public-ip" { 2 | value = aws_instance.proxy-instance.public_ip 3 | } -------------------------------------------------------------------------------- /terraform/optional-proxy-terraform/proxy-instance-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | export DEBIAN_FRONTEND=noninteractive 5 | 6 | apt update -y 7 | 8 | apt install git python3 python3-venv python3-pip libffi-dev libssl-dev ca-certificates screen -y 9 | 10 | pip install --upgrade pip 11 | pip install pipx 12 | pipx install mitmproxy # Very important to install mitmproxy via pipx and NOT apt or single binary. Weird issues with single binary installs not picking up the cert updates below when connected to gigaproxy. 13 | pipx ensurepath 14 | 15 | export PATH=$PATH:~/.local/bin 16 | 17 | mitmproxy --version 18 | 19 | # Start mitmproxy in a screen session for a short period of time to generate certificates 20 | screen -dmS mitmproxy_bootstrap bash -c "mitmproxy; sleep 10; exit" 21 | 22 | sleep 15 23 | 24 | screen -S mitmproxy_bootstrap -X quit 25 | 26 | openssl x509 -in /root/.mitmproxy/mitmproxy-ca-cert.cer -out /root/.mitmproxy/mitmproxy-ca-cert.pem 27 | 28 | MITMPROXY_CERT_PATH="/root/.mitmproxy/mitmproxy-ca-cert.pem" 29 | 30 | if [ -f "$MITMPROXY_CERT_PATH" ]; then 31 | cp "$MITMPROXY_CERT_PATH" /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt 32 | 33 | update-ca-certificates 34 | 35 | echo "mitmproxy certificate installed into the system's certificate trust store." 36 | else 37 | echo "mitmproxy certificate not found at $MITMPROXY_CERT_PATH." 38 | fi 39 | 40 | cd /root 41 | git clone https://github.com/Sprocket-Security/gigaproxy 42 | cd gigaproxy 43 | 44 | screen -dmS mitmproxy_session mitmdump -s gigaproxy.py --set auth_token="${AUTH_TOKEN}" --set proxy_endpoint="${API_ENDPOINT}v1/gigaproxy-forwarder-function" --listen-host 0.0.0.0 --listen-port 8888 --set block_global=false -------------------------------------------------------------------------------- /terraform/optional-proxy-terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_identifier" { 2 | default = "gigaproxy" 3 | description = "Prefix value to add to resources and as a tag to relevant AWS resources." 4 | type = string 5 | } 6 | 7 | variable "proxy_inbound_ip_allowed" { 8 | description = "IP range to allow on inbound connections, including netmask. Highly recommended to set this to your own public IP address, which can be retrieved at a site like https://ipv4.rawrify.com/ip -- if you really must allow all inbound, use 0.0.0.0/0" 9 | type = string 10 | } 11 | 12 | variable "aws_region" { 13 | description = "Region to deploy to. Should be passed automatically from parent Terraform code." 14 | type = string 15 | } 16 | 17 | variable "vpc_cidr" { 18 | description = "CIDR range to use for the VPC. Doesn't affect much except the private IP address assigned to the EC2 instance, which doesn't really matter unless you're launching other services." 19 | type = string 20 | default = "10.99.0.0/16" 21 | } 22 | 23 | variable "ssh_public_key" { 24 | description = "Full content of the SSH public key pair to set on the instance. Generate an SSH key on your machine and simply supply the public keypair." 25 | type = string 26 | } 27 | 28 | variable "gigaproxy_api_token" { 29 | description = "API token generated for gigaproxy. Highly recommended to generate a new one after you're done with the proxy instance and destroy it." 30 | type = string 31 | sensitive = true 32 | } 33 | 34 | variable "gigaproxy_endpoint" { 35 | description = "The API gateway execution endpoint that we can reach our gigaproxy instance on." 36 | type = string 37 | } -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "api-endpoint" { 2 | value = "${aws_api_gateway_stage.v1-stage.invoke_url}/gigaproxy-forwarder-function" 3 | } -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | # Minimal AWS provider definition, really only for enforcing default tags 2 | # Use AWS environment variables (e.g. AWS_PROFILE, AWS_REGION) for auth config 3 | provider "aws" { 4 | default_tags { 5 | tags = { 6 | Project = var.project_identifier 7 | Terraform = true 8 | } 9 | } 10 | } 11 | 12 | data "aws_caller_identity" "current-account" { 13 | 14 | } 15 | 16 | data "aws_region" "current-region" { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /terraform/proxy-instance.tf: -------------------------------------------------------------------------------- 1 | ########################### 2 | # OPTIONAL PROXY INSTANCE # 3 | ########################### 4 | 5 | # READ THE README in optional-proxy-terraform/README.md 6 | 7 | module "optional-proxy-instance" { 8 | source = "./optional-proxy-terraform" 9 | 10 | # Don't deploy this unless optional_proxy_instance var is set to true 11 | count = var.optional_proxy_instance ? 1 : 0 12 | 13 | proxy_inbound_ip_allowed = var.proxy_inbound_ip_allowed 14 | aws_region = data.aws_region.current-region.name 15 | ssh_public_key = var.proxy_public_ssh_key 16 | gigaproxy_api_token = aws_api_gateway_api_key.proxy-api-key.value 17 | gigaproxy_endpoint = aws_api_gateway_deployment.v1-deployment.invoke_url 18 | } 19 | 20 | output "proxy-public-ip" { 21 | value = var.optional_proxy_instance ? "Proxy instance public IP: ${module.optional-proxy-instance[0].proxy-public-ip}" : null 22 | } -------------------------------------------------------------------------------- /terraform/src/dependencies.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sprocket-Security/gigaproxy/fd4d865596a14264eccee786913d15c267ee0863/terraform/src/dependencies.zip -------------------------------------------------------------------------------- /terraform/src/forwarder-function/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import os 4 | import base64 5 | 6 | # Determine if the content should be treated as binary 7 | # Note there's some extra shit you have to do in API Gateway to support binary output, and even then it can sometimes be a crapshoot 8 | binary_content_types = [ 9 | 'image/', 'application/pdf', 'application/octet-stream' 10 | ] 11 | 12 | # Format incoming requests to drop headers we don't want to send out 13 | def drop_incoming_headers(event): 14 | if "Host" in event["headers"].keys(): 15 | event["headers"].pop("Host") 16 | 17 | if "X-Amzn-Trace-Id" in event["headers"].keys(): 18 | event["headers"].pop("X-Amzn-Trace-Id") 19 | 20 | if "X-Forwarded-For" in event["headers"].keys(): 21 | event["headers"].pop("X-Forwarded-For") 22 | 23 | if "x-api-key" in event["headers"].keys(): 24 | event["headers"].pop("x-api-key") 25 | 26 | 27 | def lambda_handler(event, context): 28 | # print incoming event to log if x-debug-call header is sent 29 | if "x-debug-call" in event["headers"].keys(): 30 | print(event) 31 | 32 | url = event["headers"].pop("x-forward-me-to") 33 | 34 | if not url: 35 | return { 36 | "statusCode": 400, 37 | "body": "Request did not contain necessary header." 38 | } 39 | 40 | # Extract request details 41 | method = event["httpMethod"] 42 | drop_incoming_headers(event) 43 | headers = event["headers"] 44 | body = event["body"] 45 | params = event["queryStringParameters"] 46 | 47 | # Forward the request to the destination server 48 | response = requests.request(method, url, headers=headers, data=body, params=params, verify=False) 49 | 50 | # Read the response content fully -- need to do this, especially if responses are chunked 51 | response_content = response.content 52 | 53 | # Determine the content type of the response 54 | content_type = response.headers.get('Content-Type', 'application/json') 55 | 56 | # Handle binary content or not, depending on Content-Type header 57 | if any(content_type.startswith(bct) for bct in binary_content_types): 58 | response_body = base64.b64encode(response_content).decode('utf-') 59 | is_base64 = True 60 | else: 61 | response_body = response_content.decode('utf-8') 62 | is_base64 = False 63 | 64 | # Return the response in the required format 65 | return { 66 | 'statusCode': response.status_code, 67 | 'headers': { 68 | 'Content-Type': content_type 69 | }, 70 | 'isBase64Encoded': is_base64, 71 | 'body': response_body 72 | } 73 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars.example: -------------------------------------------------------------------------------- 1 | # Enables function logs in Cloudwatch 2 | enable_function_logging = false 3 | 4 | # optional proxy vars -- refer to README 5 | optional_proxy_instance = false 6 | proxy_inbound_ip_allowed = "" 7 | proxy_public_ssh_key = "" -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_identifier" { 2 | default = "gigaproxy" 3 | description = "Prefix value to add to resources and as a tag to relevant AWS resources." 4 | type = string 5 | } 6 | 7 | variable "api_key" { 8 | description = "Value to set for API key for authentication. If not set, AWS sets it automatically (and can be retrieved from API Gateway console)." 9 | sensitive = true 10 | type = string 11 | default = "" 12 | } 13 | 14 | variable "enable_function_logging" { 15 | description = "Whether or not to enable logging on the forwarder Lambda function. Defaults to false (no logging)" 16 | type = bool 17 | default = false 18 | } 19 | 20 | variable "log_retention_period" { 21 | description = "Duration for how long logs should be kept for the forwarder function in days. Defaults to 14 days." 22 | type = number 23 | default = 14 24 | } 25 | 26 | variable "api_monthly_quota" { 27 | description = "Number of requests to allow per month to the proxy API. Defaults to 10 million. Don't rely on this too much as a hard cap, there can be some wiggle room in how this is enforced (per AWS documentation)" 28 | type = number 29 | default = 10000000 30 | } 31 | 32 | ############## 33 | # PROXY VARS # 34 | ############## 35 | 36 | /* 37 | ONLY set these vars if you need the proxy instance. Otherwise, can ignore, they will do nothing 38 | */ 39 | 40 | variable "optional_proxy_instance" { 41 | description = "Whether or not to launch an AWS EC2 instance and launch mitmproxy on that instead of locally. Useful for troubleshooting." 42 | type = bool 43 | default = false 44 | } 45 | 46 | variable "proxy_inbound_ip_allowed" { 47 | description = "IP range to allow on inbound connections, including netmask. Highly recommended to set this to your own public IP address, which can be retrieved at a site like https://ipv4.rawrify.com/ip -- if you really must allow all inbound, use 0.0.0.0/0" 48 | type = string 49 | default = "127.0.0.1/32" # Purposefully incorrect value that you must override to actually get this to work. Acts as a failsafe so you don't launch a publicly open, completely unprotected proxy instance to the internet 50 | } 51 | 52 | variable "proxy_public_ssh_key" { 53 | description = "Full content of the SSH public key pair to set on the instance. Generate an SSH key on your machine and simply supply the public keypair." 54 | type = string 55 | default = "Didn't set the SSH key properly" 56 | } --------------------------------------------------------------------------------