diff --git a/.circleci/config.yml b/.circleci/config.yml index f50d875..4e41bc3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,17 @@ jobs: command: | make docker-build no_output_timeout: 2400s + - run: sudo chown -R 100:1000 docker/config/ && sudo chmod -R 777 docker/config/ + - run: + name: Infra standup + working_directory: /home/circleci/project/docker + command: docker-compose up ganache truffle + background: true + - run: + name: Test + working_directory: /home/circleci/project/docker + command: docker-compose up vault_server + no_output_timeout: 2400s - run: name: Save Docker image command: | diff --git a/API.md b/API.md index bc2c55f..ef8ad7d 100644 --- a/API.md +++ b/API.md @@ -240,13 +240,14 @@ Submits the Merkle root of a Plasma block | name | Name of the wallet - provided in the URI. | | address | Account address which will submit the block - provided in the URI. | | contract | The address of the Block Controller contract. | -| gas_price | The gas price for the transaction in wei. Defaults to 0 - which means use the estimated gas price. | +| gas_price | The gas price for the transaction in wei. | | block_root | The Merkle root of a Plasma block. | +| nonce | Transaction order. | ### EXAMPLE ```sh -curl -X PUT -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" -d '{"block_root":"1234qweradgf1234qweradgf","contract":"0xd185aff7fb18d2045ba766287ca64992fdd79b1e"}' http://127.0.0.1:8900/v1/immutability-eth-plugin/wallets/plasma-deployer/accounts/0x888a65279D4a3A4E3cbA57D5B3Bd3eB0726655a6/plasma/submitBlock +curl -X PUT -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" -d '{"block_root":"1234qweradgf1234qweradgf","contract":"0xd185aff7fb18d2045ba766287ca64992fdd79b1e", "gas_price: "20000000000", nonce: "0""}' http://127.0.0.1:8900/v1/immutability-eth-plugin/wallets/plasma-deployer/accounts/0x888a65279D4a3A4E3cbA57D5B3Bd3eB0726655a6/plasma/submitBlock { "request_id": "00a614f3-9bd3-60f4-25be-384a8d3cc5ff", @@ -258,50 +259,10 @@ curl -X PUT -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" "from": "0x4BC91c7fA64017a94007B7452B75888cD82185F7", "gas_limit": 73623, "gas_price": 20000000000, - "nonce": 1, + "nonce": 0, "signed_transaction": "0xf889018504a817c80083011f9794d185aff7fb18d2045ba766287ca64992fdd79b1e80a4baa4769431323334717765726164676631323334717765726164676600000000000000001ca04b14e95372a41a74585c04c7967c45f2d1d51e4f5cd59b7c95a2c16ecbd63e79a04fcc461cfd165d8ba1f9cafe37ce7c025c0cec0533880abda3df754c9c749d9a", "transaction_hash": "0x6cfad4034bf147accb815922bb4f71ed8ae676e65580ab259d9d1d8713047c7f" }, "warnings": null } -``` - -### ACTIVATE CHILD CHAIN - -Activates the child chain so that child chain can start to submit child blocks to root chain. - -#### INPUTS - -| Parameter | Description | -| --- | ----------- | -| name | Name of the wallet - provided in the URI. | -| address | Account address which will submit the block - provided in the URI. | -| contract | The address of the Block Controller contract. | -| gas_price | The gas price for the transaction in wei. Defaults to 0 - which means use the estimated gas price. | - -### EXAMPLE - -```sh -curl -X PUT -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" -d '{"contract":"0xd185aff7fb18d2045ba766287ca64992fdd79b1e"}' http://127.0.0.1:8900/v1/immutability-eth-plugin/wallets/plasma-deployer/accounts/0x888a65279D4a3A4E3cbA57D5B3Bd3eB0726655a6/plasma/activateChildChain -``` - -### SUBMIT DEPOSIT BLOCK - -Submits a block for deposit - -#### INPUTS - -| Parameter | Description | -| --- | ----------- | -| name | Name of the wallet - provided in the URI. | -| address | Account address which will submit the block - provided in the URI. | -| contract | The address of the Block Controller contract. | -| gas_price | The gas price for the transaction in wei. Defaults to 0 - which means use the estimated gas price. | -| block_root | The Merkle root of a Plasma block. | - -### EXAMPLE - -```sh -curl -X PUT -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" -d '{"block_root":"1234qweradgf1234qweradgf","contract":"0xd185aff7fb18d2045ba766287ca64992fdd79b1e"}' http://127.0.0.1:8900/v1/immutability-eth-plugin/wallets/plasma-deployer/accounts/0x4BC91c7fA64017a94007B7452B75888cD82185F7/plasma/submitDepositBlock - ``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e1100..d355049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.0.7 (xx yy, 2020) + +NEW FEATURES: + +* Backup / Restore scripts created for Vault Raft Data +* Creation of the gen_overrides.sh script + +REFACTOR: + +* updated `VERSION` file to `0.0.7` +* Regionalized SSD Persistent Data Volumes for Vault Raft Data +* Vault Auditing is now enabled +* Fix Vault Raft Peering +* Replaced the Custom Vault Helm Chart with the officially supported Helm Chart from Hashicorp +* Nonce refactored to be passed in +* Only --build on test + ## 0.0.6 (August 15, 2020) NEW FEATURES: @@ -7,8 +24,6 @@ NEW FEATURES: * Enable GCR and KMS in the Vault GCP project with service accounts * CircleCI config to push `omgnetwork/vault` images into GCR -N/A - REFACTOR: * updated `VERSION` file to `0.0.6` diff --git a/docker/README.md b/docker/README.md index d121025..b911331 100644 --- a/docker/README.md +++ b/docker/README.md @@ -36,7 +36,7 @@ If this project is in `$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin You will need to trust the signer to use the vault CLI: ```bash -export VAULT_CACERT="$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/ca/certs/ca.crt" +export VAULT_CACERT="$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/ca.crt" ``` 2. Unseal Key and Root Token: `$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/unseal.json` @@ -54,7 +54,6 @@ Don't mess with this. If you want to re-initialize the Vault, then delete these: ```bash -rm -fr $GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/ca rm $GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/config/docker/unseal.json rm -fr $GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/data ``` @@ -63,7 +62,7 @@ I would strongly advise using the Vault CLI. This way you can use vault with the ```bash export VAULT_ADDR="https://127.0.0.1:8200" -export VAULT_CACERT="$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/ca/certs/ca.crt" +export VAULT_CACERT="$GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/ca.crt" export VAULT_TOKEN=$(cat $GOPATH/src/github.com/omgnetwork/immutability-eth-plugin/docker/config/unseal.json | jq -r .root_token) vault read -format=json immutability-eth-plugin/config diff --git a/docker/config/entrypoint.sh b/docker/config/entrypoint.sh index c5ea6ec..7be216b 100755 --- a/docker/config/entrypoint.sh +++ b/docker/config/entrypoint.sh @@ -169,10 +169,14 @@ function test_plugin { } if [ -f "$VAULT_CREDENTIALS" ]; then + sleep 10 unseal vault status vault secrets list + test_banner + test_plugin else + sleep 10 VAULT_INIT=$(vault operator init -key-shares=1 -key-threshold=1 -format=json | jq .) echo $VAULT_INIT > $VAULT_CREDENTIALS unseal @@ -184,6 +188,11 @@ else test_plugin fi -# Don't exit until vault dies -wait $VAULT_PID +if [ -n "$TEST" ]; then + echo "Dying." +else + echo "Don't exit until vault dies." + wait $VAULT_PID +fi + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4d00f75..1176027 100755 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -46,11 +46,8 @@ services: - testnet ports: - "8200:8900" - depends_on: - - "ganache" - links: - - "ganache" - - "truffle" + environment: + - TEST=true volumes: - "./config:/home/vault/config:rw" - "../contracts:/home/vault/contracts:ro" diff --git a/docker/lean-docker-compose.yml b/docker/lean-docker-compose.yml new file mode 100644 index 0000000..8d10698 --- /dev/null +++ b/docker/lean-docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.3" + +services: + vault_server: + image: omgnetwork/vault:latest + networks: + - testnet + ports: + - "8200:8900" + volumes: + - "./config:/home/vault/config:rw" + - "../contracts:/home/vault/contracts:ro" + - "../scripts:/home/vault/scripts:ro" + entrypoint: /home/vault/config/entrypoint.sh +networks: + testnet: + driver: bridge diff --git a/ethereum/path_plasma.go b/ethereum/path_plasma.go index 24b11dd..6e701dd 100644 --- a/ethereum/path_plasma.go +++ b/ethereum/path_plasma.go @@ -44,39 +44,11 @@ Allows the authority to submit the Merkle root of a Plasma block. }, "gas_price": { Type: framework.TypeString, - Description: "The gas price for the transaction in wei. Defaults to 0 - which means use the estimated gas price.", - Default: "0", + Description: "The gas price for the transaction in wei.", }, - "block_root": { - Type: framework.TypeString, - Description: "The Merkle root of a Plasma block.", - }, - }, - ExistenceCheck: pathExistenceCheck, - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathPlasmaSubmitBlock, - logical.UpdateOperation: b.pathPlasmaSubmitBlock, - }, - }, - { - Pattern: ContractPath(plasmaContract, "submitDepositBlock"), - HelpSynopsis: "Submits a block for deposit", - HelpDescription: ` - -Submits a block for deposit. - -`, - Fields: map[string]*framework.FieldSchema{ - "name": {Type: framework.TypeString}, - "address": {Type: framework.TypeString}, - "contract": { - Type: framework.TypeString, - Description: "The address of the Block Controller.", - }, - "gas_price": { + "nonce": { Type: framework.TypeString, - Description: "The gas price for the transaction in wei. Defaults to 0 - which means use the estimated gas price.", - Default: "0", + Description: "The nonce for the transaction.", }, "block_root": { Type: framework.TypeString, @@ -85,8 +57,8 @@ Submits a block for deposit. }, ExistenceCheck: pathExistenceCheck, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.CreateOperation: b.pathPlasmaSubmitDepositBlock, - logical.UpdateOperation: b.pathPlasmaSubmitDepositBlock, + logical.CreateOperation: b.pathPlasmaSubmitBlock, + logical.UpdateOperation: b.pathPlasmaSubmitBlock, }, }, } @@ -144,13 +116,20 @@ func (b *PluginBackend) pathPlasmaSubmitBlock(ctx context.Context, req *logical. if err != nil { return nil, err } - //transactOpts needs gas etc. Use supplied gas_price if > 0 + //transactOpts needs gas etc. Use supplied gas_price gasPriceRaw := data.Get("gas_price").(string) - - if gasPriceRaw != "0" { - transactOpts.GasPrice = util.ValidNumber(gasPriceRaw) - } - + if gasPriceRaw == "" { + return nil, fmt.Errorf("invalid gas_price") + } + transactOpts.GasPrice = util.ValidNumber(gasPriceRaw) + + //transactOpts needs nonce. Use supplied nonce + nonceRaw := data.Get("nonce").(string) + if nonceRaw == "" { + return nil, fmt.Errorf("invalid nonce") + } + transactOpts.Nonce = util.ValidNumber(nonceRaw) + plasmaSession := &plasma.PlasmaSession{ Contract: instance, // Generic contract caller binding to set the session for CallOpts: *callOpts, // Call options to use throughout this session @@ -176,88 +155,3 @@ func (b *PluginBackend) pathPlasmaSubmitBlock(ctx context.Context, req *logical. }, }, nil } - -func (b *PluginBackend) pathPlasmaSubmitDepositBlock(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - config, err := b.configured(ctx, req) - if err != nil { - return nil, err - } - address := data.Get("address").(string) - name := data.Get("name").(string) - contractAddress := common.HexToAddress(data.Get("contract").(string)) - accountJSON, err := readAccount(ctx, req, name, address) - if err != nil || accountJSON == nil { - return nil, fmt.Errorf("error reading address") - } - - chainID := util.ValidNumber(config.ChainID) - if chainID == nil { - return nil, fmt.Errorf("invalid chain ID") - } - - client, err := ethclient.Dial(config.getRPCURL()) - if err != nil { - return nil, err - } - - walletJSON, err := readWallet(ctx, req, name) - if err != nil { - return nil, err - } - - wallet, account, err := getWalletAndAccount(*walletJSON, accountJSON.Index) - if err != nil { - return nil, err - } - - instance, err := plasma.NewPlasma(contractAddress, client) - if err != nil { - return nil, err - } - callOpts := &bind.CallOpts{} - - blockRoot := [32]byte{} - - inputBlockRoot, ok := data.GetOk("block_root") - if ok { - copy(blockRoot[:], []byte(inputBlockRoot.(string))) - } else { - return nil, fmt.Errorf("invalid block root") - } - - transactOpts, err := b.NewWalletTransactor(chainID, wallet, account) - if err != nil { - return nil, err - } - - //transactOpts needs gas etc. Use supplied gas_price if > 0 - gasPriceRaw := data.Get("gas_price").(string) - - if gasPriceRaw != "0" { - transactOpts.GasPrice = util.ValidNumber(gasPriceRaw) - } - plasmaSession := &plasma.PlasmaSession{ - Contract: instance, // Generic contract caller binding to set the session for - CallOpts: *callOpts, // Call options to use throughout this session - TransactOpts: *transactOpts, - } - - tx, err := plasmaSession.SubmitDepositBlock(blockRoot) - if err != nil { - return nil, err - } - - var signedTxBuff bytes.Buffer - tx.EncodeRLP(&signedTxBuff) - return &logical.Response{ - Data: map[string]interface{}{ - "contract": contractAddress.Hex(), - "transaction_hash": tx.Hash().Hex(), - "signed_transaction": hexutil.Encode(signedTxBuff.Bytes()), - "from": account.Address.Hex(), - "nonce": tx.Nonce(), - "gas_price": tx.GasPrice(), - "gas_limit": tx.Gas(), - }, - }, nil -} diff --git a/immutability-eth-plugin b/immutability-eth-plugin new file mode 100755 index 0000000..a7c2551 Binary files /dev/null and b/immutability-eth-plugin differ diff --git a/infrastructure/README.md b/infrastructure/README.md index bd9a759..da3b95f 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -119,14 +119,14 @@ Due to the lifecycle restrictions on KMS resources by Google, this had to be rem Assuming you have already run the Terraform and have the `kms_account.key.json` file generated for the service account under your `./terraform` directory, you can now run: -```bash -./scripts/kms.sh -c ./terraform/kms_account.key.json -r $GCP_REGION -``` - > Note: > > Before running the KMS script, ensure you have the current Kubernetes context and credentials active for the GKE cluster. +```bash +./scripts/kms.sh -c ./terraform/kms_account.key.json -r $GCP_REGION +``` + This script will activate the KMS service account in the `gcloud` tool using the generate credential file path provided and create a new KMS key ring and symmetric unsealing key within that ring for you (if one or both already exist, these steps will be skipped). Once the key ring and unsealer key have been created within your GCP project, the script [injects the service account credential file into cluster secrets to be mounted into the nodes for unsealing](https://www.vaultproject.io/docs/platform/k8s/helm/run#google-kms-auto-unseal) before revoke your `gcloud` authentication session. ### Helm / Deployment @@ -221,15 +221,14 @@ $GCP_PROJECT $GKE_CLUSTER_NAME ``` -<<<<<<< HEAD ##### Start the Pods using the Helm Chart -======= ->>>>>>> 1526b2980e828e9057bfe4cbaf0a629887648fc5 + +In `k8s`, execute: Execute: ```bash -helm upgrade --atomic --cleanup-on-fail --install --wait --values vault-overrides.yaml vault hashicorp/vault +helm upgrade --atomic --cleanup-on-fail --install --wait --values vault-overrides.yaml vault hashicorp/vault --version 0.7.0 ``` ### Interact with Vault @@ -285,20 +284,13 @@ Determining how many backup files you want to keep is a business decision. There vault operator raft snapshot save snapshot-$(date +%Y%m%d-%H%M%S).raft ``` -*Rotational strategy*. In this example, a maximum of 5 snapshots are maintained at any given time. +*Rotational strategy*. Maintain a most-recent set of snapshots. This is implemented in a script and can be used as follows: -```bash -rm -f snapshot-4.raft - -for i in 3 2 1; do - let NEXT=$i+1 - mv -f snapshot-${i}.raft snapshot-${NEXT}.raft 2> /dev/null -done - -mv -f snapshot.raft snapshot-1.raft 2> /dev/null +In `infrastructure`, execute: -vault operator raft snapshot save snapshot.raft -``` +```bash +./scripts/vault_backup.sh -d [-p ] [-m ] [--help] +`` #### Restore Vault RAFT Data from a Snapshot File @@ -306,8 +298,41 @@ When you need to restore your Vault cluster back to a known-good state, identify ```bash vault operator raft snapshot restore snapshot-file.raft +```` + +If using the *Rotational strategy*, this is implemented in a script and can be used as follows: + +In `infrastructure`, execute: + +```bash +./scripts/vault_restore.sh -s [-p ] [-b ] [--help] +`` + +#### Generating New Certificates + +When you need to issue a new set of certificates to the pods, you need to follow this process: + +In `infrastructure`, execute: + +--- + +When generating certs for GKE clusters, use: `-d vault-internal.default.svc.cluster.local` +When generating certs for Minikube, use: `-d vault-internal` + +--- + +```bash +./scripts/gen_certs.sh -d +./scripts/gen_overrides.sh +helm upgrade --recreate-pods --atomic --cleanup-on-fail --install --values ./k8s/datadog-overrides.yaml datadog datadog/datadog ``` +--- + +If you are port-forwarding, you'll need to stop and restart the port-forwarder. + +--- + ### Uninstalling Vault When you're done, you can uninstall vault. diff --git a/infrastructure/k8s/vault-overrides.yaml b/infrastructure/k8s/vault-overrides.yaml index bad22f3..f6334de 100644 --- a/infrastructure/k8s/vault-overrides.yaml +++ b/infrastructure/k8s/vault-overrides.yaml @@ -1,6 +1,7 @@ global: + enabled: true tlsDisable: false - certSecretName: omgnetwork-certs-d67f2d4m49 + certSecretName: omgnetwork-certs-4ck8ktbbgb injector: enabled: false server: @@ -28,7 +29,7 @@ server: config: |- ui = false log_level = "info" - cluster_name = "vault-cluster-0829" + cluster_name = "vault-cluster-0917" listener "tcp" { tls_disable = {{ .Values.global.tlsDisable }} @@ -40,8 +41,8 @@ server: } seal "gcpckms" { - region = "us-central1" - project = "omgnetwork-test-cluster" + region = "us-west4" + project = "vault-cluster-0917" key_ring = "omgnetwork-vault-keyring" crypto_key = "omgnetwork-vault-unseal-key" } @@ -86,17 +87,18 @@ server: } image: repository: vault # TODO: will change to the omgnetwork/vault image - tag: "1.5.3" # TODO: will change to the appropriate tag for omgnetwork/vault + tag: 1.5.3 pullPolicy: IfNotPresent + updateStrategyType: RollingUpdate extraEnvironmentVars: - GOOGLE_REGION: us-central1 - GOOGLE_PROJECT: omgnetwork-test-cluster + GOOGLE_REGION: us-west4 + GOOGLE_PROJECT: vault-cluster-0917 GOOGLE_APPLICATION_CREDENTIALS: /vault/userconfig/kms-creds/kms_account.key.json extraVolumes: - - type: secret - name: "kms-creds" - - type: secret - name: omgnetwork-certs-d67f2d4m49 + - name: kms-creds + type: secret + - name: omgnetwork-certs-4ck8ktbbgb + type: secret affinity: null dev: enabled: false diff --git a/infrastructure/network/README.md b/infrastructure/network/README.md new file mode 100644 index 0000000..3c8bbb2 --- /dev/null +++ b/infrastructure/network/README.md @@ -0,0 +1,35 @@ +# Validating the infrastructure provisioned + +Prereq: You'll need 2 separate projects. A project for vault where the vault_vpc and infrastructure is to be provisioned and a project for the infrastructure that's going to represent the omisego network/vpc. You will also need a bucket for the `.ovpn` file (e.g., `gsutil mb gs://{BUCKET_NAME}`) + +## Making sure VPN connection is working + +1. Execute `terraform apply` on the infrastructure directory. This will create core networking resources. The VPN OpenVPN install script on the VPN instance generates an `.ovpn` VPN file to be used in the unsealer and places it on a bucket. +2. Get the ovpn file by executing: `gsutil cp gs://${BUCKET_NAME}/unsealer.ovpn .` +3. Once downloaded the file can be safely removed form the bucket: `gsutil rm gs://${BUCKET_NAME}/unsealer.ovpn`. +4. Access VPN from laptop. Using *Tunnelblick*, click on "**VPN Details**" and drag/drop the `unsealer.ovpn` into the "**Configurations**" drop down, then click the "**Connect**" button. +5. Once connected check your public IP by running: `curl 'https://api.ipify.org?format=json'` The returned value should match the value of the IP of the `vpn_public_instance_ip` terraform output. + +## Validating connection from test instance in Vault VPC to Unsealer laptop + +1. On the laptop, run a test vault server: `vault server -dev -dev-listen-address="0.0.0.0:8200"`. +2. In the `local_testing/vault_vpc` directory, create `terraform.tfvars` file with values required by the `variables.tf` file. +3. Execute `terraform apply`. +4. For future reference, export the IP given in the output: `export VAULT_IP=192.168.10.3`. +5. SSH to the test instance created by running the command specified in the `vault_vpc_test_instance_ssh_command` terraform output. For example: `gcloud beta compute ssh --zone us-central1-a test --tunnel-through-iap --project omisego`. +6. Check connection from instance to the unsealer laptop by running: `curl http://10.8.0.2:8200/v1/sys/health`. + +Note: don't delete the instance yet as we'll keep using it for further testing. + +## Validating connection from test instance to datadog + +1. The instance should have sent metrics to datadog. Check them by going to the datadog UI. + +## Validating connection from Omisego VPC to the Vault VPC. + +1. SSH to the test instance created by running the command specified in the `vault_vpc_test_instance_ssh_command` terrfaform output. For example: `gcloud beta compute ssh --zone us-central1-a test --tunnel-through-iap --project omisego`. +2. The startup script of the test instance starts a Vault dev server. Make sure vault server is running: `curl http://127.0.0.1:8200/v1/sys/health`. +3. In the local_testing/omisego_vpc directory, create terraform.tfvars file with values required by the variables.tf file. +4. Execute `terraform apply`. +5. SSH to the test instance created by running the command specified in the `omisego_vpc_test_instance_ssh_command` terrfaform output. For example: `gcloud beta compute ssh --zone us-central1-a test --tunnel-through-iap --project omisego`. +6. Validate connection to vault is working by executing: `curl http://${VAULT_IP}:8200/v1/sys/health`. diff --git a/infrastructure/network/firewall.tf b/infrastructure/network/firewall.tf new file mode 100644 index 0000000..bbaa54a --- /dev/null +++ b/infrastructure/network/firewall.tf @@ -0,0 +1,188 @@ +/* + * Datadog IP Ranges - https://www.terraform.io/docs/providers/datadog/d/ip_ranges.html + * This data source provides the IP ranges required to direct traffic to Datadog for + * logging and monitoring purposes. + */ +data "datadog_ip_ranges" "ips" {} + +/* + * Google Compute Firewall - https://www.terraform.io/docs/providers/google/r/compute_firewall.html + * Egrees rule that allows traffic to be directed to Datadog's network + */ +resource "google_compute_firewall" "datadog_logs_egress" { + count = var.lockdown_egress ? 1 : 0 + name = "datadog-log-egress" + network = google_compute_network.vpc.name + direction = "EGRESS" + priority = "64000" + + # Allowed ports are configured for Datadog following requirements specified here: + # https://docs.datadoghq.com/agent/guide/network/?tab=agentv6v7 + + allow { + protocol = "tcp" + ports = [ + "443", # port for most Agent data. (Metrics, APM, Live Processes/Containers) + "10516", # port for the Log collection over TCP + "10255", # port for the Kubernetes http kubelet + "10250" # port for the Kubernetes https kubelet + ] + } + + allow { + protocol = "udp" + ports = [ + "123" # Used for NTP traffic + ] + } + + destination_ranges = data.datadog_ip_ranges.ips.logs_ipv4 +} + +resource "google_compute_firewall" "datadog_agent_1_egress" { + count = var.lockdown_egress ? 1 : 0 + name = "datadog-agent-1-egress" + network = google_compute_network.vpc.name + direction = "EGRESS" + priority = "64100" + + # Allowed ports are configured for Datadog following requirements specified here: + # https://docs.datadoghq.com/agent/guide/network/?tab=agentv6v7 + + allow { + protocol = "tcp" + ports = [ + "443", # port for most Agent data. (Metrics, APM, Live Processes/Containers) + "10516", # port for the Log collection over TCP + "10255", # port for the Kubernetes http kubelet + "10250" # port for the Kubernetes https kubelet + ] + } + + allow { + protocol = "udp" + ports = [ + "123" # Used for NTP traffic + ] + } + + destination_ranges = element(chunklist(data.datadog_ip_ranges.ips.agents_ipv4, 256), 0) +} + +resource "google_compute_firewall" "datadog_agent_2_egress" { + count = var.lockdown_egress ? 1 : 0 + name = "datadog-agent-2-egress" + network = google_compute_network.vpc.name + direction = "EGRESS" + priority = "64200" + + # Allowed ports are configured for Datadog following requirements specified here: + # https://docs.datadoghq.com/agent/guide/network/?tab=agentv6v7 + + allow { + protocol = "tcp" + ports = [ + "443", # port for most Agent data. (Metrics, APM, Live Processes/Containers) + "10516", # port for the Log collection over TCP + "10255", # port for the Kubernetes http kubelet + "10250" # port for the Kubernetes https kubelet + ] + } + + allow { + protocol = "udp" + ports = [ + "123" # Used for NTP traffic + ] + } + + destination_ranges = element(chunklist(data.datadog_ip_ranges.ips.agents_ipv4, 256), 1) +} + +/* + * This firewall rule contains the required access from the omisego VPC to access Vault + */ +resource "google_compute_firewall" "omisego_vpc_access" { + name = "omisego-vpc-access" + network = google_compute_network.vpc.name + description = "Allows access from Omisego VPC" + direction = "INGRESS" + priority = "1000" + + # ICMP is allow in order to test connectivity between networks using ping + allow { + protocol = "icmp" + } + + # Port Vault listens in + allow { + protocol = "tcp" + ports = ["8200"] + } + + source_ranges = [var.omisego_subnet_cidr, var.subnet_cidr] + target_tags = ["vault"] +} + +/* + * This firewall rule allows IAP access to the network for SSH + * SSH should only be used in emergency situations + * https://www.terraform.io/docs/providers/google/r/compute_firewall.html + */ +resource "google_compute_firewall" "ssh_iap" { + name = "ssh-iap-access" + network = google_compute_network.vpc.name + direction = "INGRESS" + priority = "1100" + + source_ranges = ["35.235.240.0/20"] + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_tags = ["ssh-access"] +} + +/* + * This firewall rules for VPN ingress access + */ + +resource "google_compute_firewall" "vpn_internet" { + name = "vpn-internet" + network = google_compute_network.vpc.name + direction = "INGRESS" + priority = "1200" + + source_ranges = ["0.0.0.0/0"] + + allow { + protocol = "udp" + ports = ["1194"] + } + + target_tags = ["vpn"] +} + +resource "google_compute_firewall" "vpn_outbound" { + name = "vpn-access" + network = google_compute_network.vpc.name + direction = "INGRESS" + priority = "1300" + + allow { + protocol = "udp" + } + + allow { + protocol = "icmp" + } + + allow { + protocol = "tcp" + } + + source_tags = ["vault"] + target_tags = ["vpn"] +} diff --git a/infrastructure/network/gke.tf b/infrastructure/network/gke.tf new file mode 100644 index 0000000..7b1a7f7 --- /dev/null +++ b/infrastructure/network/gke.tf @@ -0,0 +1,58 @@ +/* + * Google Kubernetes Engine Cluster - https://www.terraform.io/docs/providers/google/r/container_cluster.html + * Creates the GKE cluster that will be running the Vault and Consul pods + */ +resource "google_container_cluster" "cluster" { + name = var.gke_cluster_name + location = var.gcp_region + + remove_default_node_pool = true + initial_node_count = 1 + + master_auth { + username = "" + password = "" + + client_certificate_config { + issue_client_certificate = false + } + } +} + +/* + * GKE Cluster Node Pool - https://www.terraform.io/docs/providers/google/r/container_node_pool.html + * Custom node pool definition to allow future control instead of using default + */ +resource "google_container_node_pool" "pool" { + name = "${var.gke_cluster_name}-node-pool" + location = var.gcp_region + cluster = google_container_cluster.cluster.name + node_count = var.gke_node_count + + management { + auto_repair = true + auto_upgrade = true + } + + node_config { + preemptible = true + machine_type = "n1-standard-1" + + metadata = { + disable-legacy-endpoints = true + } + + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + } +} + +/* + * Google Container Registry: https://www.terraform.io/docs/providers/google/r/container_registry.html + * Private container registry to push and pull proprietary pod images + */ +resource "google_container_registry" "registry" { + // No arguments are needed, `project` is inherited from provider +} diff --git a/infrastructure/network/local_testing/omisego_vpc/main.tf b/infrastructure/network/local_testing/omisego_vpc/main.tf new file mode 100644 index 0000000..d211d9d --- /dev/null +++ b/infrastructure/network/local_testing/omisego_vpc/main.tf @@ -0,0 +1,13 @@ +/* + * GCP Provider - https://www.terraform.io/docs/providers/google/guides/provider_reference.html + * Required for provisioning infrastructure in GCP + */ +provider "google" { + project = var.gcp_project_omisego + region = var.gcp_region +} + +provider "google-beta" { + project = var.gcp_project_omisego + region = var.gcp_region +} diff --git a/infrastructure/network/local_testing/omisego_vpc/omisego_vpc.tf b/infrastructure/network/local_testing/omisego_vpc/omisego_vpc.tf new file mode 100644 index 0000000..c941478 --- /dev/null +++ b/infrastructure/network/local_testing/omisego_vpc/omisego_vpc.tf @@ -0,0 +1,90 @@ +/* + * Google Compute Network - https://www.terraform.io/docs/providers/google/r/compute_network.html + * Defines VPC where Vault infrastructure is provisioned into + */ +resource "google_compute_network" "vpc" { + name = "omisego-net" + auto_create_subnetworks = "false" + routing_mode = "REGIONAL" +} + +/* + * Google Compute Subnetwork - https://www.terraform.io/docs/providers/google/r/compute_subnetwork.html + * Defines regional subnet where Vault infrastructure is provisioned into + */ +resource "google_compute_subnetwork" "subnet" { + name = "omisego-subnet" + ip_cidr_range = var.omisego_subnet_cidr + region = var.gcp_region + network = google_compute_network.vpc.self_link + + # Note: Immutability recommends enabling flow logs for observability, debugging, and incident response. + # These would incur in additional cost. + log_config { + aggregation_interval = "INTERVAL_10_MIN" + flow_sampling = 1 + metadata = "INCLUDE_ALL_METADATA" + } +} + +/* + * Network Peering - https://www.terraform.io/docs/providers/google/r/compute_network_peering.html + * Connecting VPC with clients to VPC hosting Vault + */ +resource "google_compute_network_peering" "peering" { + name = "peering-to-vault-vpc" + network = google_compute_network.vpc.self_link + peer_network = var.vault_vpc_uri +} + +/* + * This firewall rule allows IAP access to the network for SSH + * https://www.terraform.io/docs/providers/google/r/compute_firewall.html + */ +resource "google_compute_firewall" "omisego_ssh_iap" { + name = "ssh-access" + network = google_compute_network.vpc.name + + source_ranges = ["35.235.240.0/20"] + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_tags = ["ssh-access"] +} + +/* + * This grants the given user account access to SSH into the instance + */ +resource "google_iap_tunnel_instance_iam_binding" "omisego_editor" { + project = google_compute_instance.omisego_test.project + zone = google_compute_instance.omisego_test.zone + instance = google_compute_instance.omisego_test.name + role = "roles/iap.tunnelResourceAccessor" + members = [ + "user:${var.ssh_user}" + ] +} + +/* + * Instance used to test connectivity from Omisego VPC + */ +resource "google_compute_instance" "omisego_test" { + name = "omisego-testing" + machine_type = "f1-micro" + zone = var.gcp_zone + + tags = ["ssh-access"] + + boot_disk { + initialize_params { + image = "debian-cloud/debian-9" + } + } + + network_interface { + subnetwork = google_compute_subnetwork.subnet.self_link + } +} diff --git a/infrastructure/network/local_testing/omisego_vpc/outputs.tf b/infrastructure/network/local_testing/omisego_vpc/outputs.tf new file mode 100644 index 0000000..0cb89db --- /dev/null +++ b/infrastructure/network/local_testing/omisego_vpc/outputs.tf @@ -0,0 +1,7 @@ +output "omisego_vpc_test_instance_ip" { + value = google_compute_instance.omisego_test.network_interface.0.network_ip +} + +output "omisego_vpc_test_instance_ssh_command" { + value = "gcloud beta compute ssh --zone ${google_compute_instance.omisego_test.zone} ${google_compute_instance.omisego_test.name} --tunnel-through-iap --project ${var.gcp_project_omisego}" +} diff --git a/infrastructure/network/local_testing/omisego_vpc/variables.tf b/infrastructure/network/local_testing/omisego_vpc/variables.tf new file mode 100644 index 0000000..5c19156 --- /dev/null +++ b/infrastructure/network/local_testing/omisego_vpc/variables.tf @@ -0,0 +1,23 @@ +variable "gcp_project_omisego" { + description = "Name of GCP project used for represent Omisego VPC" +} + +variable "gcp_region" { + description = "GCP region to provision resources into" +} + +variable "gcp_zone" { + description = "GCP zone to provision resources into" +} + +variable "ssh_user" { + description = "Email address of the user with access to SSH into the instance" +} + +variable "vault_vpc_uri" { + description = "URI of the VPC" +} + +variable "omisego_subnet_cidr" { + description = "CIDR block for Omisego subnet" +} \ No newline at end of file diff --git a/infrastructure/network/local_testing/vault_vpc/main.tf b/infrastructure/network/local_testing/vault_vpc/main.tf new file mode 100644 index 0000000..7f89980 --- /dev/null +++ b/infrastructure/network/local_testing/vault_vpc/main.tf @@ -0,0 +1,13 @@ +/* + * GCP Provider - https://www.terraform.io/docs/providers/google/guides/provider_reference.html + * Required for provisioning infrastructure in GCP + */ +provider "google" { + project = var.gcp_project + region = var.gcp_region +} + +provider "google-beta" { + project = var.gcp_project + region = var.gcp_region +} diff --git a/infrastructure/network/local_testing/vault_vpc/outputs.tf b/infrastructure/network/local_testing/vault_vpc/outputs.tf new file mode 100644 index 0000000..f6e7d16 --- /dev/null +++ b/infrastructure/network/local_testing/vault_vpc/outputs.tf @@ -0,0 +1,7 @@ +output "vault_vpc_test_instance_ip" { + value = google_compute_instance.test.network_interface.0.network_ip +} + +output "vault_vpc_test_instance_ssh_command" { + value = "gcloud beta compute ssh --zone ${google_compute_instance.test.zone} ${google_compute_instance.test.name} --tunnel-through-iap --project ${var.gcp_project}" +} diff --git a/infrastructure/network/local_testing/vault_vpc/variables.tf b/infrastructure/network/local_testing/vault_vpc/variables.tf new file mode 100644 index 0000000..769ea14 --- /dev/null +++ b/infrastructure/network/local_testing/vault_vpc/variables.tf @@ -0,0 +1,31 @@ +variable "gcp_project" { + description = "Name of GCP project used to provision infrastructure into" +} + +variable "gcp_region" { + description = "GCP region to provision resources into" +} + +variable "gcp_zone" { + description = "GCP zone to provision resources into" +} + +variable "datadog_api_key" { + description = "API key used by Datadog agent in the instance to authenticate to Datadog" +} + +variable "ssh_user" { + description = "Email address of the user with access to SSH into the instance" +} + +variable "vault_vpc_name" { + description = "Name of the VPC" +} + +variable "vault_vpc_uri" { + description = "URI of the VPC" +} + +variable "vault_vpc_subnet" { + description = "Vault VPC's subnet" +} diff --git a/infrastructure/network/local_testing/vault_vpc/vault_vpc.tf b/infrastructure/network/local_testing/vault_vpc/vault_vpc.tf new file mode 100644 index 0000000..a59b082 --- /dev/null +++ b/infrastructure/network/local_testing/vault_vpc/vault_vpc.tf @@ -0,0 +1,48 @@ +/* + * This grants the given user account access to SSH into the instance + */ +resource "google_iap_tunnel_instance_iam_binding" "editor" { + project = google_compute_instance.test.project + zone = google_compute_instance.test.zone + instance = google_compute_instance.test.name + role = "roles/iap.tunnelResourceAccessor" + members = [ + "user:${var.ssh_user}" + ] +} + +/* + * Instance used to test Vault VPC's connectivity + */ +resource "google_compute_instance" "test" { + name = "test" + machine_type = "f1-micro" + zone = var.gcp_zone + + tags = [ + "ssh-access", + "vault" + ] + + boot_disk { + initialize_params { + image = "debian-cloud/debian-9" + } + } + + network_interface { + subnetwork = var.vault_vpc_subnet + } + + metadata_startup_script = <<-EOT + DD_AGENT_MAJOR_VERSION=7 + DD_API_KEY=var.datadog_api_key + bash -c "$(curl -L https://raw.githubusercontent.com/DataDog/datadog-agent/master/cmd/agent/install_script.sh)" + apt-get update -qq + sudo apt-get -yq install stress unzip less + wget https://releases.hashicorp.com/vault/1.3.2/vault_1.3.2_linux_amd64.zip + unzip vault_1.3.2_linux_amd64.zip + mv vault /usr/local/bin/ + vault server -dev -dev-listen-address="0.0.0.0:8200" -dev-root-token-id="totally-secure" -log-level=debug & + EOT +} diff --git a/infrastructure/network/main.tf b/infrastructure/network/main.tf new file mode 100644 index 0000000..3d68fae --- /dev/null +++ b/infrastructure/network/main.tf @@ -0,0 +1,30 @@ +/* + * GCP Provider - https://www.terraform.io/docs/providers/google/guides/provider_reference.html + * Required for provisioning infrastructure in GCP + */ +provider "google" { + project = var.gcp_project + region = var.gcp_region + batching { + enable_batching = false + } +} + +provider "google-beta" { + project = var.gcp_project + region = var.gcp_region + batching { + enable_batching = false + } +} + +/* + * Datadog Provider - https://www.terraform.io/docs/providers/datadog/index.html + * Used to retrieve Datadog's IP addresses in order to configure egreess firewall rules + * The provider requires the DATADOG_API_KEY and DATADOG_APP_KEY environment variables + */ + +provider "datadog" { + api_key = var.datadog_api_key + app_key = var.datadog_app_key +} \ No newline at end of file diff --git a/infrastructure/network/outputs.tf b/infrastructure/network/outputs.tf new file mode 100644 index 0000000..592cd83 --- /dev/null +++ b/infrastructure/network/outputs.tf @@ -0,0 +1,44 @@ +output "vpc_id" { + value = google_compute_network.vpc.id + description = "The identifier of the VPC." +} + +output "vpc_uri" { + value = google_compute_network.vpc.self_link + description = "URI of the VPC." +} + +output "subnet_uri" { + value = google_compute_subnetwork.subnet.self_link + description = "URI of the Vault subnet" +} + +output "vpn_private_instance_ip" { + value = google_compute_instance.vpn.network_interface.0.network_ip + description = "Internal IP address of the VPN instance" +} + +output "vpn_instance_ssh_command" { + value = "gcloud beta compute ssh --zone ${google_compute_instance.vpn.zone} ${google_compute_instance.vpn.name} --tunnel-through-iap --project ${var.gcp_project}" + description = "SSH command to access VPN instance" +} + +output "vpn_public_instance_ip" { + value = google_compute_address.vpn_address.address + description = "Public IP address of the VPN instance" +} + +output "bucket_ovpn_copy_command" { + value = "gsutil cp gs://${var.bucket_name}/unsealer.ovpn ." + description = "Command to retrieve ovpn file" +} + +output "bucket_ovpn_delete_command" { + value = "gsutil rm gs://${var.bucket_name}/unsealer.ovpn" + description = "Command to delete the VPN key from the bucket" +} + +output "registry_uri" { + value = google_container_registry.registry.bucket_self_link + description = "The self-link URI for the private container registry in GCR" +} diff --git a/infrastructure/network/scripts/openvpn-install.sh b/infrastructure/network/scripts/openvpn-install.sh new file mode 100755 index 0000000..ea45809 --- /dev/null +++ b/infrastructure/network/scripts/openvpn-install.sh @@ -0,0 +1,1264 @@ +#!/bin/bash + +# Secure OpenVPN server installer for Debian, Ubuntu, CentOS, Amazon Linux 2, Fedora and Arch Linux +# https://github.com/angristan/openvpn-install + +function isRoot () { + if [ "$EUID" -ne 0 ]; then + return 1 + fi +} + +function tunAvailable () { + if [ ! -e /dev/net/tun ]; then + return 1 + fi +} + +function checkOS () { + if [[ -e /etc/debian_version ]]; then + OS="debian" + # shellcheck disable=SC1091 + source /etc/os-release + + if [[ "$ID" == "debian" || "$ID" == "raspbian" ]]; then + if [[ ! $VERSION_ID =~ (8|9|10) ]]; then + echo "⚠️ Your version of Debian is not supported." + echo "" + echo "However, if you're using Debian >= 9 or unstable/testing then you can continue." + echo "Keep in mind they are not supported, though." + echo "" + until [[ $CONTINUE =~ (y|n) ]]; do + read -rp "Continue? [y/n]: " -e CONTINUE + done + if [[ "$CONTINUE" = "n" ]]; then + exit 1 + fi + fi + elif [[ "$ID" == "ubuntu" ]];then + OS="ubuntu" + if [[ ! $VERSION_ID =~ (16.04|18.04|19.04) ]]; then + echo "⚠️ Your version of Ubuntu is not supported." + echo "" + echo "However, if you're using Ubuntu > 17 or beta, then you can continue." + echo "Keep in mind they are not supported, though." + echo "" + until [[ $CONTINUE =~ (y|n) ]]; do + read -rp "Continue? [y/n]: " -e CONTINUE + done + if [[ "$CONTINUE" = "n" ]]; then + exit 1 + fi + fi + fi + elif [[ -e /etc/system-release ]]; then + # shellcheck disable=SC1091 + source /etc/os-release + if [[ "$ID" = "fedora" ]]; then + OS="fedora" + fi + if [[ "$ID" = "centos" ]]; then + OS="centos" + if [[ ! $VERSION_ID =~ (7|8) ]]; then + echo "⚠️ Your version of CentOS is not supported." + echo "" + echo "The script only support CentOS 7." + echo "" + exit 1 + fi + fi + if [[ "$ID" = "amzn" ]]; then + OS="amzn" + if [[ ! $VERSION_ID == "2" ]]; then + echo "⚠️ Your version of Amazon Linux is not supported." + echo "" + echo "The script only support Amazon Linux 2." + echo "" + exit 1 + fi + fi + elif [[ -e /etc/arch-release ]]; then + OS=arch + else + echo "Looks like you aren't running this installer on a Debian, Ubuntu, Fedora, CentOS, Amazon Linux 2 or Arch Linux system" + exit 1 + fi +} + +function initialCheck () { + if ! isRoot; then + echo "Sorry, you need to run this as root" + exit 1 + fi + if ! tunAvailable; then + echo "TUN is not available" + exit 1 + fi + checkOS +} + +function installUnbound () { + if [[ ! -e /etc/unbound/unbound.conf ]]; then + + if [[ "$OS" =~ (debian|ubuntu) ]]; then + apt-get install -y unbound + + # Configuration + echo 'interface: 10.8.0.1 +access-control: 10.8.0.1/24 allow +hide-identity: yes +hide-version: yes +use-caps-for-id: yes +prefetch: yes' >> /etc/unbound/unbound.conf + + elif [[ "$OS" =~ (centos|amzn) ]]; then + yum install -y unbound + + # Configuration + sed -i 's|# interface: 0.0.0.0$|interface: 10.8.0.1|' /etc/unbound/unbound.conf + sed -i 's|# access-control: 127.0.0.0/8 allow|access-control: 10.8.0.1/24 allow|' /etc/unbound/unbound.conf + sed -i 's|# hide-identity: no|hide-identity: yes|' /etc/unbound/unbound.conf + sed -i 's|# hide-version: no|hide-version: yes|' /etc/unbound/unbound.conf + sed -i 's|use-caps-for-id: no|use-caps-for-id: yes|' /etc/unbound/unbound.conf + + elif [[ "$OS" = "fedora" ]]; then + dnf install -y unbound + + # Configuration + sed -i 's|# interface: 0.0.0.0$|interface: 10.8.0.1|' /etc/unbound/unbound.conf + sed -i 's|# access-control: 127.0.0.0/8 allow|access-control: 10.8.0.1/24 allow|' /etc/unbound/unbound.conf + sed -i 's|# hide-identity: no|hide-identity: yes|' /etc/unbound/unbound.conf + sed -i 's|# hide-version: no|hide-version: yes|' /etc/unbound/unbound.conf + sed -i 's|# use-caps-for-id: no|use-caps-for-id: yes|' /etc/unbound/unbound.conf + + elif [[ "$OS" = "arch" ]]; then + pacman -Syu --noconfirm unbound + + # Get root servers list + curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache + + mv /etc/unbound/unbound.conf /etc/unbound/unbound.conf.old + + echo 'server: + use-syslog: yes + do-daemonize: no + username: "unbound" + directory: "/etc/unbound" + trust-anchor-file: trusted-key.key + root-hints: root.hints + interface: 10.8.0.1 + access-control: 10.8.0.1/24 allow + port: 53 + num-threads: 2 + use-caps-for-id: yes + harden-glue: yes + hide-identity: yes + hide-version: yes + qname-minimisation: yes + prefetch: yes' > /etc/unbound/unbound.conf + fi + + if [[ ! "$OS" =~ (fedora|centos|amzn) ]];then + # DNS Rebinding fix + echo "private-address: 10.0.0.0/8 +private-address: 172.16.0.0/12 +private-address: 192.168.0.0/16 +private-address: 169.254.0.0/16 +private-address: fd00::/8 +private-address: fe80::/10 +private-address: 127.0.0.0/8 +private-address: ::ffff:0:0/96" >> /etc/unbound/unbound.conf + fi + else # Unbound is already installed + echo 'include: /etc/unbound/openvpn.conf' >> /etc/unbound/unbound.conf + + # Add Unbound 'server' for the OpenVPN subnet + echo 'server: +interface: 10.8.0.1 +access-control: 10.8.0.1/24 allow +hide-identity: yes +hide-version: yes +use-caps-for-id: yes +prefetch: yes +private-address: 10.0.0.0/8 +private-address: 172.16.0.0/12 +private-address: 192.168.0.0/16 +private-address: 169.254.0.0/16 +private-address: fd00::/8 +private-address: fe80::/10 +private-address: 127.0.0.0/8 +private-address: ::ffff:0:0/96' > /etc/unbound/openvpn.conf + fi + + systemctl enable unbound + systemctl restart unbound +} + +function installQuestions () { + echo "Welcome to the OpenVPN installer!" + echo "The git repository is available at: https://github.com/angristan/openvpn-install" + echo "" + + echo "I need to ask you a few questions before starting the setup." + echo "You can leave the default options and just press enter if you are ok with them." + echo "" + echo "I need to know the IPv4 address of the network interface you want OpenVPN listening to." + echo "Unless your server is behind NAT, it should be your public IPv4 address." + + # Detect public IPv4 address and pre-fill for the user + IP=$(ip addr | grep 'inet' | grep -v inet6 | grep -vE '127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | head -1) + APPROVE_IP=${APPROVE_IP:-n} + if [[ $APPROVE_IP =~ n ]]; then + read -rp "IP address: " -e -i "$IP" IP + fi + # If $IP is a private IP address, the server must be behind NAT + if echo "$IP" | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)'; then + echo "" + echo "It seems this server is behind NAT. What is its public IPv4 address or hostname?" + echo "We need it for the clients to connect to the server." + until [[ "$ENDPOINT" != "" ]]; do + read -rp "Public IPv4 address or hostname: " -e ENDPOINT + done + fi + + echo "" + echo "Checking for IPv6 connectivity..." + echo "" + # "ping6" and "ping -6" availability varies depending on the distribution + if type ping6 > /dev/null 2>&1; then + PING6="ping6 -c3 ipv6.google.com > /dev/null 2>&1" + else + PING6="ping -6 -c3 ipv6.google.com > /dev/null 2>&1" + fi + if eval "$PING6"; then + echo "Your host appears to have IPv6 connectivity." + SUGGESTION="y" + else + echo "Your host does not appear to have IPv6 connectivity." + SUGGESTION="n" + fi + echo "" + # Ask the user if they want to enable IPv6 regardless its availability. + until [[ $IPV6_SUPPORT =~ (y|n) ]]; do + read -rp "Do you want to enable IPv6 support (NAT)? [y/n]: " -e -i $SUGGESTION IPV6_SUPPORT + done + echo "" + echo "What port do you want OpenVPN to listen to?" + echo " 1) Default: 1194" + echo " 2) Custom" + echo " 3) Random [49152-65535]" + until [[ "$PORT_CHOICE" =~ ^[1-3]$ ]]; do + read -rp "Port choice [1-3]: " -e -i 1 PORT_CHOICE + done + case $PORT_CHOICE in + 1) + PORT="1194" + ;; + 2) + until [[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ]; do + read -rp "Custom port [1-65535]: " -e -i 1194 PORT + done + ;; + 3) + # Generate random number within private ports range + PORT=$(shuf -i49152-65535 -n1) + echo "Random Port: $PORT" + ;; + esac + echo "" + echo "What protocol do you want OpenVPN to use?" + echo "UDP is faster. Unless it is not available, you shouldn't use TCP." + echo " 1) UDP" + echo " 2) TCP" + until [[ "$PROTOCOL_CHOICE" =~ ^[1-2]$ ]]; do + read -rp "Protocol [1-2]: " -e -i 1 PROTOCOL_CHOICE + done + case $PROTOCOL_CHOICE in + 1) + PROTOCOL="udp" + ;; + 2) + PROTOCOL="tcp" + ;; + esac + echo "" + echo "What DNS resolvers do you want to use with the VPN?" + echo " 1) Current system resolvers (from /etc/resolv.conf)" + echo " 2) Self-hosted DNS Resolver (Unbound)" + echo " 3) Cloudflare (Anycast: worldwide)" + echo " 4) Quad9 (Anycast: worldwide)" + echo " 5) Quad9 uncensored (Anycast: worldwide)" + echo " 6) FDN (France)" + echo " 7) DNS.WATCH (Germany)" + echo " 8) OpenDNS (Anycast: worldwide)" + echo " 9) Google (Anycast: worldwide)" + echo " 10) Yandex Basic (Russia)" + echo " 11) AdGuard DNS (Russia)" + echo " 12) NextDNS (Worldwide)" + echo " 13) Custom" + until [[ "$DNS" =~ ^[0-9]+$ ]] && [ "$DNS" -ge 1 ] && [ "$DNS" -le 12 ]; do + read -rp "DNS [1-12]: " -e -i 3 DNS + if [[ $DNS == 2 ]] && [[ -e /etc/unbound/unbound.conf ]]; then + echo "" + echo "Unbound is already installed." + echo "You can allow the script to configure it in order to use it from your OpenVPN clients" + echo "We will simply add a second server to /etc/unbound/unbound.conf for the OpenVPN subnet." + echo "No changes are made to the current configuration." + echo "" + + until [[ $CONTINUE =~ (y|n) ]]; do + read -rp "Apply configuration changes to Unbound? [y/n]: " -e CONTINUE + done + if [[ $CONTINUE = "n" ]];then + # Break the loop and cleanup + unset DNS + unset CONTINUE + fi + elif [[ $DNS == "12" ]]; then + until [[ "$DNS1" =~ ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ ]]; do + read -rp "Primary DNS: " -e DNS1 + done + until [[ "$DNS2" =~ ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$ ]]; do + read -rp "Secondary DNS (optional): " -e DNS2 + if [[ "$DNS2" == "" ]]; then + break + fi + done + fi + done + echo "" + echo "Do you want to use compression? It is not recommended since the VORACLE attack make use of it." + until [[ $COMPRESSION_ENABLED =~ (y|n) ]]; do + read -rp"Enable compression? [y/n]: " -e -i n COMPRESSION_ENABLED + done + if [[ $COMPRESSION_ENABLED == "y" ]];then + echo "Choose which compression algorithm you want to use: (they are ordered by efficiency)" + echo " 1) LZ4-v2" + echo " 2) LZ4" + echo " 3) LZ0" + until [[ $COMPRESSION_CHOICE =~ ^[1-3]$ ]]; do + read -rp"Compression algorithm [1-3]: " -e -i 1 COMPRESSION_CHOICE + done + case $COMPRESSION_CHOICE in + 1) + COMPRESSION_ALG="lz4-v2" + ;; + 2) + COMPRESSION_ALG="lz4" + ;; + 3) + COMPRESSION_ALG="lzo" + ;; + esac + fi + echo "" + echo "Do you want to customize encryption settings?" + echo "Unless you know what you're doing, you should stick with the default parameters provided by the script." + echo "Note that whatever you choose, all the choices presented in the script are safe. (Unlike OpenVPN's defaults)" + echo "See https://github.com/angristan/openvpn-install#security-and-encryption to learn more." + echo "" + until [[ $CUSTOMIZE_ENC =~ (y|n) ]]; do + read -rp "Customize encryption settings? [y/n]: " -e -i n CUSTOMIZE_ENC + done + if [[ $CUSTOMIZE_ENC == "n" ]];then + # Use default, sane and fast parameters + CIPHER="AES-128-GCM" + CERT_TYPE="1" # ECDSA + CERT_CURVE="prime256v1" + CC_CIPHER="TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256" + DH_TYPE="1" # ECDH + DH_CURVE="prime256v1" + HMAC_ALG="SHA256" + TLS_SIG="1" # tls-crypt + else + echo "" + echo "Choose which cipher you want to use for the data channel:" + echo " 1) AES-128-GCM (recommended)" + echo " 2) AES-192-GCM" + echo " 3) AES-256-GCM" + echo " 4) AES-128-CBC" + echo " 5) AES-192-CBC" + echo " 6) AES-256-CBC" + until [[ "$CIPHER_CHOICE" =~ ^[1-6]$ ]]; do + read -rp "Cipher [1-6]: " -e -i 1 CIPHER_CHOICE + done + case $CIPHER_CHOICE in + 1) + CIPHER="AES-128-GCM" + ;; + 2) + CIPHER="AES-192-GCM" + ;; + 3) + CIPHER="AES-256-GCM" + ;; + 4) + CIPHER="AES-128-CBC" + ;; + 5) + CIPHER="AES-192-CBC" + ;; + 6) + CIPHER="AES-256-CBC" + ;; + esac + echo "" + echo "Choose what kind of certificate you want to use:" + echo " 1) ECDSA (recommended)" + echo " 2) RSA" + until [[ $CERT_TYPE =~ ^[1-2]$ ]]; do + read -rp"Certificate key type [1-2]: " -e -i 1 CERT_TYPE + done + case $CERT_TYPE in + 1) + echo "" + echo "Choose which curve you want to use for the certificate's key:" + echo " 1) prime256v1 (recommended)" + echo " 2) secp384r1" + echo " 3) secp521r1" + until [[ $CERT_CURVE_CHOICE =~ ^[1-3]$ ]]; do + read -rp"Curve [1-3]: " -e -i 1 CERT_CURVE_CHOICE + done + case $CERT_CURVE_CHOICE in + 1) + CERT_CURVE="prime256v1" + ;; + 2) + CERT_CURVE="secp384r1" + ;; + 3) + CERT_CURVE="secp521r1" + ;; + esac + ;; + 2) + echo "" + echo "Choose which size you want to use for the certificate's RSA key:" + echo " 1) 2048 bits (recommended)" + echo " 2) 3072 bits" + echo " 3) 4096 bits" + until [[ "$RSA_KEY_SIZE_CHOICE" =~ ^[1-3]$ ]]; do + read -rp "RSA key size [1-3]: " -e -i 1 RSA_KEY_SIZE_CHOICE + done + case $RSA_KEY_SIZE_CHOICE in + 1) + RSA_KEY_SIZE="2048" + ;; + 2) + RSA_KEY_SIZE="3072" + ;; + 3) + RSA_KEY_SIZE="4096" + ;; + esac + ;; + esac + echo "" + echo "Choose which cipher you want to use for the control channel:" + case $CERT_TYPE in + 1) + echo " 1) ECDHE-ECDSA-AES-128-GCM-SHA256 (recommended)" + echo " 2) ECDHE-ECDSA-AES-256-GCM-SHA384" + until [[ $CC_CIPHER_CHOICE =~ ^[1-2]$ ]]; do + read -rp"Control channel cipher [1-2]: " -e -i 1 CC_CIPHER_CHOICE + done + case $CC_CIPHER_CHOICE in + 1) + CC_CIPHER="TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256" + ;; + 2) + CC_CIPHER="TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384" + ;; + esac + ;; + 2) + echo " 1) ECDHE-RSA-AES-128-GCM-SHA256 (recommended)" + echo " 2) ECDHE-RSA-AES-256-GCM-SHA384" + until [[ $CC_CIPHER_CHOICE =~ ^[1-2]$ ]]; do + read -rp"Control channel cipher [1-2]: " -e -i 1 CC_CIPHER_CHOICE + done + case $CC_CIPHER_CHOICE in + 1) + CC_CIPHER="TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256" + ;; + 2) + CC_CIPHER="TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384" + ;; + esac + ;; + esac + echo "" + echo "Choose what kind of Diffie-Hellman key you want to use:" + echo " 1) ECDH (recommended)" + echo " 2) DH" + until [[ $DH_TYPE =~ [1-2] ]]; do + read -rp"DH key type [1-2]: " -e -i 1 DH_TYPE + done + case $DH_TYPE in + 1) + echo "" + echo "Choose which curve you want to use for the ECDH key:" + echo " 1) prime256v1 (recommended)" + echo " 2) secp384r1" + echo " 3) secp521r1" + while [[ $DH_CURVE_CHOICE != "1" && $DH_CURVE_CHOICE != "2" && $DH_CURVE_CHOICE != "3" ]]; do + read -rp"Curve [1-3]: " -e -i 1 DH_CURVE_CHOICE + done + case $DH_CURVE_CHOICE in + 1) + DH_CURVE="prime256v1" + ;; + 2) + DH_CURVE="secp384r1" + ;; + 3) + DH_CURVE="secp521r1" + ;; + esac + ;; + 2) + echo "" + echo "Choose what size of Diffie-Hellman key you want to use:" + echo " 1) 2048 bits (recommended)" + echo " 2) 3072 bits" + echo " 3) 4096 bits" + until [[ "$DH_KEY_SIZE_CHOICE" =~ ^[1-3]$ ]]; do + read -rp "DH key size [1-3]: " -e -i 1 DH_KEY_SIZE_CHOICE + done + case $DH_KEY_SIZE_CHOICE in + 1) + DH_KEY_SIZE="2048" + ;; + 2) + DH_KEY_SIZE="3072" + ;; + 3) + DH_KEY_SIZE="4096" + ;; + esac + ;; + esac + echo "" + # The "auth" options behaves differently with AEAD ciphers + if [[ "$CIPHER" =~ CBC$ ]]; then + echo "The digest algorithm authenticates data channel packets and tls-auth packets from the control channel." + elif [[ "$CIPHER" =~ GCM$ ]]; then + echo "The digest algorithm authenticates tls-auth packets from the control channel." + fi + echo "Which digest algorithm do you want to use for HMAC?" + echo " 1) SHA-256 (recommended)" + echo " 2) SHA-384" + echo " 3) SHA-512" + until [[ $HMAC_ALG_CHOICE =~ ^[1-3]$ ]]; do + read -rp "Digest algorithm [1-3]: " -e -i 1 HMAC_ALG_CHOICE + done + case $HMAC_ALG_CHOICE in + 1) + HMAC_ALG="SHA256" + ;; + 2) + HMAC_ALG="SHA384" + ;; + 3) + HMAC_ALG="SHA512" + ;; + esac + echo "" + echo "You can add an additional layer of security to the control channel with tls-auth and tls-crypt" + echo "tls-auth authenticates the packets, while tls-crypt authenticate and encrypt them." + echo " 1) tls-crypt (recommended)" + echo " 2) tls-auth" + until [[ $TLS_SIG =~ [1-2] ]]; do + read -rp "Control channel additional security mechanism [1-2]: " -e -i 1 TLS_SIG + done + fi + echo "" + echo "Okay, that was all I needed. We are ready to setup your OpenVPN server now." + echo "You will be able to generate a client at the end of the installation." + APPROVE_INSTALL=${APPROVE_INSTALL:-n} + if [[ $APPROVE_INSTALL =~ n ]]; then + read -n1 -r -p "Press any key to continue..." + fi +} + +function installOpenVPN () { + if [[ $AUTO_INSTALL == "y" ]]; then + # Set default choices so that no questions will be asked. + APPROVE_INSTALL=${APPROVE_INSTALL:-y} + APPROVE_IP=${APPROVE_IP:-y} + IPV6_SUPPORT=${IPV6_SUPPORT:-n} + PORT_CHOICE=${PORT_CHOICE:-1} + PROTOCOL_CHOICE=${PROTOCOL_CHOICE:-1} + DNS=${DNS:-1} + COMPRESSION_ENABLED=${COMPRESSION_ENABLED:-n} + CUSTOMIZE_ENC=${CUSTOMIZE_ENC:-n} + CLIENT=${CLIENT:-client} + PASS=${PASS:-1} + CONTINUE=${CONTINUE:-y} + + # Behind NAT, we'll default to the publicly reachable IPv4. + PUBLIC_IPV4=$(curl ifconfig.co) + ENDPOINT=${ENDPOINT:-$PUBLIC_IPV4} + fi + + # Run setup questions first, and set other variales if auto-install + installQuestions + + # Get the "public" interface from the default route + NIC=$(ip -4 route ls | grep default | grep -Po '(?<=dev )(\S+)' | head -1) + + if [[ "$OS" =~ (debian|ubuntu) ]]; then + apt-get update + apt-get -y install ca-certificates gnupg + # We add the OpenVPN repo to get the latest version. + if [[ "$VERSION_ID" = "8" ]]; then + echo "deb http://build.openvpn.net/debian/openvpn/stable jessie main" > /etc/apt/sources.list.d/openvpn.list + wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg | apt-key add - + apt-get update + fi + if [[ "$VERSION_ID" = "16.04" ]]; then + echo "deb http://build.openvpn.net/debian/openvpn/stable xenial main" > /etc/apt/sources.list.d/openvpn.list + wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg | apt-key add - + apt-get update + fi + # Ubuntu > 16.04 and Debian > 8 have OpenVPN >= 2.4 without the need of a third party repository. + apt-get install -y openvpn iptables openssl wget ca-certificates curl + elif [[ "$OS" = 'centos' ]]; then + yum install -y epel-release + yum install -y openvpn iptables openssl wget ca-certificates curl tar + elif [[ "$OS" = 'amzn' ]]; then + amazon-linux-extras install -y epel + yum install -y openvpn iptables openssl wget ca-certificates curl + elif [[ "$OS" = 'fedora' ]]; then + dnf install -y openvpn iptables openssl wget ca-certificates curl + elif [[ "$OS" = 'arch' ]]; then + # Install required dependencies and upgrade the system + pacman --needed --noconfirm -Syu openvpn iptables openssl wget ca-certificates curl + fi + + # Find out if the machine uses nogroup or nobody for the permissionless group + if grep -qs "^nogroup:" /etc/group; then + NOGROUP=nogroup + else + NOGROUP=nobody + fi + + # An old version of easy-rsa was available by default in some openvpn packages + if [[ -d /etc/openvpn/easy-rsa/ ]]; then + rm -rf /etc/openvpn/easy-rsa/ + fi + + # Install the latest version of easy-rsa from source + local version="3.0.6" + wget -O ~/EasyRSA-unix-v${version}.tgz https://github.com/OpenVPN/easy-rsa/releases/download/v${version}/EasyRSA-unix-v${version}.tgz + tar xzf ~/EasyRSA-unix-v${version}.tgz -C ~/ + mv ~/EasyRSA-v${version} /etc/openvpn/easy-rsa + chown -R root:root /etc/openvpn/easy-rsa/ + rm -f ~/EasyRSA-unix-v${version}.tgz + + cd /etc/openvpn/easy-rsa/ || return + case $CERT_TYPE in + 1) + echo "set_var EASYRSA_ALGO ec" > vars + echo "set_var EASYRSA_CURVE $CERT_CURVE" >> vars + ;; + 2) + echo "set_var EASYRSA_KEY_SIZE $RSA_KEY_SIZE" > vars + ;; + esac + + # Generate a random, alphanumeric identifier of 16 characters for CN and one for server name + SERVER_CN="cn_$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)" + SERVER_NAME="server_$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)" + echo "set_var EASYRSA_REQ_CN $SERVER_CN" >> vars + + # Create the PKI, set up the CA, the DH params and the server certificate + ./easyrsa init-pki + + # Workaround to remove unharmful error until easy-rsa 3.0.7 + # https://github.com/OpenVPN/easy-rsa/issues/261 + sed -i 's/^RANDFILE/#RANDFILE/g' pki/openssl-easyrsa.cnf + + ./easyrsa --batch build-ca nopass + + if [[ $DH_TYPE == "2" ]]; then + # ECDH keys are generated on-the-fly so we don't need to generate them beforehand + openssl dhparam -out dh.pem $DH_KEY_SIZE + fi + + ./easyrsa build-server-full "$SERVER_NAME" nopass + EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl + + case $TLS_SIG in + 1) + # Generate tls-crypt key + openvpn --genkey --secret /etc/openvpn/tls-crypt.key + ;; + 2) + # Generate tls-auth key + openvpn --genkey --secret /etc/openvpn/tls-auth.key + ;; + esac + + # Move all the generated files + cp pki/ca.crt pki/private/ca.key "pki/issued/$SERVER_NAME.crt" "pki/private/$SERVER_NAME.key" /etc/openvpn/easy-rsa/pki/crl.pem /etc/openvpn + if [[ $DH_TYPE == "2" ]]; then + cp dh.pem /etc/openvpn + fi + + # Make cert revocation list readable for non-root + chmod 644 /etc/openvpn/crl.pem + + # Generate server.conf + echo "port $PORT" > /etc/openvpn/server.conf + if [[ "$IPV6_SUPPORT" = 'n' ]]; then + echo "proto $PROTOCOL" >> /etc/openvpn/server.conf + elif [[ "$IPV6_SUPPORT" = 'y' ]]; then + echo "proto ${PROTOCOL}6" >> /etc/openvpn/server.conf + fi + + echo "dev tun +user nobody +group $NOGROUP +persist-key +persist-tun +keepalive 10 120 +topology subnet +server 10.8.0.0 255.255.255.0 +ifconfig-pool-persist ipp.txt" >> /etc/openvpn/server.conf + + # DNS resolvers + case $DNS in + 1) + # Locate the proper resolv.conf + # Needed for systems running systemd-resolved + if grep -q "127.0.0.53" "/etc/resolv.conf"; then + RESOLVCONF='/run/systemd/resolve/resolv.conf' + else + RESOLVCONF='/etc/resolv.conf' + fi + # Obtain the resolvers from resolv.conf and use them for OpenVPN + grep -v '#' $RESOLVCONF | grep 'nameserver' | grep -E -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | while read -r line; do + echo "push \"dhcp-option DNS $line\"" >> /etc/openvpn/server.conf + done + ;; + 2) + echo 'push "dhcp-option DNS 10.8.0.1"' >> /etc/openvpn/server.conf + ;; + 3) # Cloudflare + echo 'push "dhcp-option DNS 1.0.0.1"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 1.1.1.1"' >> /etc/openvpn/server.conf + ;; + 4) # Quad9 + echo 'push "dhcp-option DNS 9.9.9.9"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 149.112.112.112"' >> /etc/openvpn/server.conf + ;; + 5) # Quad9 uncensored + echo 'push "dhcp-option DNS 9.9.9.10"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 149.112.112.10"' >> /etc/openvpn/server.conf + ;; + 6) # FDN + echo 'push "dhcp-option DNS 80.67.169.40"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 80.67.169.12"' >> /etc/openvpn/server.conf + ;; + 7) # DNS.WATCH + echo 'push "dhcp-option DNS 84.200.69.80"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 84.200.70.40"' >> /etc/openvpn/server.conf + ;; + 8) # OpenDNS + echo 'push "dhcp-option DNS 208.67.222.222"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 208.67.220.220"' >> /etc/openvpn/server.conf + ;; + 9) # Google + echo 'push "dhcp-option DNS 8.8.8.8"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 8.8.4.4"' >> /etc/openvpn/server.conf + ;; + 10) # Yandex Basic + echo 'push "dhcp-option DNS 77.88.8.8"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 77.88.8.1"' >> /etc/openvpn/server.conf + ;; + 11) # AdGuard DNS + echo 'push "dhcp-option DNS 176.103.130.130"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 176.103.130.131"' >> /etc/openvpn/server.conf + ;; + 12) # NextDNS + echo 'push "dhcp-option DNS 45.90.28.167"' >> /etc/openvpn/server.conf + echo 'push "dhcp-option DNS 45.90.30.167"' >> /etc/openvpn/server.conf + ;; + 13) # Custom DNS + echo "push \"dhcp-option DNS $DNS1\"" >> /etc/openvpn/server.conf + if [[ "$DNS2" != "" ]]; then + echo "push \"dhcp-option DNS $DNS2\"" >> /etc/openvpn/server.conf + fi + ;; + esac + echo 'push "redirect-gateway def1 bypass-dhcp"' >> /etc/openvpn/server.conf + + # IPv6 network settings if needed + if [[ "$IPV6_SUPPORT" = 'y' ]]; then + echo 'server-ipv6 fd42:42:42:42::/112 +tun-ipv6 +push tun-ipv6 +push "route-ipv6 2000::/3" +push "redirect-gateway ipv6"' >> /etc/openvpn/server.conf + fi + + if [[ $COMPRESSION_ENABLED == "y" ]]; then + echo "compress $COMPRESSION_ALG" >> /etc/openvpn/server.conf + fi + + if [[ $DH_TYPE == "1" ]]; then + echo "dh none" >> /etc/openvpn/server.conf + echo "ecdh-curve $DH_CURVE" >> /etc/openvpn/server.conf + elif [[ $DH_TYPE == "2" ]]; then + echo "dh dh.pem" >> /etc/openvpn/server.conf + fi + + case $TLS_SIG in + 1) + echo "tls-crypt tls-crypt.key 0" >> /etc/openvpn/server.conf + ;; + 2) + echo "tls-auth tls-auth.key 0" >> /etc/openvpn/server.conf + ;; + esac + + echo "crl-verify crl.pem +ca ca.crt +cert $SERVER_NAME.crt +key $SERVER_NAME.key +auth $HMAC_ALG +cipher $CIPHER +ncp-ciphers $CIPHER +tls-server +tls-version-min 1.2 +tls-cipher $CC_CIPHER +status /var/log/openvpn/status.log +verb 3" >> /etc/openvpn/server.conf + + # Create log dir + mkdir -p /var/log/openvpn + + # Enable routing + echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.d/20-openvpn.conf + if [[ "$IPV6_SUPPORT" = 'y' ]]; then + echo 'net.ipv6.conf.all.forwarding=1' >> /etc/sysctl.d/20-openvpn.conf + fi + # Apply sysctl rules + sysctl --system + + # If SELinux is enabled and a custom port was selected, we need this + if hash sestatus 2>/dev/null; then + if sestatus | grep "Current mode" | grep -qs "enforcing"; then + if [[ "$PORT" != '1194' ]]; then + semanage port -a -t openvpn_port_t -p "$PROTOCOL" "$PORT" + fi + fi + fi + + # Finally, restart and enable OpenVPN + if [[ "$OS" = 'arch' || "$OS" = 'fedora' || "$OS" = 'centos' ]]; then + # Don't modify package-provided service + cp /usr/lib/systemd/system/openvpn-server@.service /etc/systemd/system/openvpn-server@.service + + # Workaround to fix OpenVPN service on OpenVZ + sed -i 's|LimitNPROC|#LimitNPROC|' /etc/systemd/system/openvpn-server@.service + # Another workaround to keep using /etc/openvpn/ + sed -i 's|/etc/openvpn/server|/etc/openvpn|' /etc/systemd/system/openvpn-server@.service + # On fedora, the service hardcodes the ciphers. We want to manage the cipher ourselves, so we remove it from the service + if [[ "$OS" == "fedora" ]];then + sed -i 's|--cipher AES-256-GCM --ncp-ciphers AES-256-GCM:AES-128-GCM:AES-256-CBC:AES-128-CBC:BF-CBC||' /etc/systemd/system/openvpn-server@.service + fi + + systemctl daemon-reload + systemctl restart openvpn-server@server + systemctl enable openvpn-server@server + elif [[ "$OS" == "ubuntu" ]] && [[ "$VERSION_ID" == "16.04" ]]; then + # On Ubuntu 16.04, we use the package from the OpenVPN repo + # This package uses a sysvinit service + systemctl enable openvpn + systemctl start openvpn + else + # Don't modify package-provided service + cp /lib/systemd/system/openvpn\@.service /etc/systemd/system/openvpn\@.service + + # Workaround to fix OpenVPN service on OpenVZ + sed -i 's|LimitNPROC|#LimitNPROC|' /etc/systemd/system/openvpn\@.service + # Another workaround to keep using /etc/openvpn/ + sed -i 's|/etc/openvpn/server|/etc/openvpn|' /etc/systemd/system/openvpn\@.service + + systemctl daemon-reload + systemctl restart openvpn@server + systemctl enable openvpn@server + fi + + if [[ $DNS == 2 ]];then + installUnbound + fi + + # Add iptables rules in two scripts + mkdir /etc/iptables + + # Script to add rules + echo "#!/bin/sh +iptables -t nat -I POSTROUTING 1 -s 10.8.0.0/24 -o $NIC -j MASQUERADE +iptables -I INPUT 1 -i tun0 -j ACCEPT +iptables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT +iptables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT +iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" > /etc/iptables/add-openvpn-rules.sh + + if [[ "$IPV6_SUPPORT" = 'y' ]]; then + echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE +ip6tables -I INPUT 1 -i tun0 -j ACCEPT +ip6tables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT +ip6tables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT" >> /etc/iptables/add-openvpn-rules.sh + fi + + # Script to remove rules + echo "#!/bin/sh +iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o $NIC -j MASQUERADE +iptables -D INPUT -i tun0 -j ACCEPT +iptables -D FORWARD -i $NIC -o tun0 -j ACCEPT +iptables -D FORWARD -i tun0 -o $NIC -j ACCEPT +iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" > /etc/iptables/rm-openvpn-rules.sh + + if [[ "$IPV6_SUPPORT" = 'y' ]]; then + echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE +ip6tables -D INPUT -i tun0 -j ACCEPT +ip6tables -D FORWARD -i $NIC -o tun0 -j ACCEPT +ip6tables -D FORWARD -i tun0 -o $NIC -j ACCEPT" >> /etc/iptables/rm-openvpn-rules.sh + fi + + chmod +x /etc/iptables/add-openvpn-rules.sh + chmod +x /etc/iptables/rm-openvpn-rules.sh + + # Handle the rules via a systemd script + echo "[Unit] +Description=iptables rules for OpenVPN +Before=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/etc/iptables/add-openvpn-rules.sh +ExecStop=/etc/iptables/rm-openvpn-rules.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target" > /etc/systemd/system/iptables-openvpn.service + + # Enable service and apply rules + systemctl daemon-reload + systemctl enable iptables-openvpn + systemctl start iptables-openvpn + + # If the server is behind a NAT, use the correct IP address for the clients to connect to + if [[ "$ENDPOINT" != "" ]]; then + IP=$ENDPOINT + fi + + # client-template.txt is created so we have a template to add further users later + echo "client" > /etc/openvpn/client-template.txt + if [[ "$PROTOCOL" = 'udp' ]]; then + echo "proto udp" >> /etc/openvpn/client-template.txt + elif [[ "$PROTOCOL" = 'tcp' ]]; then + echo "proto tcp-client" >> /etc/openvpn/client-template.txt + fi + echo "remote $IP $PORT +dev tun +resolv-retry infinite +nobind +persist-key +persist-tun +remote-cert-tls server +verify-x509-name $SERVER_NAME name +auth $HMAC_ALG +auth-nocache +cipher $CIPHER +tls-client +tls-version-min 1.2 +tls-cipher $CC_CIPHER +setenv opt block-outside-dns # Prevent Windows 10 DNS leak +verb 3" >> /etc/openvpn/client-template.txt + +if [[ $COMPRESSION_ENABLED == "y" ]]; then + echo "compress $COMPRESSION_ALG" >> /etc/openvpn/client-template.txt +fi + + # Generate the custom client.ovpn + newClient + echo "If you want to add more clients, you simply need to run this script another time!" +} + +function newClient () { + echo "" + echo "Tell me a name for the client." + echo "Use one word only, no special characters." + + until [[ "$CLIENT" =~ ^[a-zA-Z0-9_]+$ ]]; do + read -rp "Client name: " -e CLIENT + done + + echo "" + echo "Do you want to protect the configuration file with a password?" + echo "(e.g. encrypt the private key with a password)" + echo " 1) Add a passwordless client" + echo " 2) Use a password for the client" + + until [[ "$PASS" =~ ^[1-2]$ ]]; do + read -rp "Select an option [1-2]: " -e -i 1 PASS + done + + cd /etc/openvpn/easy-rsa/ || return + case $PASS in + 1) + ./easyrsa build-client-full "$CLIENT" nopass + ;; + 2) + echo "⚠️ You will be asked for the client password below ⚠️" + ./easyrsa build-client-full "$CLIENT" + ;; + esac + + # Home directory of the user, where the client configuration (.ovpn) will be written + if [ -e "/home/$CLIENT" ]; then # if $1 is a user name + homeDir="/home/$CLIENT" + elif [ "${SUDO_USER}" ]; then # if not, use SUDO_USER + homeDir="/home/${SUDO_USER}" + else # if not SUDO_USER, use /root + homeDir="/root" + fi + + # Determine if we use tls-auth or tls-crypt + if grep -qs "^tls-crypt" /etc/openvpn/server.conf; then + TLS_SIG="1" + elif grep -qs "^tls-auth" /etc/openvpn/server.conf; then + TLS_SIG="2" + fi + + # Generates the custom client.ovpn + cp /etc/openvpn/client-template.txt "$homeDir/$CLIENT.ovpn" + { + echo "" + cat "/etc/openvpn/easy-rsa/pki/ca.crt" + echo "" + + echo "" + awk '/BEGIN/,/END/' "/etc/openvpn/easy-rsa/pki/issued/$CLIENT.crt" + echo "" + + echo "" + cat "/etc/openvpn/easy-rsa/pki/private/$CLIENT.key" + echo "" + + case $TLS_SIG in + 1) + echo "" + cat /etc/openvpn/tls-crypt.key + echo "" + ;; + 2) + echo "key-direction 1" + echo "" + cat /etc/openvpn/tls-auth.key + echo "" + ;; + esac + } >> "$homeDir/$CLIENT.ovpn" + + echo "" + echo "Client $CLIENT added, the configuration file is available at $homeDir/$CLIENT.ovpn." + echo "Download the .ovpn file and import it in your OpenVPN client." + + exit 0 +} + +function revokeClient () { + NUMBEROFCLIENTS=$(tail -n +2 /etc/openvpn/easy-rsa/pki/index.txt | grep -c "^V") + if [[ "$NUMBEROFCLIENTS" = '0' ]]; then + echo "" + echo "You have no existing clients!" + exit 1 + fi + + echo "" + echo "Select the existing client certificate you want to revoke" + tail -n +2 /etc/openvpn/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | nl -s ') ' + if [[ "$NUMBEROFCLIENTS" = '1' ]]; then + read -rp "Select one client [1]: " CLIENTNUMBER + else + read -rp "Select one client [1-$NUMBEROFCLIENTS]: " CLIENTNUMBER + fi + + CLIENT=$(tail -n +2 /etc/openvpn/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n "$CLIENTNUMBER"p) + cd /etc/openvpn/easy-rsa/ || return + ./easyrsa --batch revoke "$CLIENT" + EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl + # Cleanup + rm -f "pki/reqs/$CLIENT.req" + rm -f "pki/private/$CLIENT.key" + rm -f "pki/issued/$CLIENT.crt" + rm -f /etc/openvpn/crl.pem + cp /etc/openvpn/easy-rsa/pki/crl.pem /etc/openvpn/crl.pem + chmod 644 /etc/openvpn/crl.pem + find /home/ -maxdepth 2 -name "$CLIENT.ovpn" -delete + rm -f "/root/$CLIENT.ovpn" + sed -i "s|^$CLIENT,.*||" /etc/openvpn/ipp.txt + + echo "" + echo "Certificate for client $CLIENT revoked." +} + +function removeUnbound () { + # Remove OpenVPN-related config + sed -i 's|include: \/etc\/unbound\/openvpn.conf||' /etc/unbound/unbound.conf + rm /etc/unbound/openvpn.conf + systemctl restart unbound + + until [[ $REMOVE_UNBOUND =~ (y|n) ]]; do + echo "" + echo "If you were already using Unbound before installing OpenVPN, I removed the configuration related to OpenVPN." + read -rp "Do you want to completely remove Unbound? [y/n]: " -e REMOVE_UNBOUND + done + + if [[ "$REMOVE_UNBOUND" = 'y' ]]; then + # Stop Unbound + systemctl stop unbound + + if [[ "$OS" =~ (debian|ubuntu) ]]; then + apt-get autoremove --purge -y unbound + elif [[ "$OS" = 'arch' ]]; then + pacman --noconfirm -R unbound + elif [[ "$OS" =~ (centos|amzn) ]]; then + yum remove -y unbound + elif [[ "$OS" = 'fedora' ]]; then + dnf remove -y unbound + fi + + rm -rf /etc/unbound/ + + echo "" + echo "Unbound removed!" + else + echo "" + echo "Unbound wasn't removed." + fi +} + +function removeOpenVPN () { + echo "" + # shellcheck disable=SC2034 + read -rp "Do you really want to remove OpenVPN? [y/n]: " -e -i n REMOVE + if [[ "$REMOVE" = 'y' ]]; then + # Get OpenVPN port from the configuration + PORT=$(grep '^port ' /etc/openvpn/server.conf | cut -d " " -f 2) + + # Stop OpenVPN + if [[ "$OS" =~ (fedora|arch|centos) ]]; then + systemctl disable openvpn-server@server + systemctl stop openvpn-server@server + # Remove customised service + rm /etc/systemd/system/openvpn-server@.service + elif [[ "$OS" == "ubuntu" ]] && [[ "$VERSION_ID" == "16.04" ]]; then + systemctl disable openvpn + systemctl stop openvpn + else + systemctl disable openvpn@server + systemctl stop openvpn@server + # Remove customised service + rm /etc/systemd/system/openvpn\@.service + fi + + # Remove the iptables rules related to the script + systemctl stop iptables-openvpn + # Cleanup + systemctl disable iptables-openvpn + rm /etc/systemd/system/iptables-openvpn.service + systemctl daemon-reload + rm /etc/iptables/add-openvpn-rules.sh + rm /etc/iptables/rm-openvpn-rules.sh + + # SELinux + if hash sestatus 2>/dev/null; then + if sestatus | grep "Current mode" | grep -qs "enforcing"; then + if [[ "$PORT" != '1194' ]]; then + semanage port -d -t openvpn_port_t -p udp "$PORT" + fi + fi + fi + + if [[ "$OS" =~ (debian|ubuntu) ]]; then + apt-get autoremove --purge -y openvpn + if [[ -e /etc/apt/sources.list.d/openvpn.list ]];then + rm /etc/apt/sources.list.d/openvpn.list + apt-get update + fi + elif [[ "$OS" = 'arch' ]]; then + pacman --noconfirm -R openvpn + elif [[ "$OS" =~ (centos|amzn) ]]; then + yum remove -y openvpn + elif [[ "$OS" = 'fedora' ]]; then + dnf remove -y openvpn + fi + + # Cleanup + find /home/ -maxdepth 2 -name "*.ovpn" -delete + find /root/ -maxdepth 1 -name "*.ovpn" -delete + rm -rf /etc/openvpn + rm -rf /usr/share/doc/openvpn* + rm -f /etc/sysctl.d/20-openvpn.conf + rm -rf /var/log/openvpn + + # Unbound + if [[ -e /etc/unbound/openvpn.conf ]]; then + removeUnbound + fi + echo "" + echo "OpenVPN removed!" + else + echo "" + echo "Removal aborted!" + fi +} + +function manageMenu () { + clear + echo "Welcome to OpenVPN-install!" + echo "The git repository is available at: https://github.com/angristan/openvpn-install" + echo "" + echo "It looks like OpenVPN is already installed." + echo "" + echo "What do you want to do?" + echo " 1) Add a new user" + echo " 2) Revoke existing user" + echo " 3) Remove OpenVPN" + echo " 4) Exit" + until [[ "$MENU_OPTION" =~ ^[1-4]$ ]]; do + read -rp "Select an option [1-4]: " MENU_OPTION + done + + case $MENU_OPTION in + 1) + newClient + ;; + 2) + revokeClient + ;; + 3) + removeOpenVPN + ;; + 4) + exit 0 + ;; + esac +} + +# Check for root, TUN, OS... +initialCheck + +# Check if OpenVPN is already installed +if [[ -e /etc/openvpn/server.conf ]]; then + manageMenu +else + installOpenVPN +fi diff --git a/infrastructure/network/variables.tf b/infrastructure/network/variables.tf new file mode 100644 index 0000000..5955108 --- /dev/null +++ b/infrastructure/network/variables.tf @@ -0,0 +1,60 @@ +variable "gcp_project" { + description = "Name of GCP project used to provision infrastructure into" +} + +variable "gcp_region" { + description = "GCP region to provision resources into" +} + +variable "subnet_cidr" { + description = "Subnet used to provision resources into" +} + +variable "router_asn" { + description = "ASN used for the router. Needs to be a valid ASN number not use elsewhere" + default = 64512 +} + +variable "datadog_api_key" { + description = "Datadog API key" +} + +variable "datadog_app_key" { + description = "Datadog APP key" +} + +variable "omisego_vpc_uri" { + description = "URI of the client VPC to be peered to the Vault VPC" +} + +variable "omisego_subnet_cidr" { + description = "CIDR block of subnet used when allowing ingress access in Vault VPC firewall" +} + +variable "bucket_name" { + description = "Bucket where OpenVPN config file is stored" +} + + +variable "ssh_user_email" { + description = "Email of user allowed to SSH into VPN instance for troubleshooting purposes" +} + +variable "allow_ssh" { + description = "Boolean indicating if SSH access to VPN instance is configured" + default = false +} + +variable "lockdown_egress" { + description = "Boolean indicating if egress network access is lockdown to only Datadog IPs" + default = false +} + +variable "gke_cluster_name" { + description = "Name of the GKE Kubernetes cluster to create" +} + +variable "gke_node_count" { + description = "The number of nodes to create in the GKE node pool" + default = 1 +} diff --git a/infrastructure/network/vpc.tf b/infrastructure/network/vpc.tf new file mode 100644 index 0000000..62aef0d --- /dev/null +++ b/infrastructure/network/vpc.tf @@ -0,0 +1,71 @@ +/* + * Google Compute Network - https://www.terraform.io/docs/providers/google/r/compute_network.html + * Defines VPC where Vault infrastructure is provisioned into + */ +resource "google_compute_network" "vpc" { + name = "vault-net" + auto_create_subnetworks = "false" + routing_mode = "REGIONAL" +} + +/* + * Google Compute Subnetwork - https://www.terraform.io/docs/providers/google/r/compute_subnetwork.html + * Defines regional subnet where Vault infrastructure is provisioned into + */ +resource "google_compute_subnetwork" "subnet" { + name = "vault-subnet" + ip_cidr_range = var.subnet_cidr + region = var.gcp_region + network = google_compute_network.vpc.self_link + + # Note: Immutability recommends enabling flow logs for observability, debugging, and incident response. + # These incur in additional cost. + log_config { + aggregation_interval = "INTERVAL_10_MIN" + flow_sampling = 1 + metadata = "INCLUDE_ALL_METADATA" + } +} + +/* + * Google Compute Router - https://www.terraform.io/docs/providers/google/r/compute_router.html + * Routes subnet traffic + */ +resource "google_compute_router" "router" { + name = "vault-net-router" + region = google_compute_subnetwork.subnet.region + network = google_compute_network.vpc.self_link + + bgp { + asn = var.router_asn + } +} + +/* + * Google Compute Route NAT - https://www.terraform.io/docs/providers/google/r/compute_router_nat.html + * Routes internet bound traffic from instances with no public IPs + */ +resource "google_compute_router_nat" "nat" { + name = "vault-net-router-nat" + router = google_compute_router.router.name + region = google_compute_router.router.region + nat_ip_allocate_option = "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" + + # Note: Immutability recommends enabling flow logs for observability, debugging, and incident response. + # These would incur in additional cost. + log_config { + enable = true + filter = "ALL" + } +} + +/* + * Network Peering - https://www.terraform.io/docs/providers/google/r/compute_network_peering.html + * Connecting Omisego VPC + */ +resource "google_compute_network_peering" "peering" { + name = "peering-to-omisego-vpc" + network = google_compute_network.vpc.self_link + peer_network = var.omisego_vpc_uri +} diff --git a/infrastructure/network/vpn.tf b/infrastructure/network/vpn.tf new file mode 100644 index 0000000..bdc21aa --- /dev/null +++ b/infrastructure/network/vpn.tf @@ -0,0 +1,128 @@ +/* + * GPC Zones - https://www.terraform.io/docs/providers/google/d/google_compute_zones.html + * Used to select the zone where the instance is going to be provissioned + */ +data "google_compute_zones" "available" { + region = var.gcp_region +} + +/* + * IAP Tunnel - https://www.terraform.io/docs/providers/google/r/iap_tunnel_instance_iam.html + * An IAP tunnel grants the given user account access to SSH into the instance + * https://cloud.google.com/iap/docs/tutorial-gce + */ +resource "google_iap_tunnel_instance_iam_binding" "ssh_access" { + count = var.allow_ssh ? 1 : 0 + project = google_compute_instance.vpn.project + zone = google_compute_instance.vpn.zone + instance = google_compute_instance.vpn.name + role = "roles/iap.tunnelResourceAccessor" + members = [ + "user:${var.ssh_user_email}" + ] +} + +/* + * Service Account - https://www.terraform.io/docs/providers/google/r/google_service_account.html + * The client VPN configuration is stored in a bucket using the below service account + */ +resource "google_service_account" "vpn_service_account" { + account_id = "vpnservice" + display_name = "VPN Service Account" + description = "Service account used by VPN instance to store vpn client config in bucket" +} + +/* + * IAM Member - https://www.terraform.io/docs/providers/google/r/google_project_iam.html#google_project_iam_member-1 + * Grants service account access to manage storage objects + */ +resource "google_project_iam_member" "project" { + project = google_compute_instance.vpn.project + role = "roles/storage.objectAdmin" + member = "serviceAccount:${google_service_account.vpn_service_account.email}" +} + +/* + * Compute Address - https://www.terraform.io/docs/providers/google/r/compute_address.html + * Static public IP address that's attached to the VPN instance + */ +resource "google_compute_address" "vpn_address" { + name = "vpn-address" +} + +/* + * Storage Bucket - https://www.terraform.io/docs/providers/google/r/storage_bucket.html + * Bucket used for storing the OpenVPN client configuration file + */ +resource "google_storage_bucket" "vpn-store" { + name = var.bucket_name + location = "US" + force_destroy = true +} + +/* + * Compute Instance - https://www.terraform.io/docs/providers/google/r/compute_instance.html + * Instance running OpenVPN service + * The metadata script will download the open VPN installation script here: + * https://github.com/angristan/openvpn-install/blob/master/openvpn-install.sh + * and install openvpn generating an OpenVPN configuration file that's would be used + * by the Unsealer Vault when connecting to the network. + * For backup purposes, a copy of the OpenVPN install script has been stored in this repository + * on the scripts directory. + * Connections to the OpenVPN instance have been tested using Tunnelblick on mac os https://tunnelblick.net/ + */ +resource "google_compute_instance" "vpn" { + name = "vpn" + machine_type = "f1-micro" + zone = data.google_compute_zones.available.names[0] # "us-east4-a" + can_ip_forward = true + + tags = ["ssh-access", "vpn"] + + boot_disk { + initialize_params { + image = "debian-cloud/debian-9" + } + } + + service_account { + email = google_service_account.vpn_service_account.email + scopes = ["storage-rw"] + } + + network_interface { + subnetwork = google_compute_subnetwork.subnet.self_link + access_config { + nat_ip = google_compute_address.vpn_address.address + } + } + + metadata_startup_script = <<-EOT + apt-get update -qq + curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh + chmod +x openvpn-install.sh + AUTO_INSTALL=y \ + APPROVE_IP=${google_compute_address.vpn_address.address} \ + CLIENT=unsealer \ + DNS=3 \ + PASS=1 \ + ./openvpn-install.sh + gsutil cp /root/unsealer.ovpn gs://${var.bucket_name}/ + EOT +} + +/* + * Compute Route - https://www.terraform.io/docs/providers/google/r/compute_route.html + * Routes traffic that originates from Vault instances in the VPC to the unselear Vault through the VPN + * https://openvpn.net/vpn-server-resources/google-cloud-platform-byol-instance-quick-start-guide/ + * https://community.openvpn.net/openvpn/wiki/BridgingAndRouting + */ +resource "google_compute_route" "vpn-outbound" { + name = "vpn-outbound" + dest_range = "10.8.0.0/24" + network = google_compute_network.vpc.name + tags = ["vault"] + next_hop_instance = google_compute_instance.vpn.self_link + next_hop_instance_zone = google_compute_instance.vpn.zone + priority = 500 +} diff --git a/infrastructure/scripts/gen_certs.sh b/infrastructure/scripts/gen_certs.sh index 9d3e619..bac62dc 100755 --- a/infrastructure/scripts/gen_certs.sh +++ b/infrastructure/scripts/gen_certs.sh @@ -33,7 +33,7 @@ usage() { echo "" >&2 fi - exit -1 + exit 255 } # validate_config ensures that required variables are set diff --git a/infrastructure/scripts/gen_overrides.sh b/infrastructure/scripts/gen_overrides.sh index 2a2e6ed..fa50f7a 100755 --- a/infrastructure/scripts/gen_overrides.sh +++ b/infrastructure/scripts/gen_overrides.sh @@ -10,11 +10,12 @@ set -o pipefail ### gen_overrides.sh [options] ### ### Options: -### -d | --domain-name The DNS Domain Name of the nodes in the Vault Cluster -### -r | --region-name The GCP Region that the resources are operating in -### -p | --project-name The GCP Project that the resources are operating in -### -c | --cluster-name The GKE Cluster Name -### -h | --help Show help / usage +### -d | --domain-name The DNS Domain Name of the nodes in the Vault Cluster +### -r | --region-name The GCP Region that the resources are operating in +### -p | --project-name The GCP Project that the resources are operating in +### -c | --cluster-name The GKE Cluster Name +### -v | --server-version The Vault Server version to install +### -h | --help Show help / usage ### ### --ui The Vault UI will be enabled (disabled is default) ### --log-level The Vault Server log level (info is default) @@ -38,6 +39,7 @@ REGION=${GCP_REGION:-} PROJECT=${GCP_PROJECT:-} CLUSTER=${GKE_CLUSTER_NAME:-} +VAULT_SERVER_VERSION="1.5.3" VAULT_UI_ENABLED="false" VAULT_LOG_LEVEL="info" VAULT_REPLICAS="5" @@ -154,6 +156,7 @@ EOF cd k8s + yq w -i vault-overrides.yaml server.image.tag ${VAULT_SERVER_VERSION} yq w -i vault-overrides.yaml server.auditStorage.size ${VAULT_AUDIT_SIZE} yq w -i vault-overrides.yaml server.dataStorage.size ${VAULT_DATA_SIZE} yq w -i vault-overrides.yaml server.extraEnvironmentVars.GOOGLE_REGION ${REGION} @@ -186,6 +189,10 @@ while [[ $# -gt 0 ]]; do PROJECT=$2 shift ;; + -v | --server-version) + VAULT_SERVER_VERSION=$2 + shift + ;; --ui) VAULT_UI_ENABLED=true ;; diff --git a/infrastructure/scripts/vault_backup.sh b/infrastructure/scripts/vault_backup.sh new file mode 100755 index 0000000..1970e55 --- /dev/null +++ b/infrastructure/scripts/vault_backup.sh @@ -0,0 +1,236 @@ +#!/bin/bash + +set -u +set -o pipefail + +### +### vault_backup.sh - perform a rolling backup of the vault data +### +### Usage: +### vault_backup.sh [options] +### +### Required Options: +### -d | --dest-dir Filesytem path to the directory containing the vault backups +### +### Additional Options: +### -p | --file-prefix The filename prefix for the backups (default: vault_snapshot) +### -m | --max-backups The maximum number of backups to keep (default: 4) +### -h | --help Show help / usage +### +### Notes: +### In order to perform the backup, you must point VAULT_ADDR at the RAFT leader. You can +### use `vault operator raft list-peers` to determine this, then set up a port-forward to +### that pod, and then execute this script against that specific pod. +### +### In order to perform the vault backup, you need a Vault Token that has permissions to +### initiate the backup. +### + +DEST_DIR="" +BACKUP_FILENAME="" +FILE_PREFIX="vault_snapshot" + +(( MAX_BACKUPS=4 )) + +# usage displays some helpful information about the script and any errors that need +# to be emitted +usage() { + MESSAGE=${1:-} + + awk -F'### ' '/^###/ { print $2 }' $0 >&2 + + if [[ "${MESSAGE}" != "" ]]; then + echo "" >&2 + echo "${MESSAGE}" >&2 + echo "" >&2 + fi + + exit 255 +} + +# cleanup makes sure the temporary file is deleted +cleanup() { + if [[ "${BACKUP_FILENAME}" != "" && -f "${BACKUP_FILENAME}" ]]; then + rm -f "${BACKUP_FILENAME}" + fi +} + +# fail terminates the script and prints a message +fail() { + MESSAGE=$1 + + echo "[backup] ERROR: ${MESSAGE}" + + cleanup + + exit 254 +} + +# validate_config ensures that required variables are set +validate_config() { + if [[ "${DEST_DIR}" == "" ]]; then + usage "The Backup Destination Directory (-d) was not specified" + fi + + if [[ ${MAX_BACKUPS} -lt 0 ]]; then + fail "${MAX_BACKUPS} cannot be less than 0" + fi + + if [[ "${VAULT_ADDR}" == "" ]]; then + fail "VAULT_ADDR is not set" + fi +} + +# prepare_destination ensures that the destination exists and that it +# has the correct permissions +prepare_destination() { + local dest_dir=$1 + + if [[ ! -d "${dest_dir}" ]]; then + if ! mkdir -p "${dest_dir}"; then + fail "${dest_dir} does not exist and can't be created" + fi + fi + + chmod 750 "${dest_dir}" +} + +# perform_backup takes the RAFT snapshot +perform_backup() { + RESULT_VARIABLE=$1 + local dest_dir=$2 + local file_prefix=$3 + + local backup_filename="${file_prefix}_${RANDOM}".raft + local backup_size + + cd "${dest_dir}" || fail "perform_backup: Cannot change to directory ${dest_dir}" + + echo "[backup] Performing backup of RAFT data" + + if ! vault operator raft snapshot save "${backup_filename}"; then + cd - > /dev/null 2>&1 || fail "perform_backup: Cannot return to previous directory" + fail "Unable to perform backup of Vault RAFT data" + fi + + backup_size=$(wc -c < "${backup_filename}") + if [[ ${backup_size} -eq 0 ]]; then + cd - > /dev/null 2>&1 || fail "perform_backup: Cannot return to previous directory" + fail "The backup generated a 0-length file" + fi + + cd - > /dev/null 2>&1 || fail "perform_backup: Cannot return to previous directory" + + eval "${RESULT_VARIABLE}"="${backup_filename}" +} + +# cycle_backups ensures that "max_backups" backups are maintained +cycle_backups() { + local backup_filename=$1 + local dest_dir=$2 + local max_backups + local file_prefix=$4 + + local last_file + local cur_file + local next_file + + local next_backup + + (( max_backups=$3 )) + + cd "${dest_dir}" || fail "cycle_backups: Cannot change to directory ${dest_dir}" + + echo "[backup] Cycling backup files" + + last_file="${file_prefix}-${max_backups}.raft" + if [[ -f "${last_file}" ]]; then + if ! rm -f "${last_file}"; then + cd - > /dev/null 2>&1 || fail "cycle_backups: Cannot return to previous directory" + fail "Cannot remove backup file ${last_file}" + fi + fi + + (( cur_backup=max_backups-1 )) + + while [[ ${cur_backup} -gt 0 ]]; do + cur_file="${file_prefix}-${cur_backup}.raft" + if [[ -f "${cur_file}" ]]; then + (( next_backup=cur_backup+1 )) + next_file="${file_prefix}-${next_backup}.raft" + + if ! mv -f "${cur_file}" "${next_file}"; then + cd - > /dev/null 2>&1 || fail "cycle_backups: Cannot return to previous directory" + fail "Cannot move backup file ${cur_file} to ${next_file}" + fi + fi + + (( cur_backup-=1 )) + done + + cur_file="${file_prefix}.raft" + if [[ -f "${cur_file}" ]]; then + (( next_backup=1 )) + next_file="${file_prefix}-${next_backup}.raft" + + if ! mv -f "${cur_file}" "${next_file}"; then + cd - > /dev/null 2>&1 || fail "cycle_backups: Cannot return to previous directory" + fail "Cannot move backup file ${cur_file} to ${next_file}" + fi + fi + + mv -f "${backup_filename}" "${cur_file}" 2> /dev/null + + cd - > /dev/null 2>&1 || fail "cycle_backups: Cannot return to previous directory" +} + +# sighandler performs cleanup in the case of an interrupted process +function sighandler() { + cleanup + + echo "[backup] Process Interrupted" + + exit 1 +} + +## +## main +## +while [[ $# -gt 0 ]]; do + case $1 in + -d | --dest-dir) + DEST_DIR=$2 + shift + ;; + -m | --max-backups) + (( MAX_BACKUPS=$2 )) + shift + ;; + -p | --file-prefix) + FILE_PREFIX=$2 + shift + ;; + -h | --help) + usage + ;; + --) + shift + break + ;; + *) usage "Invalid argument: $1" 1>&2 ;; + esac + shift +done + +validate_config + +trap sighandler INT + +prepare_destination "${DEST_DIR}" +perform_backup BACKUP_FILENAME "${DEST_DIR}" "${FILE_PREFIX}" +cycle_backups "${BACKUP_FILENAME}" "${DEST_DIR}" ${MAX_BACKUPS} "${FILE_PREFIX}" +cleanup + +echo "[backup] Done" + +exit 0 \ No newline at end of file diff --git a/infrastructure/scripts/vault_restore.sh b/infrastructure/scripts/vault_restore.sh new file mode 100755 index 0000000..70c8bd3 --- /dev/null +++ b/infrastructure/scripts/vault_restore.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +set -u +set -o pipefail + +### +### vault_restore.sh - perform a restore of the vault data +### +### Usage: +### vault_restore.sh [options] +### +### Required Options: +### -s | --src-dir Filesytem path to the directory containing the vault backups +### +### Additional Options: +### -p | --file-prefix The filename prefix for the backups (default: vault_snapshot) +### -b | --backup The specific backup to restore from (default: -1) +### -h | --help Show help / usage +### +### Notes: +### In order to perform the restore, you must point VAULT_ADDR at the RAFT leader. You can +### use `vault operator raft list-peers` to determine this, then set up a port-forward to +### that pod, and then execute this script against that specific pod. +### +### In order to perform the vault restore, you need a Vault Token that has appropriate +### permissions. +### + +SRC_DIR="" +FILE_PREFIX="vault_snapshot" + +(( BACKUP_NUMBER=-1 )) + +# usage displays some helpful information about the script and any errors that need +# to be emitted +usage() { + MESSAGE=${1:-} + + awk -F'### ' '/^###/ { print $2 }' $0 >&2 + + if [[ "${MESSAGE}" != "" ]]; then + echo "" >&2 + echo "${MESSAGE}" >&2 + echo "" >&2 + fi + + exit 255 +} + +# fail terminates the script and prints a message +fail() { + MESSAGE=$1 + + echo "[restore] ERROR: ${MESSAGE}" + + exit 254 +} + +# validate_config ensures that required variables are set +validate_config() { + if [[ "${SRC_DIR}" == "" ]]; then + usage "The Backup Source Directory (-s) was not specified" + fi + + if [[ "${VAULT_ADDR}" == "" ]]; then + fail "VAULT_ADDR is not set" + fi +} + +# perform_restore takes the specified RAFT snapshot and restores the RAFT data +# from that backup. +perform_restore() { + local src_dir=$1 + local file_prefix=$2 + local backup_number + + (( backup_number=$3 )) + + local backup_filename + + if [[ ${backup_number} -eq -1 ]]; then + backup_filename="${file_prefix}.raft" + else + backup_filename="${file_prefix}-${backup_number}.raft" + fi + + cd "${src_dir}" || fail "perform_restore: Cannot change to directory ${src_dir}" + + if [[ ! "${backup_filename}" ]]; then + cd - > /dev/null 2>&1 || fail "perform_restore: Cannot return to previous directory" + fail "Specified file ${src_dir}/${backup_filename} does not exist or cannot be read" + fi + + echo "[restore] Performing restore of RAFT data from ${backup_filename}" + + if ! vault operator raft snapshot restore "${backup_filename}"; then + cd - > /dev/null 2>&1 || fail "perform_restore: Cannot return to previous directory" + fail "Unable to perform restore of Vault RAFT data" + fi + + cd - > /dev/null 2>&1 || fail "perform_restore: Cannot return to previous directory" +} + +# sighandler performs cleanup in the case of an interrupted process +function sighandler() { + echo "[restore] Process Interrupted" + + exit 1 +} + +## +## main +## +while [[ $# -gt 0 ]]; do + case $1 in + -s | --src-dir) + SRC_DIR=$2 + shift + ;; + -p | --file-prefix) + FILE_PREFIX=$2 + shift + ;; + -b | --backup-number) + (( BACKUP_NUMBER=${2} )) + shift + ;; + -h | --help) + usage + ;; + --) + shift + break + ;; + *) usage "Invalid argument: $1" 1>&2 ;; + esac + shift +done + +validate_config + +trap sighandler INT + +perform_restore "${SRC_DIR}" "${FILE_PREFIX}" ${BACKUP_NUMBER} + +echo "[restore] Done" + +exit 0 \ No newline at end of file diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf index 12fa5ea..f3abe06 100644 --- a/infrastructure/terraform/main.tf +++ b/infrastructure/terraform/main.tf @@ -17,3 +17,8 @@ provider "google-beta" { enable_batching = false } } + +provider "datadog" { + api_key = var.datadog_api_key + app_key = var.datadog_app_key +} diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf index a133610..f3462d0 100644 --- a/infrastructure/terraform/variables.tf +++ b/infrastructure/terraform/variables.tf @@ -1,3 +1,13 @@ +variable "datadog_api_key" { + description = "Datadog account API key" + type = string +} + +variable "datadog_app_key" { + description = "Datadog application key" + type = string +} + variable "gcp_project" { description = "Name of GCP project used to provision infrastructure into" type = string diff --git a/makefile b/makefile index 80bf0cb..7ec19bb 100644 --- a/makefile +++ b/makefile @@ -7,7 +7,10 @@ docker-build: docker build --build-arg always_upgrade="$(DATE)" -t omgnetwork/vault:latest . docker tag omgnetwork/vault:latest omgnetwork/vault:$(IMG_VERSION) +test: + docker-compose -f docker/docker-compose.yml up + run: - docker-compose -f docker/docker-compose.yml up --build + docker-compose -f docker/lean-docker-compose.yml up all: docker-build run \ No newline at end of file diff --git a/scripts/smoke.env.sh b/scripts/smoke.env.sh index da432ae..9a7c4ab 100755 --- a/scripts/smoke.env.sh +++ b/scripts/smoke.env.sh @@ -7,4 +7,4 @@ export RPC_URL="http://ganache:$PORT" export CONTRACTS_PATH="/home/vault/contracts/erc20/build/" export PLASMA_CONTRACT=`cat /truffleshuffle/plasma_framework_addr.out` export GAS_PRICE_LOW="1" -export GAS_PRICE_HIGH="37000000000" \ No newline at end of file +export GAS_PRICE_HIGH="37000000000" diff --git a/scripts/smoke.plasma.sh b/scripts/smoke.plasma.sh index a7a1b69..947a0bf 100755 --- a/scripts/smoke.plasma.sh +++ b/scripts/smoke.plasma.sh @@ -55,33 +55,43 @@ echo "UNAUTHORIZED=$UNAUTHORIZED" banner vault write -format=json -f -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts banner + echo "*** SHOULD FAIL! ***" echo "UNAUTHORIZED SUBMISSION OF BLOCK BY $UNAUTHORIZED" -echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" -vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" +vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner -vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$UNAUTHORIZED/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner echo "*** SHOULD SUCCEED ***" echo "AUTHORIZED SUBMISSION OF BLOCK BY $ORIGINAL_AUTHORITY" -echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" -vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" +vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner -vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=0 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner echo "*** SHOULD SUCCEED ***" echo "AUTHORIZED SUBMISSION OF BLOCK BY $ORIGINAL_AUTHORITY - USER SUPPLIED GAS PRICE" -echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" +echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" +vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +banner +vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT + +banner +echo "*** SHOULD FAIL! ***" +echo "AUTHORIZED SUBMISSION OF BLOCK BY $ORIGINAL_AUTHORITY - USER SUPPLIED GAS PRICE WITHOUT NONCE" +echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock gas_price=$GAS_PRICE_HIGH block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner -echo "*** SHOULD SUCCEED ***" -echo "AUTHORIZED SUBMISSION OF BLOCK BY $ORIGINAL_AUTHORITY" -echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitDepositBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" -vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitDepositBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +echo "*** SHOULD FAIL! ***" +echo "AUTHORIZED SUBMISSION OF BLOCK BY $ORIGINAL_AUTHORITY - USER SUPPLIED NONCE WITHOUT GAS PRICE" +echo "vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT" +vault write -format=json immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT banner -vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitDepositBlock block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +vault write -output-curl-string immutability-eth-plugin/wallets/plasma-deployer/accounts/$ORIGINAL_AUTHORITY/plasma/submitBlock nonce=1 block_root=$BLOCK_ROOT contract=$PLASMA_CONTRACT +