├── .github └── workflows │ ├── portal-CD.yaml │ └── terraform-CD.yml ├── .gitignore ├── Makefile ├── README.md ├── asg.tf ├── assets ├── 502.png ├── connect-page.png ├── gc-04.png ├── gc-1.png ├── gc-2.png ├── gc-3.png ├── import-profile-page.png ├── openvp-portal.drawio.png ├── openvp-server.drawio.png ├── openvpn-download-page.png ├── openvpn.drawio.png ├── portal-start-page.png ├── pr-deploy.png ├── terraform-plan-stdout.png └── terraform-plan.png ├── cloudwatch.tf ├── connected-page.png ├── datasources.tf ├── dns.tf ├── efs.tf ├── hooks └── pre-commit ├── iam-openvpn-portal.tf ├── iam.tf ├── keypair.tf ├── locals.tf ├── main.tf ├── nlb.tf ├── outputs.tf ├── portal.tf ├── portal ├── Dockerfile ├── README.rst ├── portal │ ├── __init__.py │ └── assets │ │ └── favicon.ico ├── publish.sh ├── requirements.txt ├── requirements_dev.txt └── setup.py ├── renovate.json ├── requirements.txt ├── secrets.tf ├── security_group.tf ├── terraform.tf ├── test_data ├── openvpn │ ├── .gitignore │ ├── datasources.tf │ ├── ecr.tf │ ├── jumphost.tf │ ├── locals.tf │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ ├── secrets.tf │ ├── ssh_keys │ │ ├── ssh_host_ecdsa_key │ │ ├── ssh_host_ecdsa_key.pub │ │ ├── ssh_host_ed25519_key │ │ ├── ssh_host_ed25519_key.pub │ │ ├── ssh_host_rsa_key │ │ └── ssh_host_rsa_key.pub │ ├── terraform.tf │ └── variables.tf └── service-network │ ├── datasources.tf │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ ├── terraform.tf │ └── variables.tf ├── tests ├── __init__.py ├── conftest.py └── test_module.py └── variables.tf /.github/workflows/portal-CD.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and Push Images to AWS ECR 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | id-token: write # This is required for aws oidc connection 11 | contents: read # This is required for actions/checkout 12 | 13 | env: 14 | AWS_REGION: us-east-1 15 | ROLE_ARN: "arn:aws:iam::493370826424:role/ih-tf-terraform-aws-openvpn-github" 16 | 17 | jobs: 18 | build-and-push-image: 19 | name: Build and Push Image 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Configure AWS Credentials 27 | uses: aws-actions/configure-aws-credentials@v4 28 | with: 29 | role-to-assume: ${{ env.ROLE_ARN }} 30 | role-session-name: github-actions 31 | aws-region: ${{ env.AWS_REGION }} 32 | 33 | - name: Login to Amazon ECR 34 | id: login-ecr-public 35 | uses: aws-actions/amazon-ecr-login@v2 36 | with: 37 | registry-type: public 38 | 39 | - name: Build, tag, and push docker image to Amazon ECR Public 40 | env: 41 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} 42 | REGISTRY_ALIAS: "infrahouse" 43 | REPOSITORY: "openvpn-portal" 44 | IMAGE_TAG: "latest" 45 | working-directory: "portal" 46 | run: | 47 | docker build -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG . 48 | docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG 49 | -------------------------------------------------------------------------------- /.github/workflows/terraform-CD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Terraform CD' 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | # Pattern matched against refs/tags 7 | tags: 8 | - "*" # Push events to every tag not containing / 9 | 10 | permissions: 11 | id-token: write # This is required for requesting the JWT 12 | contents: read 13 | 14 | env: 15 | ROLE_ARN: "arn:aws:iam::493370826424:role/ih-tf-terraform-aws-openvpn-github" 16 | AWS_DEFAULT_REGION: "us-west-1" 17 | 18 | jobs: 19 | publish: 20 | name: 'Publish Module' 21 | runs-on: ubuntu-latest 22 | environment: production 23 | timeout-minutes: 60 24 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 25 | defaults: 26 | run: 27 | shell: bash 28 | 29 | steps: 30 | # Checkout the repository to the GitHub Actions runner 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: Configure AWS Credentials 35 | uses: aws-actions/configure-aws-credentials@v2 36 | with: 37 | role-to-assume: ${{ env.ROLE_ARN }} 38 | role-session-name: github-actions 39 | aws-region: ${{ env.AWS_DEFAULT_REGION }} 40 | 41 | # Prepare Python environment 42 | - name: Setup Python Environment 43 | run: make bootstrap 44 | 45 | # Publish the module 46 | - name: Publish module 47 | run: | 48 | ih-registry upload 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | .terraform.lock.hcl 31 | terraform.tfvars 32 | tf.plan 33 | .idea 34 | /docs/_build/ 35 | /plan.stderr 36 | /plan.stdout 37 | __pycache__ 38 | .pytest_cache 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | define PRINT_HELP_PYSCRIPT 4 | import re, sys 5 | 6 | for line in sys.stdin: 7 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 8 | if match: 9 | target, help = match.groups() 10 | print("%-40s %s" % (target, help)) 11 | endef 12 | export PRINT_HELP_PYSCRIPT 13 | 14 | help: install-hooks 15 | @python -c "$$PRINT_HELP_PYSCRIPT" < Makefile 16 | 17 | .PHONY: install-hooks 18 | install-hooks: ## Install repo hooks 19 | @echo "Checking and installing hooks" 20 | @test -d .git/hooks || (echo "Looks like you are not in a Git repo" ; exit 1) 21 | @test -L .git/hooks/pre-commit || ln -fs ../../hooks/pre-commit .git/hooks/pre-commit 22 | @chmod +x .git/hooks/pre-commit 23 | 24 | 25 | .PHONY: test 26 | test: ## Run tests on the module 27 | rm -f test_data/test_module/.terraform.lock.hcl 28 | #rm -rf test_data/test_module/.terraform 29 | pytest -xvvs tests/ 30 | 31 | 32 | .PHONY: bootstrap 33 | bootstrap: ## bootstrap the development environment 34 | pip install -U "pip ~= 23.1" 35 | pip install -U "setuptools ~= 68.0" 36 | pip install -r requirements.txt 37 | 38 | .PHONY: clean 39 | clean: ## clean the repo from cruft 40 | rm -rf .pytest_cache 41 | find . -name '.terraform' -exec rm -fr {} + 42 | 43 | .PHONY: fmt 44 | fmt: format 45 | 46 | .PHONY: format 47 | format: ## Use terraform fmt to format all files in the repo 48 | @echo "Formatting terraform files" 49 | terraform fmt -recursive 50 | black tests portal 51 | 52 | define BROWSER_PYSCRIPT 53 | import os, webbrowser, sys 54 | 55 | from urllib.request import pathname2url 56 | 57 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 58 | endef 59 | export BROWSER_PYSCRIPT 60 | 61 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 62 | 63 | .PHONY: docs 64 | docs: ## generate Sphinx HTML documentation, including API docs 65 | $(MAKE) -C docs clean 66 | $(MAKE) -C docs html 67 | $(BROWSER) docs/_build/html/index.html 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-aws-openvpn 2 | The [openvpn module](https://registry.terraform.io/modules/infrahouse/openvpn/aws/latest) deploys an OpenVPN server 3 | with Google OAuth2.0 authentication. 4 | 5 | ![OpenVPN diagram](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/openvpn.drawio.png) 6 | 7 | OpenVPN Portal is a web application. It authenticates users by their Google account and generates 8 | an OpenVPN profile for them. 9 | 10 | You would put the OpenVPN server in a public subnet in your AWS cloud to give access to authorized users 11 | to AWS resources in private subnets. 12 | 13 | ## Installation 14 | 15 | To illustrate how to use the module, I will deploy a VPN server for InfraHouse. 16 | 17 | ### **Step 1**: Create a Terraform code. 18 | ```hcl 19 | module "vpn" { 20 | source = "registry.infrahouse.com/infrahouse/openvpn/aws" 21 | version = "~> 0.2" 22 | providers = { 23 | aws = aws 24 | aws.dns = aws 25 | } 26 | backend_subnet_ids = module.management.subnet_private_ids 27 | lb_subnet_ids = module.management.subnet_public_ids 28 | google_oauth_client_writer = data.aws_iam_role.AWSAdministratorAccess.arn 29 | zone_id = module.infrahouse_com.infrahouse_zone_id 30 | } 31 | 32 | data "aws_iam_role" "AWSAdministratorAccess" { 33 | name = "AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50" 34 | } 35 | ``` 36 | Our VPN setup will consist of two components: OpenVPN server and OpenVPN Portal 37 | 38 | The OpenVPN server is deployed on an autoscale group and fronted by a network load balancer. 39 | 40 | ![openvp-server](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/openvp-server.drawio.png) 41 | 42 | The OpenVPN Portal is a Web application deployed as an AWS ECS service. 43 | It talks to Google to authenticate users and distributes OpenVPN profiles needed to configure a client application. 44 | 45 | ![openvp-portal](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/openvp-portal.drawio.png) 46 | 47 | All module variables here are required. Let's go over them. 48 | 49 | * `backend_subnet_ids`. This is a list of subnet-id-s where autoscale EC2 instances will be places. 50 | Access to VPN is provided by a network load balancer, so we want the EC2 instances to run in private subnets. 51 | * `lb_subnet_ids`. This is a list of subnet-id-s where the network load balancer will be running. We want it to be in 52 | public subnets because it is going to be a publicly accessible gateway to our private AWS resources. 53 | * `google_oauth_client_writer`. The OpenVPN Portal will need credentials to talk to Google, so it can authenticate users. 54 | `google_oauth_client_writer` is an identity ARN that has a permission to update a secret with those credentials. 55 | More on that below. In this case, I will update the secret, so the value is a role ARN I get on my laptop 56 | via AWS Control Tower SSO. 57 | * `zone_id`. The module will create two public DNS names: openvpn.infrahouse.com and openvpn-portal.infrahouse.com. 58 | infrahouse.com is hosted in Route53 so `zone_id` is its zone identifier. 59 | * `providers` block. In some cases, you want the VPN and DNS resources to be managed by different roles or 60 | deployed in different AWS account. That's why I separated two providers. 61 | `aws.dns` is responsible for creating Route53 resources and the default `aws` provider does the rest. In my case, 62 | the VPN and infrahouse.com live in the same AWS account. so the providers are the same. 63 | 64 | ### **Step 2**: Create a pull request 65 | 66 | This is a commit. 67 | ``` 68 | $ git log -p -1 69 | commit b695867f71846a1e7d2fabf14e21cebd2b026516 (HEAD -> vpn) 70 | Author: Oleksandr Kuzminskyi 71 | Date: Fri Jul 5 11:35:18 2024 -0700 72 | 73 | Deploy VPN 74 | 75 | diff --git a/vpn.tf b/vpn.tf 76 | new file mode 100644 77 | index 0000000..035b925 78 | --- /dev/null 79 | +++ b/vpn.tf 80 | @@ -0,0 +1,16 @@ 81 | +module "vpn" { 82 | + source = "registry.infrahouse.com/infrahouse/openvpn/aws" 83 | + version = "~> 0.2" 84 | + providers = { 85 | + aws = aws 86 | + aws.dns = aws 87 | + } 88 | + backend_subnet_ids = module.management.subnet_private_ids 89 | + lb_subnet_ids = module.management.subnet_public_ids 90 | + google_oauth_client_writer = data.aws_iam_role.AWSAdministratorAccess.arn 91 | + zone_id = module.infrahouse_com.infrahouse_zone_id 92 | +} 93 | + 94 | +data "aws_iam_role" "AWSAdministratorAccess" { 95 | + name = "AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50" 96 | +} 97 | ``` 98 | Create the PR: 99 | ```shell 100 | $ gh pr create 101 | ? Where should we push the 'vpn' branch? infrahouse/aws-control-493370826424 102 | 103 | Creating pull request for vpn into main in infrahouse/aws-control-493370826424 104 | 105 | ? Title Deploy VPN 106 | ? Body 107 | ? What's next? Submit 108 | remote: 109 | remote: 110 | To github.com:infrahouse/aws-control-493370826424.git 111 | * [new branch] HEAD -> vpn 112 | Branch 'vpn' set up to track remote branch 'vpn' from 'origin'. 113 | https://github.com/infrahouse/aws-control-493370826424/pull/199 114 | ``` 115 | Now, the [pull request](https://github.com/infrahouse/aws-control-493370826424/pull/199) successfully ran `terraform plan` 116 | and we can review what Terraform is going to do. 117 | 118 | ![terraform-plan.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/terraform-plan.png) 119 | 120 | Normally, the module will create about 80 resources. In my case, all of them start with a "module.vnp", 121 | which is a good indicator Terraform will create resources that we expect it to do. 122 | If your plan includes resources to be changed or destroyed - double-check the STDOUT to understand what's going on. 123 | 124 | ![terraform-plan-stdout.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/terraform-plan-stdout.png) 125 | 126 | ### **Step 3**: Merge the pull request. 127 | ```shell 128 | $ gh pr merge -ds 129 | ✓ Squashed and merged pull request #199 (Deploy VPN) 130 | remote: Enumerating objects: 1, done. 131 | remote: Counting objects: 100% (1/1), done. 132 | remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 133 | Unpacking objects: 100% (1/1), 849 bytes | 169.00 KiB/s, done. 134 | From github.com:infrahouse/aws-control-493370826424 135 | * branch main -> FETCH_HEAD 136 | efd3d98..5f28f7a main -> origin/main 137 | Updating efd3d98..5f28f7a 138 | Fast-forward 139 | vpn.tf | 16 ++++++++++++++++ 140 | 1 file changed, 16 insertions(+) 141 | create mode 100644 vpn.tf 142 | ✓ Deleted branch vpn and switched to branch main 143 | ``` 144 | [Check](https://github.com/infrahouse/aws-control-493370826424/pull/199) that Terraform successfully created the VPN resources. 145 | 146 | ![pr-deploy.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/pr-deploy.png) 147 | 148 | ### **Step 4**: Configure Google OAuth2.0. 149 | 150 | Now if you open https://openvpn-portal.infrahouse.com/ in a browser, you'll see a 502 error. 151 | It's because I didn't update Google Client credentials. So, let's remedy that. 152 | 153 | ![502.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/502.png) 154 | 155 | This is a Google client secret that Terraform created. 156 | ```shell 157 | $ ih-secrets --aws-region us-west-1 --aws-profile AWSAdministratorAccess-493370826424 list | grep google 158 | | google_client20240705183915856300000015 | A JSON with Google OAuth Client ID | 159 | ``` 160 | If you get its value, it will show `NoValue`: 161 | ```shell 162 | $ ih-secrets --aws-region us-west-1 --aws-profile AWSAdministratorAccess-493370826424 get google_client20240705183915856300000015 163 | NoValue 164 | ``` 165 | #### **Step 4.1**: Create OAuth 2.0 credentials. 166 | 167 | Open [Google Cloud Console](https://console.cloud.google.com/) and create an OpenVPN project. 168 | 169 | ![gc-1.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/gc-1.png) 170 | 171 | Next, go to "Credentials". 172 | 173 | ![gc-2.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/gc-2.png) 174 | 175 | Next, Create OAuth client ID. Note, authentication requests will come from https://openvpn-portal.infrahouse.com, 176 | so I added it to "Authorized JavaScript origins". Another important setting is "Authorized redirect URIs". 177 | It must be https://openvpn-portal.infrahouse.com/login/google/authorized. For your domain it will be something like 178 | https://openvpn-portal.my-domain.com/login/google/authorized. 179 | 180 | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/gc-3.png) 181 | 182 | #### **Step 4.2**: Download OAuth 2.0 credentials. 183 | 184 | After you press a create button, you'll see a confirmation screen with a DOWNLOAD JSON link. 185 | Click it and save the file. 186 | 187 | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/gc-04.png) 188 | 189 | #### **Step 4.3**: Update Google OAuth Client ID secret value. 190 | 191 | From a previous I know the secret name is `google_client20240705183915856300000015`. Let's update it 192 | with a valid value. 193 | 194 | As a reference, the value should look like this 195 | ```shell 196 | $ jq < client_secret.json 197 | { 198 | "web": { 199 | "client_id": "145076599640-incpb6lilkj5duv3qs65n6f9r5avo482.apps.googleusercontent.com", 200 | "project_id": "openvpn-427715", 201 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 202 | "token_uri": "https://oauth2.googleapis.com/token", 203 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 204 | "client_secret": "very-secret-string", 205 | "redirect_uris": [ 206 | "https://openvpn-portal.infrahouse.com/login/google/authorized" 207 | ], 208 | "javascript_origins": [ 209 | "https://openvpn-portal.infrahouse.com" 210 | ] 211 | } 212 | } 213 | ``` 214 | 215 | ```shell 216 | $ ih-secrets \ 217 | --aws-region us-west-1 \ 218 | --aws-profile AWSAdministratorAccess-493370826424 \ 219 | set \ 220 | google_client20240705183915856300000015 \ 221 | client_secret.json 222 | ``` 223 | 224 | ### **Step 5**: Check on OpenVPN Portal. 225 | After a short time, the portal should pick up the new and value Google Client ID value. 226 | When you open https://openvpn-portal.infrahouse.com/ again, it will present a Google login window. 227 | 228 | After a successful authentication, the portal will show a page with OpenVPN client instructions. 229 | 230 | 231 | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/portal-start-page.png) 232 | 233 | ### **Step 6**: Install OpenVPN client. 234 | 235 | The portal has links to an installer for MacOS and Windows. For other OS-es you can go to https://openvpn.net/client/ 236 | and download the client from there. 237 | 238 | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/openvpn-download-page.png) 239 | 240 | ### **Step 6**: Download and import OpenVPN profile. 241 | 242 | Once the client is installed, click on https://openvpn-portal.infrahouse.com/profile and save the OpenVPN profile 243 | on your laptop. 244 | 245 | Double-click on the `aleks@infrahouse.com-openvpn.infrahouse.com.ovpn` file. It will open the client and suggest 246 | to import the profile. 247 | 248 | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/import-profile-page.png) 249 | 250 | ### **Step 7**: Connect to VPN. 251 | 252 | | Original Image | | Transformed Image | 253 | |-------------------------------------------|----|----------------------------------------------| 254 | | ![img.png](https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/main/assets/connect-page.png) | ➡️ | ![img.png](connected-page.png) | 255 | 256 | 257 | ### **Step 8**: Check network connectivity with the VPN server. 258 | 259 | The VPN server has address 172.16.0.1. Let's make sure it's reachable via the VPN. 260 | 261 | ```shell 262 | $ ping -c 3 172.16.0.1 263 | PING 172.16.0.1 (172.16.0.1) 56(84) bytes of data. 264 | 64 bytes from 172.16.0.1: icmp_seq=1 ttl=63 time=9.84 ms 265 | 64 bytes from 172.16.0.1: icmp_seq=2 ttl=63 time=12.8 ms 266 | 64 bytes from 172.16.0.1: icmp_seq=3 ttl=63 time=11.3 ms 267 | 268 | --- 172.16.0.1 ping statistics --- 269 | 3 packets transmitted, 3 received, 0% packet loss, time 2003ms 270 | rtt min/avg/max/mdev = 9.844/11.312/12.833/1.220 ms 271 | ``` 272 | However, if we try to ping the primary interface on the OpenVPN server, it's unreachable. 273 | 274 | ```shell 275 | $ ih-ec2 --aws-region us-west-1 --aws-profile AWSAdministratorAccess-493370826424 list 276 | 2024-07-05 12:37:23,046: INFO: botocore.tokens:tokens._refresher():305: Loading cached SSO token for infrahouse 277 | 2024-07-05 12:37:23,656: INFO: infrahouse_toolkit.cli.ih_ec2:__init__.ih_ec2():68: Connected to AWS as arn:aws:sts::493370826424:assumed-role/AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50/aleks 278 | 2024-07-05 12:37:23,714: INFO: botocore.tokens:tokens._refresher():305: Loading cached SSO token for infrahouse 279 | +--------------------+---------------------+----------------+-----------------+-------------------+--------------------+---------+ 280 | | Name | InstanceId | InstanceType | PublicDnsName | PublicIpAddress | PrivateIpAddress | State | 281 | +====================+=====================+================+=================+===================+====================+=========+ 282 | ... 283 | | openvpn | i-009c6fb01374dfa9e | m6in.large | | | 10.0.1.244 | running | 284 | | openvpn-portal | i-057151311f6ee0621 | t3.small | | | 10.0.1.104 | running | 285 | +--------------------+---------------------+----------------+-----------------+-------------------+--------------------+---------+ 286 | ``` 287 | 288 | ```shell 289 | $ ping -c 3 10.0.1.244 290 | PING 10.0.1.244 (10.0.1.244) 56(84) bytes of data. 291 | 292 | --- 10.0.1.244 ping statistics --- 293 | 3 packets transmitted, 0 received, 100% packet loss, time 2092ms 294 | ``` 295 | 296 | It's because we didn't let the VPN client what networks are accessible via the VPN tunnel. 297 | 298 | 299 | ### **Step 9**: Add routes to the VPN client. 300 | 301 | I want to make the management VPN in the InfraHouse cloud to be available to the VPN clients. To do that, let's 302 | amend the module configuration. 303 | 304 | ```shell 305 | $ git log -p -1 306 | commit d3c4f50dd8d427ab25ba30cadccd328bc1def7d3 (HEAD -> vpn, origin/vpn) 307 | Author: Oleksandr Kuzminskyi 308 | Date: Fri Jul 5 12:32:00 2024 -0700 309 | 310 | Add VPN routes 311 | 312 | diff --git a/vpn.tf b/vpn.tf 313 | index 035b925..83fe71f 100644 314 | --- a/vpn.tf 315 | +++ b/vpn.tf 316 | @@ -9,6 +9,12 @@ module "vpn" { 317 | lb_subnet_ids = module.management.subnet_public_ids 318 | google_oauth_client_writer = data.aws_iam_role.AWSAdministratorAccess.arn 319 | zone_id = module.infrahouse_com.infrahouse_zone_id 320 | + routes = [ 321 | + { 322 | + network : cidrhost(module.management.vpc_cidr_block, 0) 323 | + netmask : cidrnetmask(module.management.vpc_cidr_block) 324 | + } 325 | + ] 326 | } 327 | 328 | data "aws_iam_role" "AWSAdministratorAccess" { 329 | ``` 330 | 331 | Create a [pull request](https://github.com/infrahouse/aws-control-493370826424/pull/200), get it merged, 332 | and make sure it's successfully applied. 333 | 334 | ### **Step 10**: Check network availability of instances beyond the VPN server. 335 | 336 | When the Terraform change is applied, the OpenVPN autoscaling group triggers in instance refresh. 337 | It takes at least 5 to 10 minutes to rotate the instances. The openVPN client will reconnect when the server changes. 338 | Wait until it happens and check if you can ping private IP addresses of instances in your VPC. 339 | 340 | ```shell 341 | $ ih-ec2 --aws-region us-west-1 --aws-profile AWSAdministratorAccess-493370826424 list | grep openvpn 342 | | openvpn | i-04933b6fb1a9ae7c8 | m6in.large | | | | terminated | 343 | | openvpn | i-08705597ae7457604 | m6in.large | | | | terminated | 344 | | openvpn | i-00ecc4e72166d9ef0 | m6in.large | | | 10.0.3.144 | running | 345 | | openvpn | i-009c6fb01374dfa9e | m6in.large | | | | terminated | 346 | | openvpn | i-082016a9399b155f6 | m6in.large | | | 10.0.1.245 | running | 347 | | openvpn-portal | i-057151311f6ee0621 | t3.small | | | 10.0.1.104 | running | 348 | ``` 349 | 350 | ```shell 351 | $ ping -c 1 10.0.3.144 352 | PING 10.0.3.144 (10.0.3.144) 56(84) bytes of data. 353 | 64 bytes from 10.0.3.144: icmp_seq=1 ttl=62 time=63.1 ms 354 | 355 | --- 10.0.3.144 ping statistics --- 356 | 1 packets transmitted, 1 received, 0% packet loss, time 0ms 357 | rtt min/avg/max/mdev = 63.058/63.058/63.058/0.000 ms 358 | ``` 359 | 360 | ```shell 361 | $ ping -c 1 10.0.1.104 362 | PING 10.0.1.104 (10.0.1.104) 56(84) bytes of data. 363 | 64 bytes from 10.0.1.104: icmp_seq=1 ttl=253 time=7.29 ms 364 | 365 | --- 10.0.1.104 ping statistics --- 366 | 1 packets transmitted, 1 received, 0% packet loss, time 0ms 367 | rtt min/avg/max/mdev = 7.285/7.285/7.285/0.000 ms 368 | ``` 369 | ## Requirements 370 | 371 | | Name | Version | 372 | |------|---------| 373 | | [terraform](#requirement\_terraform) | ~> 1.5 | 374 | | [aws](#requirement\_aws) | ~> 5.11 | 375 | | [cloudinit](#requirement\_cloudinit) | ~> 2.3 | 376 | | [null](#requirement\_null) | ~> 3.2 | 377 | | [random](#requirement\_random) | ~> 3.6 | 378 | | [tls](#requirement\_tls) | ~> 4.0 | 379 | 380 | ## Providers 381 | 382 | | Name | Version | 383 | |------|---------| 384 | | [aws](#provider\_aws) | ~> 5.11 | 385 | | [aws.dns](#provider\_aws.dns) | ~> 5.11 | 386 | | [random](#provider\_random) | ~> 3.6 | 387 | | [tls](#provider\_tls) | ~> 4.0 | 388 | 389 | ## Modules 390 | 391 | | Name | Source | Version | 392 | |------|--------|---------| 393 | | [ca\_passkey](#module\_ca\_passkey) | registry.infrahouse.com/infrahouse/secret/aws | 0.5.0 | 394 | | [flask\_secret\_key](#module\_flask\_secret\_key) | registry.infrahouse.com/infrahouse/secret/aws | 0.5.0 | 395 | | [google\_client](#module\_google\_client) | infrahouse/secret/aws | 0.5.0 | 396 | | [instance\_profile](#module\_instance\_profile) | registry.infrahouse.com/infrahouse/instance-profile/aws | 1.4.0 | 397 | | [openvpn-portal](#module\_openvpn-portal) | registry.infrahouse.com/infrahouse/ecs/aws | 5.3.0 | 398 | | [userdata](#module\_userdata) | registry.infrahouse.com/infrahouse/cloud-init/aws | 1.16.0 | 399 | 400 | ## Resources 401 | 402 | | Name | Type | 403 | |------|------| 404 | | [aws_autoscaling_group.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group) | resource | 405 | | [aws_cloudwatch_metric_alarm.cpu_utilization_alarm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource | 406 | | [aws_efs_file_system.openvpn-config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_file_system) | resource | 407 | | [aws_efs_mount_target.openvpn-config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/efs_mount_target) | resource | 408 | | [aws_iam_policy.openvpn_portal_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | 409 | | [aws_iam_role.openvpn_portal_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | 410 | | [aws_iam_role_policy_attachment.task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | 411 | | [aws_key_pair.deployer](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/key_pair) | resource | 412 | | [aws_launch_template.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template) | resource | 413 | | [aws_lb.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb) | resource | 414 | | [aws_lb_listener.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener) | resource | 415 | | [aws_lb_target_group.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_target_group) | resource | 416 | | [aws_route53_record.vpn_cname](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | 417 | | [aws_security_group.efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 418 | | [aws_security_group.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 419 | | [aws_vpc_security_group_egress_rule.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | 420 | | [aws_vpc_security_group_egress_rule.efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | 421 | | [aws_vpc_security_group_ingress_rule.efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 422 | | [aws_vpc_security_group_ingress_rule.efs_icmp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 423 | | [aws_vpc_security_group_ingress_rule.icmp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 424 | | [aws_vpc_security_group_ingress_rule.openvpn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 425 | | [aws_vpc_security_group_ingress_rule.ssh](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | 426 | | [random_password.ca_passkey](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | 427 | | [random_password.flask_secret_key](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | 428 | | [random_string.asg_name](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 429 | | [random_string.profile-suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 430 | | [random_string.role-suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | 431 | | [tls_private_key.rsa](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource | 432 | | [aws_ami.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | 433 | | [aws_ami.ubuntu](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | 434 | | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | 435 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 436 | | [aws_default_tags.provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags) | data source | 437 | | [aws_iam_policy_document.instance_permissions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 438 | | [aws_iam_policy_document.openvpn_portal_role_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 439 | | [aws_iam_policy_document.openvpn_portal_role_permissions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 440 | | [aws_internet_gateway.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/internet_gateway) | data source | 441 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 442 | | [aws_route53_zone.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | 443 | | [aws_subnet.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | 444 | | [aws_vpc.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source | 445 | 446 | ## Inputs 447 | 448 | | Name | Description | Type | Default | Required | 449 | |------|-------------|------|---------|:--------:| 450 | | [alb\_access\_log\_force\_destroy](#input\_alb\_access\_log\_force\_destroy) | Destroy S3 bucket with access logs even if non-empty | `bool` | `false` | no | 451 | | [asg\_ami](#input\_asg\_ami) | Image for EC2 instances | `string` | `null` | no | 452 | | [asg\_health\_check\_grace\_period](#input\_asg\_health\_check\_grace\_period) | ASG will wait up to this number of minutes for instance to become healthy | `number` | `600` | no | 453 | | [asg\_max\_size](#input\_asg\_max\_size) | Maximum number of instances in ASG | `number` | `null` | no | 454 | | [asg\_min\_size](#input\_asg\_min\_size) | Minimum number of instances in ASG | `number` | `null` | no | 455 | | [backend\_subnet\_ids](#input\_backend\_subnet\_ids) | List of subnet ids where the webserver and database instances will be created | `list(string)` | n/a | yes | 456 | | [environment](#input\_environment) | Name of environment. | `string` | `"development"` | no | 457 | | [extra\_files](#input\_extra\_files) | Additional files to create on an instance. |
list(object({
content = string
path = string
permissions = string
}))
| `[]` | no | 458 | | [extra\_policies](#input\_extra\_policies) | A map of additional policy ARNs to attach to the jumphost role | `map(string)` | `{}` | no | 459 | | [extra\_repos](#input\_extra\_repos) | Additional APT repositories to configure on an instance. |
map(
object(
{
source = string
key = string
}
)
)
| `{}` | no | 460 | | [google\_oauth\_client\_writer](#input\_google\_oauth\_client\_writer) | ARN of an IAM role that can update content of google\_oauth\_client secret | `string` | n/a | yes | 461 | | [instance\_type](#input\_instance\_type) | Instance type to run the OpenVPN instances | `string` | `"m6in.large"` | no | 462 | | [key\_pair\_name](#input\_key\_pair\_name) | SSH keypair name to be deployed in EC2 instances | `string` | `null` | no | 463 | | [lb\_subnet\_ids](#input\_lb\_subnet\_ids) | List of subnet ids where the load balancer will be created | `list(string)` | n/a | yes | 464 | | [on\_demand\_base\_capacity](#input\_on\_demand\_base\_capacity) | If specified, the ASG will request spot instances and this will be the minimal number of on-demand instances. | `number` | `null` | no | 465 | | [packages](#input\_packages) | List of packages to install when the instances bootstraps. | `list(string)` | `[]` | no | 466 | | [portal-image](#input\_portal-image) | OpenVPN portal docker image | `string` | `"public.ecr.aws/infrahouse/openvpn-portal:latest"` | no | 467 | | [portal\_instance\_type](#input\_portal\_instance\_type) | AWS instance type for the portal service | `string` | `"t3.small"` | no | 468 | | [portal\_workers\_count](#input\_portal\_workers\_count) | Number of unicorn workers in OpenVPN portal | `number` | `4` | no | 469 | | [puppet\_custom\_facts](#input\_puppet\_custom\_facts) | A map of custom puppet facts | `any` | `{}` | no | 470 | | [puppet\_debug\_logging](#input\_puppet\_debug\_logging) | Enable debug logging if true. | `bool` | `false` | no | 471 | | [puppet\_environmentpath](#input\_puppet\_environmentpath) | A path for directory environments. | `string` | `"{root_directory}/environments"` | no | 472 | | [puppet\_hiera\_config\_path](#input\_puppet\_hiera\_config\_path) | Path to hiera configuration file. | `string` | `"{root_directory}/environments/{environment}/hiera.yaml"` | no | 473 | | [puppet\_manifest](#input\_puppet\_manifest) | Path to puppet manifest. By default ih-puppet will apply {root\_directory}/environments/{environment}/manifests/site.pp. | `string` | `null` | no | 474 | | [puppet\_module\_path](#input\_puppet\_module\_path) | Path to common puppet modules. | `string` | `"{root_directory}/modules"` | no | 475 | | [puppet\_root\_directory](#input\_puppet\_root\_directory) | Path where the puppet code is hosted. | `string` | `"/opt/puppet-code"` | no | 476 | | [root\_volume\_size](#input\_root\_volume\_size) | Root volume size in EC2 instance in Gigabytes | `number` | `30` | no | 477 | | [routes](#input\_routes) | List of network/netmasks in format 10.x.x.x/255.x.x.x that need to be pushed to a client. [{network: "10.0.0.0", netmask: "255.0.0.0"}] |
list(
object(
{
network : string,
netmask : string
}
)
)
| `[]` | no | 478 | | [service\_name](#input\_service\_name) | DNS hostname for the service. It's also used to name some resources like EC2 instances. | `string` | `"openvpn"` | no | 479 | | [smtp\_credentials\_secret](#input\_smtp\_credentials\_secret) | AWS secret name with SMTP credentials. The secret must contain a JSON with user and password keys. | `string` | `null` | no | 480 | | [sns\_topic\_alarm\_arn](#input\_sns\_topic\_alarm\_arn) | ARN of SNS topic for Cloudwatch alarms on base EC2 instance. | `string` | `null` | no | 481 | | [ubuntu\_codename](#input\_ubuntu\_codename) | Ubuntu version to use for the elasticsearch node | `string` | `"jammy"` | no | 482 | | [users](#input\_users) | A list of maps with user definitions according to the cloud-init format | `any` | `null` | no | 483 | | [zone\_id](#input\_zone\_id) | Domain name zone ID where the website will be available | `string` | n/a | yes | 484 | 485 | ## Outputs 486 | 487 | | Name | Description | 488 | |------|-------------| 489 | | [autoscaling\_group\_name](#output\_autoscaling\_group\_name) | Autoscaling group name. | 490 | | [google\_client\_secret](#output\_google\_client\_secret) | google\_client secret name. OpenVPN portal admin must update the secret with a Google OAuth client JSON. | 491 | | [load\_balancer\_arn](#output\_load\_balancer\_arn) | ARN of the load balancer for the OpenVPN portal | 492 | -------------------------------------------------------------------------------- /asg.tf: -------------------------------------------------------------------------------- 1 | 2 | module "userdata" { 3 | source = "registry.infrahouse.com/infrahouse/cloud-init/aws" 4 | version = "1.16.0" 5 | environment = var.environment 6 | role = "openvpn_server" 7 | puppet_debug_logging = var.puppet_debug_logging 8 | puppet_environmentpath = var.puppet_environmentpath 9 | puppet_hiera_config_path = var.puppet_hiera_config_path 10 | puppet_module_path = var.puppet_module_path 11 | puppet_root_directory = var.puppet_root_directory 12 | puppet_manifest = var.puppet_manifest 13 | ubuntu_codename = var.ubuntu_codename 14 | pre_runcmd = [ 15 | "aws ec2 modify-instance-attribute --no-source-dest-check --instance-id $(ec2metadata --instance-id)" 16 | ] 17 | packages = concat( 18 | var.packages, 19 | [ 20 | "awscli", 21 | "nfs-common" 22 | ] 23 | ) 24 | extra_files = var.extra_files 25 | extra_repos = var.extra_repos 26 | 27 | custom_facts = merge( 28 | var.puppet_custom_facts, 29 | { 30 | openvpn : { 31 | ca_key_passphrase_secret : module.ca_passkey.secret_name 32 | openvpn_port : local.openvpn_tcp_port 33 | routes : var.routes 34 | } 35 | }, 36 | { 37 | "efs" : { 38 | "file_system_id" : aws_efs_file_system.openvpn-config.id 39 | "dns_name" : aws_efs_file_system.openvpn-config.dns_name 40 | } 41 | }, 42 | var.smtp_credentials_secret != null ? { 43 | postfix : { 44 | smtp_credentials : var.smtp_credentials_secret 45 | } 46 | } : {} 47 | ) 48 | } 49 | 50 | resource "aws_launch_template" "openvpn" { 51 | name_prefix = "openvpn-" 52 | instance_type = var.instance_type 53 | key_name = local.key_pair_name 54 | image_id = var.asg_ami == null ? data.aws_ami.ubuntu.id : var.asg_ami 55 | iam_instance_profile { 56 | arn = module.instance_profile.instance_profile_arn 57 | } 58 | metadata_options { 59 | http_tokens = "required" 60 | http_endpoint = "enabled" 61 | } 62 | block_device_mappings { 63 | device_name = data.aws_ami.selected.root_device_name 64 | ebs { 65 | volume_size = var.root_volume_size 66 | delete_on_termination = true 67 | } 68 | } 69 | user_data = module.userdata.userdata 70 | tags = local.default_module_tags 71 | vpc_security_group_ids = [ 72 | aws_security_group.openvpn.id 73 | ] 74 | tag_specifications { 75 | resource_type = "volume" 76 | tags = merge( 77 | data.aws_default_tags.provider.tags, 78 | local.default_module_tags 79 | ) 80 | } 81 | tag_specifications { 82 | resource_type = "network-interface" 83 | tags = merge( 84 | data.aws_default_tags.provider.tags, 85 | local.default_module_tags 86 | ) 87 | } 88 | 89 | } 90 | 91 | resource "random_string" "asg_name" { 92 | length = 6 93 | special = false 94 | } 95 | locals { 96 | asg_name = "${aws_launch_template.openvpn.name}-${random_string.asg_name.result}" 97 | } 98 | 99 | resource "aws_autoscaling_group" "openvpn" { 100 | name = local.asg_name 101 | max_size = var.asg_max_size == null ? length(var.backend_subnet_ids) + 1 : var.asg_max_size 102 | min_size = var.asg_min_size == null ? length(var.backend_subnet_ids) : var.asg_min_size 103 | vpc_zone_identifier = var.backend_subnet_ids 104 | health_check_type = "ELB" 105 | health_check_grace_period = 900 106 | max_instance_lifetime = 90 * 24 * 3600 107 | dynamic "launch_template" { 108 | for_each = var.on_demand_base_capacity == null ? [1] : [] 109 | content { 110 | id = aws_launch_template.openvpn.id 111 | version = aws_launch_template.openvpn.latest_version 112 | } 113 | } 114 | dynamic "mixed_instances_policy" { 115 | for_each = var.on_demand_base_capacity == null ? [] : [1] 116 | content { 117 | instances_distribution { 118 | on_demand_base_capacity = var.on_demand_base_capacity 119 | on_demand_percentage_above_base_capacity = 0 120 | } 121 | launch_template { 122 | launch_template_specification { 123 | launch_template_id = aws_launch_template.openvpn.id 124 | version = aws_launch_template.openvpn.latest_version 125 | } 126 | } 127 | } 128 | } 129 | target_group_arns = [ 130 | aws_lb_target_group.openvpn.arn 131 | ] 132 | instance_refresh { 133 | strategy = "Rolling" 134 | preferences { 135 | min_healthy_percentage = 100 136 | } 137 | } 138 | tag { 139 | key = "Name" 140 | propagate_at_launch = true 141 | value = "openvpn" 142 | } 143 | dynamic "tag" { 144 | for_each = merge( 145 | local.default_module_tags, 146 | data.aws_default_tags.provider.tags 147 | ) 148 | 149 | content { 150 | key = tag.key 151 | propagate_at_launch = true 152 | value = tag.value 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /assets/502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/502.png -------------------------------------------------------------------------------- /assets/connect-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/connect-page.png -------------------------------------------------------------------------------- /assets/gc-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/gc-04.png -------------------------------------------------------------------------------- /assets/gc-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/gc-1.png -------------------------------------------------------------------------------- /assets/gc-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/gc-2.png -------------------------------------------------------------------------------- /assets/gc-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/gc-3.png -------------------------------------------------------------------------------- /assets/import-profile-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/import-profile-page.png -------------------------------------------------------------------------------- /assets/openvp-portal.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/openvp-portal.drawio.png -------------------------------------------------------------------------------- /assets/openvp-server.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/openvp-server.drawio.png -------------------------------------------------------------------------------- /assets/openvpn-download-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/openvpn-download-page.png -------------------------------------------------------------------------------- /assets/openvpn.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/openvpn.drawio.png -------------------------------------------------------------------------------- /assets/portal-start-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/portal-start-page.png -------------------------------------------------------------------------------- /assets/pr-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/pr-deploy.png -------------------------------------------------------------------------------- /assets/terraform-plan-stdout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/terraform-plan-stdout.png -------------------------------------------------------------------------------- /assets/terraform-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/assets/terraform-plan.png -------------------------------------------------------------------------------- /cloudwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_metric_alarm" "cpu_utilization_alarm" { 2 | count = var.sns_topic_alarm_arn != null ? 1 : 0 3 | alarm_name = format("CPU Alarm on ASG %s", aws_autoscaling_group.openvpn.name) 4 | comparison_operator = "GreaterThanThreshold" 5 | metric_name = "CPUUtilization" 6 | statistic = "Average" 7 | evaluation_periods = 1 8 | period = 60 9 | threshold = 90 10 | namespace = "AWS/EC2" 11 | alarm_actions = [var.sns_topic_alarm_arn] 12 | alarm_description = format("%s alarm - CPU exceeds 90 percent", aws_autoscaling_group.openvpn.name) 13 | dimensions = { 14 | AutoScalingGroupName = aws_autoscaling_group.openvpn.name 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /connected-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/connected-page.png -------------------------------------------------------------------------------- /datasources.tf: -------------------------------------------------------------------------------- 1 | data "aws_region" "current" {} 2 | data "aws_default_tags" "provider" {} 3 | data "aws_availability_zones" "available" { 4 | state = "available" 5 | } 6 | 7 | data "aws_caller_identity" "current" {} 8 | 9 | data "aws_ami" "ubuntu" { 10 | most_recent = true 11 | 12 | filter { 13 | name = "name" 14 | values = [local.ami_name_pattern] 15 | } 16 | 17 | filter { 18 | name = "architecture" 19 | values = ["x86_64"] 20 | } 21 | 22 | filter { 23 | name = "virtualization-type" 24 | values = ["hvm"] 25 | } 26 | 27 | filter { 28 | name = "state" 29 | values = [ 30 | "available" 31 | ] 32 | } 33 | 34 | owners = ["099720109477"] # Canonical 35 | } 36 | 37 | 38 | data "aws_subnet" "selected" { 39 | id = var.backend_subnet_ids[0] 40 | } 41 | 42 | data "aws_route53_zone" "current" { 43 | provider = aws.dns 44 | zone_id = var.zone_id 45 | } 46 | 47 | data "aws_vpc" "selected" { 48 | id = data.aws_subnet.selected.vpc_id 49 | } 50 | 51 | data "aws_internet_gateway" "current" { 52 | filter { 53 | name = "attachment.vpc-id" 54 | values = [data.aws_vpc.selected.id] 55 | } 56 | } 57 | 58 | data "aws_ami" "selected" { 59 | filter { 60 | name = "image-id" 61 | values = [ 62 | var.asg_ami == null ? data.aws_ami.ubuntu.id : var.asg_ami 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dns.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route53_record" "vpn_cname" { 2 | provider = aws.dns 3 | name = "${var.service_name}.${data.aws_route53_zone.current.name}" 4 | type = "CNAME" 5 | zone_id = data.aws_route53_zone.current.zone_id 6 | ttl = 300 7 | records = [ 8 | aws_lb.openvpn.dns_name 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /efs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "openvpn-config" { 2 | creation_token = "${var.service_name}-config" 3 | tags = merge( 4 | { 5 | Name = "${var.service_name}-config" 6 | }, 7 | local.default_module_tags 8 | ) 9 | } 10 | 11 | resource "aws_efs_mount_target" "openvpn-config" { 12 | for_each = toset(var.backend_subnet_ids) 13 | file_system_id = aws_efs_file_system.openvpn-config.id 14 | subnet_id = each.key 15 | security_groups = [ 16 | aws_security_group.efs.id 17 | ] 18 | lifecycle { 19 | create_before_destroy = false 20 | } 21 | } 22 | 23 | resource "aws_security_group" "efs" { 24 | description = "Security group for EFS volume" 25 | name_prefix = "openvpn-efs-" 26 | vpc_id = data.aws_subnet.selected.vpc_id 27 | 28 | tags = merge( 29 | { 30 | Name : "OpenVPN config" 31 | }, 32 | local.default_module_tags 33 | ) 34 | } 35 | 36 | resource "aws_vpc_security_group_ingress_rule" "efs" { 37 | description = "Allow NFS traffic to EFS volume" 38 | security_group_id = aws_security_group.efs.id 39 | from_port = 2049 40 | to_port = 2049 41 | ip_protocol = "tcp" 42 | cidr_ipv4 = data.aws_vpc.selected.cidr_block 43 | tags = merge({ 44 | Name = "NFS traffic" 45 | }, 46 | local.default_module_tags 47 | ) 48 | } 49 | 50 | resource "aws_vpc_security_group_ingress_rule" "efs_icmp" { 51 | description = "Allow all ICMP traffic" 52 | security_group_id = aws_security_group.efs.id 53 | from_port = -1 54 | to_port = -1 55 | ip_protocol = "icmp" 56 | cidr_ipv4 = "0.0.0.0/0" 57 | tags = merge( 58 | { 59 | Name = "ICMP traffic" 60 | }, 61 | local.default_module_tags 62 | ) 63 | } 64 | 65 | resource "aws_vpc_security_group_egress_rule" "efs" { 66 | security_group_id = aws_security_group.efs.id 67 | ip_protocol = "-1" 68 | cidr_ipv4 = "0.0.0.0/0" 69 | tags = merge( 70 | { 71 | Name = "EFS outgoing traffic" 72 | }, 73 | local.default_module_tags 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Happy coding!" 4 | -------------------------------------------------------------------------------- /iam-openvpn-portal.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "openvpn_portal_role_assume" { 2 | statement { 3 | actions = ["sts:AssumeRole"] 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = ["ecs-tasks.amazonaws.com"] 8 | } 9 | condition { 10 | test = "StringEquals" 11 | values = [ 12 | data.aws_caller_identity.current.account_id 13 | ] 14 | variable = "aws:SourceAccount" 15 | } 16 | condition { 17 | test = "ArnLike" 18 | values = [ 19 | "arn:aws:ecs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:*" 20 | ] 21 | variable = "aws:SourceArn" 22 | } 23 | } 24 | } 25 | 26 | data "aws_iam_policy_document" "openvpn_portal_role_permissions" { 27 | statement { 28 | actions = [ 29 | "sts:GetCallerIdentity" 30 | ] 31 | resources = ["*"] 32 | } 33 | statement { 34 | actions = [ 35 | "secretsmanager:GetSecretValue" 36 | ] 37 | resources = [ 38 | module.google_client.secret_arn, 39 | ] 40 | } 41 | } 42 | 43 | resource "aws_iam_policy" "openvpn_portal_role" { 44 | name_prefix = "openvpn-portal-" 45 | policy = data.aws_iam_policy_document.openvpn_portal_role_permissions.json 46 | } 47 | 48 | resource "aws_iam_role" "openvpn_portal_role" { 49 | name_prefix = "openvpn-portal-" 50 | assume_role_policy = data.aws_iam_policy_document.openvpn_portal_role_assume.json 51 | } 52 | 53 | resource "aws_iam_role_policy_attachment" "task_role" { 54 | policy_arn = aws_iam_policy.openvpn_portal_role.arn 55 | role = aws_iam_role.openvpn_portal_role.name 56 | } 57 | -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "instance_permissions" { 2 | source_policy_documents = var.extra_instance_profile_permissions != null ? [var.extra_instance_profile_permissions] : [] 3 | 4 | statement { 5 | actions = ["sts:GetCallerIdentity"] 6 | resources = ["*"] 7 | } 8 | statement { 9 | actions = [ 10 | "ec2:DescribeInstances", 11 | ] 12 | resources = [ 13 | "*" 14 | ] 15 | 16 | } 17 | statement { 18 | actions = [ 19 | "ec2:ModifyInstanceAttribute" 20 | ] 21 | resources = [ 22 | "*" 23 | ] 24 | condition { 25 | test = "StringEquals" 26 | values = [ 27 | aws_autoscaling_group.openvpn.name 28 | ] 29 | variable = "ec2:ResourceTag/aws:autoscaling:groupName" 30 | } 31 | } 32 | } 33 | 34 | resource "random_string" "profile-suffix" { 35 | length = 12 36 | special = false 37 | } 38 | 39 | module "instance_profile" { 40 | source = "registry.infrahouse.com/infrahouse/instance-profile/aws" 41 | version = "1.4.0" 42 | permissions = data.aws_iam_policy_document.instance_permissions.json 43 | profile_name = "openvpn-${random_string.profile-suffix.result}" 44 | extra_policies = merge( 45 | var.extra_policies 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /keypair.tf: -------------------------------------------------------------------------------- 1 | resource "tls_private_key" "rsa" { 2 | algorithm = "RSA" 3 | rsa_bits = 4096 4 | } 5 | 6 | resource "aws_key_pair" "deployer" { 7 | public_key = tls_private_key.rsa.public_key_openssh 8 | tags = merge( 9 | { 10 | service : var.service_name 11 | }, 12 | local.default_module_tags 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | resource "random_string" "role-suffix" { 2 | length = 6 3 | special = false 4 | } 5 | 6 | locals { 7 | default_module_tags = { 8 | environment : var.environment 9 | service : var.service_name 10 | account : data.aws_caller_identity.current.account_id 11 | created_by_module : "infrahouse/openvpn/aws" 12 | 13 | } 14 | openvpn_tcp_port = 1194 15 | key_pair_name = var.key_pair_name == null ? aws_key_pair.deployer.key_name : var.key_pair_name 16 | 17 | ami_name_pattern = contains( 18 | ["focal", "jammy"], var.ubuntu_codename 19 | ) ? "ubuntu/images/hvm-ssd/ubuntu-${var.ubuntu_codename}-*" : "ubuntu/images/hvm-ssd-gp3/ubuntu-${var.ubuntu_codename}-*" 20 | 21 | } 22 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/main.tf -------------------------------------------------------------------------------- /nlb.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | name_prefix = substr("openvpn", 0, 6) 3 | } 4 | 5 | resource "aws_lb" "openvpn" { 6 | name_prefix = local.name_prefix 7 | load_balancer_type = "network" 8 | subnets = var.lb_subnet_ids 9 | enable_cross_zone_load_balancing = true 10 | security_groups = [ 11 | aws_security_group.openvpn.id 12 | ] 13 | tags = local.default_module_tags 14 | } 15 | 16 | resource "aws_lb_target_group" "openvpn" { 17 | name_prefix = local.name_prefix 18 | port = local.openvpn_tcp_port 19 | protocol = "TCP" 20 | vpc_id = data.aws_vpc.selected.id 21 | tags = local.default_module_tags 22 | stickiness { 23 | enabled = true 24 | type = "source_ip" 25 | } 26 | health_check { 27 | protocol = "TCP" 28 | port = local.openvpn_tcp_port 29 | } 30 | } 31 | 32 | resource "aws_lb_listener" "openvpn" { 33 | load_balancer_arn = aws_lb.openvpn.arn 34 | port = local.openvpn_tcp_port 35 | protocol = "TCP" 36 | tags = local.default_module_tags 37 | default_action { 38 | type = "forward" 39 | target_group_arn = aws_lb_target_group.openvpn.arn 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_client_secret" { 2 | description = "google_client secret name. OpenVPN portal admin must update the secret with a Google OAuth client JSON." 3 | value = module.google_client.secret_name 4 | } 5 | 6 | output "load_balancer_arn" { 7 | description = "ARN of the load balancer for the OpenVPN portal" 8 | value = module.openvpn-portal.load_balancer_arn 9 | } 10 | 11 | output "autoscaling_group_name" { 12 | description = "Autoscaling group name." 13 | value = aws_autoscaling_group.openvpn.name 14 | } 15 | -------------------------------------------------------------------------------- /portal.tf: -------------------------------------------------------------------------------- 1 | module "openvpn-portal" { 2 | source = "registry.infrahouse.com/infrahouse/ecs/aws" 3 | version = "5.7.1" 4 | providers = { 5 | aws = aws 6 | aws.dns = aws.dns 7 | } 8 | environment = var.environment 9 | service_name = "${var.service_name}-portal" 10 | docker_image = var.portal-image 11 | load_balancer_subnets = var.lb_subnet_ids 12 | asg_subnets = var.backend_subnet_ids 13 | zone_id = data.aws_route53_zone.current.zone_id 14 | dns_names = ["${var.service_name}-portal"] 15 | internet_gateway_id = data.aws_internet_gateway.current.id 16 | ssh_key_name = local.key_pair_name 17 | container_port = 8080 18 | container_healthcheck_command = "curl -sf http://localhost:8080/status || exit 1" 19 | service_health_check_grace_period_seconds = 300 20 | healthcheck_path = "/status" 21 | healthcheck_response_code_matcher = "200" 22 | idle_timeout = 600 23 | task_desired_count = 1 24 | task_min_count = 1 25 | task_max_count = 1 26 | asg_min_size = 1 27 | asg_max_size = 1 28 | on_demand_base_capacity = var.on_demand_base_capacity 29 | asg_instance_type = var.portal_instance_type 30 | container_cpu = 400 # One vCPU is 1024 31 | container_memory = 200 # Value in MB 32 | access_log_force_destroy = var.alb_access_log_force_destroy 33 | cloudinit_extra_commands = var.cloudinit_extra_commands 34 | 35 | extra_instance_profile_permissions = var.extra_instance_profile_permissions 36 | task_efs_volumes = { 37 | data : { 38 | file_system_id : aws_efs_file_system.openvpn-config.id 39 | container_path : "/etc/openvpn" 40 | } 41 | } 42 | 43 | task_environment_variables = concat( 44 | [ 45 | { 46 | name : "DEBUG", 47 | value : true, 48 | }, 49 | { 50 | name : "AWS_DEFAULT_REGION", 51 | value : data.aws_region.current.name 52 | }, 53 | { 54 | name : "FLASK_SECRET_KEY", 55 | value : module.flask_secret_key.secret_name 56 | }, 57 | { 58 | name : "GOOGLE_OAUTH_CLIENT_SECRET_NAME", 59 | value : module.google_client.secret_name 60 | }, 61 | { 62 | name : "OPENVPN_HOSTNAME", 63 | value : aws_route53_record.vpn_cname.fqdn 64 | }, 65 | { 66 | name : "OPENVPN_PORT", 67 | value : local.openvpn_tcp_port 68 | }, 69 | { 70 | name : "WORKERS", 71 | value : var.portal_workers_count 72 | } 73 | ] 74 | ) 75 | task_role_arn = aws_iam_role.openvpn_portal_role.arn 76 | users = var.users 77 | } 78 | -------------------------------------------------------------------------------- /portal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | ENV WORKERS 4 8 | RUN apt-get update && \ 9 | apt-get install -y --no-install-recommends easy-rsa && \ 10 | apt-get clean && \ 11 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 12 | 13 | COPY . . 14 | 15 | CMD uvicorn portal:asgi_app --host 0.0.0.0 --workers $WORKERS --port 8080 16 | -------------------------------------------------------------------------------- /portal/README.rst: -------------------------------------------------------------------------------- 1 | OpenVPN Portal 2 | ============== 3 | -------------------------------------------------------------------------------- /portal/portal/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from io import BytesIO 3 | from logging import getLogger 4 | from os import environ 5 | from os import path as osp 6 | from subprocess import check_call 7 | from textwrap import dedent 8 | 9 | 10 | import boto3 11 | import requests 12 | from asgiref.wsgi import WsgiToAsgi 13 | from flask import ( 14 | Flask, 15 | redirect, 16 | url_for, 17 | session, 18 | abort, 19 | request, 20 | send_file, 21 | send_from_directory, 22 | ) 23 | from flask_dance.contrib.google import make_google_blueprint, google 24 | from infrahouse_toolkit.cli.ih_secrets.cmd_get import get_secret 25 | from infrahouse_toolkit.logging import setup_logging 26 | from oauthlib.oauth2 import TokenExpiredError 27 | from werkzeug.middleware.proxy_fix import ProxyFix 28 | 29 | LOG = getLogger() 30 | DEBUG = bool(environ.get("DEBUG")) 31 | EASY_RSA = "/usr/share/easy-rsa/easyrsa" 32 | setup_logging(LOG, debug=DEBUG) 33 | 34 | 35 | aws_client = boto3.client("secretsmanager") 36 | 37 | app = Flask(__name__, static_folder="assets") 38 | app.wsgi_app = ProxyFix(app.wsgi_app) 39 | app.secret_key = get_secret(aws_client, environ["FLASK_SECRET_KEY"]) 40 | google_oauth_client_secret_value = json.loads( 41 | get_secret(aws_client, environ["GOOGLE_OAUTH_CLIENT_SECRET_NAME"]) 42 | ) 43 | openvpn_config_directory = environ.get("OPENVPN_CONFIG_DIRECTORY", "/etc/openvpn") 44 | 45 | # Replace with your Google OAuth2 credentials 46 | google_bp = make_google_blueprint( 47 | client_id=google_oauth_client_secret_value["web"]["client_id"], 48 | client_secret=google_oauth_client_secret_value["web"]["client_secret"], 49 | scope=[ 50 | "openid", 51 | "https://www.googleapis.com/auth/userinfo.profile", 52 | "https://www.googleapis.com/auth/userinfo.email", 53 | ], 54 | ) 55 | 56 | app.register_blueprint(google_bp, url_prefix="/login") 57 | asgi_app = WsgiToAsgi(app) 58 | 59 | 60 | @app.route("/") 61 | def index(): 62 | LOG.debug("google.authorized = %s", google.authorized) 63 | if not google.authorized: 64 | return redirect(url_for("google.login")) 65 | 66 | try: 67 | resp = google.get("/oauth2/v2/userinfo") 68 | assert resp.ok, resp.text 69 | LOG.debug("get('/oauth2/v2/userinfo') = %s", resp.text) 70 | email = resp.json()["email"] 71 | name = resp.json()["name"] 72 | 73 | # Generate a certificate if it doesn't exist 74 | ensure_certificate(openvpn_config_directory, email) 75 | 76 | return index_page(name, email) 77 | except TokenExpiredError: 78 | return redirect(url_for("google.login")) 79 | 80 | 81 | @app.route("/profile") 82 | def profile(): 83 | LOG.debug("google.authorized = %s", google.authorized) 84 | if not google.authorized: 85 | return redirect(url_for("google.login")) 86 | 87 | openvpn_hostname = environ["OPENVPN_HOSTNAME"] 88 | resp = google.get("/oauth2/v2/userinfo") 89 | assert resp.ok, resp.text 90 | LOG.debug("get('/oauth2/v2/userinfo') = %s", resp.text) 91 | email = resp.json()["email"] 92 | 93 | file_obj = BytesIO() 94 | file_obj.write( 95 | generate_profile( 96 | openvpn_config_directory, 97 | email, 98 | openvpn_hostname, 99 | environ["OPENVPN_PORT"], 100 | ).encode() 101 | ) 102 | file_obj.seek(0) # Reset file pointer to beginning 103 | 104 | return send_file( 105 | file_obj, 106 | mimetype="application/x-openvpn-profile", 107 | as_attachment=True, 108 | download_name=f"{email}-{openvpn_hostname}.ovpn", 109 | ) 110 | 111 | 112 | @app.route("/login/google") 113 | def google_login(): 114 | redirect_url = redirect(url_for("google.login")) 115 | LOG.debug("redirect_url = %s", redirect_url) 116 | return redirect_url 117 | 118 | 119 | @app.route("/logout") 120 | def logout(): 121 | if google.authorized: 122 | userinfo = google.get("/oauth2/v2/userinfo") 123 | token = google.token["access_token"] 124 | # Revoke the token on Google's side 125 | resp = requests.post( 126 | "https://accounts.google.com/o/oauth2/revoke", 127 | params={"token": token}, 128 | headers={"content-type": "application/x-www-form-urlencoded"}, 129 | ) 130 | if resp.status_code == 200: 131 | # Clear the user session 132 | del google.token 133 | session.clear() 134 | LOG.info(f"Successful logout for %s", json.dumps(userinfo.json(), indent=4)) 135 | return redirect(url_for("index")) 136 | else: 137 | return "Failed to revoke token", 400 138 | return redirect(url_for("index")) 139 | 140 | 141 | @app.route("/status") 142 | def status(): 143 | if not osp.exists(openvpn_config_directory): 144 | LOG.error( 145 | "OpenVPN configuration directory %s doesn't exist.", 146 | openvpn_config_directory, 147 | ) 148 | abort(500) 149 | 150 | return "OK" 151 | 152 | 153 | @app.route("/favicon.ico") 154 | def favicon(): 155 | return send_from_directory( 156 | app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon" 157 | ) 158 | 159 | 160 | def generate_client_key(config_dir, email): 161 | # Generate request 162 | check_call( 163 | [EASY_RSA, f"--vars={config_dir}/vars", "gen-req", email, "nopass"], 164 | cwd=config_dir, 165 | env={"EASYRSA_REQ_CN": email}, 166 | ) 167 | # Sign the client request 168 | check_call( 169 | [EASY_RSA, f"--vars={config_dir}/vars", "sign-req", "client", email], 170 | env={"EASYRSA_PASSIN": f"file:{openvpn_config_directory}/ca_passphrase"}, 171 | cwd=config_dir, 172 | ) 173 | 174 | 175 | def ensure_certificate(config_dir, email): 176 | cert_path = osp.join(config_dir, "pki", "issued", f"{email}.crt") 177 | if not osp.exists(cert_path): 178 | generate_client_key(config_dir, email) 179 | 180 | 181 | def generate_profile(config_dir, email, vpn_hostname, vpn_port): 182 | return f""" 183 | client 184 | dev tun 185 | proto tcp 186 | remote {vpn_hostname} {vpn_port} 187 | nobind 188 | 189 | # Certificate Authorities, Client Certificate, and Client Key 190 | 191 | {open(osp.join(config_dir, "pki/ca.crt"), encoding="UTF-8").read()} 192 | 193 | 194 | 195 | {open(osp.join(config_dir, f"pki/issued/{email}.crt"), encoding="UTF-8").read()} 196 | 197 | 198 | 199 | {open(osp.join(config_dir, f"pki/private/{email}.key"), encoding="UTF-8").read()} 200 | 201 | 202 | 203 | {open(osp.join(config_dir, f"ta.key"), encoding="UTF-8").read()} 204 | 205 | key-direction 1 206 | 207 | cipher AES-256-CBC 208 | auth SHA256 209 | 210 | # Verbosity level 211 | verb 3 212 | """ 213 | 214 | 215 | def index_page(name, email): 216 | domain = email.split("@")[1] 217 | return dedent( 218 | f""" 219 | 220 | 221 | 222 | 223 | 224 | OpenVPN Portal: {domain} 225 | 226 | 231 | 232 | 233 |

Welcome to {domain} OpenVPN Portal

234 |

235 | Logged as {name}<{email}>. Logout 236 |

237 |

VPN client setup instructions

238 |

Step 1: Download the client app

239 | 240 | 241 | 242 | 243 | 244 | 252 | 264 | 265 | 266 | 267 | 268 |
MacOSWindowsOther
245 |

MacOS Installer

246 |

247 | 248 | Installation instructions and alternative versions 249 | 250 |

251 |
253 |

254 | 255 | Windows Installer 256 | 257 |

258 |

259 | 260 | Installation instructions and alternative versions 261 | 262 |

263 |
Other OS-es
269 |

270 | Step 2: Download your OpenVPN profile. 271 |

272 |

273 | Step 3: Find the profile in the file manager and open it. Follow the onscreen instructions. 274 | 275 | 276 | """ 277 | ) 278 | -------------------------------------------------------------------------------- /portal/portal/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/portal/portal/assets/favicon.ico -------------------------------------------------------------------------------- /portal/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | export AWS_DEFAULT_REGION=us-east-2 6 | 7 | AWS_PROFILE="infrahouse-admin-cicd" 8 | 9 | aws --profile $AWS_PROFILE sts get-caller-identity || aws --profile infrahouse-admin-cicd sso login 10 | 11 | aws --profile $AWS_PROFILE ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 303467602807.dkr.ecr.us-east-2.amazonaws.com 12 | docker build -t portal . 13 | docker tag portal:latest 303467602807.dkr.ecr.us-east-2.amazonaws.com/portal:latest 14 | docker push 303467602807.dkr.ecr.us-east-2.amazonaws.com/portal:latest 15 | 16 | 17 | aws --profile $AWS_PROFILE ecs update-service --cluster openvpn-portal --service openvpn-portal --force-new-deployment > /dev/null 18 | echo "Restarting the portal service. Please wait..." 19 | aws --profile $AWS_PROFILE ecs wait services-stable --cluster openvpn-portal --services openvpn-portal 20 | echo "done" 21 | -------------------------------------------------------------------------------- /portal/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref ~= 3.8 2 | boto3 ~= 1.26 3 | botocore ~= 1.29 4 | infrahouse-toolkit 5 | Flask ~= 3.0 6 | Flask-Dance ~= 7.1 7 | uvicorn ~= 0.30 -------------------------------------------------------------------------------- /portal/requirements_dev.txt: -------------------------------------------------------------------------------- 1 | black ~= 23.1.0 2 | build ~= 0.10 3 | bump2version ~= 1.0 4 | isort ~= 5.12 5 | mdformat ~= 0.7 6 | mdformat-gfm ~= 0.3 7 | pylint ~= 2.17 8 | pytest ~= 7.2 9 | pytest-cov ~= 4.1 10 | pytest-timeout ~= 2.1 11 | sphinx ~= 6.1 12 | tox ~= 4.4 13 | twine ~= 4.0 14 | yamllint ~= 1.29 15 | -------------------------------------------------------------------------------- /portal/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import find_packages, setup 6 | 7 | CODEC = "utf-8" 8 | 9 | with open("README.rst", encoding=CODEC) as readme_file: 10 | readme = readme_file.read() 11 | 12 | HISTORY = "See git log" 13 | 14 | 15 | def parse_requirements(req_file): 16 | """ 17 | Parse file with requirements and return a dictionary 18 | consumable by setuptools. 19 | 20 | :param req_file: path to requirements file. 21 | :type req_file: str 22 | :return: Dictionary with requirements. 23 | :rtype: dict 24 | """ 25 | with open(req_file, encoding=CODEC) as f_descr: 26 | reqs = f_descr.read().strip().split("\n") 27 | return [x for x in reqs if x and not x.strip().startswith("#")] 28 | 29 | 30 | requirements = parse_requirements("requirements.txt") 31 | test_requirements = parse_requirements("requirements_dev.txt") 32 | 33 | setup( 34 | author="Oleksandr Kuzminskyi", 35 | author_email="aleks@infrahouse.com", 36 | python_requires=">=3.8", 37 | classifiers=[ 38 | "Development Status :: 2 - Pre-Alpha", 39 | "Intended Audience :: Developers", 40 | "License :: OSI Approved :: Apache Software License", 41 | "Natural Language :: English", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.8", 44 | "Programming Language :: Python :: 3.9", 45 | "Programming Language :: Python :: 3.10", 46 | ], 47 | description="A collection of tools for building infrastructure.", 48 | entry_points={ 49 | "console_scripts": [ 50 | "ih-certbot=infrahouse_toolkit.cli.ih_certbot:ih_certbot", 51 | "ih-ec2=infrahouse_toolkit.cli.ih_ec2:ih_ec2", 52 | "ih-elastic=infrahouse_toolkit.cli.ih_elastic:ih_elastic", 53 | "ih-github=infrahouse_toolkit.cli.ih_github:ih_github", 54 | "ih-plan=infrahouse_toolkit.cli.ih_plan:ih_plan", 55 | "ih-puppet=infrahouse_toolkit.cli.ih_puppet:ih_puppet", 56 | "ih-registry=infrahouse_toolkit.cli.ih_registry:ih_registry", 57 | "ih-s3-reprepro=infrahouse_toolkit.cli.ih_s3_reprepro:ih_s3_reprepro", 58 | "ih-secrets=infrahouse_toolkit.cli.ih_secrets:ih_secrets", 59 | "ih-skeema=infrahouse_toolkit.cli.ih_skeema:ih_skeema", 60 | ], 61 | }, 62 | install_requires=requirements, 63 | license="Apache Software License 2.0", 64 | long_description=readme + "\n\n" + HISTORY, 65 | include_package_data=True, 66 | keywords="infrahouse-toolkit", 67 | name="infrahouse-toolkit", 68 | packages=find_packages(include=["infrahouse_toolkit", "infrahouse_toolkit.*"]), 69 | test_suite="tests", 70 | tests_require=test_requirements, 71 | url="https://github.com/infrahouse/infrahouse-toolkit", 72 | version="2.24.1", 73 | zip_safe=False, 74 | ) 75 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "prConcurrentLimit": 1 7 | } 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black ~= 24.3 2 | boto3 ~= 1.26 3 | infrahouse-toolkit ~= 2.0 4 | pytest ~= 8.2 5 | requests ~= 2.31 6 | -------------------------------------------------------------------------------- /secrets.tf: -------------------------------------------------------------------------------- 1 | resource "random_password" "ca_passkey" { 2 | length = 31 3 | } 4 | module "ca_passkey" { 5 | source = "registry.infrahouse.com/infrahouse/secret/aws" 6 | version = "0.5.0" 7 | secret_description = "OpenVPN CA Key Passphrase" 8 | secret_name_prefix = "openvpn_ca_passphrase" 9 | secret_value = random_password.ca_passkey.result 10 | tags = local.default_module_tags 11 | readers = [ 12 | module.instance_profile.instance_role_arn 13 | ] 14 | } 15 | 16 | 17 | resource "random_password" "flask_secret_key" { 18 | special = false 19 | length = 31 20 | } 21 | module "flask_secret_key" { 22 | source = "registry.infrahouse.com/infrahouse/secret/aws" 23 | version = "0.5.0" 24 | secret_description = "Flask secret key" 25 | secret_name_prefix = "flask_secret_key" 26 | secret_value = random_password.flask_secret_key.result 27 | tags = local.default_module_tags 28 | readers = [ 29 | aws_iam_role.openvpn_portal_role.arn 30 | ] 31 | } 32 | 33 | module "google_client" { 34 | source = "infrahouse/secret/aws" 35 | version = "0.5.0" 36 | secret_description = "A JSON with Google OAuth Client ID" 37 | secret_name_prefix = "google_client" 38 | tags = local.default_module_tags 39 | readers = [ 40 | aws_iam_role.openvpn_portal_role.arn 41 | ] 42 | writers = [ 43 | var.google_oauth_client_writer 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /security_group.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "openvpn" { 2 | vpc_id = data.aws_subnet.selected.vpc_id 3 | name_prefix = "openvpn" 4 | description = "Manage traffic to openvpn" 5 | tags = merge({ 6 | Name : "openvpn" 7 | }, 8 | local.default_module_tags 9 | ) 10 | } 11 | 12 | resource "aws_vpc_security_group_ingress_rule" "ssh" { 13 | description = "Allow SSH traffic" 14 | security_group_id = aws_security_group.openvpn.id 15 | from_port = 22 16 | to_port = 22 17 | ip_protocol = "tcp" 18 | cidr_ipv4 = data.aws_vpc.selected.cidr_block 19 | tags = merge({ 20 | Name = "SSH access" 21 | }, 22 | local.default_module_tags 23 | ) 24 | } 25 | 26 | resource "aws_vpc_security_group_ingress_rule" "openvpn" { 27 | description = "Allow NLB health checks" 28 | security_group_id = aws_security_group.openvpn.id 29 | from_port = local.openvpn_tcp_port 30 | to_port = local.openvpn_tcp_port 31 | ip_protocol = "tcp" 32 | cidr_ipv4 = "0.0.0.0/0" 33 | tags = merge( 34 | { 35 | Name = "OpenVPN access" 36 | }, 37 | local.default_module_tags 38 | ) 39 | } 40 | 41 | resource "aws_vpc_security_group_ingress_rule" "icmp" { 42 | description = "Allow all ICMP traffic" 43 | security_group_id = aws_security_group.openvpn.id 44 | from_port = -1 45 | to_port = -1 46 | ip_protocol = "icmp" 47 | cidr_ipv4 = "0.0.0.0/0" 48 | tags = merge( 49 | { 50 | Name = "ICMP traffic" 51 | }, 52 | local.default_module_tags 53 | ) 54 | } 55 | 56 | resource "aws_vpc_security_group_egress_rule" "default" { 57 | description = "Allow all traffic" 58 | security_group_id = aws_security_group.openvpn.id 59 | ip_protocol = "-1" 60 | cidr_ipv4 = "0.0.0.0/0" 61 | tags = merge( 62 | { 63 | Name = "outgoing traffic" 64 | }, 65 | local.default_module_tags 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.11" 8 | configuration_aliases = [ 9 | aws.dns # AWS provider for DNS 10 | ] 11 | 12 | } 13 | null = { 14 | source = "hashicorp/null" 15 | version = "~> 3.2" 16 | } 17 | cloudinit = { 18 | source = "hashicorp/cloudinit" 19 | version = "~> 2.3" 20 | } 21 | random = { 22 | source = "hashicorp/random" 23 | version = "~> 3.6" 24 | } 25 | tls = { 26 | source = "hashicorp/tls" 27 | version = "~> 4.0" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test_data/openvpn/.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | .terraform.tfstate.lock.info 3 | -------------------------------------------------------------------------------- /test_data/openvpn/datasources.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "this" {} 2 | data "aws_region" "current" {} 3 | data "aws_availability_zones" "available" { 4 | state = "available" 5 | } 6 | 7 | data "aws_route53_zone" "test-zone" { 8 | name = var.test_zone 9 | } 10 | 11 | data "aws_vpc" "mgmt" { 12 | id = var.vpc_id 13 | } 14 | -------------------------------------------------------------------------------- /test_data/openvpn/ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "portal" { 2 | name = "portal" 3 | force_delete = true 4 | } 5 | -------------------------------------------------------------------------------- /test_data/openvpn/jumphost.tf: -------------------------------------------------------------------------------- 1 | module "jumphost" { 2 | source = "registry.infrahouse.com/infrahouse/jumphost/aws" 3 | version = "~> 2.3" 4 | environment = var.environment 5 | keypair_name = aws_key_pair.mediapc.key_name 6 | route53_zone_id = data.aws_route53_zone.test-zone.zone_id 7 | subnet_ids = var.backend_subnet_ids 8 | nlb_subnet_ids = var.lb_subnet_ids 9 | asg_min_size = 1 10 | asg_max_size = 1 11 | puppet_hiera_config_path = "/opt/infrahouse-puppet-data/environments/${var.environment}/hiera.yaml" 12 | packages = [ 13 | "infrahouse-puppet-data" 14 | ] 15 | ssh_host_keys = [ 16 | { 17 | type : "rsa" 18 | private : file("${path.module}/ssh_keys/ssh_host_rsa_key") 19 | public : file("${path.module}/ssh_keys/ssh_host_rsa_key.pub") 20 | }, 21 | { 22 | type : "ecdsa" 23 | private : file("${path.module}/ssh_keys/ssh_host_ecdsa_key") 24 | public : file("${path.module}/ssh_keys/ssh_host_ecdsa_key.pub") 25 | }, 26 | { 27 | type : "ed25519" 28 | private : file("${path.module}/ssh_keys/ssh_host_ed25519_key") 29 | public : file("${path.module}/ssh_keys/ssh_host_ed25519_key.pub") 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test_data/openvpn/locals.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/test_data/openvpn/locals.tf -------------------------------------------------------------------------------- /test_data/openvpn/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_key_pair" "mediapc" { 2 | public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDpgAP1z1Lxg9Uv4tam6WdJBcAftZR4ik7RsSr6aNXqfnTj4civrhd/q8qMqF6wL//3OujVDZfhJcffTzPS2XYhUxh/rRVOB3xcqwETppdykD0XZpkHkc8XtmHpiqk6E9iBI4mDwYcDqEg3/vrDAGYYsnFwWmdDinxzMH1Gei+NPTmTqU+wJ1JZvkw3WBEMZKlUVJC/+nuv+jbMmCtm7sIM4rlp2wyzLWYoidRNMK97sG8+v+mDQol/qXK3Fuetj+1f+vSx2obSzpTxL4RYg1kS6W1fBlSvstDV5bQG4HvywzN5Y8eCpwzHLZ1tYtTycZEApFdy+MSfws5vPOpggQlWfZ4vA8ujfWAF75J+WABV4DlSJ3Ng6rLMW78hVatANUnb9s4clOS8H6yAjv+bU3OElKBkQ10wNneoFIMOA3grjPvPp5r8dI0WDXPIznJThDJO5yMCy3OfCXlu38VDQa1sjVj1zAPG+Vn2DsdVrl50hWSYSB17Zww0MYEr8N5rfFE= aleks@MediaPC" 3 | } 4 | 5 | module "openvpn" { 6 | source = "../../" 7 | providers = { 8 | aws = aws 9 | aws.dns = aws 10 | } 11 | backend_subnet_ids = var.backend_subnet_ids 12 | lb_subnet_ids = var.lb_subnet_ids 13 | zone_id = data.aws_route53_zone.test-zone.zone_id 14 | key_pair_name = aws_key_pair.mediapc.key_name 15 | asg_min_size = 1 16 | asg_max_size = 1 17 | portal-image = "303467602807.dkr.ecr.us-east-2.amazonaws.com/portal:latest" 18 | google_oauth_client_writer = "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14" 19 | alb_access_log_force_destroy = true 20 | portal_workers_count = 1 21 | portal_instance_type = "t3a.nano" 22 | routes = [ 23 | { 24 | network : cidrhost(data.aws_vpc.mgmt.cidr_block, 0) 25 | netmask : cidrnetmask(data.aws_vpc.mgmt.cidr_block) 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test_data/openvpn/outputs.tf: -------------------------------------------------------------------------------- 1 | output "google_client_secret" { 2 | value = module.openvpn.google_client_secret 3 | } 4 | -------------------------------------------------------------------------------- /test_data/openvpn/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | assume_role { 4 | role_arn = var.role_arn 5 | } 6 | default_tags { 7 | tags = { 8 | "created_by" : "infrahouse/terraform-aws-openvpn" # GitHub repository that created a resource 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test_data/openvpn/secrets.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/test_data/openvpn/secrets.tf -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS 3 | 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSdAFLWU32BzriOQIW2mkUfgVGKK6p9 4 | u2JWSfS+LqgXG+MuBm2JiE5vAfKWpK/Av5xpM/O1N72doWZGaiB4FuCVAAAAsIfMo2uHzK 5 | NrAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0AUtZTfYHOuI5A 6 | hbaaRR+BUYorqn27YlZJ9L4uqBcb4y4GbYmITm8B8pakr8C/nGkz87U3vZ2hZkZqIHgW4J 7 | UAAAAhAP/CyeyAKY2OyBbPWppDOZOYfMqAghqE+KuTA5PR5EbMAAAAEnJvb3RAaXAtMTAt 8 | MS0yLTIzMAECAwQF 9 | -----END OPENSSH PRIVATE KEY----- -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0AUtZTfYHOuI5AhbaaRR+BUYorqn27YlZJ9L4uqBcb4y4GbYmITm8B8pakr8C/nGkz87U3vZ2hZkZqIHgW4JU= root@ip-10-1-2-230 -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_ed25519_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBnSzfAx9D/GGUauBHTK/IalwmE0oPILaGlM8eGr1atxgAAAJi7L00iuy9N 4 | IgAAAAtzc2gtZWQyNTUxOQAAACBnSzfAx9D/GGUauBHTK/IalwmE0oPILaGlM8eGr1atxg 5 | AAAEBFOvSMke4ZDhubWM3CqKjMCqLxm5h4eJLYhx44Ndw+NGdLN8DH0P8YZRq4EdMr8hqX 6 | CYTSg8gtoaUzx4avVq3GAAAAEnJvb3RAaXAtMTAtMS0yLTIzMAECAw== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_ed25519_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGdLN8DH0P8YZRq4EdMr8hqXCYTSg8gtoaUzx4avVq3G root@ip-10-1-2-230 -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAYEAtmJ6V5RxOHmQuUwKFrMBKjNHcHnC3hFDXlHcmRNjaim6Kqr6HL92 4 | GvTxza06kUkgxQ/1dxdvi36m+Gr1bITVg4S9oi9hNzxIJve7nu1wVSF+dGPVIkxNeXzN6X 5 | 28vnJCqlojo/s6GXv1x4UKLMpPCecsIHSEXpDvW4ZhUMKl2DCxrS9+4W9MFOx0xG2hYCIK 6 | dkKOQ8bWRo3zvgjBiW7ELx/l3lBNU+d9wr1ssNUe32KZUWX5VMEN0ltG9UFm201vPUXCxt 7 | lLujHGZabERWf1I8XwxPkY97gOsLDYZTyXh6ECcTvE4ZiuMMEPxJEMEQx9IzOPeWmS0g0f 8 | iLaswxSPJalENCR1MqlSvyaOele48vUw5efE7g+So0rvdroxFyYt9cdjLOLi3J6YaEuWyJ 9 | 06S3bbVq2bhAANcR/uTXiaoL14qbZ6WryQXmq9wL86eLY4/9M5+dYZKbnQf87ssAksl8TO 10 | dk0gRjwkkmrvgppsL4tcx1RcenVIhs+kSQC4DlMVAAAFiKpHhMiqR4TIAAAAB3NzaC1yc2 11 | EAAAGBALZieleUcTh5kLlMChazASozR3B5wt4RQ15R3JkTY2opuiqq+hy/dhr08c2tOpFJ 12 | IMUP9XcXb4t+pvhq9WyE1YOEvaIvYTc8SCb3u57tcFUhfnRj1SJMTXl8zel9vL5yQqpaI6 13 | P7Ohl79ceFCizKTwnnLCB0hF6Q71uGYVDCpdgwsa0vfuFvTBTsdMRtoWAiCnZCjkPG1kaN 14 | 874IwYluxC8f5d5QTVPnfcK9bLDVHt9imVFl+VTBDdJbRvVBZttNbz1FwsbZS7oxxmWmxE 15 | Vn9SPF8MT5GPe4DrCw2GU8l4ehAnE7xOGYrjDBD8SRDBEMfSMzj3lpktINH4i2rMMUjyWp 16 | RDQkdTKpUr8mjnpXuPL1MOXnxO4PkqNK73a6MRcmLfXHYyzi4tyemGhLlsidOkt221atm4 17 | QADXEf7k14mqC9eKm2elq8kF5qvcC/Oni2OP/TOfnWGSm50H/O7LAJLJfEznZNIEY8JJJq 18 | 74KabC+LXMdUXHp1SIbPpEkAuA5TFQAAAAMBAAEAAAGACrLgAIi06TjvsCr20Yo6cy5duQ 19 | pEz9rdOT3ImL3Y1awGdMUhqwjhMFIg6Bg1aLs3M1Aibalqh0Ph9A3EK/47zyf81B7UOgBX 20 | hC+r6iCoDQUMDQmj1vajudDEJyJTgtbNidQupd/RcpgWshegDJdpMl3E9STDSg/VoVMSnN 21 | aaPMJt0x4HjZ5bgm5ffXv9Mi2KWxHAP4ISZOz5Th28iqe8gD4uXA+ZBacSoGyVC7zTajZi 22 | DjK9lkbv3QsyhaW1Qf/lVoCFTkkj/oGGP+FP5z82ZD1tMf/qYfyjmdWkB3Lccw4ymFBBbq 23 | TnK+buaEcD8jXyqM4Cqi0TXTR71kAxiIwhlFCkSZgxez2DPphZD10c2TSaTneemZtr8kL/ 24 | 7LyU1qlHp1BsrwNzUJy41AysIdZp4hZGcFWoewS0mrhidFBbTksF6waO5p1jWhI90aI8+w 25 | Z+tO/sBrO81tnM9ql0DRnAAvBzObcVYZcGNuIe39W7n4P3n/A/v26xBE9fUKrqBkWBAAAA 26 | wQDemzDClfPTxPoWNAmUqUDnB9vlWryoVczVliO6O1HTGpHoOIDNSJ2dGo7E60Y1O1n2vV 27 | lEpGAwN8HeZxq4cST08DS/U60mN7LIggoCGevGn5cM5EqCZwYzMYB4cZZgXQqh5POU5uBA 28 | G5Mu9tYnIOIaGuIgLI7yeJoFAoFTmbcFWewpiNB2zfr+RHDC8Xs7JsSPS/rhYA9RLwAjw1 29 | OrYejWPZLXMTZQe4omhRj7FiXQdg0IhuCmTzBELD/dzAfIELUAAADBAPMuD//fOpqSGXk4 30 | 90vDhuwW7AmVVzJVduUS0BwJA2UwrF1BaWYKcrKFkqKcwxde9gcb24rtkR6aCUn70UuLMw 31 | ljRfXZEGM/socpPrEpj+8z12tHVLI4zJ/Qt2Nh4cE7LfVY/Vj9NHitcP/kI5Rjv1GaHofJ 32 | PAuc0LU7hcRCNEESBH5qE7otFqQZ27kmc3DcMlP7E7Celm7T8ItT/08u0ty21irYXPVhiw 33 | ltL0Yu9oaIlq6riS5iwstOMwvWcNe8lQAAAMEAv//taV8TlUmDWwI7uLytP4lpFpD5TuAj 34 | G+/FCPz7SFXeJgioaXWpbf0YchV59wm2t0Zms9G2AK6Wf6GKKTt5aeWsxAfmUjwYjQesOJ 35 | LyYlJtIpPN0+EaPOG0SgaunvcGu2wM9M++47QmeWEEfHouHUh+xa9ilSt/rEtSoZuvwGXN 36 | m4KFdT2zFjWnp7bhMbGt4L9DkmMm87N5E2lIiVvFnyq/f8I5FeIy0KLwLaQGbsQooM+ww4 37 | AJ6HjwL0hR2ByBAAAAEXJvb3RAaXAtMTAtMC0wLTQyAQ== 38 | -----END OPENSSH PRIVATE KEY----- -------------------------------------------------------------------------------- /test_data/openvpn/ssh_keys/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2YnpXlHE4eZC5TAoWswEqM0dwecLeEUNeUdyZE2NqKboqqvocv3Ya9PHNrTqRSSDFD/V3F2+Lfqb4avVshNWDhL2iL2E3PEgm97ue7XBVIX50Y9UiTE15fM3pfby+ckKqWiOj+zoZe/XHhQosyk8J5ywgdIRekO9bhmFQwqXYMLGtL37hb0wU7HTEbaFgIgp2Qo5DxtZGjfO+CMGJbsQvH+XeUE1T533CvWyw1R7fYplRZflUwQ3SW0b1QWbbTW89RcLG2Uu6McZlpsRFZ/UjxfDE+Rj3uA6wsNhlPJeHoQJxO8ThmK4wwQ/EkQwRDH0jM495aZLSDR+ItqzDFI8lqUQ0JHUyqVK/Jo56V7jy9TDl58TuD5KjSu92ujEXJi31x2Ms4uLcnphoS5bInTpLdttWrZuEAA1xH+5NeJqgvXiptnpavJBear3Avzp4tjj/0zn51hkpudB/zuywCSyXxM52TSBGPCSSau+Cmmwvi1zHVFx6dUiGz6RJALgOUxU= root@ip-10-0-0-42 -------------------------------------------------------------------------------- /test_data/openvpn/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | //noinspection HILUnresolvedReference 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.11" 7 | } 8 | cloudinit = { 9 | source = "hashicorp/cloudinit" 10 | version = "~> 2.3" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test_data/openvpn/variables.tf: -------------------------------------------------------------------------------- 1 | variable "environment" { 2 | default = "development" 3 | } 4 | variable "region" {} 5 | variable "role_arn" {} 6 | variable "test_zone" {} 7 | 8 | variable "backend_subnet_ids" {} 9 | variable "lb_subnet_ids" {} 10 | variable "vpc_id" {} -------------------------------------------------------------------------------- /test_data/service-network/datasources.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "this" {} 2 | data "aws_region" "current" {} 3 | data "aws_availability_zones" "available" { 4 | state = "available" 5 | } 6 | -------------------------------------------------------------------------------- /test_data/service-network/main.tf: -------------------------------------------------------------------------------- 1 | module "service-network" { 2 | source = "infrahouse/service-network/aws" 3 | version = "~> 2.3" 4 | service_name = "service-network" 5 | vpc_cidr_block = "10.1.0.0/16" 6 | management_cidr_block = "10.1.0.0/16" 7 | # must be enabled for EFS 8 | enable_dns_hostnames = true 9 | enable_dns_support = true 10 | 11 | subnets = [ 12 | { 13 | cidr = "10.1.0.0/24" 14 | availability-zone = data.aws_availability_zones.available.names[0] 15 | map_public_ip_on_launch = true 16 | create_nat = true 17 | forward_to = null 18 | }, 19 | { 20 | cidr = "10.1.1.0/24" 21 | availability-zone = data.aws_availability_zones.available.names[1] 22 | map_public_ip_on_launch = true 23 | create_nat = true 24 | forward_to = null 25 | }, 26 | { 27 | cidr = "10.1.2.0/24" 28 | availability-zone = data.aws_availability_zones.available.names[0] 29 | map_public_ip_on_launch = false 30 | create_nat = false 31 | forward_to = "10.1.0.0/24" 32 | }, 33 | { 34 | cidr = "10.1.3.0/24" 35 | availability-zone = data.aws_availability_zones.available.names[1] 36 | map_public_ip_on_launch = false 37 | create_nat = false 38 | forward_to = "10.1.1.0/24" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test_data/service-network/outputs.tf: -------------------------------------------------------------------------------- 1 | output "subnet_public_ids" { 2 | value = module.service-network.subnet_public_ids 3 | } 4 | 5 | output "subnet_private_ids" { 6 | value = module.service-network.subnet_private_ids 7 | } 8 | 9 | output "internet_gateway_id" { 10 | value = module.service-network.internet_gateway_id 11 | } 12 | 13 | output "vpc_id" { 14 | value = module.service-network.vpc_id 15 | } 16 | -------------------------------------------------------------------------------- /test_data/service-network/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | assume_role { 4 | role_arn = var.role_arn 5 | } 6 | default_tags { 7 | tags = { 8 | "created_by" : "infrahouse/terraform-aws-openvpn" # GitHub repository that created a resource 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test_data/service-network/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | //noinspection HILUnresolvedReference 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.11" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test_data/service-network/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | } 3 | variable "role_arn" { 4 | } 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-openvpn/5fc77bb9c54f84c0c64f37b95e5faf311d2e56ac/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import boto3 4 | import pytest 5 | import logging 6 | from os import path as osp 7 | 8 | from infrahouse_toolkit.logging import setup_logging 9 | from infrahouse_toolkit.terraform import terraform_apply 10 | 11 | # "303467602807" is our test account 12 | TEST_ACCOUNT = "303467602807" 13 | TEST_ROLE_ARN = "arn:aws:iam::303467602807:role/openvpn-tester" 14 | DEFAULT_PROGRESS_INTERVAL = 10 15 | TRACE_TERRAFORM = False 16 | DESTROY_AFTER = True 17 | UBUNTU_CODENAME = "jammy" 18 | 19 | LOG = logging.getLogger(__name__) 20 | REGION = "us-east-2" 21 | TEST_ZONE = "ci-cd.infrahouse.com" 22 | TERRAFORM_ROOT_DIR = "test_data" 23 | 24 | 25 | setup_logging(LOG, debug=True) 26 | 27 | 28 | @pytest.fixture(scope="session") 29 | def aws_iam_role(): 30 | sts = boto3.client("sts") 31 | return sts.assume_role( 32 | RoleArn=TEST_ROLE_ARN, RoleSessionName=TEST_ROLE_ARN.split("/")[1] 33 | ) 34 | 35 | 36 | @pytest.fixture(scope="session") 37 | def boto3_session(aws_iam_role): 38 | return boto3.Session( 39 | aws_access_key_id=aws_iam_role["Credentials"]["AccessKeyId"], 40 | aws_secret_access_key=aws_iam_role["Credentials"]["SecretAccessKey"], 41 | aws_session_token=aws_iam_role["Credentials"]["SessionToken"], 42 | ) 43 | 44 | 45 | @pytest.fixture(scope="session") 46 | def ec2_client(boto3_session): 47 | assert boto3_session.client("sts").get_caller_identity()["Account"] == TEST_ACCOUNT 48 | return boto3_session.client("ec2", region_name=REGION) 49 | 50 | 51 | @pytest.fixture(scope="session") 52 | def ec2_client_map(ec2_client, boto3_session): 53 | regions = [reg["RegionName"] for reg in ec2_client.describe_regions()["Regions"]] 54 | ec2_map = {reg: boto3_session.client("ec2", region_name=reg) for reg in regions} 55 | 56 | return ec2_map 57 | 58 | 59 | @pytest.fixture() 60 | def route53_client(boto3_session): 61 | return boto3_session.client("route53", region_name=REGION) 62 | 63 | 64 | @pytest.fixture() 65 | def elbv2_client(boto3_session): 66 | return boto3_session.client("elbv2", region_name=REGION) 67 | 68 | 69 | @pytest.fixture() 70 | def autoscaling_client(boto3_session): 71 | assert boto3_session.client("sts").get_caller_identity()["Account"] == TEST_ACCOUNT 72 | return boto3_session.client("autoscaling", region_name=REGION) 73 | 74 | 75 | @pytest.fixture() 76 | def service_network(boto3_session): 77 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "service-network") 78 | # Create service network 79 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 80 | fp.write( 81 | dedent( 82 | f""" 83 | role_arn = "{TEST_ROLE_ARN}" 84 | region = "{REGION}" 85 | """ 86 | ) 87 | ) 88 | with terraform_apply( 89 | terraform_module_dir, 90 | destroy_after=DESTROY_AFTER, 91 | json_output=True, 92 | enable_trace=TRACE_TERRAFORM, 93 | ) as tf_service_network_output: 94 | yield tf_service_network_output 95 | 96 | 97 | @pytest.fixture() 98 | def ses(boto3_session): 99 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "ses") 100 | # Create service network 101 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 102 | fp.write( 103 | dedent( 104 | f""" 105 | role_arn = "{TEST_ROLE_ARN}" 106 | test_zone = "{TEST_ZONE}" 107 | region = "{REGION}" 108 | """ 109 | ) 110 | ) 111 | with terraform_apply( 112 | terraform_module_dir, 113 | destroy_after=DESTROY_AFTER, 114 | json_output=True, 115 | enable_trace=TRACE_TERRAFORM, 116 | ) as tf_output: 117 | yield tf_output 118 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import path as osp 3 | from textwrap import dedent 4 | 5 | from infrahouse_toolkit.terraform import terraform_apply 6 | 7 | from tests.conftest import ( 8 | LOG, 9 | TRACE_TERRAFORM, 10 | DESTROY_AFTER, 11 | TEST_ZONE, 12 | TEST_ROLE_ARN, 13 | REGION, 14 | TERRAFORM_ROOT_DIR, 15 | ) 16 | 17 | 18 | def test_module(service_network): 19 | subnet_public_ids = service_network["subnet_public_ids"]["value"] 20 | subnet_private_ids = service_network["subnet_private_ids"]["value"] 21 | vpc_id = service_network["vpc_id"]["value"] 22 | 23 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "openvpn") 24 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 25 | fp.write( 26 | dedent( 27 | f""" 28 | region = "{REGION}" 29 | role_arn = "{TEST_ROLE_ARN}" 30 | test_zone = "{TEST_ZONE}" 31 | 32 | lb_subnet_ids = {json.dumps(subnet_public_ids)} 33 | backend_subnet_ids = {json.dumps(subnet_private_ids)} 34 | vpc_id = "{vpc_id}" 35 | """ 36 | ) 37 | ) 38 | 39 | with terraform_apply( 40 | terraform_module_dir, 41 | destroy_after=DESTROY_AFTER, 42 | json_output=True, 43 | enable_trace=TRACE_TERRAFORM, 44 | ) as tf_output: 45 | LOG.info("%s", json.dumps(tf_output, indent=4)) 46 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "alb_access_log_force_destroy" { 2 | description = "Destroy S3 bucket with access logs even if non-empty" 3 | type = bool 4 | default = false 5 | } 6 | variable "asg_ami" { 7 | description = "Image for EC2 instances" 8 | type = string 9 | default = null 10 | } 11 | 12 | variable "asg_health_check_grace_period" { 13 | description = "ASG will wait up to this number of minutes for instance to become healthy" 14 | type = number 15 | default = 600 16 | } 17 | 18 | variable "asg_min_size" { 19 | description = "Minimum number of instances in ASG" 20 | type = number 21 | default = null 22 | } 23 | 24 | variable "asg_max_size" { 25 | description = "Maximum number of instances in ASG" 26 | type = number 27 | default = null 28 | } 29 | 30 | variable "backend_subnet_ids" { 31 | description = "List of subnet ids where the webserver and database instances will be created" 32 | type = list(string) 33 | } 34 | 35 | variable "environment" { 36 | description = "Name of environment." 37 | type = string 38 | default = "development" 39 | } 40 | 41 | variable "extra_files" { 42 | description = "Additional files to create on an instance." 43 | type = list(object({ 44 | content = string 45 | path = string 46 | permissions = string 47 | })) 48 | default = [] 49 | } 50 | 51 | variable "extra_policies" { 52 | description = "A map of additional policy ARNs to attach to the jumphost role" 53 | type = map(string) 54 | default = {} 55 | } 56 | 57 | 58 | variable "extra_repos" { 59 | description = "Additional APT repositories to configure on an instance." 60 | type = map( 61 | object( 62 | { 63 | source = string 64 | key = string 65 | } 66 | ) 67 | ) 68 | default = {} 69 | } 70 | 71 | # Example of the secret content 72 | # { 73 | # "web": { 74 | # "client_id": "***.apps.googleusercontent.com", 75 | # "project_id": "bookstack-424221", 76 | # "auth_uri": "https://accounts.google.com/o/oauth2/auth", 77 | # "token_uri": "https://oauth2.googleapis.com/token", 78 | # "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 79 | # "client_secret": "***", 80 | # "redirect_uris": [ 81 | # "https://bookstack.ci-cd.infrahouse.com" 82 | # ], 83 | # "javascript_origins": [ 84 | # "https://bookstack.ci-cd.infrahouse.com" 85 | # ] 86 | # } 87 | #} 88 | variable "google_oauth_client_writer" { 89 | description = "ARN of an IAM role that can update content of google_oauth_client secret" 90 | type = string 91 | } 92 | 93 | variable "instance_type" { 94 | description = "Instance type to run the OpenVPN instances" 95 | type = string 96 | default = "m6in.large" 97 | } 98 | 99 | variable "key_pair_name" { 100 | description = "SSH keypair name to be deployed in EC2 instances" 101 | type = string 102 | default = null 103 | } 104 | 105 | variable "lb_subnet_ids" { 106 | description = "List of subnet ids where the load balancer will be created" 107 | type = list(string) 108 | } 109 | 110 | variable "on_demand_base_capacity" { 111 | description = "If specified, the ASG will request spot instances and this will be the minimal number of on-demand instances." 112 | type = number 113 | default = null 114 | } 115 | 116 | variable "portal-image" { 117 | description = "OpenVPN portal docker image" 118 | default = "public.ecr.aws/infrahouse/openvpn-portal:latest" 119 | } 120 | 121 | variable "packages" { 122 | description = "List of packages to install when the instances bootstraps." 123 | type = list(string) 124 | default = [] 125 | } 126 | 127 | variable "portal_instance_type" { 128 | description = "AWS instance type for the portal service" 129 | type = string 130 | default = "t3.small" 131 | } 132 | 133 | variable "portal_workers_count" { 134 | description = "Number of unicorn workers in OpenVPN portal" 135 | type = number 136 | default = 4 137 | } 138 | 139 | variable "puppet_custom_facts" { 140 | description = "A map of custom puppet facts" 141 | type = any 142 | default = {} 143 | } 144 | 145 | variable "puppet_debug_logging" { 146 | description = "Enable debug logging if true." 147 | type = bool 148 | default = false 149 | } 150 | 151 | variable "puppet_environmentpath" { 152 | description = "A path for directory environments." 153 | default = "{root_directory}/environments" 154 | } 155 | 156 | variable "puppet_hiera_config_path" { 157 | description = "Path to hiera configuration file." 158 | default = "{root_directory}/environments/{environment}/hiera.yaml" 159 | } 160 | 161 | variable "puppet_manifest" { 162 | description = "Path to puppet manifest. By default ih-puppet will apply {root_directory}/environments/{environment}/manifests/site.pp." 163 | type = string 164 | default = null 165 | } 166 | 167 | variable "puppet_module_path" { 168 | description = "Path to common puppet modules." 169 | default = "{root_directory}/modules" 170 | } 171 | 172 | variable "puppet_root_directory" { 173 | description = "Path where the puppet code is hosted." 174 | default = "/opt/puppet-code" 175 | } 176 | 177 | variable "root_volume_size" { 178 | description = "Root volume size in EC2 instance in Gigabytes" 179 | type = number 180 | default = 30 181 | } 182 | 183 | variable "routes" { 184 | description = "List of network/netmasks in format 10.x.x.x/255.x.x.x that need to be pushed to a client. [{network: \"10.0.0.0\", netmask: \"255.0.0.0\"}]" 185 | type = list( 186 | object( 187 | { 188 | network : string, 189 | netmask : string 190 | } 191 | ) 192 | ) 193 | default = [] 194 | } 195 | 196 | variable "service_name" { 197 | description = "DNS hostname for the service. It's also used to name some resources like EC2 instances." 198 | default = "openvpn" 199 | } 200 | 201 | variable "smtp_credentials_secret" { 202 | description = "AWS secret name with SMTP credentials. The secret must contain a JSON with user and password keys." 203 | type = string 204 | default = null 205 | } 206 | 207 | variable "ubuntu_codename" { 208 | description = "Ubuntu version to use for the elasticsearch node" 209 | type = string 210 | default = "jammy" 211 | } 212 | 213 | variable "users" { 214 | description = "A list of maps with user definitions according to the cloud-init format" 215 | default = null 216 | type = any 217 | # Check https://cloudinit.readthedocs.io/en/latest/reference/examples.html#including-users-and-groups 218 | # for fields description and examples. 219 | # type = list( 220 | # object( 221 | # { 222 | # name : string 223 | # expiredate : optional(string) 224 | # gecos : optional(string) 225 | # homedir : optional(string) 226 | # primary_group : optional(string) 227 | # groups : optional(string) # Comma separated list of strings e.g. groups: users, admin 228 | # selinux_user : optional(string) 229 | # lock_passwd : optional(bool) 230 | # inactive : optional(number) 231 | # passwd : optional(string) 232 | # no_create_home : optional(bool) 233 | # no_user_group : optional(bool) 234 | # no_log_init : optional(bool) 235 | # ssh_import_id : optional(list(string)) 236 | # ssh_authorized_keys : optional(list(string)) 237 | # sudo : any # Can be either false or a list of strings e.g. sudo = ["ALL=(ALL) NOPASSWD:ALL"] 238 | # system : optional(bool) 239 | # snapuser : optional(string) 240 | # } 241 | # ) 242 | # ) 243 | } 244 | 245 | variable "zone_id" { 246 | description = "Domain name zone ID where the website will be available" 247 | type = string 248 | } 249 | 250 | variable "sns_topic_alarm_arn" { 251 | description = "ARN of SNS topic for Cloudwatch alarms on base EC2 instance." 252 | type = string 253 | default = null 254 | } 255 | 256 | variable "extra_instance_profile_permissions" { 257 | description = "A JSON with a permissions policy document. The policy will be attached to the ASG instance profile." 258 | type = string 259 | default = null 260 | } 261 | 262 | variable "cloudinit_extra_commands" { 263 | description = "Extra commands for run on ASG." 264 | type = list(string) 265 | default = [] 266 | } 267 | --------------------------------------------------------------------------------