diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..40079d03 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.dockerignore +.git +.github +.gitignore +.travis.yml +CONTRIBUTING.md +Dockerfile +README.md +config.cfg +configs +docs +logo.png +tests diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index a7c92bf1..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,38 +0,0 @@ -### OS / Environment - - - -### Ansible version - - - -### Version of components from `requirements.txt` - - - -### Summary of the problem - - - -### Steps to reproduce the behavior - - - -### The way of deployment (cloud or local) - - - -### Expected behavior - - - -### Actual behavior - - - -### Full log - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..cd9b4c6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: +1. Do this.. +2. Do that.. +3. .. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Additional context** + +Add any other context about the problem here. + +**Full log** + + + +``` +PUT THE OUTPUT HERE +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..066b2d92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index b68ae839..de4fd233 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ configs/* inventory_users *.kate-swp env -.DS_Store \ No newline at end of file +.DS_Store +venvs/* +!venvs/.gitinit diff --git a/.travis.yml b/.travis.yml index f4780b48..47a58a95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,20 +4,32 @@ python: "2.7" sudo: required dist: trusty +services: + - docker + matrix: fast_finish: true addons: apt: sources: - - sourceline: 'ppa:ubuntu-lxc/stable' + - sourceline: 'ppa:ubuntu-lxc/stable' + - sourceline: 'ppa:wireguard/wireguard' packages: - - python-pip - - lxc - - lxc-templates - - expect-dev - - debootstrap - - shellcheck + - python-pip + - lxd + - expect-dev + - debootstrap + - shellcheck + - tree + - bridge-utils + - dnsutils + - build-essential + - libssl-dev + - libffi-dev + - python-dev + - linux-headers-$(uname -r) + - wireguard-dkms cache: directories: @@ -26,35 +38,37 @@ cache: before_cache: - mkdir $HOME/lxc - - sudo tar cf $HOME/lxc/cache.tar /var/cache/lxc/ + - sudo tar cf $HOME/lxc/cache.tar /var/lib/lxd/images/ - sudo chown $USER. $HOME/lxc/cache.tar env: - - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial - - LXC_NAME=ubuntu1704 LXC_DISTRO=ubuntu LXC_RELEASE=zesty + - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=18.04 + +before_install: + - test "${LXC_NAME}" != "docker" && sudo modprobe wireguard || docker build -t travis/algo . install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." - - export LXC_ROOTFS=/var/lib/lxc/$LXC_NAME/rootfs - - 'sudo lxc-create -n $LXC_NAME -t ubuntu -- -r $LXC_RELEASE --mirror http://mirrors.us.kernel.org/ubuntu --packages python || true' - - 'sudo lxc-start -n $LXC_NAME && until (sudo lxc-info -n $LXC_NAME | grep -q ^IP:); do printf . && sleep 1; done && sleep 2' - - export LXC_IP="$(sudo lxc-info -Hin $LXC_NAME)" - - sudo /bin/bash -c "printf '\n$LXC_IP test.lxc\n' >> /etc/hosts" - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' - - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ - - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt + - chmod 0644 ~/.ssh/config + - echo -e "#cloud-config\nssh_authorized_keys:\n - $(cat ~/.ssh/id_rsa.pub)" | sudo lxc profile set default user.user-data - + - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge + - sudo service lxd restart + - sudo lxc launch ${LXC_DISTRO}:${LXC_RELEASE} ${LXC_NAME} + - until host ${LXC_NAME}.lxd 10.0.8.1 -t A; do sleep 3; done + - export LXC_IP="$(dig ${LXC_NAME}.lxd @10.0.8.1 +short)" + - pip install -r requirements.txt - pip install ansible-lint - gem install awesome_bot + - ansible-playbook --version + - tree . -L 2 script: # - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new -# - shellcheck algo -# - ansible-lint deploy.yml users.yml deploy_client.yml - - ansible-playbook deploy.yml --syntax-check - - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" - -after_script: +# - shellcheck algo +# - ansible-lint main.yml users.yml deploy_client.yml + - ansible-playbook main.yml --syntax-check + - ./tests/local-deploy.sh - ./tests/update-users.sh notifications: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..27bd579d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +## 20 Oct 2018 +### Added +- AWS Lightsail + +## 7 Sep 2018 +### Changed +- Azure: Deployment via Azure Resource Manager + +## 27 Aug 2018 +### Changed +- Large refactor to support Ansible 2.5. [Details](https://github.com/trailofbits/algo/pull/976) +- Add a new cloud provider - Vultr + +### Upgrade notes +- If any problems encountered follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch +- You can't update users on your old servers with the new code. Use the old code before this release or rebuild the server from scratch +- Update AWS IAM permissions for your user as per [issue](https://github.com/trailofbits/algo/issues/1079#issuecomment-416577599) + +## 04 Jun 2018 +### Changed +- Switched to [new cipher suite](https://github.com/trailofbits/algo/issues/981) + +## 24 May 2018 +### Changed +- Switched to Ubuntu 18.04 + +### Removed +- Lightsail support until they have Ubuntu 18.04 + +### Fixed +- Scaleway API paginagion + +## 30 Apr 2018 +### Added +- WireGuard support + +### Removed +- Android StrongSwan profiles + +### Release notes +- StrongSwan profiles for Android are deprecated now. Use WireGuard + +## 25 Apr 2018 +### Added +- DNScrypt-proxy added +- Switched to CloudFlare DNS-over-HTTPS by default + +## 19 Apr 2018 +### Added +- IPv6 in subjectAltName of the certificates. This allows connecting to the Algo instance via the main IPv6 address + +### Fixed +- IPv6 DNS addresses were not passing to the client + +### Release notes +- In order to use the IPv6 address as the connection endpoint you need to [reinit](https://github.com/trailofbits/algo/blob/master/config.cfg#L14) the PKI and [reconfigure](https://github.com/trailofbits/algo#configure-the-vpn-clients) your devices with new certificates. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6fa1d0fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM python:2-alpine + +ARG VERSION="git" +ARG PACKAGES="bash libffi openssh-client openssl rsync tini" +ARG BUILD_PACKAGES="gcc libffi-dev linux-headers make musl-dev openssl-dev" + +LABEL name="algo" \ + version="${VERSION}" \ + description="Set up a personal IPsec VPN in the cloud" \ + maintainer="Trail of Bits " + +RUN apk --no-cache add ${PACKAGES} +RUN adduser -D -H -u 19857 algo +RUN mkdir -p /algo && mkdir -p /algo/configs + +WORKDIR /algo +COPY requirements.txt . +RUN apk --no-cache add ${BUILD_PACKAGES} && \ + python -m pip --no-cache-dir install -U pip && \ + python -m pip --no-cache-dir install virtualenv && \ + python -m virtualenv env && \ + source env/bin/activate && \ + python -m pip --no-cache-dir install -r requirements.txt && \ + apk del ${BUILD_PACKAGES} +COPY . . +RUN chmod 0755 /algo/algo-docker.sh + +# Because of the bind mounting of `configs/`, we need to run as the `root` user +# This may break in cases where user namespacing is enabled, so hopefully Docker +# sorts out a way to set permissions on bind-mounted volumes (`docker run -v`) +# before userns becomes default +# Note that not running as root will break if we don't have a matching userid +# in the container. The filesystem has also been set up to assume root. +USER root +CMD [ "/algo/algo-docker.sh" ] +ENTRYPOINT [ "/sbin/tini", "--" ] diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..03a88d72 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ + + +## Description + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + +## Types of changes + +- [] Bug fix (non-breaking change which fixes an issue) +- [] New feature (non-breaking change which adds functionality) +- [] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist: + + +- [] I have read the **CONTRIBUTING** document. +- [] My code follows the code style of this project. +- [] My change requires a change to the documentation. +- [] I have updated the documentation accordingly. +- [] I have added tests to cover my changes. +- [] All new and existing tests passed. diff --git a/README.md b/README.md index 55b1bd6f..8737d5da 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC ## Features -* Supports only IKEv2 with strong crypto: AES-GCM, SHA2, and P-256 +* Supports only IKEv2 with strong crypto (AES-GCM, SHA2, and P-256) and [WireGuard](https://www.wireguard.com/) * Generates Apple profiles to auto-configure iOS and macOS devices * Includes a helper script to add and remove users * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Microsoft Azure, Google Compute Engine, or your own server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server ## Anti-features @@ -29,7 +29,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or an OpenStack based cloud hosting. 2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. @@ -56,7 +56,10 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. **Install Algo's remaining dependencies.** Use the same Terminal window as the previous step and run: ```bash - $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt + $ python -m virtualenv --python=`which python2` env && + source env/bin/activate && + python -m pip install -U pip virtualenv && + python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. @@ -64,7 +67,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 6. **Start the deployment.** Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-from-ansible.md](docs/deploy-from-ansible.md). -That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. +That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later, **it will only be displayed this time**. You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](#configure-the-vpn-clients) below. @@ -94,13 +97,13 @@ Certificates and configuration files that users will need are placed in the `con ### Android Devices -No version of Android supports IKEv2. Install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more a more detailed walkthrough. +WireGuard is used to provide VPN services on Android. Install the [WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). Import the corresponding `wireguard/.conf` file to your device, then setup a new connection with it. See the [Android setup instructions](/docs/client-android.md) for more detailed walkthrough. ### Windows 10 -Copy your PowerShell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows client and run the following command as Administrator to configure the VPN connection. +Copy your PowerShell script `windows_{username}.ps1` to the Windows client and run the following command as Administrator to configure the VPN connection. ``` -powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 Add +powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 -Add ``` For a manual installation, see the [Windows setup instructions](/docs/client-windows.md). @@ -113,7 +116,7 @@ Network Manager does not support AES-GCM. In order to support Linux Desktop clie Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. -#### Ubuntu Server 16.04 example +#### Ubuntu Server 18.04 example 1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan 2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` @@ -129,11 +132,13 @@ One common use case is to let your server access your local LAN without going th conn lan-passthrough leftsubnet=192.168.1.1/24 # Replace with your LAN subnet - rightsubnet=192.168.1.1/24 # Replac with your LAND subnet + rightsubnet=192.168.1.1/24 # Replace with your LAN subnet authby=never # No authentication necessary type=pass # passthrough auto=route # no need to ipsec up lan-passthrough +To configure the connection to come up at boot time replace `auto=add` with `auto=start`. + ### Other Devices Depending on the platform, you may need one or multiple of the following files. @@ -187,11 +192,15 @@ After this process completes, the Algo VPN server will contains only the users l * Client setup - Setup [Android](docs/client-android.md) clients - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible + - Setup Ubuntu clients to use [WireGuard](docs/client-linux-wireguard.md) * Cloud setup + - Configure [Amazon EC2](docs/cloud-amazon-ec2.md) - Configure [Azure](docs/cloud-azure.md) + - Configure [DigitalOcean](docs/cloud-do.md) + - Configure [Google Cloud Platform](docs/cloud-gce.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server + - Deploy to your own [Ubuntu 18.04](docs/deploy-to-ubuntu.md) server - Deploy to an [unsupported cloud provider](docs/deploy-to-unsupported-cloud.md) * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) diff --git a/algo b/algo index 392464e1..07a2875c 100755 --- a/algo +++ b/algo @@ -2,502 +2,21 @@ set -e -ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" -if [ -f "$ACTIVATE_SCRIPT" ] +if [ -z ${VIRTUAL_ENV+x} ] then - source $ACTIVATE_SCRIPT -else - echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" - exit 1 + ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" + if [ -f "$ACTIVATE_SCRIPT" ] + then + source $ACTIVATE_SCRIPT + else + echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" + exit 1 + fi fi -SKIP_TAGS="_null encrypted" -ADDITIONAL_PROMPT="[pasted values will not be displayed]" - -additional_roles () { - -read -p " -Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to cellular networks? -[y/N]: " -r OnDemandEnabled_Cellular -OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} -if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi - -read -p " -Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to Wi-Fi? -[y/N]: " -r OnDemandEnabled_WIFI -OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi - -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then - read -p " -List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) -: " -r OnDemandEnabled_WIFI_EXCLUDE - OnDemandEnabled_WIFI_EXCLUDE=${OnDemandEnabled_WIFI_EXCLUDE:-_null} - EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=\"$OnDemandEnabled_WIFI_EXCLUDE\"" -fi - -read -p " -Do you want to install a DNS resolver on this VPN server, to block ads while surfing? -[y/N]: " -r dns_enabled -dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi - -read -p " -Do you want each user to have their own account for SSH tunneling? -[y/N]: " -r ssh_tunneling_enabled -ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} -if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi - -read -p " -Do you want to apply operating system security enhancements on the server? (warning: replaces your sshd_config) -[y/N]: " -r security_enabled -security_enabled=${security_enabled:-n} -if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi - -read -p " -Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) -[y/N]: " -r Win10_Enabled -Win10_Enabled=${Win10_Enabled:-n} -if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi - -read -p " -Do you want to retain the CA key? (required to add users in the future, but less secure) -[y/N]: " -r Store_CAKEY -Store_CAKEY=${Store_CAKEY:-N} -if [[ "$Store_CAKEY" =~ ^(n|N)$ ]]; then EXTRA_VARS+=" Store_CAKEY=N"; fi - -} - -deploy () { - - ansible-playbook deploy.yml -t "${ROLES// /,}" -e "${EXTRA_VARS}" --skip-tags "${SKIP_TAGS// /,}" - -} - -azure () { - read -p " -Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_secret - - read -p " - -Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_tenant - - read -p " - -Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_client_id - - read -p " - -Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_subscription_id - - read -p " - -Name the vpn server: -[algo]: " -r azure_server_name - azure_server_name=${azure_server_name:-algo} - - read -p " - - What region should the server be located in? (https://azure.microsoft.com/en-us/regions/) - 1. South Central US - 2. Central US - 3. North Europe - 4. West Europe - 5. Southeast Asia - 6. Japan West - 7. Japan East - 8. Australia Southeast - 9. Australia East - 10. Canada Central - 11. West US 2 - 12. West Central US - 13. UK South - 14. UK West - 15. West US - 16. Brazil South - 17. Canada East - 18. Central India - 19. East Asia - 20. Germany Central - 21. Germany Northeast - 22. Korea Central - 23. Korea South - 24. North Central US - 25. South India - 26. West India - 27. East US - 28. East US 2 - -Enter the number of your desired region: -[1]: " -r azure_region - azure_region=${azure_region:-1} - - case "$azure_region" in - 1) region="southcentralus" ;; - 2) region="centralus" ;; - 3) region="northeurope" ;; - 4) region="westeurope" ;; - 5) region="southeastasia" ;; - 6) region="japanwest" ;; - 7) region="japaneast" ;; - 8) region="australiasoutheast" ;; - 9) region="australiaeast" ;; - 10) region="canadacentral" ;; - 11) region="westus2" ;; - 12) region="westcentralus" ;; - 13) region="uksouth" ;; - 14) region="ukwest" ;; - 15) region="westus" ;; - 16) region="brazilsouth" ;; - 17) region="canadaeast" ;; - 18) region="centralindia" ;; - 19) region="eastasia" ;; - 20) region="germanycentral" ;; - 21) region="germanynortheast" ;; - 22) region="koreacentral" ;; - 23) region="koreasouth" ;; - 24) region="northcentralus" ;; - 25) region="southindia" ;; - 26) region="westindia" ;; - 27) region="eastus" ;; - 28) region="eastus2" ;; - esac - - ROLES="azure vpn cloud" - EXTRA_VARS="azure_secret=$azure_secret azure_tenant=$azure_tenant azure_client_id=$azure_client_id azure_subscription_id=$azure_subscription_id azure_server_name=$azure_server_name ssh_public_key=$ssh_public_key region=$region" -} - -digitalocean () { - read -p " -Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): -$ADDITIONAL_PROMPT -: " -rs do_access_token - - read -p " - -Name the vpn server: -[algo.local]: " -r do_server_name - do_server_name=${do_server_name:-algo.local} - - read -p " - - What region should the server be located in? - 1. Amsterdam (Datacenter 2) - 2. Amsterdam (Datacenter 3) - 3. Frankfurt - 4. London - 5. New York (Datacenter 1) - 6. New York (Datacenter 2) - 7. New York (Datacenter 3) - 8. San Francisco (Datacenter 1) - 9. San Francisco (Datacenter 2) - 10. Singapore - 11. Toronto - 12. Bangalore -Enter the number of your desired region: -[7]: " -r region - region=${region:-7} - - case "$region" in - 1) do_region="ams2" ;; - 2) do_region="ams3" ;; - 3) do_region="fra1" ;; - 4) do_region="lon1" ;; - 5) do_region="nyc1" ;; - 6) do_region="nyc2" ;; - 7) do_region="nyc3" ;; - 8) do_region="sfo1" ;; - 9) do_region="sfo2" ;; - 10) do_region="sgp1" ;; - 11) do_region="tor1" ;; - 12) do_region="blr1" ;; - esac - -ROLES="digitalocean vpn cloud" -EXTRA_VARS="do_access_token=$do_access_token do_server_name=$do_server_name do_region=$do_region" -} - -ec2 () { - read -p " -Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). -$ADDITIONAL_PROMPT -[AKIA...]: " -rs aws_access_key - - read -p " - -Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -$ADDITIONAL_PROMPT -[ABCD...]: " -rs aws_secret_key - -read -p " - -Name the vpn server: -[algo]: " -r aws_server_name - aws_server_name=${aws_server_name:-algo} - - read -p " - - What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-east-2 US East (Ohio) - 3. us-west-1 US West (N. California) - 4. us-west-2 US West (Oregon) - 5. ap-south-1 Asia Pacific (Mumbai) - 6. ap-northeast-2 Asia Pacific (Seoul) - 7. ap-southeast-1 Asia Pacific (Singapore) - 8. ap-southeast-2 Asia Pacific (Sydney) - 9. ap-northeast-1 Asia Pacific (Tokyo) - 10. eu-central-1 EU (Frankfurt) - 11. eu-west-1 EU (Ireland) - 12. eu-west-2 EU (London) - 13. ca-central-1 Canada (Central) - 14. sa-east-1 São Paulo -Enter the number of your desired region: -[1]: " -r aws_region - aws_region=${aws_region:-1} - - case "$aws_region" in - 1) region="us-east-1" ;; - 2) region="us-east-2" ;; - 3) region="us-west-1" ;; - 4) region="us-west-2" ;; - 5) region="ap-south-1" ;; - 6) region="ap-northeast-2" ;; - 7) region="ap-southeast-1" ;; - 8) region="ap-southeast-2" ;; - 9) region="ap-northeast-1" ;; - 10) region="eu-central-1" ;; - 11) region="eu-west-1" ;; - 12) region="eu-west-2";; - 13) region="ca-central-1" ;; - 14) region="sa-east-1" ;; - esac - - ROLES="ec2 vpn cloud" - EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" -} - -gce () { - read -p " -Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): -: " -r credentials_file - - read -p " - -Name the vpn server: -[algo]: " -r server_name - server_name=${server_name:-algo} - - read -p " - - What zone should the server be located in? - 1. Western US (Oregon A) - 2. Western US (Oregon B) - 3. Western US (Oregon C) - 4. Central US (Iowa A) - 5. Central US (Iowa B) - 6. Central US (Iowa C) - 7. Central US (Iowa F) - 8. Eastern US (Northern Virginia A) - 9. Eastern US (Northern Virginia B) - 10. Eastern US (Northern Virginia C) - 11. Eastern US (South Carolina B) - 12. Eastern US (South Carolina C) - 13. Eastern US (South Carolina D) - 14. Western Europe (Belgium B) - 15. Western Europe (Belgium C) - 16. Western Europe (Belgium D) - 17. Western Europe (London A) - 18. Western Europe (London B) - 19. Western Europe (London C) - 20. Western Europe (Frankfurt A) - 21. Western Europe (Frankfurt B) - 22. Western Europe (Frankfurt C) - 23. Southeast Asia (Singapore A) - 24. Southeast Asia (Singapore B) - 25. East Asia (Taiwan A) - 26. East Asia (Taiwan B) - 27. East Asia (Taiwan C) - 28. Northeast Asia (Tokyo A) - 29. Northeast Asia (Tokyo B) - 30. Northeast Asia (Tokyo C) - 31. Australia (Sydney A) - 32. Australia (Sydney B) - 33. Australia (Sydney C) - 34. South America (São Paulo A) - 35. South America (São Paulo B) - 36. South America (São Paulo C) -Please choose the number of your zone. Press enter for default (#14) zone. -[14]: " -r region - region=${region:-14} - - case "$region" in - 1) zone="us-west1-a" ;; - 2) zone="us-west1-b" ;; - 3) zone="us-west1-c" ;; - 4) zone="us-central1-a" ;; - 5) zone="us-central1-b" ;; - 6) zone="us-central1-c" ;; - 7) zone="us-central1-f" ;; - 8) zone="us-east4-a" ;; - 9) zone="us-east4-b" ;; - 10) zone="us-east4-c" ;; - 11) zone="us-east1-b" ;; - 12) zone="us-east1-c" ;; - 13) zone="us-east1-d" ;; - 14) zone="europe-west1-b" ;; - 15) zone="europe-west1-c" ;; - 16) zone="europe-west1-d" ;; - 17) zone="europe-west2-a" ;; - 18) zone="europe-west2-b" ;; - 19) zone="europe-west2-c" ;; - 20) zone="europe-west3-a" ;; - 21) zone="europe-west3-b" ;; - 22) zone="europe-west3-c" ;; - 23) zone="asia-southeast1-a" ;; - 24) zone="asia-southeast1-b" ;; - 25) zone="asia-east1-a" ;; - 26) zone="asia-east1-b" ;; - 27) zone="asia-east1-c" ;; - 28) zone="asia-northeast1-a" ;; - 29) zone="asia-northeast1-b" ;; - 30) zone="asia-northeast1-c" ;; - 31) zone="australia-southeast1-a" ;; - 32) zone="australia-southeast1-b" ;; - 33) zone="australia-southeast1-c" ;; - 34) zone="southamerica-east1-a" ;; - 35) zone="southamerica-east1-b" ;; - 36) zone="southamerica-east1-c" ;; - esac - - ROLES="gce vpn cloud" - EXTRA_VARS="credentials_file=$credentials_file gce_server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1316" -} - -non_cloud () { - read -p " -Enter the IP address of your server: (or use localhost for local installation) -[localhost]: " -r server_ip - server_ip=${server_ip:-localhost} - - read -p " - -What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) -[root]: " -r server_user - server_user=${server_user:-root} - -if [ "x${server_ip}" = "xlocalhost" ]; then - myip="" -else - myip=${server_ip} -fi - - read -p " - -Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$myip]: " -r IP_subject - IP_subject=${IP_subject:-$myip} - -if [ "x${IP_subject}" = "x" ]; then - echo "no server IP given. exiting." - exit 1 -fi - - ROLES="local vpn" - EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" - SKIP_TAGS+=" cloud update-alternatives" - - read -p " - -Was this server deployed by Algo previously? -[y/N]: " -r Deployed_By_Algo -Deployed_By_Algo=${Deployed_By_Algo:-n} -if [[ "$Deployed_By_Algo" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Deployed_By_Algo=Y"; fi - -} - -algo_provisioning () { - echo -n " - What provider would you like to use? - 1. DigitalOcean - 2. Amazon EC2 - 3. Microsoft Azure - 4. Google Compute Engine - 5. Install to existing Ubuntu 16.04 server - -Enter the number of your desired provider -: " - - read -r N - - case "$N" in - 1) digitalocean; ;; - 2) ec2; ;; - 3) azure; ;; - 4) gce; ;; - 5) non_cloud; ;; - *) exit 1 ;; - esac - - additional_roles - deploy -} - -user_management () { - - read -p " -Enter the IP address of your server: (or use localhost for local installation) -: " -r server_ip - - read -p " -What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) -[root]: " -r server_user - server_user=${server_user:-root} - -read -p " -Do you want each user to have their own account for SSH tunneling? -[y/N]: " -r ssh_tunneling_enabled -ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} - -if [ "x${server_ip}" = "xlocalhost" ]; then - myip="" -else - myip=${server_ip} -fi - -read -p " - -Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$myip]: " -r IP_subject -IP_subject=${IP_subject:-$myip} - -if [ "x${IP_subject}" = "x" ]; then -echo "no server IP given. exiting." -exit 1 -fi - - read -p " -Enter the password for the private CA key: -$ADDITIONAL_PROMPT -: " -rs easyrsa_CA_password - -ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject_alt_name=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" -t update-users --skip-tags common -} - case "$1" in - update-users) user_management ;; - *) algo_provisioning ;; + update-users) PLAYBOOK=users.yml; ARGS="${@:2} -t update-users";; + *) PLAYBOOK=main.yml; ARGS=${@} ;; esac + +ansible-playbook ${PLAYBOOK} ${ARGS} diff --git a/algo-docker.sh b/algo-docker.sh new file mode 100644 index 00000000..858f6204 --- /dev/null +++ b/algo-docker.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -eEo pipefail + +ALGO_DIR="/algo" +DATA_DIR="/data" + +umask 0077 + +usage() { + retcode="${1:-0}" + echo "To run algo from Docker:" + echo "" + echo "docker run --cap-drop=all -it -v :"${DATA_DIR}" trailofbits/algo:latest" + echo "" + exit ${retcode} +} + +if [ ! -f "${DATA_DIR}"/config.cfg ] ; then + echo "Looks like you're not bind-mounting your config.cfg into this container." + echo "algo needs a configuration file to run." + echo "" + usage -1 +fi + +if [ ! -e /dev/console ] ; then + echo "Looks like you're trying to run this container without a TTY." + echo "If you don't pass `-t`, you can't interact with the algo script." + echo "" + usage -1 +fi + +# To work around problems with bind-mounting Windows volumes, we need to +# copy files out of ${DATA_DIR}, ensure appropriate line endings and permissions, +# then copy the algo-generated files into ${DATA_DIR}. + +tr -d '\r' < "${DATA_DIR}"/config.cfg > "${ALGO_DIR}"/config.cfg +test -d "${DATA_DIR}"/configs && rsync -qLktr --delete "${DATA_DIR}"/configs "${ALGO_DIR}"/ + +"${ALGO_DIR}"/algo ${ALGO_ARGS} +retcode=${?} + +rsync -qLktr --delete "${ALGO_DIR}"/configs "${DATA_DIR}"/ +exit ${retcode} diff --git a/algo-showenv.sh b/algo-showenv.sh new file mode 100755 index 00000000..4793be9f --- /dev/null +++ b/algo-showenv.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# +# Print information about Algo's invocation environment to aid in debugging. +# This is normally called from Ansible right before a deployment gets underway. + +# Skip printing this header if we're just testing with no arguments. +if [[ $# -gt 0 ]]; then + echo "" + echo "--> Please include the following block of text when reporting issues:" + echo "" +fi + +if [[ ! -f ./algo ]]; then + echo "This should be run from the top level Algo directory" +fi + +# Determine the operating system. +case "$(uname -s)" in + Linux) + OS="Linux ($(uname -r) $(uname -v))" + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + # I hope this isn't dangerous. + . /etc/os-release + if [[ ${PRETTY_NAME} ]]; then + OS="${PRETTY_NAME}" + elif [[ ${NAME} ]]; then + OS="${NAME} ${VERSION}" + fi + fi + STAT="stat -c %y" + ;; + Darwin) + OS="$(sw_vers -productName) $(sw_vers -productVersion)" + STAT="stat -f %Sm" + ;; + *) + OS="Unknown" + ;; +esac + +# Determine if virtualization is being used with Linux. +VIRTUALIZED="" +if [[ -x $(command -v systemd-detect-virt) ]]; then + DETECT_VIRT="$(systemd-detect-virt)" + if [[ ${DETECT_VIRT} != "none" ]]; then + VIRTUALIZED=" (Virtualized: ${DETECT_VIRT})" + fi +elif [[ -f /.dockerenv ]]; then + VIRTUALIZED=" (Virtualized: docker)" +fi + +echo "Algo running on: ${OS}${VIRTUALIZED}" + +# Determine the currentness of the Algo software. +if [[ -d .git && -x $(command -v git) ]]; then + ORIGIN="$(git remote get-url origin)" + COMMIT="$(git log --max-count=1 --oneline --no-decorate --no-color)" + if [[ ${ORIGIN} == "https://github.com/trailofbits/algo.git" ]]; then + SOURCE="clone" + else + SOURCE="fork" + fi + echo "Created from git ${SOURCE}. Last commit: ${COMMIT}" +elif [[ -f LICENSE && ${STAT} ]]; then + CREATED="$(${STAT} LICENSE)" + echo "ZIP file created: ${CREATED}" +fi + +# The Python version might be useful to know. +if [[ -x ./env/bin/python ]]; then + ./env/bin/python --version 2>&1 +elif [[ -f ./algo ]]; then + echo "env/bin/python not found: has 'python -m virtualenv ...' been run?" +fi + +# Just print out all command line arguments, which are expected +# to be Ansible variables. +if [[ $# -gt 0 ]]; then + echo "Runtime variables:" + for VALUE in "$@"; do + echo " ${VALUE}" + done +fi + +exit 0 diff --git a/ansible.cfg b/ansible.cfg index 8c63b5ea..aef40841 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -4,6 +4,7 @@ pipelining = True retry_files_enabled = False host_key_checking = False timeout = 60 +stdout_callback = full_skip [paramiko_connection] record_host_keys = False @@ -11,3 +12,4 @@ record_host_keys = False [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 -o IdentitiesOnly=yes scp_if_ssh = True +retries = 30 diff --git a/cloud.yml b/cloud.yml new file mode 100644 index 00000000..671c7765 --- /dev/null +++ b/cloud.yml @@ -0,0 +1,49 @@ +--- +- name: Provision the server + hosts: localhost + tags: always + vars_files: + - config.cfg + + pre_tasks: + - block: + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml + tags: always + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always + + roles: + - role: cloud-digitalocean + when: algo_provider == "digitalocean" + - role: cloud-ec2 + when: algo_provider == "ec2" + - role: cloud-vultr + when: algo_provider == "vultr" + - role: cloud-gce + when: algo_provider == "gce" + - role: cloud-azure + when: algo_provider == "azure" + - role: cloud-lightsail + when: algo_provider == "lightsail" + - role: cloud-scaleway + when: algo_provider == "scaleway" + - role: cloud-openstack + when: algo_provider == "openstack" + - role: local + when: algo_provider == "local" + + post_tasks: + - block: + - name: Local post-tasks + import_tasks: playbooks/cloud-post.yml + become: false + tags: cloud + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/config.cfg b/config.cfg index 10ea5cd2..168c359c 100644 --- a/config.cfg +++ b/config.cfg @@ -1,7 +1,10 @@ --- -# Add as many users as you want for your VPN server here. -# Credentials will be generated for each one. +# Add up to 250 users here. +# For each user, configuration files will be generated for both an IPsec +# connection and a WireGuard connection. Multiple client devices can share an +# IPsec configuration but WireGuard clients must each use a unique +# WireGuard configuration. users: - dan - jack @@ -10,42 +13,87 @@ users: ### Advanced users only below this line ### -# If True re-init all existing certificates. (True or False) -easyrsa_reinit_existent: False +# If True re-init all existing certificates. Boolean +keys_clean_all: False + +# Clean up cloud python environments +clean_environment: false vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' +wireguard_enabled: true +wireguard_port: 51820 -server_name: "{{ ansible_ssh_host }}" -IP_subject_alt_name: "{{ ansible_ssh_host }}" +# MSS is the TCP Max Segment Size +# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation +# This appears to be necessary on (at least) Google Cloud, +# however, some routers also require a change to this parameter +# See also: +# - https://github.com/trailofbits/algo/issues/216 +# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu +# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan +#max_mss: 1316 # StrongSwan log level # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 +# Algo will use the following lists to block ads. You can add new block lists +# after deployment by modifying the line starting "BLOCKLIST_URLS=" at: +# /usr/local/sbin/adblock.sh +# If you load very large blocklists, you may also have to modify resource limits: +# /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf adblock_lists: - "http://winhelp2002.mvps.org/hosts.txt" - "https://adaway.org/hosts.txt" - "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "https://hosts-file.net/ad_servers.txt" +# Enable DNS encryption. +# If 'false', 'dns_servers' should be specified below. +dns_encryption: true + +# DNS servers which will be used if 'dns_encryption' is 'true'. Multiple +# providers may be specified, but avoid mixing providers that filter results +# (like Cisco) with those that don't (like Cloudflare) or you could get +# inconsistent results. The list of available public providers can be found +# here: +# https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v2/public-resolvers.md +dnscrypt_servers: + ipv4: + - cloudflare +# - google + ipv6: + - cloudflare-ipv6 + +# DNS servers which will be used if 'dns_encryption' is 'false'. +# The default is to use Cloudflare. dns_servers: ipv4: - - 8.8.8.8 - - 8.8.4.4 + - 1.1.1.1 + - 1.0.0.1 ipv6: - - 2001:4860:4860::8888 - - 2001:4860:4860::8844 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 # IP address for the local dns resolver local_service_ip: 172.16.0.1 +# Your Algo server will automatically install security updates. Some updates +# require a reboot to take effect but your Algo server will not reboot itself +# automatically unless you change 'enabled' below from 'false' to 'true', in +# which case a reboot will take place if necessary at the time specified (as +# HH:MM) in the time zone of your Algo server. The default time zone is UTC. +unattended_reboot: + enabled: false + time: 06:00 + pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" # Block traffic between connected clients -BetweenClients_DROP: Y +BetweenClients_DROP: true congrats: common: | @@ -54,11 +102,11 @@ congrats: "# Config files and certificates are in the ./configs/ directory. #" "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver {{ local_service_ip }} #" + "# Local DNS resolver {{ local_service_ip }} #" p12_pass: | - "# The p12 and SSH keys password for new users is {{ easyrsa_p12_export_password }} #" + "# The p12 and SSH keys password for new users is {{ p12_export_password }} #" ca_key_pass: | - "# The CA key password is {{ easyrsa_CA_password }} #" + "# The CA key password is {{ CA_password }} #" ssh_access: | "# Shell access: ssh -i {{ ansible_ssh_private_key_file|default(omit) }} {{ ansible_ssh_user|default(omit) }}@{{ ansible_ssh_host|default(omit) }} #" @@ -70,25 +118,46 @@ SSH_keys: cloud_providers: azure: size: Basic_A0 - image: - offer: UbuntuServer - publisher: Canonical - sku: '16.04-LTS' # 16.04-LTS / 17.04 - version: latest + image: 18.04-LTS digitalocean: - size: 512mb - image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-04-x64 + size: s-1vcpu-1gb + image: "ubuntu-18-04-x64" + # Change the encrypted flag to "true" to enable AWS volume encryption, for encryption of data at rest. + # Warning: the Algo script will take approximately 6 minutes longer to complete. + # Also note that the documented AWS minimum permissions aren't sufficient. + # You will have to edit the AWS user policy documented at + # https://github.com/trailofbits/algo/blob/master/docs/cloud-amazon-ec2.md to also allow "ec2:CopyImage". + # See https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage-edit.html ec2: + encrypted: false size: t2.micro image: - name: "ubuntu-xenial-16.04" # ubuntu-xenial-16.04 / ubuntu-zesty-17.04 + name: "ubuntu-bionic-18.04" owner: "099720109477" gce: size: f1-micro - image: ubuntu-1604 # ubuntu-1604 / ubuntu-1704 + image: ubuntu-1804 + external_static_ip: false + lightsail: + size: nano_1_0 + image: ubuntu_18_04 + scaleway: + size: START1-S + image: Ubuntu Bionic Beaver + arch: x86_64 + openstack: + flavor_ram: ">=512" + image: Ubuntu-18.04 + vultr: + os: Ubuntu 18.04 x64 + size: 1024 MB RAM,25 GB SSD,1.00 TB BW local: fail_hint: - Sorry, but something went wrong! - Please check the troubleshooting guide. - https://trailofbits.github.io/algo/troubleshooting.html + +booleans_map: + Y: true + y: true diff --git a/deploy.yml b/deploy.yml deleted file mode 100644 index 6caa70c8..00000000 --- a/deploy.yml +++ /dev/null @@ -1,96 +0,0 @@ -- name: Configure the server - hosts: localhost - tags: algo - vars_files: - - config.cfg - - pre_tasks: - - block: - - name: Local pre-tasks - include: playbooks/local.yml - tags: [ 'always' ] - - - name: Local pre-tasks - include: playbooks/local_ssh.yml - become: false - when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" - tags: [ 'local' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - - roles: - - { role: cloud-digitalocean, tags: ['digitalocean'] } - - { role: cloud-ec2, tags: ['ec2'] } - - { role: cloud-gce, tags: ['gce'] } - - { role: cloud-azure, tags: ['azure'] } - - { role: local, tags: ['local'] } - - post_tasks: - - block: - - name: Local post-tasks - include: playbooks/post.yml - become: false - tags: [ 'cloud' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - -- name: Configure the server and install required software - hosts: vpn-host - gather_facts: false - tags: algo - become: true - vars_files: - - config.cfg - - pre_tasks: - - block: - - name: Common pre-tasks - include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - - roles: - - { role: security, tags: [ 'security' ] } - - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - - { role: vpn, tags: [ 'vpn' ] } - - post_tasks: - - block: - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - - " {% if Store_CAKEY is defined and Store_CAKEY == 'N' %}{% else %}{{ congrats.ca_key_pass }}{% endif %}" - - " {% if cloud_deployment is defined %}{{ congrats.ssh_access }}{% endif %}" - tags: always - - - name: Save the CA key password - local_action: > - shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password - become: no - tags: tests - - - name: Delete the CA key - local_action: - module: file - path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" - state: absent - become: no - tags: always - when: Store_CAKEY is defined and Store_CAKEY == "N" - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always diff --git a/docs/client-android.md b/docs/client-android.md index 91c85fcc..553b5071 100644 --- a/docs/client-android.md +++ b/docs/client-android.md @@ -2,48 +2,5 @@ ## Installation via profiles -1. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android). -2. Copy `android_{username}.sswan` and `android_{username}_helper.html` to your phone's internal storage. -3. Open the helper file in a browser (e.g., Google Chrome). -4. Click on the link. It opens the StrongSwan app and configures the VPN with your profile. - -## Manual installation - -**NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [strongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. - -| Instruction | Screenshot(s) | -| ----------- | ---------- | -| 1. Copy your `{username}.p12` certificate to your phone's internal storage. | | -| 2. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android) (Android 4+) | | -| 3. Open the app and tap "ADD VPN PROFILE" in the top right. | [![step3-thumb]][step3-screen] | -| 4. Enter the IP address or hostname of your Algo server and set the "VPN Type" to "IKEv2 Certificate". | [![step4-thumb]][step4-screen] | -| 5. Tap "Select user certificate". You will be shown a prompt, tap "INSTALL". | [![step5-thumb]][step5-screen] | -| 6. Use the "Open from" menu to select your certificate. If you downloaded your certificate to your phone, you may find that using the "Downloads" shortcut results in your `{username}.p12` certificate being grayed out. If this happens go back to the "Open from" menu and tap on the name of your phone. This will bring up the filesystem. From here, navigate to the folder where you saved your cert (such as "Downloads"), and try again. | [![step6-thumb]][step6-screen] | -| 7. Enter the password for your certificate. This password was printed to your console at the end of running the `algo` deployment script. Please note that in some cases, extracting the certificate can take several minutes. | [![step7-thumb]][step7-screen] | -| 8. Give your certificate a name (it will default to your Algo username), and ensure that "Credential use" is set to "VPN and apps". Tap "OK". | [![step8-thumb]][step8-screen] | -| 9. You'll then be brought to another prompt. Ensure your newly imported certificate is selected, and tap "ALLOW". Then, tap "SAVE" in the top right. | [![step9-thumb]][step9-screen] | -| 10. You will be returned to the main menu, and your newly-configured VPN profile should be listed. Tap the profile to connect. | [![step10-thumb]][step10-screen] | - -## Troubleshooting -### Tapping the VPN profile in strongSwan has no effect. -Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before attempting to enable a strongSwan VPN. If any other VPN is active, strongSwan may silently fail to initialize a VPN connection. On Android 7, your can manage your VPNs by going to: Settings > Tap "More" under "Wireless & networks" > VPN > tap the gear icon next to any non-strongSwan VPNs listed and ensure they are disabled. - - -[step3-thumb]: https://i.imgur.com/LPwIGJE.png -[step4-thumb]: https://i.imgur.com/sFkDILg.png -[step5-thumb]: https://i.imgur.com/IliT5oD.png -[step6-thumb]: https://i.imgur.com/oghdCVp.png -[step7-thumb]: https://i.imgur.com/nDzJ7KS.png -[step8-thumb]: https://i.imgur.com/RPXSpCo.png -[step9-thumb]: https://i.imgur.com/uMinDPe.png -[step10-thumb]: https://i.imgur.com/hUEDjdo.png - - -[step3-screen]: https://i.imgur.com/xNMihCd.png -[step4-screen]: https://i.imgur.com/xYjoNNO.png -[step5-screen]: https://i.imgur.com/4qhKT1Z.png -[step6-screen]: https://i.imgur.com/MAaQuxH.png -[step7-screen]: https://i.imgur.com/aT2MPih.png -[step8-screen]: https://i.imgur.com/gvaKzkh.png -[step9-screen]: https://i.imgur.com/eZp8DNb.png -[step10-screen]: https://i.imgur.com/Nd8rYMJ.png +1. [Install the WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). +2. Open QR code `configs//wireguard/.png` and scan it in the WireGuard app diff --git a/docs/client-linux-wireguard.md b/docs/client-linux-wireguard.md new file mode 100644 index 00000000..3430959c --- /dev/null +++ b/docs/client-linux-wireguard.md @@ -0,0 +1,55 @@ +# Using Ubuntu Server as a Client with WireGuard + +## Install WireGuard + +To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from an Ubuntu Server 16.04 (Xenial) or 18.04 (Bionic) client, first install WireGuard on the client: + +```shell +# Add the WireGuard repository: +sudo add-apt-repository ppa:wireguard/wireguard + +# Update the list of available packages (not necessary on Bionic): +sudo apt update + +# Install the tools and kernel module: +sudo apt install wireguard +``` + +(For installation on other Linux distributions, see the [Installation](https://www.wireguard.com/install/) page on the WireGuard site.) + +## Locate the Config File + +The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your Linux client. + +If your client is running Bionic (or another Linux that uses `systemd-resolved` for DNS) you should first edit the config file. Comment out the line that begins with `DNS =` and replace it with: +``` +PostUp = systemd-resolve -i %i --set-dns=172.16.0.1 --set-domain=~. +``` +Use the IP address shown on the `DNS =` line (for most, this will be `172.16.0.1`). If the `DNS =` line contains multiple IP addresses, use multiple `--set-dns=` options. + +## Configure WireGuard + +Finally, install the config file on your client as `/etc/wireguard/wg0.conf` and start WireGuard: + +```shell +# Install the config file to the WireGuard configuration directory on your +# Bionic or Xenial client: +sudo install -o root -g root -m 600 .conf /etc/wireguard/wg0.conf + +# Start the WireGuard VPN: +sudo systemctl start wg-quick@wg0 + +# Check that it started properly: +sudo systemctl status wg-quick@wg0 + +# Verify the connection to the Algo VPN: +sudo wg + +# See that your client is using the IP address of your Algo VPN: +curl ipv4.icanhazip.com + +# Optionally configure the connection to come up at boot time: +sudo systemctl enable wg-quick@wg0 +``` + +(If your Linux distribution does not use `systemd`, you can bring up WireGuard with `sudo wg-quick up wg0`). diff --git a/docs/client-linux.md b/docs/client-linux.md index a5155501..94a6445f 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -61,11 +61,11 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * Name: your choice, e.g.: *ikev2-1.2.3.4* * Gateway: * Address: IP of the Algo VPN server, e.g: `1.2.3.4` - * Certificate: `cacert.pem` found at `/path/to/algo/1.2.3.4/cacert.pem` + * Certificate: `cacert.pem` found at `/path/to/algo/configs/1.2.3.4/cacert.pem` * Client: * Authentication: *Certificate/Private key* - * Certificate: `user-name.crt` found at `/path/to/algo/1.2.3.4/pki/certs/user-name.crt` - * Private key: `user-name.key` found at `/path/to/algo/1.2.3.4/pki/private/user-name.key` + * Certificate: `user-name.crt` found at `/path/to/algo/configs/1.2.3.4/pki/certs/user-name.crt` + * Private key: `user-name.key` found at `/path/to/algo/configs/1.2.3.4/pki/private/user-name.key` * Options: * Check *Request an inner IP address*, connection will fail without this option * Optionally check *Enforce UDP encapsulation* @@ -73,6 +73,6 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * For the later 2 options, hover to option in the settings to see a description * Cipher proposal: * Check *Enable custom proposals* - * IKE: `aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256` - * ESP: `aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256` -* Apply and turn the connection on, you should now be connected \ No newline at end of file + * IKE: `aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384` + * ESP: `aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384` +* Apply and turn the connection on, you should now be connected diff --git a/docs/client-macos-wireguard.md b/docs/client-macos-wireguard.md new file mode 100644 index 00000000..0d1db781 --- /dev/null +++ b/docs/client-macos-wireguard.md @@ -0,0 +1,33 @@ +# Using MacOS as a Client with WireGuard + +## Install WireGuard + +To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from MacOS + +``` +# Install the wireguard-go userspace driver +brew install wireguard-tools +``` + +## Locate the Config File + +The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your device. + +## Configure WireGuard + +Finally, install the config file on your client as `/usr/local/etc/wireguard/wg0.conf` and start WireGuard: + +``` +# Install the config file to the WireGuard configuration directory on your MacOS device +mkdir /usr/local/etc/wireguard/ +cp .conf /usr/local/etc/wireguard/wg0.conf + +# Start the WireGuard VPN: +sudo wg-quick up wg0 + +# Verify the connection to the Algo VPN: +wg + +# See that your client is using the IP address of your Algo VPN: +curl ipv4.icanhazip.com +``` diff --git a/docs/client-windows.md b/docs/client-windows.md index 1013585c..53b62f22 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -1,27 +1,71 @@ # Windows client manual setup -Windows clients have a more complicated setup than most others. Follow the steps below to set one up: +## Automatic installation -1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. -2. Import the CA certificate to the local machine Trusted Root certificate store. -3. Open PowerShell as Administrator. Navigate to your copied files. -4. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. +To install automatically, use the generated user Powershell script. + +1. Copy the user PowerShell script (`windows_USER.ps1`) to the client computer. +2. Open Powershell as Administrator. +3. Run the following command: +```powershell +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add +``` +4. The command has help information available. To view its full help, run this from Powershell: +```powershell +Get-Help -Name .\windows_USER.ps1 -Full | more +``` + +## Manual installation + +1. Copy the CA certificate (`cacert.pem`) and user certificate (`USER.p12`) to the client computer +2. Open PowerShell as Administrator. Navigate to your copied files. +3. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. ```powershell Set-ExecutionPolicy Unrestricted -Scope CurrentUser ``` -5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. -6. After you execute the user script, set the Execution Policy back before you close the PowerShell window. +4. In the same window, run the necessary commands to install the certificates and create the VPN configuration. Note the lines at the top defining the VPN address, USER.p12 file location, and CA certificate location - change those lines to the IP address of your Algo server and the location you saved those two files. Also note that it will prompt for the "User p12 password", which is printed at the end of a successful Algo deployment. + +If you have more than one account on your Windows 10 machine (e.g. one with administrator privileges and one without) and would like to have the VPN connection available to all users, then insert the line `AllUserConnection = $true` after `$EncryptionLevel = "Required"`. + +```powershell +$VpnServerAddress = "1.2.3.4" +$UserP12Path = "$Home\Downloads\USER.p12" +$CaCertPath = "$Home\Downloads\cacert.pem" +$VpnName = "Algo VPN $VpnServerAddress IKEv2" +$p12Pass = Read-Host -AsSecureString -Prompt "User p12 password" + +Import-PfxCertificate -FilePath $UserP12Path -CertStoreLocation Cert:\LocalMachine\My -Password $p12Pass +Import-Certificate -FilePath $CaCertPath -CertStoreLocation Cert:\LocalMachine\Root + +$addVpnParams = @{ + Name = $VpnName + ServerAddress = $VpnServerAddress + TunnelType = "IKEv2" + AuthenticationMethod = "MachineCertificate" + EncryptionLevel = "Required" +} +Add-VpnConnection @addVpnParams + +$setVpnParams = @{ + ConnectionName = $VpnName + AuthenticationTransformConstants = "GCMAES256" + CipherTransformConstants = "GCMAES256" + EncryptionMethod = "AES256" + IntegrityCheckMethod = "SHA384" + DHGroup = "ECP384" + PfsGroup = "ECP384" + Force = $true +} +Set-VpnConnectionIPsecConfiguration @setVpnParams + +``` + +5. After you execute the user script, set the Execution Policy back before you close the PowerShell window. ```powershell Set-ExecutionPolicy Restricted -Scope CurrentUser ``` Your VPN is now installed and ready to use. - -If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: - -```powershell -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -``` diff --git a/docs/cloud-amazon-ec2.md b/docs/cloud-amazon-ec2.md new file mode 100644 index 00000000..1e81988f --- /dev/null +++ b/docs/cloud-amazon-ec2.md @@ -0,0 +1,117 @@ +# Amazon EC2 cloud setup + +## AWS account creation + +Creating an Amazon AWS account requires giving Amazon a phone number that can receive a call and has a number pad to enter a PIN challenge displayed in the browser. This phone system prompt occasionally fails to correctly validate input, but try again (request a new PIN in the browser) until you succeed. + +### Select an EC2 plan + +The cheapest EC2 plan you can choose is the "Free Plan" a.k.a. the "AWS Free Tier." It is only available to new AWS customers, it has limits on usage, and it converts to standard pricing after 12 months (the "introductory period"). After you exceed the usage limits, after the 12 month period, or if you are an existing AWS customer, then you will pay standard pay-as-you-go service prices. + +*Note*: Your Algo instance will not stop working when you hit the bandwidth limit, you will just start accumulating service charges on your AWS account. + +As of the time of this writing (July 2018), the Free Tier limits include "750 hours of Amazon EC2 Linux t2.micro instance usage" per month, 15 GB of bandwidth (outbound) per month, and 30 GB of cloud storage. Algo will not even use 1% of the storage limit, but you may have to monitor your bandwidth usage or keep an eye out for the email from Amazon when you are about to exceed the Free Tier limits. + +### Create an AWS permissions policy + +In the AWS console, find the policies menu: click Services > IAM > Policies. Click Create Policy. + +Here, you have the policy editor. Switch to the JSON tab and copy-paste over the existing empty policy with [the minimum required AWS policy needed for Algo deployment](https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md#minimum-required-iam-permissions-for-deployment). + +![Creating a new permissions policy in the AWS console.](/docs/images/aws-ec2-new-policy.png) + +### Set up an AWS user + +In the AWS console, find the users (“Identity and Access Management”, a.k.a. IAM users) menu: click Services > IAM. + +Activate multi-factor authentication (MFA) on your root account. The simplest choice is the mobile app "Google Authenticator." A hardware U2F token is ideal (less prone to a phishing attack), but a TOTP authenticator like this is good enough. + +![The new user screen in the AWS console.](/docs/images/aws-ec2-new-user.png) + +Now "Create individual IAM users" and click Add User. Create a user name. I chose “algovpn”. Then click the box next to Programmatic Access. Then click Next. + +![The IAM user naming screen in the AWS console.](/docs/images/aws-ec2-new-user-name.png) + +Next, click “Attach existing policies directly.” Type “Algo” in the search box to filter the policies. Find “AlgoVPN_Provisioning” (the policy you created) and click the checkbox next to that. Click Next when you’re done. + +![Attaching a policy to an IAM user in the AWS console.](/docs/images/aws-ec2-attach-policy.png) + +The user creation confirmation screen should look like this if you've done everything correctly. + +![New user creation confirmation screen in the AWS console.](/docs/images/aws-ec2-new-user-confirm.png) + +On the final screen, click the Download CSV button. This file includes the AWS access keys you’ll need during the Algo set-up process. Click Close, and you’re all set. + +![Downloading the credentials for an AWS IAM user.](/docs/images/aws-ec2-new-user-csv.png) + +## Using EC2 during Algo setup + +After you have downloaded Algo and installed its dependencies, the next step is running Algo to provision the VPN server on your AWS account. + +First you will be asked which server type to setup. You would want to enter "2" to use Amazon EC2. + +``` +$ ./algo + + What provider would you like to use? + 1. DigitalOcean + 2. Amazon EC2 + 3. Microsoft Azure + 4. Google Compute Engine + 5. Scaleway + 6. OpenStack (DreamCompute optimised) + 7. Install to existing Ubuntu 16.04 server (Advanced) + +Enter the number of your desired provider +: 2 +``` + +Next you will be asked for the AWS Access Key (Access Key ID) and AWS Secret Key (Secret Access Key) that you received in the CSV file when you setup the account (don't worry if you don't see your text entered in the console; the key input is hidden here by Algo). + +``` +Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). +[pasted values will not be displayed] +[AKIA...]: + +Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +[pasted values will not be displayed] +[ABCD...]: +``` + +You will be prompted for the server name to enter. Feel free to leave this as the default ("algo") if you are not certain how this will affect your setup. Here we chose to call it "algovpn". + +``` +Name the vpn server: +[algo]: algovpn +``` + +After entering the server name, the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. + +``` + What region should the server be located in? + 1. us-east-1 US East (N. Virginia) + 2. us-east-2 US East (Ohio) + 3. us-west-1 US West (N. California) + 4. us-west-2 US West (Oregon) + 5. ca-central-1 Canada (Central) + 6. eu-central-1 EU (Frankfurt) + 7. eu-west-1 EU (Ireland) + 8. eu-west-2 EU (London) + 9. eu-west-3 EU (Paris) + 10. ap-northeast-1 Asia Pacific (Tokyo) + 11. ap-northeast-2 Asia Pacific (Seoul) + 12. ap-northeast-3 Asia Pacific (Osaka-Local) + 13. ap-southeast-1 Asia Pacific (Singapore) + 14. ap-southeast-2 Asia Pacific (Sydney) + 15. ap-south-1 Asia Pacific (Mumbai) + 16. sa-east-1 South America (São Paulo) + +Enter the number of your desired region: +[1]: 10 +``` + +You will then be asked the remainder of the standard Algo setup questions. + +## Cleanup +If you've installed Algo onto EC2 multiple times, your AWS account may become cluttered with unused or deleted resources e.g. instances, VPCs, subnets, etc. This may cause future installs to fail. The easiest way to clean up after you're done with a server is to go to "CloudFormation" from the console and delete the CloudFormation stack associated with that server. Please note that unless you've enabled termination protection on your instance, deleting the stack this way will delete your instance without warning, so be sure you are deleting the correct stack. diff --git a/docs/cloud-azure.md b/docs/cloud-azure.md index ae836815..261f4bcf 100644 --- a/docs/cloud-azure.md +++ b/docs/cloud-azure.md @@ -12,8 +12,8 @@ | 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | | 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | | 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | -| 11. Select a role (Contributor will enough for all)| [![step11-thumb]][step11-screen] | -| 12. Swith next to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | +| 11. Select a role (Contributor will be sufficient)| [![step11-thumb]][step11-screen] | +| 12. Next, switch to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | Now you can use Environment Variables: diff --git a/docs/cloud-do.md b/docs/cloud-do.md new file mode 100644 index 00000000..675754a9 --- /dev/null +++ b/docs/cloud-do.md @@ -0,0 +1,87 @@ +# DigitalOcean cloud setup + +## API Token creation + +First, login into your DigitalOcean account. + +Select **API** from the titlebar. This will take you to the "Applications & API" page. + +![The Applications & API page](/docs/images/do-api.png) + +On the **Tokens/Keys** tab, select **Generate New Token**. A dialog will pop up. In that dialog, give your new token a name, and make sure **Write** is checked off. Click the **Generate Token** button when you are ready. + +![The new token dialog, showing a form requesting a name and confirmation on the scope for the new token.](/docs/images/do-new-token.png) + +You will be returned to the **Tokens/Keys** tab, and your new key will be shown under the **Personal Access Tokens** header. + +![The new token in the listing.](/docs/images/do-view-token.png) + +Copy or note down the hash that shows below the name you entered, as this will be necessary for the steps below. This value will disappear if you leave this page, and you'll need to regenerate it if you forget it. + +## Using DigitalOcean with Algo (command) + +These steps are for people who run Algo using Docker or using the "algo" command. + +First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. + +``` + What provider would you like to use? + 1. DigitalOcean + 2. Amazon Lightsail + 3. Amazon EC2 + 4. Microsoft Azure + 5. Google Compute Engine + 6. Scaleway + 7. OpenStack (DreamCompute optimised) + 8. Install to existing Ubuntu 18.04 server + +Enter the number of your desired provider +: 1 +``` + +Next you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (don't worry if don't see any output, as the key input is hidden by Algo). + +``` +Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): +[pasted values will not be displayed] +: +``` + +You will be prompted for the server name to enter. Feel free to leave this as the default ("algo.local") if you are not certain how this will affect your setup. + +``` +Name the vpn server: +[algo.local]: +``` + +After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. + +``` + What region should the server be located in? + 1. Amsterdam (Datacenter 2) + 2. Amsterdam (Datacenter 3) + 3. Frankfurt + 4. London + 5. New York (Datacenter 1) + 6. New York (Datacenter 2) + 7. New York (Datacenter 3) + 8. San Francisco (Datacenter 1) + 9. San Francisco (Datacenter 2) + 10. Singapore + 11. Toronto + 12. Bangalore +Enter the number of your desired region: +[7]: 11 +``` + +You will then be asked the remainder of the setup questions. + +## Using DigitalOcean with Algo (via Ansible) + +If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_token`. + +For example, + + ansible-playbook deploy.yml -e 'provider=digitalocean do_token=my_secret_token' + +Where "my_secret_token" is your API Token. For more references see [deploy-from-ansible](deploy-from-ansible.md) diff --git a/docs/cloud-gce.md b/docs/cloud-gce.md new file mode 100644 index 00000000..c8467655 --- /dev/null +++ b/docs/cloud-gce.md @@ -0,0 +1,41 @@ +# Google Cloud Platform setup + +* Follow the [`gcloud` installation instructions](https://cloud.google.com/sdk/) + +* Log into your account using `gcloud init` + +### Creating a project + +The recommendation on GCP is to group resources into **Projects**, so we will create a new project for our VPN server and use a service account restricted to it. + +```bash +## Create the project to group the resources +### You might need to change it to have a global unique project id +PROJECT_ID=${USER}-algo-vpn +BILLING_ID="$(gcloud beta billing accounts list --format="value(ACCOUNT_ID)")" + +gcloud projects create ${PROJECT_ID} --name algo-vpn --set-as-default +gcloud beta billing projects link ${PROJECT_ID} --billing-account ${BILLING_ID} + +## Create an account that have access to the VPN +gcloud iam service-accounts create algo-vpn --display-name "Algo VPN" +gcloud iam service-accounts keys create configs/gce.json \ + --iam-account algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member serviceAccount:algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/compute.admin +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member serviceAccount:algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/iam.serviceAccountUser + +## Enable the services +gcloud services enable compute.googleapis.com + +./algo -e "provider=gce" -e "gce_credentials_file=$(pwd)/configs/gce.json" + +``` + +**Attention:** take care of the `configs/gce.json` file, which contains the credentials to manage your Google Cloud account, including create and delete servers on this project. + + +There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md). diff --git a/docs/cloud-vultr.md b/docs/cloud-vultr.md new file mode 100644 index 00000000..3448e773 --- /dev/null +++ b/docs/cloud-vultr.md @@ -0,0 +1,8 @@ +### Configuration file + +You need to create a configuration file in INI format with your api key (https://my.vultr.com/settings/#settingsapi) + +``` +[default] +key = +``` diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 5c92a32b..946c045b 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -11,74 +11,81 @@ You can deploy Algo non-interactively by running the Ansible playbooks directly Here is a full example for DigitalOcean: ```shell -ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' +ansible-playbook main.yml -e "provider=digitalocean + server_name=algo + ondemand_cellular=false + ondemand_wifi=false + local_dns=true + ssh_tunneling=true + windows=false + store_cakey=true + region=ams3 + do_token=token" ``` +See below for more information about providers and extra variables + +### Variables + +- `provider` - (Required) The provider to use. See possible values below +- `server_name` - (Required) Server name. Default: algo +- `ondemand_cellular` (Optional) VPN On Demand when connected to cellular networks. Default: false +- `ondemand_wifi` - (Optional. See `ondemand_wifi_exclude`) VPN On Demand when connected to WiFi networks. Default: false +- `ondemand_wifi_exclude` (Required if `ondemand_wifi` set) - WiFi networks to exclude from using the VPN. Comma-separated values +- `local_dns` - (Optional) Enable a DNS resolver. Default: false +- `ssh_tunneling` - (Optional) Enable SSH tunneling for each user. Default: false +- `windows` - (Optional) Enables compatible ciphers and key exchange to support Windows clietns, less secure. Default: false +- `store_cakey` - (Optional) Whether or not keep the CA key (required to add users in the future, but less secure). Default: false + +If any of those unspecified ansible will ask the user to input + ### Ansible roles -Required tags: - -- cloud +Roles can be activated by specifying an extra variable `provider` Cloud roles: -- role: cloud-digitalocean, tags: digitalocean -- role: cloud-ec2, tags: ec2 -- role: cloud-gce, tags: gce +- role: cloud-digitalocean, provider: digitalocean +- role: cloud-ec2, provider: ec2 +- role: cloud-vultr, provider: vultr +- role: cloud-gce, provider: gce +- role: cloud-azure, provider: azure +- role: cloud-scaleway, provider: scaleway +- role: cloud-openstack, provider: openstack Server roles: -- role: vpn, tags: vpn -- role: dns_adblocking, tags: dns, adblock -- role: security, tags: security -- role: ssh_tunneling, tags: ssh_tunneling +- role: vpn +- role: dns_adblocking +- role: dns_encryption +- role: ssh_tunneling +- role: wireguard Note: The `vpn` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: -- OnDemandEnabled_WIFI=Y -- OnDemandEnabled_WIFI_EXCLUDE=HomeNet -- OnDemandEnabled_Cellular=Y +- ondemand_wifi: true +- ondemand_wifi_exclude: HomeNet,OfficeWifi +- ondemand_cellular: true ### Local Installation -Required tags: - -- local +- role: local, provider: local Required variables: -- server_ip -- server_user -- IP_subject_alt_name +- server - IP address of your server +- ca_password - Password for the private CA key -Note that by default, the iptables rules on your existing server will be overwritten. If you don't want to overwrite the iptables rules, you can use the `--skip-tags iptables` flag, for example: - -```shell -ansible-playbook deploy.yml -t local,vpn --skip-tags iptables -e 'server_ip=172.217.2.238 server_user=algo IP_subject_alt_name=172.217.2.238' -``` +Note that by default, the iptables rules on your existing server will be overwritten. If you don't want to overwrite the iptables rules, you can use the `--skip-tags iptables` flag. ### Digital Ocean Required variables: -- do_access_token -- do_server_name -- do_region +- do_token +- region -Possible options for `do_region`: - -- ams2 -- ams3 -- fra1 -- lon1 -- nyc1 -- nyc2 -- nyc3 -- sfo1 -- sfo2 -- sgp1 -- tor1 -- blr1 +Possible options can be gathered calling to https://api.digitalocean.com/v2/regions ### Amazon EC2 @@ -86,28 +93,13 @@ Required variables: - aws_access_key - aws_secret_key -- aws_server_name -- ssh_public_key - region -Possible options for `region`: +Possible options can be gathered via cli `aws ec2 describe-regions` -- us-east-1 -- us-east-2 -- us-west-1 -- us-west-2 -- ap-south-1 -- ap-northeast-2 -- ap-southeast-1 -- ap-southeast-2 -- ap-northeast-1 -- eu-central-1 -- eu-west-1 -- eu-west-2 +Additional variables: -Additional tags: - -- [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) (enabled by default) +- [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) - Encrypted EBS boot volume. Boolean (Default: false) #### Minimum required IAM permissions for deployment: @@ -121,6 +113,7 @@ Additional tags: "Action": [ "ec2:DescribeImages", "ec2:DescribeKeyPairs", + "ec2:DescribeRegions", "ec2:ImportKeyPair" ], "Resource": [ @@ -179,43 +172,76 @@ Additional tags: Required variables: -- credentials_file -- server_name -- ssh_public_key -- zone +- gce_credentials_file +- [region](https://cloud.google.com/compute/docs/regions-zones/) -Possible options for `zone`: +### Vultr -- us-west1-a -- us-west1-b -- us-west1-c -- us-central1-a -- us-central1-b -- us-central1-c -- us-central1-f -- us-east4-a -- us-east4-b -- us-east4-c -- us-east1-b -- us-east1-c -- us-east1-d -- europe-west1-b -- europe-west1-c -- europe-west1-d -- europe-west2-a -- europe-west2-b -- europe-west2-c -- europe-west3-a -- europe-west3-b -- europe-west3-c -- asia-southeast1-a -- asia-southeast1-b -- asia-east1-a -- asia-east1-b -- asia-east1-c -- asia-northeast1-a -- asia-northeast1-b -- asia-northeast1-c -- australia-southeast1-a -- australia-southeast1-b -- australia-southeast1-c +Required variables: + +- [vultr_config](https://github.com/trailofbits/algo/docs/cloud-vultr.md) +- [region](https://api.vultr.com/v1/regions/list) + +### Azure + +Required variables: + +- azure_secret +- azure_tenant +- azure_client_id +- azure_subscription_id +- [region](https://azure.microsoft.com/en-us/global-infrastructure/regions/) + +### Lightsail + +Required variables: + +- aws_access_key +- aws_secret_key +- region + +Possible options can be gathered via cli `aws lightsail get-regions` + +### Scaleway + +Required variables: + +- [scaleway_token](https://www.scaleway.com/docs/generate-an-api-token/) +- [scaleway_org](https://cloud.scaleway.com/#/billing) +- region + +Possible regions: + +- ams1 +- par1 + +### OpenStack + +You need to source the rc file prior to run Algo. Download it from the OpenStack dashboard->Compute->API Access and source it in the shell (eg: source /tmp/dhc-openrc.sh) + + +### Local + +Required variables: + +- server - IP or hostname to access the server via SSH +- endpoint - Public IP address of your server +- ssh_user + + +### Update users + +Playbook: + +``` +users.yml +``` + +Required variables: + +- server - IP or hostname to access the server via SSH +- ca_password - Password to access the CA key + +Tags required: + +- update-users diff --git a/docs/deploy-from-docker.md b/docs/deploy-from-docker.md new file mode 100644 index 00000000..2efd5e32 --- /dev/null +++ b/docs/deploy-from-docker.md @@ -0,0 +1,69 @@ +# Docker Support + +While it is not possible to run your Algo server from within a Docker container, it is possible to use Docker to provision your Algo server. + +## Limitations + +1. [Advanced](deploy-from-ansible.md) installations are not currently supported; you must use the interactive `algo` script. +2. This has not yet been tested with user namespacing enabled. +3. If you're running this on Windows, take care when editing files under `configs/` to ensure that line endings are set appropriately for Unix systems. + +## Deploying an Algo Server with Docker + +1. Install [Docker](https://www.docker.com/community-edition#/download) -- setup and configuration is not covered here +2. Create a local directory to hold your VPN configs (e.g. `C:\Users\trailofbits\Documents\VPNs\`) +3. Create a local copy of [config.cfg](https://github.com/trailofbits/algo/blob/master/config.cfg), with required modifications (e.g. `C:\Users\trailofbits\Documents\VPNs\config.cfg`) +4. Run the Docker container, mounting your configurations appropriately (assuming the container is named `trailofbits/algo` with a tag `latest`): + - From Windows: + ```powershell + C:\Users\trailofbits> docker run --cap-drop=all -it \ + -v C:\Users\trailofbits\Documents\VPNs:/data \ + trailofbits/algo:latest + ``` + - From Linux: + ```bash + $ docker run --cap-drop=all -it \ + -v /home/trailofbits/Documents/VPNs:/data \ + trailofbits/algo:latest + ``` +5. When it exits, you'll be left with a fully populated `configs` directory, containing all appropriate configuration data for your clients, and for future server management + +### Providing Additional Files +f +If you need to provide additional files -- like authorization files for Google Cloud Project -- you can simply specify an additional `-v` parameter, and provide the appropriate path when prompted by `algo`. + +For example, you can specify `-v C:\Users\trailofbits\Documents\VPNs\gce_auth.json:/algo/gce_auth.json`, making the local path to your credentials JSON file `/algo/gce_auth.json`. + +## Managing an Algo Server with Docker + +Even though the container itself is transient, because you've persisted the configuration data, you can use the same Docker image to manage your Algo server. This is done by setting the environment variable `ALGO_ARGS`. + +If you want to use Algo to update the users on an existing server, specify `-e "ALGO_ARGS=update-users"` in your `docker run` command: +```powershell +$ docker run --cap-drop=all -it \ + -e "ALGO_ARGS=update-users" \ + -v C:\Users\trailofbits\Documents\VPNs:/data \ + trailofbits/algo:latest +``` + +## Building Your Own Docker Image + +You can use the Dockerfile provided in this repository as-is, or modify it to suit your needs. Further instructions on building an image can be found in the [Docker engine](https://docs.docker.com/engine/) documents. + +## Security Considerations + +Using Docker is largely no different from running Algo yourself, with a couple of notable exceptions: we run as root within the container, and you're retrieving your content from Docker Hub. + +To work around the limitations of bind mounts in docker, we have to run as root within the container. To mitigate concerns around doing this, we pass the `--cap-drop=all` parameter to `docker run`, which effectively removes all privileges from the root account, reducing it to a generic user account that happens to have a userid of 0. Further steps can be taken by applying `seccomp` profiles to the container; this is being considered as a future improvement. + +Docker themselves provide a concept of [Content Trust](https://docs.docker.com/engine/security/trust/content_trust/) for image management, which helps to ensure that the image you download is, in fact, the image that was uploaded. Content trust is still under development, and while we may be using it, its implementation, limitations, and constraints are documented with Docker. + +## Future Improvements + +1. Even though we're taking care to drop all capabilities to minimize the impact of running as root, we can probably include not only a `seccomp` profile, but also AppArmor and/or SELinux profiles as well. +2. The Docker image doesn't natively support [advanced](deploy-from-ansible.md) Algo deployments, which is useful for scripting. This can be done by launching an interactive shell and running the commands yourself. +3. The way configuration is passed into and out of the container is a bit kludgy. Hopefully future improvements in Docker volumes will make this a bit easier to handle. + +## Advanced Usage + +If you want to poke around the Docker container yourself, you can do so by changing your `entrypoint`. Pass `--entrypoint=/bin/ash` as a parameter to `docker run`, and you'll be dropped into a full Linux shell in the container. diff --git a/docs/deploy-to-freebsd.md b/docs/deploy-to-freebsd.md index 71440cc5..a0c04d4c 100644 --- a/docs/deploy-to-freebsd.md +++ b/docs/deploy-to-freebsd.md @@ -26,5 +26,7 @@ device crypto ## Installation ```shell -ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$server_ip Store_CAKEY=N" --skip-tags cloud +ansible-playbook main.yml -e "provider=local" ``` + +And follow the instructions diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 62e58f94..f3ba0669 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,20 +1,11 @@ # Local deployment -It is possible to download the Algo scripts to your own Ubuntu 16.04 server and run the scripts locally. +You can use Algo to configure a local server as an Algo VPN rather than create and configure a new server on a cloud provider. -In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. - -tl;dr: - -```shell -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible python-pip -pip install virtualenv -pip install --upgrade pip -git clone https://github.com/trailofbits/algo -cd algo -python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt -./algo +Install the Algo scripts on your server and follow the normal installation instructions, then choose: ``` +Install to existing Ubuntu 18.04 server (Advanced) +``` +Make sure your server is running the operating system specified. -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). +**PLEASE NOTE**: Algo is intended for use as a _dedicated_ VPN server. If you install Algo on an existing server, then any existing services might break. In particular, the firewall rules will be overwritten. If you don't want to overwrite the rules you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md), after which you'll need to implement the necessary rules yourself. diff --git a/docs/deploy-to-unsupported-cloud.md b/docs/deploy-to-unsupported-cloud.md index 3e1e5dab..7fd176f7 100644 --- a/docs/deploy-to-unsupported-cloud.md +++ b/docs/deploy-to-unsupported-cloud.md @@ -2,7 +2,7 @@ Algo officially supports DigitalOcean, Amazon Web Services, Microsoft Azure, and Google Cloud Engine. If you want to deploy Algo on another virtual hosting provider, that provider must support: -1. the base operating system image that Algo uses (Ubuntu 16.04), and +1. the base operating system image that Algo uses (Ubuntu 18.04), and 2. a minimum of certain kernel modules required for the strongSwan IPsec server. Please see the [Required Kernel Modules](https://wiki.strongswan.org/projects/strongswan/wiki/KernelModules) documentation from strongSwan for a list of the specific required modules and a script to check for them. As a first step, we recommend running their shell script to determine initial compatibility with your new hosting provider. diff --git a/docs/faq.md b/docs/faq.md index 362a0d20..db11965d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,6 +8,9 @@ * [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) * [I deployed an Algo server. Can you update it with new features?](#i-deployed-an-algo-server-can-you-update-it-with-new-features) * [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) +* [Can DNS filtering be disabled?](#can-dns-filtering-be-disabled) +* [Wasn't IPSEC backdoored by the US government?](#wasnt-ipsec-backdoored-by-the-us-government) +* [What inbound ports are used?](#what-inbound-ports-are-used) ## Has Algo been audited? @@ -44,3 +47,31 @@ In the future, we will make it easier for users who want to update their own ser ## Where did the name "Algo" come from? Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). + +## Can DNS filtering be disabled? + +There is no official way to disable DNS filtering, but there is a workaround: SSH to your Algo server (using the 'shell access' command printed upon a successful deployment), edit `/etc/ipsec.conf`, and change `rightdns=172.16.0.1` to `rightdns=8.8.8.8`. Then run `ipsec restart`. If all else fails, we recommend deploying a new Algo server without the adblocking feature enabled. + +## Wasn't IPSEC backdoored by the US government? + +No. + +[Per security researcher Thomas Ptacek](https://news.ycombinator.com/item?id=2014197): + +> In 2001, Angelos Keromytis --- then a grad student at Penn, now a Columbia professor --- added support for hardware-accelerated IPSEC NICs. When you have an IPSEC NIC, the channel between the NIC and the IPSEC stack keeps state to tell the stack not to bother doing the things the NIC already did, among them validating the IPSEC ESP authenticator. Angelos' code had a bug; it appears to have done the software check only when the hardware had already done it, and skipped it otherwise. +> +> The bug happened during a change that simultaneously refactored and added a feature to OpenBSD's ESP code; a comparison that should have been == was instead !=; the "if" statement with the bug was originally and correctly !=, but should have been flipped based on how the code was refactored. +> +> HD Moore may as we speak be going through the pain of reconstituting a nearly decade-old version of OpenBSD to verify the bug, but stipulate that it was there, and here's what you get: IPSEC ESP packet authentication was disabled if you didn't have hardware IPSEC. There is probably an elaborate man-in-the-middle scenario in which this could get you traffic inspection, but it's nowhere nearly as straightforward as leaking key bits. +> +> To entertain the conspiracy theory, you're still suggesting that the FBI not only introduced this bug, but also developed the technology required to MITM ESP sessions, bouncing them through some secret FBI-developed middlebox. +> +> One year later, Jason Wright from NETSEC (the company at the heart of the [I think silly] allegations about OpenBSD IPSEC backdoors) fixed the bug. +> +> It's interesting that the bug was fixed without an advisory (oh to be a fly on the wall on ICB that day; Theo had a, um, a, "way" with his dev team). On the other hand, we don't know what releases of OpenBSD actually had the bug right now. +> +> It seems vanishingly unlikely that there could have been anything deliberate about this series of changes. You are unlikely to find anyone who will impugn Angelos. Meanwhile, the diffs tell exactly the opposite of the story that Greg Perry told. + +## What inbound ports are used? + +You should only need 22/TCP, 500/UDP, 4500/UDP, and 51820/UDP opened on any firewall that sits between your clients and your Algo server. diff --git a/docs/images/aws-ec2-attach-policy.png b/docs/images/aws-ec2-attach-policy.png new file mode 100644 index 00000000..00108240 Binary files /dev/null and b/docs/images/aws-ec2-attach-policy.png differ diff --git a/docs/images/aws-ec2-new-policy-review.png b/docs/images/aws-ec2-new-policy-review.png new file mode 100644 index 00000000..d50e3722 Binary files /dev/null and b/docs/images/aws-ec2-new-policy-review.png differ diff --git a/docs/images/aws-ec2-new-policy.png b/docs/images/aws-ec2-new-policy.png new file mode 100644 index 00000000..691512e7 Binary files /dev/null and b/docs/images/aws-ec2-new-policy.png differ diff --git a/docs/images/aws-ec2-new-user-confirm.png b/docs/images/aws-ec2-new-user-confirm.png new file mode 100644 index 00000000..5aae7892 Binary files /dev/null and b/docs/images/aws-ec2-new-user-confirm.png differ diff --git a/docs/images/aws-ec2-new-user-csv.png b/docs/images/aws-ec2-new-user-csv.png new file mode 100644 index 00000000..5ecea674 Binary files /dev/null and b/docs/images/aws-ec2-new-user-csv.png differ diff --git a/docs/images/aws-ec2-new-user-name.png b/docs/images/aws-ec2-new-user-name.png new file mode 100644 index 00000000..1028663d Binary files /dev/null and b/docs/images/aws-ec2-new-user-name.png differ diff --git a/docs/images/aws-ec2-new-user.png b/docs/images/aws-ec2-new-user.png new file mode 100644 index 00000000..86651f3e Binary files /dev/null and b/docs/images/aws-ec2-new-user.png differ diff --git a/docs/images/do-api.png b/docs/images/do-api.png new file mode 100644 index 00000000..f3fccb78 Binary files /dev/null and b/docs/images/do-api.png differ diff --git a/docs/images/do-new-token.png b/docs/images/do-new-token.png new file mode 100644 index 00000000..c05bc80b Binary files /dev/null and b/docs/images/do-new-token.png differ diff --git a/docs/images/do-view-token.png b/docs/images/do-view-token.png new file mode 100644 index 00000000..402ed04a Binary files /dev/null and b/docs/images/do-view-token.png differ diff --git a/docs/index.md b/docs/index.md index b9b94bb6..84f07185 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,10 +11,11 @@ - Setup [Generic/Linux](client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](cloud-azure.md) + - Configure [DigitalOcean](cloud-do.md) + - Configure [Vultr](cloud-vultr.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - - Deploy to your own [Ubuntu 16.04](deploy-to-ubuntu.md) server + - Deploy to your own [Ubuntu 18.04](deploy-to-ubuntu.md) server - Deploy to an [unsupported cloud provider](deploy-to-unsupported-cloud.md) * [FAQ](faq.md) * [Troubleshooting](troubleshooting.md) - diff --git a/docs/setup-roles.md b/docs/setup-roles.md index 697fc5f9..1523d181 100644 --- a/docs/setup-roles.md +++ b/docs/setup-roles.md @@ -20,6 +20,9 @@ * **DNS-based Adblocking** * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations +* **DNS encryption** + * Install [dnscrypt-proxy](https://github.com/jedisct1/dnscrypt-proxy) + * Constrains dingo with AppArmor and cgroups CPU and memory limitations * **SSH Tunneling** * Adds a restricted `algo` group with no shell access and limited SSH forwarding options * Creates one limited, local account per user and an SSH public key for each diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a862ac26..e9335947 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,15 +1,24 @@ # Troubleshooting +First of all, check [this](https://github.com/trailofbits/algo#features) and ensure that you are deploying to the supported ubuntu version. + * [Installation Problems](#installation-problems) * [Error: "You have not agreed to the Xcode license agreements"](#error-you-have-not-agreed-to-the-xcode-license-agreements) * [Error: checking whether the C compiler works... no](#error-checking-whether-the-c-compiler-works-no) * [Error: "fatal error: 'openssl/opensslv.h' file not found"](#error-fatal-error-opensslopensslvh-file-not-found) * [Error: "TypeError: must be str, not bytes"](#error-typeerror-must-be-str-not-bytes) * [Error: "ansible-playbook: command not found"](#error-ansible-playbook-command-not-found) + * [Error: "Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION](#could-not-fetch-url--tlsv1_alert_protocol_version) * [Bad owner or permissions on .ssh](#bad-owner-or-permissions-on-ssh) * [The region you want is not available](#the-region-you-want-is-not-available) - * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) - * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) + * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) + * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) + * [AWS: not authorized to perform: cloudformation:UpdateStack](#aws-not-authorized-to-perform-cloudformationupdatestack) + * [DigitalOcean: error tagging resource 'xxxxxxxx': param is missing or the value is empty: resources](#digitalocean-error-tagging-resource) + * [Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid](#windows-the-value-of-parameter-linuxconfigurationsshpublickeyskeydata-is-invalid) + * [Docker: Failed to connect to the host via ssh](#docker-failed-to-connect-to-the-host-via-ssh) + * [Wireguard: Unable to find 'configs/...' in expected paths](#wireguard-unable-to-find-configs-in-expected-paths) + * [Ubuntu Error: "unable to write 'random state" when generating CA password](#ubuntu-error-unable-to-write-random-state-when-generating-ca-password") * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -18,7 +27,9 @@ * [I can't get my router to connect to the Algo server](#i-cant-get-my-router-to-connect-to-the-algo-server) * [I can't get Network Manager to connect to the Algo server](#i-cant-get-network-manager-to-connect-to-the-algo-server) * [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) + * [Clients appear stuck in a reconnection loop](#clients-appear-stuck-in-a-reconnection-loop) * ["Error 809" or IKE_AUTH requests that never make it to the server](#error-809-or-ike_auth-requests-that-never-make-it-to-the-server) + * [Windows: Parameter is incorrect](#windows-parameter-is-incorrect) * [I have a problem not covered here](#i-have-a-problem-not-covered-here) ## Installation Problems @@ -63,7 +74,7 @@ checking for gcc... gcc checking whether the C compiler works... no configure: error: in '/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto': configure: error: C compiler cannot create executables See config.log for more details Traceback (most recent call last): -File "", line 1, in +File "", line 1, in ... cmd_obj.run() File "/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto/setup.py", line 278, in run @@ -75,7 +86,7 @@ You don't have a working compiler installed. You should install the XCode compil ### Error: "fatal error: 'openssl/opensslv.h' file not found" -On macOS, you tried to install pycrypto and encountered the following error: +On macOS, you tried to install `cryptography` and encountered the following error: ``` build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found @@ -94,7 +105,7 @@ Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/p Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. +You are running an old version of `pip` that cannot download the binary `cryptography` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. ### Error: "TypeError: must be str, not bytes" @@ -114,6 +125,22 @@ You tried to install Algo and you see an error that reads "ansible-playbook: com You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. +### Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION + +You tried to install Algo and you received an error like this one: + +``` +Could not fetch URL https://pypi.python.org/simple/secretstorage/: There was a problem confirming the ssl certificate: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:590) - skipping + Could not find a version that satisfies the requirement SecretStorage<3 (from -r requirements.txt (line 2)) (from versions: ) +No matching distribution found for SecretStorage<3 (from -r requirements.txt (line 2)) +``` + +It's time to upgrade your python. + +`brew upgrade python2` + +You can also download python 2.7.x from python.org. + ### Bad owner or permissions on .ssh You tried to run Algo and it quickly exits with an error about a bad owner or permissions: @@ -148,7 +175,7 @@ In order to fix this issue, delete the `algo.pem` and `algo.pem.pub` keys from y ### AWS: "Deploy the template fails" with CREATE_FAILED -You tried to deploy to Algo to AWS and you received an error like this one: +You tried to deploy Algo to AWS and you received an error like this one: ``` TASK [cloud-ec2 : Make a cloudformation template] ****************************** @@ -160,7 +187,104 @@ fatal: [localhost]: FAILED! => {"changed": true, "events": ["StackEvent AWS::Clo Algo builds a [Cloudformation](https://aws.amazon.com/cloudformation/) template to deploy to AWS. You can find the entire contents of the Cloudformation template in `configs/algo.yml`. In order to troubleshoot this issue, login to the AWS console, go to the Cloudformation service, find the failed deployment, click the events tab, and find the corresponding "CREATE_FAILED" events. Note that all AWS resources created by Algo are tagged with `Environment => Algo` for easy identification. -In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. +In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must either [delete the VPCs from previous deployments](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/working-with-vpcs.html#VPC_Deleting), or [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. + +### AWS: not authorized to perform: cloudformation:UpdateStack + +You tried to deploy Algo to AWS and you received an error like this one: + +``` +TASK [cloud-ec2 : Deploy the template] ***************************************** +fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "User: arn:aws:iam::082851645362:user/algo is not authorized to perform: cloudformation:UpdateStack on resource: arn:aws:cloudformation:us-east-1:082851645362:stack/algo/*"} +``` + +This error indicates you already have Algo deployed to Cloudformation. Need to [delete it](cloud-amazon-ec2.md#cleanup) first, then re-deploy. + +### DigitalOcean: error tagging resource + +You tried to deploy Algo to DigitalOcean and you received an error like this one: + +``` +TASK [cloud-digitalocean : Tag the droplet] ************************************ +failed: [localhost] (item=staging) => {"failed": true, "item": "staging", "msg": "error tagging resource '73204383': param is missing or the value is empty: resources"} +failed: [localhost] (item=dbserver) => {"failed": true, "item": "dbserver", "msg": "error tagging resource '73204383': param is missing or the value is empty: resources"} +``` + +The error is caused because Digital Ocean changed its API to treat the tag argument as a string instead of a number. + +1. Download [doctl](https://github.com/digitalocean/doctl) +2. Run `doctl auth init`; it will ask you for your token which you can get (or generate) on the API tab at DigitalOcean +3. Once you are authorized on DO, you can run `doctl compute tag list` to see the list of tags +4. Run `doctl compute tag delete enivronment:algo --force` to delete the environment:algo tag +5. Finally run `doctl compute tag list` to make sure that the tag has been deleted +6. Run algo as directed + +### Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid + +You tried to deploy Algo from Windows and you received an error like this one: + +``` +TASK [cloud-azure : Create an instance]. +fatal: [localhost]: FAILED! => {"changed": false, +"msg": "Error creating or updating virtual machine AlgoVPN - Azure Error: +InvalidParameter\n +Message: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid.\n +Target: linuxConfiguration.ssh.publicKeys.keyData"} +``` + +This is related to [the chmod issue](https://github.com/Microsoft/WSL/issues/81) inside /mnt directory which is NTFS. The fix is to place Algo outside of /mnt directory. + +### Docker: Failed to connect to the host via ssh + +You tried to deploy Algo from Docker and you received an error like this one: + +``` +Failed to connect to the host via ssh: +Warning: Permanently added 'xxx.xxx.xxx.xxx' (ECDSA) to the list of known hosts.\r\n +Control socket connect(/root/.ansible/cp/6d9d22e981): Connection refused\r\n +Failed to connect to new control master\r\n +``` + +You need to add the following to the ansible.cfg in repo root: + +``` +[ssh_connection] +control_path_dir=/dev/shm/ansible_control_path +``` + +### Wireguard: Unable to find 'configs/...' in expected paths + +You tried to run Algo and you received an error like this one: + +``` +TASK [wireguard : Generate public keys] ******************************************************************************** +[WARNING]: Unable to find 'configs/xxx.xxx.xxx.xxx/wireguard//private/dan' in expected paths. + +fatal: [localhost]: FAILED! => {"msg": "An unhandled exception occurred while running the lookup plugin 'file'. Error was a , original message: could not locate file in lookup: configs/xxx.xxx.xxx.xxx/wireguard//private/dan"} +``` +This error is usually hit when using the local install option on a server that isn't Ubuntu 18.04. You should upgrade your server to Ubuntu 18.04. If this doesn't work, try removing `*.lock` files at /etc/wireguard/ as follows: + +```ssh +sudo rm -rf /etc/wireguard/*.lock +``` +Then immediately re-run `./algo`. + +### Ubuntu Error: "unable to write 'random state" when generating CA password + +When running Algo, you received an error like this: + +``` +TASK [common : Generate password for the CA key] *********************************************************************************************************************************************************** +fatal: [xxx.xxx.xxx.xxx -> localhost]: FAILED! => {"changed": true, "cmd": "openssl rand -hex 16", "delta": "0:00:00.024776", "end": "2018-11-26 13:13:55.879921", "msg": "non-zero return code", "rc": 1, "start": "2018-11-26 13:13:55.855145", "stderr": "unable to write 'random state'", "stderr_lines": ["unable to write 'random state'"], "stdout": "xxxxxxxxxxxxxxxxxxx", "stdout_lines": ["xxxxxxxxxxxxxxxxxxx"]} +``` + +This happens when your user does not have ownership of the `$HOME/.rnd` file, which is a seed for randomization. To fix this issue, give your user ownership of the file with this command: + +``` +sudo chown $USER:$USER $HOME/.rnd +``` + +Now, run Algo again. ## Connection Problems @@ -196,9 +320,11 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network ### Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +This issue appears intermittently due to issues with MTU size. Different networks may require the MTU within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. -E.g., On Linux (client -- Ubuntu 16.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: +Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. + +E.g., On Linux (client -- Ubuntu 18.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: ``` $ ping -M do -s 1500 www.google.com PING www.google.com (74.125.22.147) 1500(1528) bytes of data. @@ -209,12 +335,74 @@ Then, set the MTU size on your network adapter (wlan0 or eth0): $ sudo ifconfig wlan0 mtu 1438 ``` +You can also set the `max_mss` variable to a new value in config.cfg, and then redeploy your server rather than reconfigure the current one in-place. + +### Clients appear stuck in a reconnection loop + +If you're using 'Connect on Demand' on iOS and your client device appears stuck in a reconnection loop after switching from WiFi to LTE or vice versa, you may want to try disabling DoS protection in strongSwan. + +The configuration value can be found in `/etc/strongswan.d/charon.conf`. After making the change you must reload or restart ipsec. + +Example command: +``` +sed -i -e 's/#*.dos_protection = yes/dos_protection = no/' /etc/strongswan.d/charon.conf && ipsec restart +``` + ### "Error 809" or IKE_AUTH requests that never make it to the server On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). +### Error: name 'basestring' is not defined + +``` +TASK [cloud-digitalocean : Creating a droplet...] ******************************************* +An exception occurred during task execution. To see the full traceback, use -vvv. The error was: NameError: name 'basestring' is not defined +fatal: [localhost]: FAILED! => {"changed": false, "msg": "name 'basestring' is not defined"} +``` + +If you get something like the above it's likely you're not using a python2 virtualenv. + +Ensure running `python2.7` drops you into a python 2 shell (it looks something like this) + +``` +user@homebook ~ $ python2.7 +Python 2.7.10 (default, Feb 7 2017, 00:08:15) +[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +Then rerun the dependency installation explicitly using python 2.7 + +``` +python2.7 -m virtualenv --python=`which python2.7` env && source env/bin/activate && python2.7 -m pip install -U pip && python2.7 -m pip install -r requirements.txt +``` + +### Windows: Parameter is incorrect + +The problem may happen if you recently moved to a new server, where you have Algo VPN. + +1. Clear the Networking caches: + - Run CMD (click windows start menu, type 'cmd', right click on 'Command Prompt' and select "Run as Administrator"). + - Type the commands below: + ``` + netsh int ip reset + netsh int ipv6 reset + netsh winsock reset + ``` + +3. Restart your computer +4. Reset Device Manager adaptors: + - Open Device Manager + - Find Network Adapters + - Uninstall WAN Miniport drivers (IKEv2, IP, IPv6, etc) + - Click Action > Scan for hardware changes + - The adapters you just uninstalled should come back + +The VPN connection should work again + ## I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Gitter](https://gitter.im/trailofbits/algo) and ask for help. If you think you found a new issue in Algo, [file an issue](https://github.com/trailofbits/algo/issues/new). diff --git a/input.yml b/input.yml new file mode 100644 index 00000000..f24ab2ba --- /dev/null +++ b/input.yml @@ -0,0 +1,138 @@ +--- +- name: Ask user for the input + hosts: localhost + tags: always + vars: + defaults: + server_name: algo + ondemand_cellular: false + ondemand_wifi: false + local_dns: false + ssh_tunneling: false + windows: false + store_cakey: false + providers_map: + - { name: DigitalOcean, alias: digitalocean } + - { name: Amazon Lightsail, alias: lightsail } + - { name: Amazon EC2, alias: ec2 } + - { name: Vultr, alias: vultr } + - { name: Microsoft Azure, alias: azure } + - { name: Google Compute Engine, alias: gce } + - { name: Scaleway, alias: scaleway} + - { name: OpenStack (DreamCompute optimised), alias: openstack } + - { name: Install to existing Ubuntu 18.04 server (Advanced), alias: local } + vars_files: + - config.cfg + + tasks: + - pause: + prompt: | + What provider would you like to use? + {% for p in providers_map %} + {{ loop.index }}. {{ p['name']}} + {% endfor %} + + Enter the number of your desired provider + register: _algo_provider + when: provider is undefined + + - name: Set facts based on the input + set_fact: + algo_provider: "{{ provider | default(providers_map[_algo_provider.user_input|default(omit)|int - 1]['alias']) }}" + + - pause: + prompt: | + Name the vpn server + [algo] + register: _algo_server_name + when: + - server_name is undefined + - algo_provider != "local" + + - pause: + prompt: | + Do you want macOS/iOS clients to enable "VPN On Demand" when connected to cellular networks? + [y/N] + register: _ondemand_cellular + when: ondemand_cellular is undefined + + - pause: + prompt: | + Do you want macOS/iOS clients to enable "VPN On Demand" when connected to Wi-Fi? + [y/N] + register: _ondemand_wifi + when: ondemand_wifi is undefined + + - pause: + prompt: | + List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN + (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) + register: _ondemand_wifi_exclude + when: + - ondemand_wifi_exclude is undefined + - (ondemand_wifi|default(false)|bool) or + (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) + + - pause: + prompt: | + Do you want to install a DNS resolver on this VPN server, to block ads while surfing? + [y/N] + register: _local_dns + when: local_dns is undefined + + - pause: + prompt: | + Do you want each user to have their own account for SSH tunneling? + [y/N] + register: _ssh_tunneling + when: ssh_tunneling is undefined + + - pause: + prompt: | + Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) + [y/N] + register: _windows + when: windows is undefined + + - pause: + prompt: | + Do you want to retain the CA key? (required to add users in the future, but less secure) + [y/N] + register: _store_cakey + when: store_cakey is undefined + + - name: Set facts based on the input + set_fact: + algo_server_name: >- + {% if server_name is defined %}{% set _server = server_name %} + {%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input != "" %}{% set _server = _algo_server_name.user_input %} + {%- else %}{% set _server = defaults['server_name'] %}{% endif -%} + {{ _server | regex_replace('(?!\.)(\W|_)', '-') }} + algo_ondemand_cellular: >- + {% if ondemand_cellular is defined %}{{ ondemand_cellular | bool }} + {%- elif _ondemand_cellular.user_input is defined and _ondemand_cellular.user_input != "" %}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }} + {%- else %}false{% endif %} + algo_ondemand_wifi: >- + {% if ondemand_wifi is defined %}{{ ondemand_wifi | bool }} + {%- elif _ondemand_wifi.user_input is defined and _ondemand_wifi.user_input != "" %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} + {%- else %}false{% endif %} + algo_ondemand_wifi_exclude: >- + {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input }} + {%- else %}_null{% endif %} + algo_local_dns: >- + {% if local_dns is defined %}{{ local_dns | bool }} + {%- elif _local_dns.user_input is defined and _local_dns.user_input != "" %}{{ booleans_map[_local_dns.user_input] | default(defaults['local_dns']) }} + {%- else %}false{% endif %} + algo_ssh_tunneling: >- + {% if ssh_tunneling is defined %}{{ ssh_tunneling | bool }} + {%- elif _ssh_tunneling.user_input is defined and _ssh_tunneling.user_input != "" %}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }} + {%- else %}false{% endif %} + algo_windows: >- + {% if windows is defined %}{{ windows | bool }} + {%- elif _windows.user_input is defined and _windows.user_input != "" %}{{ booleans_map[_windows.user_input] | default(defaults['windows']) }} + {%- else %}false{% endif %} + algo_store_cakey: >- + {% if store_cakey is defined %}{{ store_cakey | bool }} + {%- elif _store_cakey.user_input is defined and _store_cakey.user_input != "" %}{{ booleans_map[_store_cakey.user_input] | default(defaults['store_cakey']) }} + {%- else %}false{% endif %} diff --git a/library/ec2_ami_copy.py b/library/ec2_ami_copy.py deleted file mode 100644 index 629a48c6..00000000 --- a/library/ec2_ami_copy.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'version': '1.1'} - -DOCUMENTATION = ''' ---- -module: ec2_ami_copy -short_description: copies AMI between AWS regions, return new image id -description: - - Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5 -version_added: "2.0" -options: - source_region: - description: - - the source region that AMI should be copied from - required: true - source_image_id: - description: - - the id of the image in source region that should be copied - required: true - name: - description: - - The name of the new image to copy - required: true - default: null - description: - description: - - An optional human-readable string describing the contents and purpose of the new AMI. - required: false - default: null - encrypted: - description: - - Whether or not to encrypt the target image - required: false - default: null - version_added: "2.2" - kms_key_id: - description: - - KMS key id used to encrypt image. If not specified, uses default EBS Customer Master Key (CMK) for your account. - required: false - default: null - version_added: "2.2" - wait: - description: - - wait for the copied AMI to be in state 'available' before returning. - required: false - default: false - tags: - description: - - a hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}' - required: false - default: null - -author: Amir Moulavi , Tim C -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Basic AMI Copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - -# AMI copy wait until available -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - wait: yes - register: image_id - -# Named AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - name: My-Awesome-AMI - description: latest patch - -# Tagged AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - tags: - Name: My-Super-AMI - Patch: 1.2.3 - -# Encrypted AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - -# Encrypted AMI copy with specified key -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, get_aws_connection_info) - -try: - import boto - import boto.ec2 - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - from botocore.exceptions import ClientError, NoCredentialsError, NoRegionError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - - - -def copy_image(ec2, module): - """ - Copies an AMI - - module : AnsibleModule object - ec2: ec2 connection object - """ - - tags = module.params.get('tags') - - params = {'SourceRegion': module.params.get('source_region'), - 'SourceImageId': module.params.get('source_image_id'), - 'Name': module.params.get('name'), - 'Description': module.params.get('description'), - 'Encrypted': module.params.get('encrypted'), -# 'KmsKeyId': module.params.get('kms_key_id') - } - if module.params.get('kms_key_id'): - params['KmsKeyId'] = module.params.get('kms_key_id') - - try: - image_id = ec2.copy_image(**params)['ImageId'] - if module.params.get('wait'): - ec2.get_waiter('image_available').wait(ImageIds=[image_id]) - if module.params.get('tags'): - ec2.create_tags( - Resources=[image_id], - Tags=[{'Key' : k, 'Value': v} for k,v in module.params.get('tags').items()] - ) - - module.exit_json(changed=True, image_id=image_id) - except ClientError as ce: - module.fail_json(msg=ce) - except NoCredentialsError: - module.fail_json(msg="Unable to locate AWS credentials") - except Exception as e: - module.fail_json(msg=str(e)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - source_region=dict(required=True), - source_image_id=dict(required=True), - name=dict(required=True), - description=dict(default=''), - encrypted=dict(type='bool', required=False), - kms_key_id=dict(type='str', required=False), - wait=dict(type='bool', default=False, required=False), - tags=dict(type='dict'))) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - # TODO: Check botocore version - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if HAS_BOTO3: - - try: - ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, - **aws_connect_params) - except NoRegionError: - module.fail_json(msg='AWS Region is required') - else: - module.fail_json(msg='boto3 required for this module') - - copy_image(ec2, module) - - -if __name__ == '__main__': - main() diff --git a/library/gce_region_facts.py b/library/gce_region_facts.py new file mode 100644 index 00000000..65acfb63 --- /dev/null +++ b/library/gce_region_facts.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# Copyright 2013 Google Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: gce_region_facts +version_added: "5.3" +short_description: Gather facts about GCE regions. +description: + - Gather facts about GCE regions. +options: + service_account_email: + version_added: "1.6" + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: "1.6" + description: + - path to the pem file associated with the service account email + This option is deprecated. Use 'credentials_file'. + required: false + default: null + aliases: [] + credentials_file: + version_added: "2.1.0" + description: + - path to the JSON file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: "1.6" + description: + - your GCE project ID + required: false + default: null + aliases: [] + requirements: + - "python >= 2.6" + - "apache-libcloud >= 0.13.3, >= 0.17.0 if using JSON credentials" +author: "Jack Ivanov (@jackivanov)" +''' + +EXAMPLES = ''' +# Gather facts about all regions +- gce_region_facts: +''' + +RETURN = ''' +regions: + returned: on success + description: > + Each element consists of a dict with all the information related + to that region. + type: list + sample: "[{ + "name": "asia-east1", + "status": "UP", + "zones": [ + { + "name": "asia-east1-a", + "status": "UP" + }, + { + "name": "asia-east1-b", + "status": "UP" + }, + { + "name": "asia-east1-c", + "status": "UP" + } + ] + }]" +''' +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, ResourceExistsError, ResourceNotFoundError + _ = Provider.GCE + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.gce import gce_connect, unexpected_error_msg + + +def main(): + module = AnsibleModule( + argument_spec=dict( + service_account_email=dict(), + pem_file=dict(type='path'), + credentials_file=dict(type='path'), + project_id=dict(), + ) + ) + + if not HAS_LIBCLOUD: + module.fail_json(msg='libcloud with GCE support (0.17.0+) required for this module') + + gce = gce_connect(module) + + changed = False + gce_regions = [] + + try: + regions = gce.ex_list_regions() + for r in regions: + gce_region = {} + gce_region['name'] = r.name + gce_region['status'] = r.status + gce_region['zones'] = [] + for z in r.zones: + gce_zone = {} + gce_zone['name'] = z.name + gce_zone['status'] = z.status + gce_region['zones'].append(gce_zone) + gce_regions.append(gce_region) + json_output = { 'regions': gce_regions } + module.exit_json(changed=False, results=json_output) + except ResourceNotFoundError: + pass + + +if __name__ == '__main__': + main() diff --git a/library/lightsail.py b/library/lightsail.py new file mode 100644 index 00000000..99e49ac7 --- /dev/null +++ b/library/lightsail.py @@ -0,0 +1,551 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: lightsail +short_description: Create or delete a virtual machine instance in AWS Lightsail +description: + - Creates or instances in AWS Lightsail and optionally wait for it to be 'running'. +version_added: "2.4" +author: "Nick Ball (@nickball)" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent', 'running', 'restarted', 'stopped'] + name: + description: + - Name of the instance + required: true + default : null + zone: + description: + - AWS availability zone in which to launch the instance. Required when state='present' + required: false + default: null + blueprint_id: + description: + - ID of the instance blueprint image. Required when state='present' + required: false + default: null + bundle_id: + description: + - Bundle of specification info for the instance. Required when state='present' + required: false + default: null + user_data: + description: + - Launch script that can configure the instance with additional data + required: false + default: null + key_pair_name: + description: + - Name of the key pair to use with the instance + required: false + default: null + wait: + description: + - Wait for the instance to be in state 'running' before returning. If wait is "no" an ip_address may not be returned + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - How long before wait gives up, in seconds. + default: 300 + open_ports: + description: + - Adds public ports to an Amazon Lightsail instance. + default: null + suboptions: + from_port: + description: Begin of the range + required: true + default: null + to_port: + description: End of the range + required: true + default: null + protocol: + description: Accepted traffic protocol. + required: true + choices: + - udp + - tcp + - all + default: null +requirements: + - "python >= 2.6" + - boto3 + +extends_documentation_fragment: + - aws + - ec2 +''' + + +EXAMPLES = ''' +# Create a new Lightsail instance, register the instance details +- lightsail: + state: present + name: myinstance + region: us-east-1 + zone: us-east-1a + blueprint_id: ubuntu_16_04 + bundle_id: nano_1_0 + key_pair_name: id_rsa + user_data: " echo 'hello world' > /home/ubuntu/test.txt" + wait_timeout: 500 + open_ports: + - from_port: 4500 + to_port: 4500 + protocol: udp + - from_port: 500 + to_port: 500 + protocol: udp + register: my_instance + +- debug: + msg: "Name is {{ my_instance.instance.name }}" + +- debug: + msg: "IP is {{ my_instance.instance.publicIpAddress }}" + +# Delete an instance if present +- lightsail: + state: absent + region: us-east-1 + name: myinstance + +''' + +RETURN = ''' +changed: + description: if a snapshot has been modified/created + returned: always + type: bool + sample: + changed: true +instance: + description: instance data + returned: always + type: dict + sample: + arn: "arn:aws:lightsail:us-east-1:448830907657:Instance/1fef0175-d6c8-480e-84fa-214f969cda87" + blueprint_id: "ubuntu_16_04" + blueprint_name: "Ubuntu" + bundle_id: "nano_1_0" + created_at: "2017-03-27T08:38:59.714000-04:00" + hardware: + cpu_count: 1 + ram_size_in_gb: 0.5 + is_static_ip: false + location: + availability_zone: "us-east-1a" + region_name: "us-east-1" + name: "my_instance" + networking: + monthly_transfer: + gb_per_month_allocated: 1024 + ports: + - access_direction: "inbound" + access_from: "Anywhere (0.0.0.0/0)" + access_type: "public" + common_name: "" + from_port: 80 + protocol: tcp + to_port: 80 + - access_direction: "inbound" + access_from: "Anywhere (0.0.0.0/0)" + access_type: "public" + common_name: "" + from_port: 22 + protocol: tcp + to_port: 22 + private_ip_address: "172.26.8.14" + public_ip_address: "34.207.152.202" + resource_type: "Instance" + ssh_key_name: "keypair" + state: + code: 16 + name: running + support_code: "588307843083/i-0997c97831ee21e33" + username: "ubuntu" +''' + +import time +import traceback + +try: + import botocore + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 +except ImportError: + # will be caught by imported HAS_BOTO3 + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, + HAS_BOTO3, camel_dict_to_snake_dict) + + +def create_instance(module, client, instance_name): + """ + Create an instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to delete + + Returns a dictionary of instance information + about the new instance. + + """ + + changed = False + + # Check if instance already exists + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + zone = module.params.get('zone') + blueprint_id = module.params.get('blueprint_id') + bundle_id = module.params.get('bundle_id') + user_data = module.params.get('user_data') + user_data = '' if user_data is None else user_data + + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + if module.params.get('key_pair_name'): + key_pair_name = module.params.get('key_pair_name') + else: + key_pair_name = '' + + if module.params.get('open_ports'): + open_ports = module.params.get('open_ports') + else: + open_ports = '[]' + + resp = None + if inst is None: + try: + resp = client.create_instances( + instanceNames=[ + instance_name + ], + availabilityZone=zone, + blueprintId=blueprint_id, + bundleId=bundle_id, + userData=user_data, + keyPairName=key_pair_name, + ) + resp = resp['operations'][0] + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Unable to create instance {0}, error: {1}'.format(instance_name, e)) + + inst = _find_instance_info(client, instance_name) + + # Wait for instance to become running + if wait: + while (wait_max > time.time()) and (inst is not None and inst['state']['name'] != "running"): + try: + time.sleep(2) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to start instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(1) + + # Timed out + if wait and not changed and wait_max <= time.time(): + module.fail_json(msg="Wait for instance start timeout at %s" % time.asctime()) + + # Attempt to open ports + if open_ports: + if inst is not None: + try: + for o in open_ports: + resp = client.open_instance_public_ports( + instanceName=instance_name, + portInfo={ + 'fromPort': o['from_port'], + 'toPort': o['to_port'], + 'protocol': o['protocol'] + } + ) + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Error opening ports for instance {0}, error: {1}'.format(instance_name, e)) + + changed = True + + return (changed, inst) + + +def delete_instance(module, client, instance_name): + """ + Terminates an instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to delete + + Returns a dictionary of instance information + about the instance deleted (pre-deletion). + + If the instance to be deleted is running + "changed" will be set to False. + + """ + + # It looks like deleting removes the instance immediately, nothing to wait for + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before deleting + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to delete instance {0}. Check that you have permissions to perform the operation.".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to delete instance {0}.".format(instance_name), exception=traceback.format_exc()) + # sleep and retry + time.sleep(10) + + # Attempt to delete + if inst is not None: + while not changed and ((wait and wait_max > time.time()) or (not wait)): + try: + client.delete_instance(instanceName=instance_name) + changed = True + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Error deleting instance {0}, error: {1}'.format(instance_name, e)) + + # Timed out + if wait and not changed and wait_max <= time.time(): + module.fail_json(msg="wait for instance delete timeout at %s" % time.asctime()) + + return (changed, inst) + + +def restart_instance(module, client, instance_name): + """ + Reboot an existing instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to reboot + + Returns a dictionary of instance information + about the restarted instance + + If the instance was not able to reboot, + "changed" will be set to False. + + Wait will not apply here as this is an OS-level operation + """ + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before state change + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to restart instance {0}. Check that you have permissions to perform the operation.".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to restart instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(3) + + # send reboot + if inst is not None: + try: + client.reboot_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Unable to reboot instance {0}, error: {1}'.format(instance_name, e)) + changed = True + + return (changed, inst) + + +def startstop_instance(module, client, instance_name, state): + """ + Starts or stops an existing instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to start/stop + state: Target state ("running" or "stopped") + + Returns a dictionary of instance information + about the instance started/stopped + + If the instance was not able to state change, + "changed" will be set to False. + + """ + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before state change + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to start/stop instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(1) + + # Try state change + if inst is not None and inst['state']['name'] != state: + try: + if state == 'running': + client.start_instance(instanceName=instance_name) + else: + client.stop_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(instance_name, e)) + changed = True + # Grab current instance info + inst = _find_instance_info(client, instance_name) + + return (changed, inst) + + +def core(module): + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg='region must be specified') + + client = None + try: + client = boto3_conn(module, conn_type='client', resource='lightsail', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg='Failed while connecting to the lightsail service: %s' % e, exception=traceback.format_exc()) + + changed = False + state = module.params['state'] + name = module.params['name'] + + if state == 'absent': + changed, instance_dict = delete_instance(module, client, name) + elif state in ('running', 'stopped'): + changed, instance_dict = startstop_instance(module, client, name, state) + elif state == 'restarted': + changed, instance_dict = restart_instance(module, client, name) + elif state == 'present': + changed, instance_dict = create_instance(module, client, name) + + module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(instance_dict)) + + +def _find_instance_info(client, instance_name): + ''' handle exceptions where this function is called ''' + inst = None + try: + inst = client.get_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + raise + return inst['instance'] + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent', 'stopped', 'running', 'restarted']), + zone=dict(type='str'), + blueprint_id=dict(type='str'), + bundle_id=dict(type='str'), + key_pair_name=dict(type='str'), + user_data=dict(type='str'), + wait=dict(type='bool', default=True), + wait_timeout=dict(default=300), + open_ports=dict(type='list') + )) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + if not HAS_BOTOCORE: + module.fail_json(msg='Python module "botocore" is missing, please install it') + + try: + core(module) + except (botocore.exceptions.ClientError, Exception) as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/library/lightsail_region_facts.py b/library/lightsail_region_facts.py new file mode 100644 index 00000000..8da4c00c --- /dev/null +++ b/library/lightsail_region_facts.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: lightsail_region_facts +short_description: Gather facts about AWS Lightsail regions. +description: + - Gather facts about AWS Lightsail regions. +version_added: "2.5.3" +author: "Jack Ivanov (@jackivanov)" +options: +requirements: + - "python >= 2.6" + - boto3 + +extends_documentation_fragment: + - aws + - ec2 +''' + + +EXAMPLES = ''' +# Gather facts about all regions +- lightsail_region_facts: +''' + +RETURN = ''' +regions: + returned: on success + description: > + Each element consists of a dict with all the information related + to that region. + type: list + sample: "[{ + "availabilityZones": [], + "continentCode": "NA", + "description": "This region is recommended to serve users in the eastern United States", + "displayName": "Virginia", + "name": "us-east-1" + }]" +''' + +import time +import traceback + +try: + import botocore + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 +except ImportError: + # will be caught by imported HAS_BOTO3 + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, + HAS_BOTO3, camel_dict_to_snake_dict) + +def main(): + argument_spec = ec2_argument_spec() + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + if not HAS_BOTOCORE: + module.fail_json(msg='Python module "botocore" is missing, please install it') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + + client = None + try: + client = boto3_conn(module, conn_type='client', resource='lightsail', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg='Failed while connecting to the lightsail service: %s' % e, exception=traceback.format_exc()) + + response = client.get_regions( + includeAvailabilityZones=False + ) + module.exit_json(changed=False, results=response) + except (botocore.exceptions.ClientError, Exception) as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/main.yml b/main.yml new file mode 100644 index 00000000..faf4c2d1 --- /dev/null +++ b/main.yml @@ -0,0 +1,9 @@ +--- +- name: Include prompts playbook + import_playbook: input.yml + +- name: Include cloud provisioning playbook + import_playbook: cloud.yml + +- name: Include server configuration playbook + import_playbook: server.yml diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml new file mode 100644 index 00000000..283ed60a --- /dev/null +++ b/playbooks/cloud-post.yml @@ -0,0 +1,45 @@ +--- +- name: Set subjectAltName as afact + set_fact: + IP_subject_alt_name: "{% if algo_provider == 'local' %}{{ IP_subject_alt_name }}{% else %}{{ cloud_instance_ip }}{% endif %}" + +- name: Add the server to an inventory group + add_host: + name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + groups: vpn-host + ansible_connection: "{% if cloud_instance_ip == 'localhost' %}local{% else %}ssh{% endif %}" + ansible_ssh_user: "{{ ansible_ssh_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + algo_provider: "{{ algo_provider }}" + algo_server_name: "{{ algo_server_name }}" + algo_ondemand_cellular: "{{ algo_ondemand_cellular }}" + algo_ondemand_wifi: "{{ algo_ondemand_wifi }}" + algo_ondemand_wifi_exclude: "{{ algo_ondemand_wifi_exclude }}" + algo_local_dns: "{{ algo_local_dns }}" + algo_ssh_tunneling: "{{ algo_ssh_tunneling }}" + algo_windows: "{{ algo_windows }}" + algo_store_cakey: "{{ algo_store_cakey }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Additional variables for the server + add_host: + name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + when: algo_provider != 'local' + +- name: Wait until SSH becomes ready... + wait_for: + port: 22 + host: "{{ cloud_instance_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present + when: cloud_instance_ip != "localhost" + +- debug: + var: IP_subject_alt_name + +- name: A short pause, in order to be sure the instance is ready + pause: + seconds: 20 diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml new file mode 100644 index 00000000..338e70dd --- /dev/null +++ b/playbooks/cloud-pre.yml @@ -0,0 +1,38 @@ +--- +- name: Display the invocation environment + local_action: + module: shell + ./algo-showenv.sh \ + 'algo_provider "{{ algo_provider }}"' \ + 'algo_ondemand_cellular "{{ algo_ondemand_cellular }}"' \ + 'algo_ondemand_wifi "{{ algo_ondemand_wifi }}"' \ + 'algo_ondemand_wifi_exclude "{{ algo_ondemand_wifi_exclude }}"' \ + 'algo_local_dns "{{ algo_local_dns }}"' \ + 'algo_ssh_tunneling "{{ algo_ssh_tunneling }}"' \ + 'algo_windows "{{ algo_windows }}"' \ + 'wireguard_enabled "{{ wireguard_enabled }}"' \ + 'dns_encryption "{{ dns_encryption }}"' \ + > /dev/tty + +- name: Install the requirements + local_action: + module: pip + state: latest + name: + - pyOpenSSL + - jinja2==2.8 + - segno + tags: always + +- name: Generate the SSH private key + openssl_privatekey: + path: "{{ SSH_keys.private }}" + size: 2048 + mode: "0600" + type: RSA + +- name: Generate the SSH public key + openssl_publickey: + path: "{{ SSH_keys.public }}" + privatekey_path: "{{ SSH_keys.private }}" + format: OpenSSH diff --git a/playbooks/common.yml b/playbooks/common.yml deleted file mode 100644 index 04a3966c..00000000 --- a/playbooks/common.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- - -- name: Check the system - raw: uname -a - register: OS - -- name: Ubuntu pre-tasks - include: ubuntu.yml - when: '"Ubuntu" in OS.stdout' - -- name: FreeBSD pre-tasks - include: freebsd.yml - when: '"FreeBSD" in OS.stdout' - -- include: facts/main.yml diff --git a/playbooks/facts/FreeBSD.yml b/playbooks/facts/FreeBSD.yml deleted file mode 100644 index 0d025fc0..00000000 --- a/playbooks/facts/FreeBSD.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- - -- set_fact: - config_prefix: "/usr/local/" - root_group: wheel - ssh_service_name: sshd - apparmor_enabled: false - strongswan_additional_plugins: - - kernel-pfroute - - kernel-pfkey diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml deleted file mode 100644 index 02d991ff..00000000 --- a/playbooks/facts/main.yml +++ /dev/null @@ -1,45 +0,0 @@ ---- - -- name: Gather Facts - setup: - -- name: Ensure the algo ssh key exist on the server - authorized_key: - user: "{{ ansible_ssh_user }}" - state: present - key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - tags: [ 'cloud' ] - -- name: Enable IPv6 - set_fact: - ipv6_support: true - when: ansible_default_ipv6.gateway is defined - -- name: Set facts if the deployment in a cloud - set_fact: - cloud_deployment: true - tags: ['cloud'] - -- name: Generate password for the CA key - local_action: - module: shell - openssl rand -hex 16 - become: no - register: CA_password - -- name: Generate p12 export password - local_action: - module: shell - openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' - become: no - register: p12_export_password_generated - when: p12_export_password is not defined - -- name: Define password facts - set_fact: - easyrsa_p12_export_password: "{{ p12_export_password|default(p12_export_password_generated.stdout) }}" - easyrsa_CA_password: "{{ CA_password.stdout }}" - -- name: Define the commonName - set_fact: - IP_subject_alt_name: "{{ IP_subject_alt_name }}" diff --git a/playbooks/freebsd.yml b/playbooks/freebsd.yml deleted file mode 100644 index 8cf0579f..00000000 --- a/playbooks/freebsd.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- - -- name: FreeBSD / HardenedBSD | Install prerequisites - raw: sleep 10 && env ASSUME_ALWAYS_YES=YES sudo pkg install -y python27 - -- name: FreeBSD / HardenedBSD | Configure defaults - raw: sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7 - -- include: facts/FreeBSD.yml diff --git a/playbooks/local.yml b/playbooks/local.yml deleted file mode 100644 index be2ecc9f..00000000 --- a/playbooks/local.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- - -- name: Generate the SSH private key - shell: > - echo -e 'n' | - ssh-keygen -b 2048 -C {{ SSH_keys.comment }} - -t rsa -f {{ SSH_keys.private }} -q -N "" - args: - creates: "{{ SSH_keys.private }}" - -- name: Generate the SSH public key - shell: > - echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} - > {{ SSH_keys.public }} - changed_when: false - -- name: Change mode for the SSH private key - file: - path: "{{ SSH_keys.private }}" - mode: 0600 - -- name: Ensure the dynamic inventory exists - blockinfile: - dest: configs/inventory.dynamic - marker: "# {mark} ALGO MANAGED BLOCK" - create: yes - block: | - [algo:children] - {% for group in cloud_providers.keys() %} - {{ group }} - {% endfor %} diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml deleted file mode 100644 index b2b30b77..00000000 --- a/playbooks/local_ssh.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- - -- name: Ensure the local ssh directory is exist - file: - path: ~/.ssh/ - state: directory - -- name: Copy the algo ssh key to the local ssh directory - copy: - src: "{{ SSH_keys.private }}" - dest: ~/.ssh/algo.pem - mode: '0600' diff --git a/playbooks/post.yml b/playbooks/post.yml deleted file mode 100644 index f9f41983..00000000 --- a/playbooks/post.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- - -- name: Wait until SSH becomes ready... - wait_for: - port: 22 - host: "{{ cloud_instance_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present - -- name: A short pause, in order to be sure the instance is ready - pause: - seconds: 20 - -- include: local_ssh.yml diff --git a/playbooks/ubuntu.yml b/playbooks/ubuntu.yml deleted file mode 100644 index d67cbde4..00000000 --- a/playbooks/ubuntu.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- - -- name: Ubuntu | Install prerequisites - raw: sleep 10 && sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - -- name: Ubuntu | Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - tags: - - update-alternatives diff --git a/playbooks/win_script_rebuild.yml b/playbooks/win_script_rebuild.yml new file mode 100644 index 00000000..12bdfe91 --- /dev/null +++ b/playbooks/win_script_rebuild.yml @@ -0,0 +1,67 @@ +--- + +# This playbook is designed to help when modifying the Windows script template +# in roles/vpn/templates/client_windows.ps1.j2 +# It rebuilds the client_USER.ps1 scripts for each user defined in config.cfg, +# without redeploying users or opening an SSH connection to the Algo server at +# all. +# +# This playbook is _not_ part of a normal Algo deployment. +# It is only intended to speed up development of the client_USER.ps1 Windows +# Algo install scripts. +# +# REQUIREMENTS +# - Algo must have been deployed once +# - Windows users must have been enabled at deployment time +# - All users defined in config.cfg must not have changed +# - Only one Algo deployment exists in the configs/ directory +# - There must be exactly one subfolder in the configs/ directory: +# the folder named after the IP of the algo server + +- hosts: localhost + gather_facts: False + tags: always + vars_files: + - ../config.cfg + + tasks: + + - name: Get config subdir + shell: find ../configs/* -maxdepth 0 -type d | sed 's/.*\///' + register: config_subdir_result + - fail: + msg: + - "Found wrong number of config subdirs... stdout:" + - "{{ config_subdir_result.split('\n') }}" + when: config_subdir_result.stdout.split('\n') | length != 1 + - set_fact: + IP_subject_alt_name: "{{ config_subdir_result.stdout }}" + - debug: + var: IP_subject_alt_name + + - name: Register p12 PayloadContent + shell: cat private/{{ item }}.p12 | base64 + register: PayloadContent + args: + chdir: "../configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" + + - name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , '../configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + + - name: Build the windows client powershell script + template: + src: ../roles/vpn/templates/client_windows.ps1.j2 + dest: ../configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 + mode: 0600 + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + + - name: List windows client powershell scripts + debug: + msg: "configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1" + with_items: + - "{{ users }}" diff --git a/requirements.txt b/requirements.txt index 67ec4a10..38f36dac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1 @@ -msrestazure -setuptools>=11.3 -ansible>=2.1,<2.2.1 -dopy==0.3.5 -boto>=2.5 -boto3 -azure==2.0.0rc5 -msrest==0.4.1 -apache-libcloud -six -pyopenssl -jinja2==2.8 +ansible==2.5.2 diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 68397148..60fafed2 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -2,7 +2,7 @@ setup: - name: Include system based facts and tasks - include: systems/main.yml + import_tasks: systems/main.yml - name: Install prerequisites package: name="{{ item }}" state=present diff --git a/roles/client/tasks/systems/main.yml b/roles/client/tasks/systems/main.yml index 85da1ebd..ba24c939 100644 --- a/roles/client/tasks/systems/main.yml +++ b/roles/client/tasks/systems/main.yml @@ -1,13 +1,13 @@ --- -- include: Debian.yml +- include_tasks: Debian.yml when: ansible_distribution == 'Debian' -- include: Ubuntu.yml +- include_tasks: Ubuntu.yml when: ansible_distribution == 'Ubuntu' -- include: CentOS.yml +- include_tasks: CentOS.yml when: ansible_distribution == 'CentOS' -- include: Fedora.yml +- include_tasks: Fedora.yml when: ansible_distribution == 'Fedora' diff --git a/roles/cloud-azure/defaults/main.yml b/roles/cloud-azure/defaults/main.yml new file mode 100644 index 00000000..dbd82f36 --- /dev/null +++ b/roles/cloud-azure/defaults/main.yml @@ -0,0 +1,215 @@ +--- +azure_venv: "{{ playbook_dir }}/configs/.venvs/azure" +_azure_regions: > + [ + { + "displayName": "East Asia", + "latitude": "22.267", + "longitude": "114.188", + "name": "eastasia", + "subscriptionId": null + }, + { + "displayName": "Southeast Asia", + "latitude": "1.283", + "longitude": "103.833", + "name": "southeastasia", + "subscriptionId": null + }, + { + "displayName": "Central US", + "latitude": "41.5908", + "longitude": "-93.6208", + "name": "centralus", + "subscriptionId": null + }, + { + "displayName": "East US", + "latitude": "37.3719", + "longitude": "-79.8164", + "name": "eastus", + "subscriptionId": null + }, + { + "displayName": "East US 2", + "latitude": "36.6681", + "longitude": "-78.3889", + "name": "eastus2", + "subscriptionId": null + }, + { + "displayName": "West US", + "latitude": "37.783", + "longitude": "-122.417", + "name": "westus", + "subscriptionId": null + }, + { + "displayName": "North Central US", + "latitude": "41.8819", + "longitude": "-87.6278", + "name": "northcentralus", + "subscriptionId": null + }, + { + "displayName": "South Central US", + "latitude": "29.4167", + "longitude": "-98.5", + "name": "southcentralus", + "subscriptionId": null + }, + { + "displayName": "North Europe", + "latitude": "53.3478", + "longitude": "-6.2597", + "name": "northeurope", + "subscriptionId": null + }, + { + "displayName": "West Europe", + "latitude": "52.3667", + "longitude": "4.9", + "name": "westeurope", + "subscriptionId": null + }, + { + "displayName": "Japan West", + "latitude": "34.6939", + "longitude": "135.5022", + "name": "japanwest", + "subscriptionId": null + }, + { + "displayName": "Japan East", + "latitude": "35.68", + "longitude": "139.77", + "name": "japaneast", + "subscriptionId": null + }, + { + "displayName": "Brazil South", + "latitude": "-23.55", + "longitude": "-46.633", + "name": "brazilsouth", + "subscriptionId": null + }, + { + "displayName": "Australia East", + "latitude": "-33.86", + "longitude": "151.2094", + "name": "australiaeast", + "subscriptionId": null + }, + { + "displayName": "Australia Southeast", + "latitude": "-37.8136", + "longitude": "144.9631", + "name": "australiasoutheast", + "subscriptionId": null + }, + { + "displayName": "South India", + "latitude": "12.9822", + "longitude": "80.1636", + "name": "southindia", + "subscriptionId": null + }, + { + "displayName": "Central India", + "latitude": "18.5822", + "longitude": "73.9197", + "name": "centralindia", + "subscriptionId": null + }, + { + "displayName": "West India", + "latitude": "19.088", + "longitude": "72.868", + "name": "westindia", + "subscriptionId": null + }, + { + "displayName": "Canada Central", + "latitude": "43.653", + "longitude": "-79.383", + "name": "canadacentral", + "subscriptionId": null + }, + { + "displayName": "Canada East", + "latitude": "46.817", + "longitude": "-71.217", + "name": "canadaeast", + "subscriptionId": null + }, + { + "displayName": "UK South", + "latitude": "50.941", + "longitude": "-0.799", + "name": "uksouth", + "subscriptionId": null + }, + { + "displayName": "UK West", + "latitude": "53.427", + "longitude": "-3.084", + "name": "ukwest", + "subscriptionId": null + }, + { + "displayName": "West Central US", + "latitude": "40.890", + "longitude": "-110.234", + "name": "westcentralus", + "subscriptionId": null + }, + { + "displayName": "West US 2", + "latitude": "47.233", + "longitude": "-119.852", + "name": "westus2", + "subscriptionId": null + }, + { + "displayName": "Korea Central", + "latitude": "37.5665", + "longitude": "126.9780", + "name": "koreacentral", + "subscriptionId": null + }, + { + "displayName": "Korea South", + "latitude": "35.1796", + "longitude": "129.0756", + "name": "koreasouth", + "subscriptionId": null + }, + { + "displayName": "France Central", + "latitude": "46.3772", + "longitude": "2.3730", + "name": "francecentral", + "subscriptionId": null + }, + { + "displayName": "France South", + "latitude": "43.8345", + "longitude": "2.1972", + "name": "francesouth", + "subscriptionId": null + }, + { + "displayName": "Australia Central", + "latitude": "-35.3075", + "longitude": "149.1244", + "name": "australiacentral", + "subscriptionId": null + }, + { + "displayName": "Australia Central 2", + "latitude": "-35.3075", + "longitude": "149.1244", + "name": "australiacentral2", + "subscriptionId": null + } + ] diff --git a/roles/cloud-azure/files/deployment.json b/roles/cloud-azure/files/deployment.json new file mode 100644 index 00000000..646ea8a1 --- /dev/null +++ b/roles/cloud-azure/files/deployment.json @@ -0,0 +1,209 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "AlgoServerName": { + "type": "string" + }, + "sshKeyData": { + "type": "string" + }, + "location": { + "type": "string" + }, + "WireGuardPort": { + "type": "int" + }, + "vmSize": { + "type": "string" + }, + "imageReferenceSku": { + "type": "string" + } + }, + "variables": { + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('AlgoServerName'))]", + "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', parameters('AlgoServerName'))]" + }, + "resources": [ + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/networkSecurityGroups", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "securityRules": [ + { + "name": "AllowSSH", + "properties": { + "description": "Locks inbound down to ssh default port 22.", + "protocol": "Tcp", + "sourcePortRange": "*", + "destinationPortRange": "22", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 100, + "direction": "Inbound" + } + }, + { + "name": "AllowIPSEC500", + "properties": { + "description": "Allow UDP to port 500", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "500", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 110, + "direction": "Inbound" + } + }, + { + "name": "AllowIPSEC4500", + "properties": { + "description": "Allow UDP to port 4500", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "4500", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 120, + "direction": "Inbound" + } + }, + { + "name": "AllowWireGuard", + "properties": { + "description": "Locks inbound down to ssh default port 22.", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "[parameters('WireGuardPort')]", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 130, + "direction": "Inbound" + } + } + ] + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "publicIPAllocationMethod": "Static" + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/virtualNetworks", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.10.0.0/16" + ] + }, + "subnets": [ + { + "name": "[parameters('AlgoServerName')]", + "properties": { + "addressPrefix": "10.10.0.0/24" + } + } + ] + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/networkInterfaces", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkSecurityGroups/', parameters('AlgoServerName'))]", + "[concat('Microsoft.Network/publicIPAddresses/', parameters('AlgoServerName'))]", + "[concat('Microsoft.Network/virtualNetworks/', parameters('AlgoServerName'))]" + ], + "properties": { + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('AlgoServerName'))]" + }, + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('AlgoServerName'))]" + }, + "subnet": { + "id": "[variables('subnet1Ref')]" + } + } + } + ] + } + }, + { + "apiVersion": "2016-04-30-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', parameters('AlgoServerName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "osProfile": { + "computerName": "[parameters('AlgoServerName')]", + "adminUsername": "ubuntu", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "/home/ubuntu/.ssh/authorized_keys", + "keyData": "[parameters('sshKeyData')]" + } + ] + } + } + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "[parameters('imageReferenceSku')]", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('AlgoServerName'))]" + } + ] + } + } + } + ], + "outputs": { + "publicIPAddresses": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',parameters('AlgoServerName')),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", + } + } +} diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 4cf621fa..38adc741 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,141 +1,48 @@ --- - block: - - set_fact: - resource_group: "Algo_{{ region }}" - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + - name: Build python virtual environment + import_tasks: venv.yml - - name: Create a resource group - azure_rm_resourcegroup: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - name: "{{ resource_group }}" - location: "{{ region }}" - tags: - Environment: Algo + - block: + - name: Include prompts + import_tasks: prompts.yml - - name: Create a virtual network - azure_rm_virtualnetwork: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_net - address_prefixes: "10.10.0.0/16" - tags: - Environment: Algo + - set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ azure_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} - - name: Create a security group - azure_rm_securitygroup: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: AlgoSecGroup - purge_rules: yes - rules: - - name: AllowSSH - protocol: Tcp - destination_port_range: 22 - access: Allow - priority: 100 - direction: Inbound - - name: AllowIPSEC500 - protocol: Udp - destination_port_range: 500 - access: Allow - priority: 110 - direction: Inbound - - name: AllowIPSEC4500 - protocol: Udp - destination_port_range: 4500 - access: Allow - priority: 120 - direction: Inbound + - name: Create AlgoVPN Server + azure_rm_deployment: + state: present + deployment_name: "AlgoVPN-{{ algo_server_name }}" + template: "{{ lookup('file', 'deployment.json') }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group_name: "AlgoVPN-{{ algo_server_name }}" + parameters: + AlgoServerName: + value: "{{ algo_server_name }}" + sshKeyData: + value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + location: + value: "{{ algo_region }}" + WireGuardPort: + value: "{{ wireguard_port }}" + vmSize: + value: "{{ cloud_providers.azure.size }}" + imageReferenceSku: + value: "{{ cloud_providers.azure.image }}" + register: azure_rm_deployment - - name: Create a subnet - azure_rm_subnet: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_subnet - address_prefix: "10.10.0.0/24" - virtual_network: algo_net - security_group_name: AlgoSecGroup - tags: - Environment: Algo - - - name: Create an instance - azure_rm_virtualmachine: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - admin_username: ubuntu - virtual_network: algo_net - name: "{{ azure_server_name }}" - ssh_password_enabled: false - vm_size: "{{ cloud_providers.azure.size }}" - tags: - Environment: Algo - ssh_public_keys: - - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } - image: "{{ cloud_providers.azure.image }}" - register: azure_rm_virtualmachine - - # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? - - - set_fact: - ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" - networkinterface_name: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].name }}" - - - name: Ensure the network interface includes all required parameters - azure_rm_networkinterface: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - name: "{{ networkinterface_name }}" - resource_group: "{{ resource_group }}" - virtual_network_name: algo_net - subnet_name: algo_subnet - security_group_name: AlgoSecGroup - - - name: Add the instance to an inventory group - add_host: - name: "{{ ip_address }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: azure - ipv6_support: no - - - set_fact: - cloud_instance_ip: "{{ ip_address }}" - - - name: Ensure the group azure exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[azure]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[azure\]' - regexp: "^{{ cloud_instance_ip }}.*" - line: "{{ cloud_instance_ip }}" + - set_fact: + cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ azure_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml new file mode 100644 index 00000000..28d42521 --- /dev/null +++ b/roles/cloud-azure/tasks/prompts.yml @@ -0,0 +1,70 @@ +--- +- pause: + prompt: | + Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_secret + when: + - azure_secret is undefined + - lookup('env','AZURE_SECRET')|length <= 0 + +- pause: + prompt: | + Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_tenant + when: + - azure_tenant is undefined + - lookup('env','AZURE_TENANT')|length <= 0 + +- pause: + prompt: | + Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_client_id + when: + - azure_client_id is undefined + - lookup('env','AZURE_CLIENT_ID')|length <= 0 + +- pause: + prompt: | + Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_subscription_id + when: + - azure_subscription_id is undefined + - lookup('env','AZURE_SUBSCRIPTION_ID')|length <= 0 + +- set_fact: + secret: "{{ azure_secret | default(_azure_secret.user_input|default(None)) | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(_azure_tenant.user_input|default(None)) | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(_azure_client_id.user_input|default(None)) | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(_azure_subscription_id.user_input|default(None)) | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + +- block: + - name: Set facts about the regions + set_fact: + azure_regions: "{{ _azure_regions|from_json | sort(attribute='name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in azure_regions %} + {%- if r['name'] == "eastus" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + {% for r in azure_regions %} + {{ loop.index }}. {{ r['displayName'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-azure/tasks/venv.yml b/roles/cloud-azure/tasks/venv.yml new file mode 100644 index 00000000..cbadf8de --- /dev/null +++ b/roles/cloud-azure/tasks/venv.yml @@ -0,0 +1,32 @@ +--- +- name: Clean up the environment + file: + dest: "{{ azure_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - packaging + - requests[security] + - azure-mgmt-compute>=2.0.0,<3 + - azure-mgmt-network>=1.3.0,<2 + - azure-mgmt-storage>=1.5.0,<2 + - azure-mgmt-resource>=1.1.0,<2 + - azure-storage>=0.35.1,<0.36 + - azure-cli-core>=2.0.12,<3 + - msrest==0.4.29 + - msrestazure==0.4.31 + - azure-mgmt-dns>=1.0.1,<2 + - azure-mgmt-keyvault>=0.40.0,<0.41 + - azure-mgmt-batch>=4.1.0,<5 + - azure-mgmt-sql>=0.7.1,<0.8 + - azure-mgmt-web>=0.32.0,<0.33 + - azure-mgmt-containerservice>=2.0.0,<3.0.0 + - azure-mgmt-containerregistry>=1.0.1 + - azure-mgmt-rdbms==1.2.0 + - azure-mgmt-containerinstance==0.4.0 + state: latest + virtualenv: "{{ azure_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-digitalocean/defaults/main.yml b/roles/cloud-digitalocean/defaults/main.yml new file mode 100644 index 00000000..34ba5f86 --- /dev/null +++ b/roles/cloud-digitalocean/defaults/main.yml @@ -0,0 +1,2 @@ +--- +digitalocean_venv: "{{ playbook_dir }}/configs/.venvs/digitalocean" diff --git a/roles/cloud-digitalocean/handlers/main.yml b/roles/cloud-digitalocean/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 66308423..488ea2d1 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,106 +1,108 @@ - block: - - name: Set the DigitalOcean Access Token fact - set_fact: - do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN'), true) }}" - public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + - name: Build python virtual environment + import_tasks: venv.yml - block: - - name: "Delete the existing Algo SSH keys" - digital_ocean: - state: absent - command: ssh - api_token: "{{ do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - until: ssh_keys.changed != true - retries: 10 - delay: 1 + - name: Include prompts + import_tasks: prompts.yml - rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes + - name: Set additional facts + set_fact: + algo_do_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ do_regions[_algo_region.user_input | int -1 ]['slug'] }} + {%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - - debug: var=ssh_keys + - block: + - name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - fail: - msg: "Please, ensure that your API token is not read-only." + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes - - name: "Upload the SSH key" - digital_ocean: - state: present - command: ssh - ssh_pub_key: "{{ public_key }}" - api_token: "{{ do_token }}" - name: "{{ SSH_keys.comment }}" - register: do_ssh_key + - debug: var=ssh_keys - - name: "Creating a droplet..." - digital_ocean: - state: present - command: droplet - name: "{{ do_server_name }}" - region_id: "{{ do_region }}" - size_id: "{{ cloud_providers.digitalocean.size }}" - image_id: "{{ cloud_providers.digitalocean.image }}" - ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" - unique_name: yes - api_token: "{{ do_token }}" - ipv6: yes - register: do + - fail: + msg: "Please, ensure that your API token is not read-only." - - name: Add the droplet to an inventory group - add_host: - name: "{{ do.droplet.ip_address }}" - groups: vpn-host - ansible_ssh_user: root - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - do_access_token: "{{ do_token }}" - do_droplet_id: "{{ do.droplet.id }}" - cloud_provider: digitalocean - ipv6_support: true + - name: "Upload the SSH key" + digital_ocean: + state: present + command: ssh + ssh_pub_key: "{{ public_key }}" + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: do_ssh_key - - set_fact: - cloud_instance_ip: "{{ do.droplet.ip_address }}" + - name: "Creating a droplet..." + digital_ocean: + state: present + command: droplet + name: "{{ algo_server_name }}" + region_id: "{{ algo_do_region }}" + size_id: "{{ cloud_providers.digitalocean.size }}" + image_id: "{{ cloud_providers.digitalocean.image }}" + ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" + unique_name: yes + api_token: "{{ algo_do_token }}" + ipv6: yes + register: do - - name: Tag the droplet - digital_ocean_tag: - name: "Environment:Algo" - resource_id: "{{ do.droplet.id }}" - api_token: "{{ do_token }}" - state: present + - set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" + ansible_ssh_user: root - - name: Get droplets - uri: - url: "https://api.digitalocean.com/v2/droplets?tag_name=Environment:Algo" - method: GET - status_code: 200 - headers: - Content-Type: "application/json" - Authorization: "Bearer {{ do_token }}" - register: do_droplets + - name: Tag the droplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ algo_do_token }}" + state: present - - name: Ensure the group digitalocean exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[digitalocean]' + - block: + - name: "Delete the new Algo SSH key" + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[digitalocean\]' - regexp: "^{{ item.networks.v4[0].ip_address }}.*" - line: "{{ item.networks.v4[0].ip_address }}" - with_items: - - "{{ do_droplets.json.droplets }}" + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + + - fail: + msg: "Please, ensure that your API token is not read-only." + environment: + PYTHONPATH: "{{ digitalocean_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-digitalocean/tasks/prompts.yml b/roles/cloud-digitalocean/tasks/prompts.yml new file mode 100644 index 00000000..f2804ca8 --- /dev/null +++ b/roles/cloud-digitalocean/tasks/prompts.yml @@ -0,0 +1,46 @@ +--- +- pause: + prompt: | + Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): + echo: false + register: _do_token + when: + - do_token is undefined + - lookup('env','DO_API_TOKEN')|length <= 0 + +- name: Set the token as a fact + set_fact: + algo_do_token: "{{ do_token | default(_do_token.user_input|default(None)) | default(lookup('env','DO_API_TOKEN'), true) }}" + +- name: Get regions + uri: + url: https://api.digitalocean.com/v2/regions + method: GET + status_code: 200 + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ algo_do_token }}" + register: _do_regions + +- name: Set facts about thre regions + set_fact: + do_regions: "{{ _do_regions.json.regions | sort(attribute='slug') }}" + +- name: Set default region + set_fact: + default_region: >- + {% for r in do_regions %} + {%- if r['slug'] == "nyc3" %}{{ loop.index }}{% endif %} + {%- endfor %} + +- pause: + prompt: | + What region should the server be located in? + {% for r in do_regions %} + {{ loop.index }}. {{ r['slug'] }} {{ r['name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-digitalocean/tasks/venv.yml b/roles/cloud-digitalocean/tasks/venv.yml new file mode 100644 index 00000000..80e85b9f --- /dev/null +++ b/roles/cloud-digitalocean/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ digitalocean_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: dopy + version: 0.3.5 + virtualenv: "{{ digitalocean_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 b/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 deleted file mode 100644 index 7db27bbb..00000000 --- a/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 +++ /dev/null @@ -1,6 +0,0 @@ -iface eth0 inet6 static - address {{ item.ip_address }} - netmask {{ item.netmask }} - gateway {{ item.gateway }} - autoconf 0 - dns-nameservers 2001:4860:4860::8844 2001:4860:4860::8888 diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 045fe455..12b3f19d 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -1,5 +1,7 @@ --- - +ami_search_encrypted: omit +encrypted: "{{ cloud_providers.ec2.encrypted }}" ec2_vpc_nets: cidr_block: 172.16.0.0/16 subnet_cidr: 172.16.254.0/23 +ec2_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-ec2/templates/stack.yml.j2 b/roles/cloud-ec2/files/stack.yml similarity index 76% rename from roles/cloud-ec2/templates/stack.yml.j2 rename to roles/cloud-ec2/files/stack.yml index 694386f8..3660613b 100644 --- a/roles/cloud-ec2/templates/stack.yml.j2 +++ b/roles/cloud-ec2/files/stack.yml @@ -1,13 +1,21 @@ --- - AWSTemplateFormatVersion: '2010-09-09' Description: 'Algo VPN stack' +Parameters: + InstanceTypeParameter: + Type: String + Default: t2.micro + PublicSSHKeyParameter: + Type: String + ImageIdParameter: + Type: String + WireGuardPort: + Type: String Resources: - VPC: Type: AWS::EC2::VPC Properties: - CidrBlock: {{ ec2_vpc_nets.cidr_block }} + CidrBlock: 172.16.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default @@ -35,7 +43,7 @@ Resources: Subnet: Type: AWS::EC2::Subnet Properties: - CidrBlock: {{ ec2_vpc_nets.subnet_cidr }} + CidrBlock: 172.16.254.0/23 MapPublicIpOnLaunch: false Tags: - Key: Environment @@ -126,6 +134,10 @@ Resources: FromPort: '4500' ToPort: '4500' CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: !Ref WireGuardPort + ToPort: !Ref WireGuardPort + CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: Algo @@ -141,43 +153,32 @@ Resources: Metadata: AWS::CloudFormation::Init: config: - users: - ubuntu: - groups: - - "sudo" - homeDir: "/home/ubuntu/" files: /home/ubuntu/.ssh/authorized_keys: - content: {{ lookup('file', SSH_keys.public) }} + content: + Ref: PublicSSHKeyParameter mode: "000644" owner: "ubuntu" group: "ubuntu" Properties: - InstanceType: {{ cloud_providers.ec2.size }} + InstanceType: + Ref: InstanceTypeParameter InstanceInitiatedShutdownBehavior: terminate SecurityGroupIds: - Ref: InstanceSecurityGroup - ImageId: {{ ami_image }} + ImageId: + Ref: ImageIdParameter SubnetId: !Ref Subnet Ipv6AddressCount: 1 UserData: "Fn::Base64": !Sub | #!/bin/bash -xe - # http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-migrate-ipv6.html - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1013597 - cat < /etc/network/interfaces.d/60-default-with-ipv6.cfg - iface eth0 inet6 dhcp - up sysctl net.ipv6.conf.\$IFACE.accept_ra=2 - pre-down ip link set dev \$IFACE up - EOF - ifdown eth0; ifup eth0 - dhclient -6 apt-get update - apt-get -y install python-setuptools - easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz - cfn-init -v --stack {{ stack_name }} --resource EC2Instance --region {{ region }} - cfn-signal -e $? --stack {{ stack_name }} --resource EC2Instance --region {{ region }} + apt-get -y install python-pip + pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz + cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} + cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} Tags: - Key: Name Value: Algo diff --git a/roles/cloud-ec2/handlers/main.yml b/roles/cloud-ec2/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml index 1f24b007..27977203 100644 --- a/roles/cloud-ec2/tasks/cloudformation.yml +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -1,18 +1,17 @@ --- - -- name: Make a cloudformation template - template: - src: stack.yml.j2 - dest: "configs/{{ aws_server_name }}.yml" - - name: Deploy the template cloudformation: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" stack_name: "{{ stack_name }}" state: "present" - region: "{{ region }}" - template: "configs/{{ aws_server_name }}.yml" + region: "{{ algo_region }}" + template: roles/cloud-ec2/files/stack.yml + template_parameters: + InstanceTypeParameter: "{{ cloud_providers.ec2.size }}" + PublicSSHKeyParameter: "{{ lookup('file', SSH_keys.public) }}" + ImageIdParameter: "{{ ami_image }}" + WireGuardPort: "{{ wireguard_port }}" tags: Environment: Algo - register: stack \ No newline at end of file + register: stack diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml index 11779ea4..967e274d 100644 --- a/roles/cloud-ec2/tasks/encrypt_image.yml +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -1,37 +1,27 @@ +--- - name: Check if the encrypted image already exist - ec2_ami_find: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" - owner: self - sort: creationDate - sort_order: descending - sort_end: 1 - state: available - ami_tags: - Algo: "encrypted" - region: "{{ region }}" + ec2_ami_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + owners: self + region: "{{ algo_region }}" + filters: + state: available + "tag:Algo": encrypted register: search_crypt -- set_fact: - ami_image: "{{ search_crypt.results[0].ami_id }}" - when: search_crypt.results - - name: Copy to an encrypted image ec2_ami_copy: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" encrypted: yes name: algo kms_key_id: "{{ kms_key_id | default(omit) }}" - region: "{{ region }}" - source_image_id: "{{ ami_image }}" - source_region: "{{ region }}" + region: "{{ algo_region }}" + source_image_id: "{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}" + source_region: "{{ algo_region }}" + wait: true tags: Algo: "encrypted" - wait: true - register: enc_image - when: not search_crypt.results - -- set_fact: - ami_image: "{{ enc_image.image_id }}" - when: not search_crypt.results + register: ami_search_encrypted + when: search_crypt.images|length|int == 0 diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index e32e70a5..ea3a67a4 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,67 +1,46 @@ - block: - - set_fact: - access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" - secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" - stack_name: "{{ aws_server_name | replace('.', '-') }}" + - name: Build python virtual environment + import_tasks: venv.yml - - name: Locate official AMI for region - ec2_ami_find: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" - owner: "{{ cloud_providers.ec2.image.owner }}" - sort: creationDate - sort_order: descending - sort_end: 1 - region: "{{ region }}" - register: ami_search + - block: + - name: Include prompts + import_tasks: prompts.yml - - set_fact: - ami_image: "{{ ami_search.results[0].ami_id }}" + - set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ aws_regions[_algo_region.user_input | int -1 ]['region_name'] }} + {%- else %}{{ aws_regions[default_region | int - 1]['region_name'] }}{% endif %} + stack_name: "{{ algo_server_name | replace('.', '-') }}" - - include: encrypt_image.yml - tags: [encrypted] + - name: Locate official AMI for region + ec2_ami_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + owners: "{{ cloud_providers.ec2.image.owner }}" + region: "{{ algo_region }}" + filters: + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" + register: ami_search - - include: cloudformation.yml + - import_tasks: encrypt_image.yml + when: encrypted - - name: Add new instance to host group - add_host: - hostname: "{{ stack.stack_outputs.ElasticIP }}" - groupname: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: ec2 - ipv6_support: yes + - name: Set the ami id as a fact + set_fact: + ami_image: >- + {% if ami_search_encrypted.image_id is defined %}{{ ami_search_encrypted.image_id }} + {%- elif search_crypt.images is defined and search_crypt.images|length >= 1 %}{{ (search_crypt.images | sort(attribute='creation_date') | last)['image_id'] }} + {%- else %}{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}{% endif %} - - set_fact: - cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" + - name: Deploy the stack + import_tasks: cloudformation.yml - - name: Get EC2 instances - ec2_remote_facts: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - region: "{{ region }}" - filters: - instance-state-name: running - "tag:Environment": Algo - register: algo_instances - - - name: Ensure the group ec2 exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[ec2]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[ec2\]' - regexp: "^{{ item.public_ip_address }}.*" - line: "{{ item.public_ip_address }}" - with_items: - - "{{ algo_instances.instances }}" + - set_fact: + cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ ec2_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-ec2/tasks/prompts.yml b/roles/cloud-ec2/tasks/prompts.yml new file mode 100644 index 00000000..2993f694 --- /dev/null +++ b/roles/cloud-ec2/tasks/prompts.yml @@ -0,0 +1,55 @@ +--- +- pause: + prompt: | + Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md) + echo: false + register: _aws_access_key + when: + - aws_access_key is undefined + - lookup('env','AWS_ACCESS_KEY_ID')|length <= 0 + +- pause: + prompt: | + Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + echo: false + register: _aws_secret_key + when: + - aws_secret_key is undefined + - lookup('env','AWS_SECRET_ACCESS_KEY')|length <= 0 + +- set_fact: + access_key: "{{ aws_access_key | default(_aws_access_key.user_input|default(None)) | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(_aws_secret_key.user_input|default(None)) | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + +- block: + - name: Get regions + aws_region_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + region: us-east-1 + register: _aws_regions + + - name: Set facts about the regions + set_fact: + aws_regions: "{{ _aws_regions.regions | sort(attribute='region_name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in aws_regions %} + {%- if r['region_name'] == "us-east-1" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + {% for r in aws_regions %} + {{ loop.index }}. {{ r['region_name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-ec2/tasks/venv.yml b/roles/cloud-ec2/tasks/venv.yml new file mode 100644 index 00000000..be2eeced --- /dev/null +++ b/roles/cloud-ec2/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ ec2_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ ec2_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-gce/defaults/main.yml b/roles/cloud-gce/defaults/main.yml new file mode 100644 index 00000000..d771cc8f --- /dev/null +++ b/roles/cloud-gce/defaults/main.yml @@ -0,0 +1,2 @@ +--- +gce_venv: "{{ playbook_dir }}/configs/.venvs/gce" diff --git a/roles/cloud-gce/handlers/main.yml b/roles/cloud-gce/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index f198b7ab..e04b3d80 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,69 +1,60 @@ - block: - - set_fact: - credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" - ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + - name: Build python virtual environment + import_tasks: venv.yml - - set_fact: - credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" + - block: + - name: Include prompts + import_tasks: prompts.yml - - set_fact: - service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" - project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" - server_name: "{{ gce_server_name | replace('_', '-') }}" + - name: Network configured + gce_net: + name: "algo-net-{{ algo_server_name }}" + fwname: "algo-net-{{ algo_server_name }}-fw" + allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" + state: "present" + mode: auto + src_range: 0.0.0.0/0 + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" - - name: Network configured - gce_net: - name: "algo-net-{{ server_name }}" - fwname: "algo-net-{{ server_name }}-fw" - allowed: "udp:500,4500;tcp:22" - state: "present" - mode: auto - src_range: 0.0.0.0/0 - service_account_email: "{{ credentials_file_lookup.client_email }}" - credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + - block: + - name: External IP allocated + gce_eip: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + name: "{{ algo_server_name }}" + region: "{{ algo_region.split('-')[0:2] | join('-') }}" + state: present + register: gce_eip - - name: "Creating a new instance..." - gce: - instance_names: "{{ server_name }}" - zone: "{{ zone }}" - machine_type: "{{ cloud_providers.gce.size }}" - image: "{{ cloud_providers.gce.image }}" - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" - metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ server_name }}" - tags: - - "environment-algo" - register: google_vm + - name: Set External IP as a fact + set_fact: + external_ip: "{{ gce_eip.address }}" + when: cloud_providers.gce.external_static_ip - - name: Add the instance to an inventory group - add_host: - name: "{{ google_vm.instance_data[0].public_ip }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: gce - ipv6_support: no + - name: "Creating a new instance..." + gce: + instance_names: "{{ algo_server_name }}" + zone: "{{ algo_region }}" + external_ip: "{{ external_ip | default('ephemeral') }}" + machine_type: "{{ cloud_providers.gce.size }}" + image: "{{ cloud_providers.gce.image }}" + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' + network: "algo-net-{{ algo_server_name }}" + tags: + - "environment-algo" + register: google_vm - - set_fact: - cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" - - - name: Ensure the group gce exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[gce]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[gce\]' - regexp: "^{{ google_vm.instance_data[0].public_ip }}.*" - line: "{{ google_vm.instance_data[0].public_ip }}" + - set_fact: + cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ gce_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-gce/tasks/prompts.yml b/roles/cloud-gce/tasks/prompts.yml new file mode 100644 index 00000000..b054cc9e --- /dev/null +++ b/roles/cloud-gce/tasks/prompts.yml @@ -0,0 +1,67 @@ +--- +- pause: + prompt: | + Enter the local path to your credentials JSON file + (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts) + register: _gce_credentials_file + when: + - gce_credentials_file is undefined + - lookup('env','GCE_CREDENTIALS_FILE_PATH')|length <= 0 + +- set_fact: + credentials_file_path: "{{ gce_credentials_file | default(_gce_credentials_file.user_input|default(None)) | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + +- set_fact: + credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" + +- set_fact: + service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" + project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + +- block: + - name: Get regions + gce_region_facts: + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ credentials_file_lookup.project_id }}" + register: _gce_regions + + - name: Set facts about the regions + set_fact: + gce_regions: >- + [{%- for region in _gce_regions.results.regions | sort(attribute='name') -%} + {% if region.status == "UP" %} + {% for zone in region.zones | sort(attribute='name') %} + {% if zone.status == "UP" %} + '{{ zone.name }}' + {% endif %}{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %}{% if not loop.last %},{% endif %} + {%- endfor -%}] + + - name: Set facts about the default region + set_fact: + default_region: >- + {% for region in gce_regions %} + {%- if region == "us-east1-b" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://cloud.google.com/compute/docs/regions-zones/) + {% for r in gce_regions %} + {{ loop.index }}. {{ r }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _gce_region + when: region is undefined + +- set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _gce_region.user_input is defined and _gce_region.user_input != "" %}{{ gce_regions[_gce_region.user_input | int -1 ] }} + {%- else %}{{ gce_regions[default_region | int - 1] }}{% endif %} diff --git a/roles/cloud-gce/tasks/venv.yml b/roles/cloud-gce/tasks/venv.yml new file mode 100644 index 00000000..078efe5b --- /dev/null +++ b/roles/cloud-gce/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ gce_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - apache-libcloud + - pycrypto + state: latest + virtualenv: "{{ gce_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-lightsail/defaults/main.yml b/roles/cloud-lightsail/defaults/main.yml new file mode 100644 index 00000000..06ae0ee9 --- /dev/null +++ b/roles/cloud-lightsail/defaults/main.yml @@ -0,0 +1,2 @@ +--- +lightsail_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml new file mode 100644 index 00000000..21e3d459 --- /dev/null +++ b/roles/cloud-lightsail/tasks/main.yml @@ -0,0 +1,50 @@ +- block: + - name: Build python virtual environment + import_tasks: venv.yml + + - block: + - name: Include prompts + import_tasks: prompts.yml + + - name: Create an instance + lightsail: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + name: "{{ algo_server_name }}" + state: present + region: "{{ algo_region }}" + zone: "{{ algo_region }}a" + blueprint_id: "{{ cloud_providers.lightsail.image }}" + bundle_id: "{{ cloud_providers.lightsail.size }}" + wait_timeout: 300 + open_ports: + - from_port: 4500 + to_port: 4500 + protocol: udp + - from_port: 500 + to_port: 500 + protocol: udp + - from_port: "{{ wireguard_port }}" + to_port: "{{ wireguard_port }}" + protocol: udp + user_data: | + #!/bin/bash + mkdir -p /home/ubuntu/.ssh/ + echo "{{ lookup('file', '{{ SSH_keys.public }}') }}" >> /home/ubuntu/.ssh/authorized_keys + chown -R ubuntu: /home/ubuntu/.ssh/ + chmod 0700 /home/ubuntu/.ssh/ + chmod 0600 /home/ubuntu/.ssh/* + test + register: algo_instance + + - set_fact: + cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ lightsail_venv }}/lib/python2.7/site-packages/" + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml new file mode 100644 index 00000000..1c98c5ac --- /dev/null +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -0,0 +1,61 @@ +--- +- pause: + prompt: | + Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md) + echo: false + register: _aws_access_key + when: + - aws_access_key is undefined + - lookup('env','AWS_ACCESS_KEY_ID')|length <= 0 + +- pause: + prompt: | + Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + echo: false + register: _aws_secret_key + when: + - aws_secret_key is undefined + - lookup('env','AWS_SECRET_ACCESS_KEY')|length <= 0 + +- set_fact: + access_key: "{{ aws_access_key | default(_aws_access_key.user_input|default(None)) | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(_aws_secret_key.user_input|default(None)) | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + +- block: + - name: Get regions + lightsail_region_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + region: us-east-1 + register: _lightsail_regions + + - name: Set facts about the regions + set_fact: + lightsail_regions: "{{ _lightsail_regions.results.regions | sort(attribute='name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in lightsail_regions %} + {%- if r['name'] == "us-east-1" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) + {% for r in lightsail_regions %} + {{ (loop.index|string + '.').ljust(3) }} {{ r['name'].ljust(20) }} {{ r['displayName'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined + +- set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ lightsail_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ lightsail_regions[default_region | int - 1]['name'] }}{% endif %} diff --git a/roles/cloud-lightsail/tasks/venv.yml b/roles/cloud-lightsail/tasks/venv.yml new file mode 100644 index 00000000..9816fea1 --- /dev/null +++ b/roles/cloud-lightsail/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ lightsail_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ lightsail_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-openstack/defaults/main.yml b/roles/cloud-openstack/defaults/main.yml new file mode 100644 index 00000000..3bec06b2 --- /dev/null +++ b/roles/cloud-openstack/defaults/main.yml @@ -0,0 +1,2 @@ +--- +openstack_venv: "{{ playbook_dir }}/configs/.venvs/openstack" diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml new file mode 100644 index 00000000..75b3db6d --- /dev/null +++ b/roles/cloud-openstack/tasks/main.yml @@ -0,0 +1,89 @@ +--- +- fail: + msg: "OpenStack credentials are not set. Download it from the OpenStack dashboard->Compute->API Access and source it in the shell (eg: source /tmp/dhc-openrc.sh)" + when: lookup('env', 'OS_AUTH_URL') == "" + +- block: + - name: Build python virtual environment + import_tasks: venv.yml + + - block: + - name: Security group created + os_security_group: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}-security_group" + description: AlgoVPN security group + register: os_security_group + + - name: Security rules created + os_security_group_rule: + state: "{{ state|default('present') }}" + security_group: "{{ os_security_group.id }}" + protocol: "{{ item.proto }}" + port_range_min: "{{ item.port_min }}" + port_range_max: "{{ item.port_max }}" + remote_ip_prefix: "{{ item.range }}" + with_items: + - { proto: tcp, port_min: 22, port_max: 22, range: 0.0.0.0/0 } + - { proto: icmp, port_min: -1, port_max: -1, range: 0.0.0.0/0 } + - { proto: udp, port_min: 4500, port_max: 4500, range: 0.0.0.0/0 } + - { proto: udp, port_min: 500, port_max: 500, range: 0.0.0.0/0 } + - { proto: udp, port_min: "{{ wireguard_port }}", port_max: "{{ wireguard_port }}", range: 0.0.0.0/0 } + + - name: Keypair created + os_keypair: + state: "{{ state|default('present') }}" + name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" + public_key_file: "{{ SSH_keys.public }}" + register: os_keypair + + - name: Gather facts about flavors + os_flavor_facts: + ram: "{{ cloud_providers.openstack.flavor_ram }}" + + - name: Gather facts about images + os_image_facts: + image: "{{ cloud_providers.openstack.image }}" + + - name: Gather facts about public networks + os_networks_facts: + + - name: Set the network as a fact + set_fact: + public_network_id: "{{ item.id }}" + when: + - item['router:external']|default(omit) + - item['admin_state_up']|default(omit) + - item['status'] == 'ACTIVE' + with_items: "{{ openstack_networks }}" + + - name: Set facts + set_fact: + flavor_id: "{{ (openstack_flavors | sort(attribute='ram'))[0]['id'] }}" + image_id: "{{ openstack_image['id'] }}" + keypair_name: "{{ os_keypair.key.name }}" + security_group_name: "{{ os_security_group['secgroup']['name'] }}" + + - name: Server created + os_server: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}" + image: "{{ image_id }}" + flavor: "{{ flavor_id }}" + key_name: "{{ keypair_name }}" + security_groups: "{{ security_group_name }}" + nics: + - net-id: "{{ public_network_id }}" + register: os_server + + - set_fact: + cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ openstack_venv }}/lib/python2.7/site-packages/" + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-openstack/tasks/venv.yml b/roles/cloud-openstack/tasks/venv.yml new file mode 100644 index 00000000..e2c4f86a --- /dev/null +++ b/roles/cloud-openstack/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ openstack_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: shade + state: latest + virtualenv: "{{ openstack_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-scaleway/defaults/main.yml b/roles/cloud-scaleway/defaults/main.yml new file mode 100644 index 00000000..00c1dc10 --- /dev/null +++ b/roles/cloud-scaleway/defaults/main.yml @@ -0,0 +1,4 @@ +--- +scaleway_regions: + - alias: par1 + - alias: ams1 diff --git a/roles/cloud-scaleway/tasks/image_facts.yml b/roles/cloud-scaleway/tasks/image_facts.yml new file mode 100644 index 00000000..41269845 --- /dev/null +++ b/roles/cloud-scaleway/tasks/image_facts.yml @@ -0,0 +1,10 @@ +--- +- name: Set image id as a fact + set_fact: + image_id: "{{ item.id }}" + no_log: true + when: + - cloud_providers.scaleway.image == item.name + - cloud_providers.scaleway.arch == item.arch + - server_disk_size == item.root_volume.size + with_items: "{{ outer_item['json']['images'] }}" diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml new file mode 100644 index 00000000..87ec1d7f --- /dev/null +++ b/roles/cloud-scaleway/tasks/main.yml @@ -0,0 +1,140 @@ +- block: + - name: Include prompts + import_tasks: prompts.yml + + - name: Set disk size + set_fact: + server_disk_size: 50000000000 + + - name: Check server size + set_fact: + server_disk_size: 25000000000 + when: cloud_providers.scaleway.size == "START1-XS" + + - name: Check if server exists + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + status_code: 200 + register: scaleway_servers + + - name: Set server id as a fact + set_fact: + server_id: "{{ item.id }}" + no_log: true + when: algo_server_name == item.name + with_items: "{{ scaleway_servers.json.servers }}" + + - name: Create a server if it doesn't exist + block: + - name: Get the organization id + uri: + url: https://account.cloud.online.net/organizations + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + status_code: 200 + register: scaleway_organizations + + - name: Set organization id as a fact + set_fact: + organization_id: "{{ item.id }}" + no_log: true + when: algo_scaleway_org == item.name + with_items: "{{ scaleway_organizations.json.organizations }}" + + - name: Get total count of images + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/images" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + status_code: 200 + register: scaleway_pages + + - name: Get images + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/images?per_page=100&page={{ item }}" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + status_code: 200 + register: scaleway_images + with_sequence: start=1 end={{ ((scaleway_pages.x_total_count|int / 100)| round )|int }} + + - name: Set image id as a fact + include_tasks: image_facts.yml + with_items: "{{ scaleway_images['results'] }}" + loop_control: + loop_var: outer_item + + - name: Create a server + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers/" + method: POST + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + body: + organization: "{{ organization_id }}" + name: "{{ algo_server_name }}" + image: "{{ image_id }}" + commercial_type: "{{cloud_providers.scaleway.size }}" + enable_ipv6: true + boot_type: local + tags: + - Environment:Algo + - AUTHORIZED_KEY={{ lookup('file', SSH_keys.public)|regex_replace(' ', '_') }} + status_code: 201 + body_format: json + register: algo_instance + + - name: Set server id as a fact + set_fact: + server_id: "{{ algo_instance.json.server.id }}" + when: server_id is not defined + + - name: Power on the server + uri: + url: https://cp-{{ algo_region }}.scaleway.com/servers/{{ server_id }}/action + method: POST + headers: + Content-Type: application/json + X-Auth-Token: "{{ algo_scaleway_token }}" + body: + action: poweron + status_code: 202 + body_format: json + ignore_errors: true + no_log: true + + - name: Wait for the server to become running + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers/{{ server_id }}" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ algo_scaleway_token }}" + status_code: 200 + until: + - algo_instance.json.server.state is defined + - algo_instance.json.server.state == "running" + retries: 20 + delay: 30 + register: algo_instance + + - set_fact: + cloud_instance_ip: "{{ algo_instance['json']['server']['public_ip']['address'] }}" + ansible_ssh_user: root + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml new file mode 100644 index 00000000..22c3f1aa --- /dev/null +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -0,0 +1,34 @@ +--- +- pause: + prompt: | + Enter your auth token (https://www.scaleway.com/docs/generate-an-api-token/) + echo: false + register: _scaleway_token + when: scaleway_token is undefined + +- pause: + prompt: | + Enter your organization name (https://cloud.scaleway.com/#/billing) + register: _scaleway_org + when: scaleway_org is undefined + +- pause: + prompt: | + What region should the server be located in? + {% for r in scaleway_regions %} + {{ loop.index }}. {{ r['alias'] }} + {% endfor %} + + Enter the number of your desired region + [{{ scaleway_regions.0.alias }}] + register: _algo_region + when: region is undefined + +- name: Set scaleway facts + set_fact: + algo_scaleway_token: "{{ scaleway_token | default(_scaleway_token.user_input) }}" + algo_scaleway_org: "{{ scaleway_org | default(_scaleway_org.user_input|default(omit)) }}" + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ scaleway_regions[_algo_region.user_input | int -1 ]['alias'] }} + {%- else %}{{ scaleway_regions.0.alias }}{% endif %} diff --git a/roles/cloud-vultr/tasks/main.yml b/roles/cloud-vultr/tasks/main.yml new file mode 100644 index 00000000..78e514d0 --- /dev/null +++ b/roles/cloud-vultr/tasks/main.yml @@ -0,0 +1,36 @@ +- block: + - name: Include prompts + import_tasks: prompts.yml + + - name: Upload the SSH key + vr_ssh_key: + name: "{{ SSH_keys.comment }}" + ssh_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + register: ssh_key + + - name: Creating a server + vr_server: + name: "{{ algo_server_name }}" + hostname: "{{ algo_server_name }}" + os: "{{ cloud_providers.vultr.os }}" + plan: "{{ cloud_providers.vultr.size }}" + region: "{{ algo_vultr_region }}" + state: started + tag: Environment:Algo + ssh_key: "{{ ssh_key.vultr_ssh_key.name }}" + ipv6_enabled: true + auto_backup_enabled: false + notify_activate: false + register: vultr_server + + - set_fact: + cloud_instance_ip: "{{ vultr_server.vultr_server.v4_main_ip }}" + ansible_ssh_user: root + + environment: + VULTR_API_CONFIG: "{{ algo_vultr_config }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml new file mode 100644 index 00000000..84e0cfd9 --- /dev/null +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -0,0 +1,56 @@ +--- +- pause: + prompt: | + Enter the local path to your configuration INI file + (https://github.com/trailofbits/algo/docs/cloud-vultr.md): + register: _vultr_config + when: vultr_config is undefined + +- name: Set the token as a fact + set_fact: + algo_vultr_config: "{{ vultr_config | default(_vultr_config.user_input) | default(lookup('env','VULTR_API_CONFIG'), true) }}" + +- name: Get regions + uri: + url: https://api.vultr.com/v1/regions/list + method: GET + status_code: 200 + register: _vultr_regions + +- name: Format regions + set_fact: + regions: >- + [ {% for k, v in _vultr_regions.json.items() %} + {{ v }}{% if not loop.last %},{% endif %} + {% endfor %} ] + +- name: Set regions as a fact + set_fact: + vultr_regions: "{{ regions | sort(attribute='country') }}" + +- name: Set default region + set_fact: + default_region: >- + {% for r in vultr_regions %} + {%- if r['DCID'] == "1" %}{{ loop.index }}{% endif %} + {%- endfor %} + +- pause: + prompt: | + What region should the server be located in? + (https://www.vultr.com/locations/): + {% for r in vultr_regions %} + {{ loop.index }}. {{ r['name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined + +- name: Set the desired region as a fact + set_fact: + algo_vultr_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ vultr_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ vultr_regions[default_region | int - 1]['name'] }}{% endif %} diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 00000000..f358d3e1 --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,2 @@ +--- +install_headers: true diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 2272403c..1415245e 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -7,8 +7,11 @@ - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush -- name: restart loopback - shell: ifdown lo:100 && ifup lo:100 +- name: restart systemd-networkd + systemd: + name: systemd-networkd + state: restarted + daemon_reload: true - name: restart loopback bsd shell: > diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml new file mode 100644 index 00000000..29ee3f55 --- /dev/null +++ b/roles/common/tasks/facts.yml @@ -0,0 +1,30 @@ +--- +- block: + - name: Generate password for the CA key + local_action: + module: shell + openssl rand -hex 16 + register: CA_password + + - name: Generate p12 export password + local_action: + module: shell + openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' + register: p12_password_generated + when: p12_password is not defined + tags: update-users + become: false + +- name: Define facts + set_fact: + p12_export_password: "{{ p12_password|default(p12_password_generated.stdout) }}" + tags: update-users + +- set_fact: + CA_password: "{{ CA_password.stdout }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Set IPv6 support as a fact + set_fact: + ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" + tags: always diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 67d247d8..9f200189 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,6 +1,15 @@ --- - - set_fact: + config_prefix: "/usr/local/" + strongswan_shell: /usr/sbin/nologin + strongswan_home: /var/empty + root_group: wheel + ssh_service_name: sshd + apparmor_enabled: false + strongswan_additional_plugins: + - kernel-pfroute + - kernel-pfkey + ansible_python_interpreter: /usr/local/bin/python2.7 tools: - git - subversion @@ -17,6 +26,15 @@ tags: - always +- setup: + +- name: Install tools + package: name="{{ item }}" state=present + with_items: + - "{{ tools|default([]) }}" + tags: + - always + - name: Loopback included into the rc config blockinfile: dest: /etc/rc.conf @@ -24,7 +42,7 @@ block: | cloned_interfaces="lo100" ifconfig_lo100="inet {{ local_service_ip }} netmask 255.255.255.255" - ifconfig_lo100="inet6 FCAA::1/64" + ifconfig_lo100_ipv6="inet6 FCAA::1/64" notify: - restart loopback bsd tags: diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 781930e2..73e6783f 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,26 +1,26 @@ --- - block: - - include: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - name: Check the system + raw: uname -a + register: OS - - include: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - include_tasks: ubuntu.yml + when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' - - name: Install tools - package: name="{{ item }}" state=present - with_items: - - "{{ tools|default([]) }}" - tags: - - always + - include_tasks: freebsd.yml + when: '"FreeBSD" in OS.stdout' - - name: Sysctl tuning - sysctl: name="{{ item.item }}" value="{{ item.value }}" - with_items: - - "{{ sysctl|default([]) }}" - tags: - - always + - name: Gather additional facts + import_tasks: facts.yml - - meta: flush_handlers + - name: Sysctl tuning + sysctl: name="{{ item.item }}" value="{{ item.value }}" + with_items: + - "{{ sysctl|default([]) }}" + tags: + - always + + - meta: flush_handlers rescue: - debug: var=fail_hint tags: always diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index b512af61..9c6e6a5b 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -1,60 +1,88 @@ --- +- block: + - name: Ubuntu | Install prerequisites + apt: + name: "{{ item }}" + update_cache: true + with_items: + - python2.7 + - sudo -- name: Install software updates - apt: update_cache=yes upgrade=dist - tags: - - cloud + - name: Ubuntu | Configure defaults + alternatives: + name: python + link: /usr/bin/python + path: /usr/bin/python2.7 + priority: 1 + tags: + - update-alternatives + vars: + ansible_python_interpreter: /usr/bin/python3 -- name: Check if reboot is required - shell: > - if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi - args: - executable: /bin/bash - register: reboot_required - tags: - - cloud +- name: Gather facts + setup: -- name: Reboot - shell: sleep 2 && shutdown -r now "Ansible updates triggered" - async: 1 - poll: 0 - when: reboot_required is defined and reboot_required.stdout == 'required' - ignore_errors: true - tags: - - cloud +- name: Cloud only tasks + block: + - name: Install software updates + apt: + update_cache: true + install_recommends: true + upgrade: dist -- name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ inventory_hostname }}" - search_regex: OpenSSH - delay: 10 - timeout: 320 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - tags: - - cloud + - name: Check if reboot is required + shell: > + if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi + args: + executable: /bin/bash + register: reboot_required + + - name: Reboot + shell: sleep 2 && shutdown -r now "Ansible updates triggered" + async: 1 + poll: 0 + when: reboot_required is defined and reboot_required.stdout == 'required' + ignore_errors: true + + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ inventory_hostname }}" + search_regex: OpenSSH + delay: 10 + timeout: 320 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false + when: algo_provider != "local" + +- name: Include unatteded upgrades configuration + import_tasks: unattended-upgrades.yml - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" with_items: - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } - tags: - - cloud - name: Loopback for services configured - template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg + template: + src: 10-algo-lo100.network.j2 + dest: /etc/systemd/network/10-algo-lo100.network notify: - - restart loopback + - restart systemd-networkd tags: - always -- name: Loopback included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present - notify: - - restart loopback +- name: systemd services enabled and started + systemd: + name: "{{ item }}" + state: started + enabled: true + daemon_reload: true + with_items: + - systemd-networkd + - systemd-resolved tags: - always @@ -78,7 +106,6 @@ - apparmor-utils - uuid-runtime - coreutils - - sendmail - iptables-persistent - cgroup-tools - openssl @@ -91,3 +118,19 @@ value: 1 tags: - always + +- name: Install tools + package: name="{{ item }}" state=present + with_items: + - "{{ tools|default([]) }}" + tags: + - always + +- name: Install headers + apt: + name: "{{ item }}" + state: present + when: install_headers + with_items: + - linux-headers-generic + - "linux-headers-{{ ansible_kernel }}" diff --git a/roles/common/tasks/unattended-upgrades.yml b/roles/common/tasks/unattended-upgrades.yml new file mode 100644 index 00000000..d0beae0a --- /dev/null +++ b/roles/common/tasks/unattended-upgrades.yml @@ -0,0 +1,29 @@ +--- +- name: Install unattended-upgrades + apt: + name: unattended-upgrades + state: latest + +- name: Configure unattended-upgrades + template: + src: 50unattended-upgrades.j2 + dest: /etc/apt/apt.conf.d/50unattended-upgrades + owner: root + group: root + mode: 0644 + +- name: Periodic upgrades configured + template: + src: 10periodic.j2 + dest: /etc/apt/apt.conf.d/10periodic + owner: root + group: root + mode: 0644 + +- name: Unattended reboots configured + template: + src: 60unattended-reboot.j2 + dest: /etc/apt/apt.conf.d/60unattended-reboot + owner: root + group: root + mode: 0644 diff --git a/roles/common/templates/10-algo-lo100.network.j2 b/roles/common/templates/10-algo-lo100.network.j2 new file mode 100644 index 00000000..257396c6 --- /dev/null +++ b/roles/common/templates/10-algo-lo100.network.j2 @@ -0,0 +1,7 @@ +[Match] +Name=lo + +[Network] +Label=lo:100 +Address={{ local_service_ip }}/32 +Address=FCAA::1/64 diff --git a/roles/common/templates/10-loopback-services.cfg.j2 b/roles/common/templates/10-loopback-services.cfg.j2 deleted file mode 100644 index 09f572de..00000000 --- a/roles/common/templates/10-loopback-services.cfg.j2 +++ /dev/null @@ -1,9 +0,0 @@ -auto lo:100 -iface lo:100 inet static - address {{ local_service_ip }} - netmask 255.255.255.255 - -iface lo:100 inet6 static - address FCAA::1 - netmask 64 - autoconf 0 diff --git a/roles/security/templates/10periodic.j2 b/roles/common/templates/10periodic.j2 similarity index 76% rename from roles/security/templates/10periodic.j2 rename to roles/common/templates/10periodic.j2 index 75870203..5d37e9fc 100644 --- a/roles/security/templates/10periodic.j2 +++ b/roles/common/templates/10periodic.j2 @@ -1,4 +1,4 @@ APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; -APT::Periodic::Unattended-Upgrade "1"; \ No newline at end of file +APT::Periodic::Unattended-Upgrade "1"; diff --git a/roles/security/templates/50unattended-upgrades.j2 b/roles/common/templates/50unattended-upgrades.j2 similarity index 97% rename from roles/security/templates/50unattended-upgrades.j2 rename to roles/common/templates/50unattended-upgrades.j2 index 5f8fb159..0c55b702 100644 --- a/roles/security/templates/50unattended-upgrades.j2 +++ b/roles/common/templates/50unattended-upgrades.j2 @@ -15,7 +15,7 @@ Unattended-Upgrade::Package-Blacklist { }; // This option allows you to control if on a unclean dpkg exit -// unattended-upgrades will automatically run +// unattended-upgrades will automatically run // dpkg --force-confold --configure -a // The default is true, to ensure updates keep getting installed //Unattended-Upgrade::AutoFixInterruptedDpkg "false"; @@ -46,7 +46,7 @@ Unattended-Upgrade::Package-Blacklist { //Unattended-Upgrade::Remove-Unused-Dependencies "false"; // Automatically reboot *WITHOUT CONFIRMATION* -// if the file /var/run/reboot-required is found after the upgrade +// if the file /var/run/reboot-required is found after the upgrade //Unattended-Upgrade::Automatic-Reboot "false"; // If automatic reboot is enabled and needed, reboot at the specific diff --git a/roles/common/templates/60unattended-reboot.j2 b/roles/common/templates/60unattended-reboot.j2 new file mode 100644 index 00000000..6af49126 --- /dev/null +++ b/roles/common/templates/60unattended-reboot.j2 @@ -0,0 +1,2 @@ +Unattended-Upgrade::Automatic-Reboot "{{ unattended_reboot.enabled|lower }}"; +Unattended-Upgrade::Automatic-Reboot-Time "{{ unattended_reboot.time }}"; diff --git a/roles/dns_adblocking/meta/main.yml b/roles/dns_adblocking/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/dns_adblocking/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/dns_adblocking/tasks/freebsd.yml b/roles/dns_adblocking/tasks/freebsd.yml index a08e2342..1b73921a 100644 --- a/roles/dns_adblocking/tasks/freebsd.yml +++ b/roles/dns_adblocking/tasks/freebsd.yml @@ -2,3 +2,11 @@ - name: FreeBSD / HardenedBSD | Enable dnsmasq lineinfile: dest=/etc/rc.conf regexp=^dnsmasq_enable= line='dnsmasq_enable="YES"' + +- name: The dnsmasq additional directories created + file: + dest: "{{ item }}" + state: directory + mode: '0755' + with_items: + - "{{ config_prefix|default('/') }}etc/dnsmasq.d" diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 2ba74b77..6a44dbee 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,23 +1,15 @@ --- - block: - - - name: The DNS tag is defined - set_fact: - local_dns: Y - - name: Dnsmasq installed package: name=dnsmasq - - name: Ensure that the dnsmasq user exist - user: name=dnsmasq groups=nogroup append=yes state=present - - name: The dnsmasq directory created file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup - - include: ubuntu.yml + - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include: freebsd.yml + - include_tasks: freebsd.yml when: ansible_distribution == 'FreeBSD' - name: Dnsmasq configured @@ -38,8 +30,8 @@ - name: Adblock script added to cron cron: name: Adblock hosts update - minute: 10 - hour: 2 + minute: "{{ range(0, 60) | random }}" + hour: "{{ range(0, 24) | random }}" job: /usr/local/sbin/adblock.sh user: root diff --git a/roles/dns_adblocking/tasks/ubuntu.yml b/roles/dns_adblocking/tasks/ubuntu.yml index 8e4cf3d0..ffc88876 100644 --- a/roles/dns_adblocking/tasks/ubuntu.yml +++ b/roles/dns_adblocking/tasks/ubuntu.yml @@ -7,13 +7,13 @@ owner: root group: root mode: 0600 - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true notify: - restart dnsmasq - name: Ubuntu | Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true tags: ['apparmor'] - name: Ubuntu | Ensure that the dnsmasq service directory exist diff --git a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 index 98cbbddb..30e5359b 100644 --- a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 +++ b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 @@ -1,4 +1,5 @@ [Service] -MemoryLimit=16777216 +MemoryHigh=128M +MemoryMax=192M CPUAccounting=true -CPUQuota=5% +CPUQuota=20% diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 3b8c4c5c..c52b6b9c 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -55,7 +55,7 @@ # If you don't want dnsmasq to read /etc/resolv.conf or any other # file, getting its servers from this file instead (see below), then # uncomment this. -#no-resolv +no-resolv # If you don't want dnsmasq to poll /etc/resolv.conf or other resolv # files for changes and re-read them then uncomment this. @@ -88,8 +88,14 @@ # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 -server=8.8.8.8 -server=8.8.4.4 +{% if dns_encryption %} +server={{ local_service_ip }}#5353 +{% else %} +{% for host in dns_servers.ipv4 %} +server={{ host }} +{% endfor %} +stop-dns-rebind +{% endif %} # and this sets the source (ie local) address used to talk to # 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that @@ -98,7 +104,7 @@ server=8.8.4.4 # If you want dnsmasq to change uid and gid to something other # than the default, edit the following lines. -user=nobody +user=dnsmasq group=nogroup # If you want dnsmasq to listen for DHCP and DNS requests only on @@ -659,7 +665,7 @@ bind-interfaces # Include another lot of configuration options. #conf-file=/etc/dnsmasq.more.conf -conf-dir=/etc/dnsmasq.d +conf-dir={{ config_prefix|default('/') }}etc/dnsmasq.d/,*.conf # Include all the files in a directory except those ending in .bak #conf-dir=/etc/dnsmasq.d,.bak diff --git a/roles/dns_encryption/defaults/main.yml b/roles/dns_encryption/defaults/main.yml new file mode 100644 index 00000000..1869e6a2 --- /dev/null +++ b/roles/dns_encryption/defaults/main.yml @@ -0,0 +1,13 @@ +--- +algo_local_dns: false +listen_port: "{% if algo_local_dns %}5353{% else %}53{% endif %}" +# the version used if the latest unavailable (in case of Github API rate limited) +dnscrypt_proxy_version: 2.0.10 +apparmor_enabled: true +dns_encryption: true +ipv6_support: false +dnscrypt_servers: + ipv4: + - cloudflare + ipv6: + - cloudflare-ipv6 diff --git a/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades b/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades new file mode 100644 index 00000000..632bb318 --- /dev/null +++ b/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades @@ -0,0 +1,4 @@ +// Automatically upgrade packages from these (origin:archive) pairs +Unattended-Upgrade::Allowed-Origins { + "LP-PPA-shevchuk-dnscrypt-proxy:${distro_codename}"; +}; diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy new file mode 100644 index 00000000..7e900bc5 --- /dev/null +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -0,0 +1,29 @@ +#include + +/usr/bin/dnscrypt-proxy { + #include + #include + #include + + capability chown, + capability dac_override, + capability net_bind_service, + capability setgid, + capability setuid, + capability sys_resource, + + /etc/dnscrypt-proxy/** r, + /usr/bin/dnscrypt-proxy mr, + /tmp/public-resolvers.md* rw, + + /tmp/*.tmp w, + owner /tmp/*.tmp r, + + /run/systemd/notify rw, + /lib/x86_64-linux-gnu/ld-*.so mr, + @{PROC}/sys/kernel/hostname r, + @{PROC}/sys/net/core/somaxconn r, + /etc/ld.so.cache r, + /usr/local/lib/{@{multiarch}/,}libldns.so* mr, + /usr/local/lib/{@{multiarch}/,}libsodium.so* mr, +} diff --git a/roles/dns_encryption/handlers/main.yml b/roles/dns_encryption/handlers/main.yml new file mode 100644 index 00000000..fe677147 --- /dev/null +++ b/roles/dns_encryption/handlers/main.yml @@ -0,0 +1,17 @@ +--- +- name: daemon reload + systemd: + daemon_reload: true + +- name: restart dnscrypt-proxy + systemd: + name: dnscrypt-proxy + state: restarted + daemon_reload: true + when: ansible_distribution == 'Ubuntu' + +- name: restart dnscrypt-proxy + service: + name: dnscrypt-proxy + state: restarted + when: ansible_distribution == 'FreeBSD' diff --git a/roles/dns_encryption/tasks/freebsd.yml b/roles/dns_encryption/tasks/freebsd.yml new file mode 100644 index 00000000..bdada6fe --- /dev/null +++ b/roles/dns_encryption/tasks/freebsd.yml @@ -0,0 +1,10 @@ +--- +- name: Install dnscrypt-proxy + package: + name: dnscrypt-proxy2 + +- name: Enable mac_portacl + lineinfile: + path: /etc/rc.conf + line: 'dnscrypt_proxy_mac_portacl_enable="YES"' + when: listen_port|int == 53 diff --git a/roles/dns_encryption/tasks/main.yml b/roles/dns_encryption/tasks/main.yml new file mode 100644 index 00000000..5740703c --- /dev/null +++ b/roles/dns_encryption/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: Include tasks for Ubuntu + include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + +- name: Include tasks for FreeBSD + include_tasks: freebsd.yml + when: ansible_distribution == 'FreeBSD' + +- name: dnscrypt-proxy ip-blacklist configured + template: + src: ip-blacklist.txt.j2 + dest: "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/ip-blacklist.txt" + notify: + - restart dnscrypt-proxy + +- name: dnscrypt-proxy configured + template: + src: dnscrypt-proxy.toml.j2 + dest: "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/dnscrypt-proxy.toml" + notify: + - restart dnscrypt-proxy + +- name: dnscrypt-proxy enabled and started + service: + name: dnscrypt-proxy + state: started + enabled: true + +- meta: flush_handlers diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml new file mode 100644 index 00000000..89515ddb --- /dev/null +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -0,0 +1,57 @@ +--- +- name: Add the repository + apt_repository: + state: present + codename: bionic + repo: ppa:shevchuk/dnscrypt-proxy + register: result + until: result is succeeded + retries: 10 + delay: 3 + +- name: Install dnscrypt-proxy + apt: + name: dnscrypt-proxy + state: latest + update_cache: true + +- name: Configure unattended-upgrades + copy: + src: 50-dnscrypt-proxy-unattended-upgrades + dest: /etc/apt/apt.conf.d/50-dnscrypt-proxy-unattended-upgrades + owner: root + group: root + mode: 0644 + +- block: + - name: Ubuntu | Unbound profile for apparmor configured + copy: + src: apparmor.profile.dnscrypt-proxy + dest: /etc/apparmor.d/usr.bin.dnscrypt-proxy + owner: root + group: root + mode: 0600 + notify: restart dnscrypt-proxy + + - name: Ubuntu | Enforce the dnscrypt-proxy AppArmor policy + command: aa-enforce usr.bin.dnscrypt-proxy + changed_when: false + tags: apparmor + when: apparmor_enabled|default(false)|bool == true + +- name: Ubuntu | Ensure that the dnscrypt-proxy service directory exist + file: + path: /etc/systemd/system/dnscrypt-proxy.service.d/ + state: directory + mode: 0755 + owner: root + group: root + +- name: Ubuntu | Add capabilities to bind ports + copy: + dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-capabilities.conf + content: | + [Service] + AmbientCapabilities=CAP_NET_BIND_SERVICE + notify: + - restart dnscrypt-proxy diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 new file mode 100644 index 00000000..d954ff8b --- /dev/null +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -0,0 +1,474 @@ + +############################################## +# # +# dnscrypt-proxy configuration # +# # +############################################## + +## This is an example configuration file. +## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml" +## +## Online documentation is available here: https://dnscrypt.info/doc + + + +################################## +# Global settings # +################################## + +## List of servers to use +## +## Servers from the "public-resolvers" source (see down below) can +## be viewed here: https://dnscrypt.info/public-servers +## +## If this line is commented, all registered servers matching the require_* filters +## will be used. +## +## The proxy will automatically pick the fastest, working servers from the list. +## Remove the leading # first to enable this; lines starting with # are ignored. + +{# Allow either list to be empty. Output nothing if both are empty. #} +{% set servers = [] %} +{% if dnscrypt_servers.ipv4 %}{% set servers = dnscrypt_servers.ipv4 %}{% endif %} +{% if ipv6_support and dnscrypt_servers.ipv6 %}{% set servers = servers + dnscrypt_servers.ipv6 %}{% endif %} +{% if servers %}server_names = ['{{ servers | join("', '") }}']{% endif %} + + +## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. +## Note: When using systemd socket activation, choose an empty set (i.e. [] ). + +listen_addresses = ['{{ local_service_ip }}:{{ listen_port }}'] + + +## Maximum number of simultaneous client connections to accept + +max_clients = 250 + + +## Require servers (from static + remote sources) to satisfy specific properties + +# Use servers reachable over IPv4 +ipv4_servers = true + +# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity +ipv6_servers = {{ ipv6_support | bool | lower }} + +# Use servers implementing the DNSCrypt protocol +dnscrypt_servers = true + +# Use servers implementing the DNS-over-HTTPS protocol +doh_servers = true + + +## Require servers defined by remote sources to satisfy specific properties + +# Server must support DNS security extensions (DNSSEC) +require_dnssec = true + +# Server must not log user queries (declarative) +require_nolog = true + +# Server must not enforce its own blacklist (for parental control, ads blocking...) +require_nofilter = true + + + +## Always use TCP to connect to upstream servers + +force_tcp = false + + +## How long a DNS query will wait for a response, in milliseconds + +timeout = 2500 + + +## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds + +keepalive = 30 + + +## Load-balancing strategy: 'p2' (default), 'ph', 'fastest' or 'random' + +lb_strategy = 'p2' + + +## Log level (0-6, default: 2 - 0 is very verbose, 6 only contains fatal errors) + +log_level = 2 + + +## log file for the application + +# log_file = 'dnscrypt-proxy.log' + + +## Use the system logger (syslog on Unix, Event Log on Windows) + +use_syslog = true + + +## Delay, in minutes, after which certificates are reloaded + +cert_refresh_delay = 240 + + +## DNSCrypt: Create a new, unique key for every single DNS query +## This may improve privacy but can also have a significant impact on CPU usage +## Only enable if you don't have a lot of network load + +dnscrypt_ephemeral_keys = true + + +## DoH: Disable TLS session tickets - increases privacy but also latency + +tls_disable_session_tickets = true + + +## DoH: Use a specific cipher suite instead of the server preference +## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 +## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 +## +## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...), +## the following suite improves performance. +## This may also help on Intel CPUs running 32-bit operating systems. +## +## Keep tls_cipher_suite empty if you have issues fetching sources or +## connecting to some DoH servers. Google and Cloudflare are fine with it. + +# tls_cipher_suite = [49195] + + +## Fallback resolver +## This is a normal, non-encrypted DNS resolver, that will be only used +## for one-shot queries when retrieving the initial resolvers list, and +## only if the system DNS configuration doesn't work. +## No user application queries will ever be leaked through this resolver, +## and it will not be used after IP addresses of resolvers URLs have been found. +## It will never be used if lists have already been cached, and if stamps +## don't include host names without IP addresses. +## It will not be used if the configured system DNS works. +## A resolver supporting DNSSEC is recommended. This may become mandatory. +## +## People in China may need to use 114.114.114.114:53 here. +## Other popular options include 8.8.8.8 and 1.1.1.1. + +fallback_resolver = '{% if ansible_distribution == "FreeBSD" %}{{ ansible_dns.nameservers.0 }}:53{% else %}127.0.0.53:53{% endif %}' + + +## Never try to use the system DNS settings; unconditionally use the +## fallback resolver. + +ignore_system_dns = true + + +## Automatic log files rotation + +# Maximum log files size in MB +log_files_max_size = 10 + +# How long to keep backup files, in days +log_files_max_age = 7 + +# Maximum log files backups to keep (or 0 to keep all backups) +log_files_max_backups = 1 + + + +######################### +# Filters # +######################### + +## Immediately respond to IPv6-related queries with an empty response +## This makes things faster when there is no IPv6 connectivity, but can +## also cause reliability issues with some stub resolvers. In +## particular, enabling this on macOS is not recommended. + +block_ipv6 = false + + + +################################################################################## +# Route queries for specific domains to a dedicated set of servers # +################################################################################## + +## Example map entries (one entry per line): +## example.com 9.9.9.9 +## example.net 9.9.9.9,8.8.8.8,1.1.1.1 + +# forwarding_rules = 'forwarding-rules.txt' + + + +############################### +# Cloaking rules # +############################### + +## Cloaking returns a predefined address for a specific name. +## In addition to acting as a HOSTS file, it can also return the IP address +## of a different name. It will also do CNAME flattening. +## +## Example map entries (one entry per line) +## example.com 10.1.1.1 +## www.google.com forcesafesearch.google.com + +# cloaking_rules = 'cloaking-rules.txt' + + + +########################### +# DNS cache # +########################### + +## Enable a DNS cache to reduce latency and outgoing traffic + +cache = true + + +## Cache size + +cache_size = 512 + + +## Minimum TTL for cached entries + +cache_min_ttl = 600 + + +## Maximum TTL for cached entries + +cache_max_ttl = 86400 + + +## Minimum TTL for negatively cached entries + +cache_neg_min_ttl = 60 + + +## Maximum TTL for negatively cached entries + +cache_neg_max_ttl = 600 + + + +############################### +# Query logging # +############################### + +## Log client queries to a file + +[query_log] + + ## Path to the query log file (absolute, or relative to the same directory as the executable file) + + # file = 'query.log' + + + ## Query log format (currently supported: tsv and ltsv) + + format = 'tsv' + + + ## Do not log these query types, to reduce verbosity. Keep empty to log everything. + + # ignored_qtypes = ['DNSKEY', 'NS'] + + + +############################################ +# Suspicious queries logging # +############################################ + +## Log queries for nonexistent zones +## These queries can reveal the presence of malware, broken/obsolete applications, +## and devices signaling their presence to 3rd parties. + +[nx_log] + + ## Path to the query log file (absolute, or relative to the same directory as the executable file) + + # file = 'nx.log' + + + ## Query log format (currently supported: tsv and ltsv) + + format = 'tsv' + + + +###################################################### +# Pattern-based blocking (blacklists) # +###################################################### + +## Blacklists are made of one pattern per line. Example of valid patterns: +## +## example.com +## =example.com +## *sex* +## ads.* +## ads*.example.* +## ads*.example[0-9]*.com +## +## Example blacklist files can be found at https://download.dnscrypt.info/blacklists/ +## A script to build blacklists from public feeds can be found in the +## `utils/generate-domains-blacklists` directory of the dnscrypt-proxy source code. + +[blacklist] + + ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) + + # blacklist_file = 'blacklist.txt' + + + ## Optional path to a file logging blocked queries + + # log_file = 'blocked.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +########################################################### +# Pattern-based IP blocking (IP blacklists) # +########################################################### + +## IP blacklists are made of one pattern per line. Example of valid patterns: +## +## 127.* +## fe80:abcd:* +## 192.168.1.4 + +[ip_blacklist] + + ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) + + blacklist_file = 'ip-blacklist.txt' + + + ## Optional path to a file logging blocked queries + + # log_file = 'ip-blocked.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +###################################################### +# Pattern-based whitelisting (blacklists bypass) # +###################################################### + +## Whitelists support the same patterns as blacklists +## If a name matches a whitelist entry, the corresponding session +## will bypass names and IP filters. +## +## Time-based rules are also supported to make some websites only accessible at specific times of the day. + +[whitelist] + + ## Path to the file of whitelisting rules (absolute, or relative to the same directory as the executable file) + + # whitelist_file = 'whitelist.txt' + + + ## Optional path to a file logging whitelisted queries + + # log_file = 'whitelisted.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +########################################## +# Time access restrictions # +########################################## + +## One or more weekly schedules can be defined here. +## Patterns in the name-based blocklist can optionally be followed with @schedule_name +## to apply the pattern 'schedule_name' only when it matches a time range of that schedule. +## +## For example, the following rule in a blacklist file: +## *.youtube.* @time-to-sleep +## would block access to YouTube only during the days, and period of the days +## define by the 'time-to-sleep' schedule. +## +## {after='21:00', before= '7:00'} matches 0:00-7:00 and 21:00-0:00 +## {after= '9:00', before='18:00'} matches 9:00-18:00 + +[schedules] + + # [schedules.'time-to-sleep'] + # mon = [{after='21:00', before='7:00'}] + # tue = [{after='21:00', before='7:00'}] + # wed = [{after='21:00', before='7:00'}] + # thu = [{after='21:00', before='7:00'}] + # fri = [{after='23:00', before='7:00'}] + # sat = [{after='23:00', before='7:00'}] + # sun = [{after='21:00', before='7:00'}] + + # [schedules.'work'] + # mon = [{after='9:00', before='18:00'}] + # tue = [{after='9:00', before='18:00'}] + # wed = [{after='9:00', before='18:00'}] + # thu = [{after='9:00', before='18:00'}] + # fri = [{after='9:00', before='17:00'}] + + + +######################### +# Servers # +######################### + +## Remote lists of available servers +## Multiple sources can be used simultaneously, but every source +## requires a dedicated cache file. +## +## Refer to the documentation for URLs of public sources. +## +## A prefix can be prepended to server names in order to +## avoid collisions if different sources share the same for +## different servers. In that case, names listed in `server_names` +## must include the prefixes. +## +## If the `urls` property is missing, cache files and valid signatures +## must be already present; This doesn't prevent these cache files from +## expiring after `refresh_delay` hours. + +[sources] + + ## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers + + [sources.'public-resolvers'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md'] + cache_file = '/tmp/public-resolvers.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + + ## Another example source, with resolvers censoring some websites not appropriate for children + ## This is a subset of the `public-resolvers` list, so enabling both is useless + + # [sources.'parental-control'] + # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v2/parental-control.md'] + # cache_file = 'parental-control.md' + # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + + + +## Optional, local, static list of additional servers +## Mostly useful for testing your own servers. + +[static] + + # [static.'google'] + # stamp = 'sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA' diff --git a/roles/dns_encryption/templates/ip-blacklist.txt.j2 b/roles/dns_encryption/templates/ip-blacklist.txt.j2 new file mode 100644 index 00000000..d2189ff2 --- /dev/null +++ b/roles/dns_encryption/templates/ip-blacklist.txt.j2 @@ -0,0 +1,44 @@ +0.0.0.0 +10.* +127.* +169.254.* +172.16.* +172.17.* +172.18.* +172.19.* +172.20.* +172.21.* +172.22.* +172.23.* +172.24.* +172.25.* +172.26.* +172.27.* +172.28.* +172.29.* +172.30.* +172.31.* +192.168.* +::ffff:0.0.0.0 +::ffff:10.* +::ffff:127.* +::ffff:169.254.* +::ffff:172.16.* +::ffff:172.17.* +::ffff:172.18.* +::ffff:172.19.* +::ffff:172.20.* +::ffff:172.21.* +::ffff:172.22.* +::ffff:172.23.* +::ffff:172.24.* +::ffff:172.25.* +::ffff:172.26.* +::ffff:172.27.* +::ffff:172.28.* +::ffff:172.29.* +::ffff:172.30.* +::ffff:172.31.* +::ffff:192.168.* +fd00::* +fe80::* diff --git a/roles/local/handlers/main.yml b/roles/local/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index 555baa45..5803cff9 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -1,40 +1,8 @@ --- - block: - - name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - cloud_provider: local - when: server_ip != "localhost" + - name: Include prompts + import_tasks: prompts.yml - - name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_connection: local - cloud_provider: local - when: server_ip == "localhost" - - - set_fact: - cloud_instance_ip: "{{ server_ip }}" - - - name: Ensure the group local exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[local]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[local\]' - regexp: "^{{ server_ip }}.*" - line: "{{ server_ip }}" rescue: - debug: var=fail_hint tags: always diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml new file mode 100644 index 00000000..1f5edc2e --- /dev/null +++ b/roles/local/tasks/prompts.yml @@ -0,0 +1,44 @@ +--- +- pause: + prompt: | + Enter the IP address of your server: (or use localhost for local installation): + [localhost] + register: _algo_server + when: server is undefined + +- name: Set the facts + set_fact: + cloud_instance_ip: >- + {% if server is defined %}{{ server }} + {%- elif _algo_server.user_input is defined and _algo_server.user_input != "" %}{{ _algo_server.user_input }} + {%- else %}localhost{% endif %} + +- pause: + prompt: | + What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) + [root] + register: _algo_ssh_user + when: + - ssh_user is undefined + - cloud_instance_ip != "localhost" + +- name: Set the facts + set_fact: + ansible_ssh_user: >- + {% if ssh_user is defined %}{{ ssh_user }} + {%- elif _algo_ssh_user.user_input is defined and _algo_ssh_user.user_input != "" %}{{ _algo_ssh_user.user_input }} + {%- else %}root{% endif %} + +- pause: + prompt: | + Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) + [{{ cloud_instance_ip }}] + register: _endpoint + when: endpoint is undefined + +- name: Set the facts + set_fact: + IP_subject_alt_name: >- + {% if endpoint is defined %}{{ endpoint }} + {%- elif _endpoint.user_input is defined and _endpoint.user_input != "" %}{{ _endpoint.user_input }} + {%- else %}{{ cloud_instance_ip }}{% endif %} diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml deleted file mode 100644 index ab98db63..00000000 --- a/roles/security/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: restart ssh - service: name="{{ ssh_service_name|default('ssh') }}" state=restarted - -- name: flush routing cache - shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/meta/main.yml b/roles/security/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/security/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml deleted file mode 100644 index 2f279122..00000000 --- a/roles/security/tasks/main.yml +++ /dev/null @@ -1,161 +0,0 @@ ---- -- block: - - name: Install tools - apt: name="{{ item }}" state=latest - with_items: - - unattended-upgrades - - - name: Configure unattended-upgrades - template: - src: 50unattended-upgrades.j2 - dest: /etc/apt/apt.conf.d/50unattended-upgrades - owner: root - group: root - mode: 0644 - - - name: Periodic upgrades configured - template: - src: 10periodic.j2 - dest: /etc/apt/apt.conf.d/10periodic - owner: root - group: root - mode: 0644 - - - name: Find directories for minimizing access - stat: - path: "{{ item }}" - register: minimize_access_directories - with_items: - - '/usr/local/sbin' - - '/usr/local/bin' - - '/usr/sbin' - - '/usr/bin' - - '/sbin' - - '/bin' - - - name: Minimize access - file: - path: '{{ item.stat.path }}' - mode: 'go-w' - recurse: yes - when: item.stat.isdir - with_items: "{{ minimize_access_directories.results }}" - no_log: True - - - name: Change shadow ownership to root and mode to 0600 - file: - dest: '/etc/shadow' - owner: root - group: root - mode: 0600 - - - name: change su-binary to only be accessible to user and group root - file: - dest: '/bin/su' - owner: root - group: root - mode: 0750 - - # Core dumps - - - name: Restrict core dumps (with PAM) - lineinfile: - dest: /etc/security/limits.conf - line: "* hard core 0" - state: present - - - name: Restrict core dumps (with sysctl) - sysctl: - name: fs.suid_dumpable - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - - # Kernel fixes - - - name: Disable Source Routed Packet Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.accept_source_route - - net.ipv4.conf.default.accept_source_route - notify: - - flush routing cache - - - name: Disable ICMP Redirect Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects - - - name: Disable Secure ICMP Redirect Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.secure_redirects - - net.ipv4.conf.default.secure_redirects - notify: - - flush routing cache - - - name: Enable Bad Error Message Protection - sysctl: - name: net.ipv4.icmp_ignore_bogus_error_responses - value: 1 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - notify: - - flush routing cache - - - name: Enable RFC-recommended Source Route Validation - sysctl: - name: "{{item}}" - value: 1 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.rp_filter - - net.ipv4.conf.default.rp_filter - notify: - - flush routing cache - - - name: Do not send ICMP redirects (we are not a router) - sysctl: - name: net.ipv4.conf.all.send_redirects - value: 0 - - - name: SSH config - template: - src: sshd_config.j2 - dest: /etc/ssh/sshd_config - owner: root - group: root - mode: 0644 - notify: - - restart ssh - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 deleted file mode 100644 index 4bdb2601..00000000 --- a/roles/security/templates/sshd_config.j2 +++ /dev/null @@ -1,51 +0,0 @@ -Port 22 -# ListenAddress :: -# ListenAddress 0.0.0.0 -Protocol 2 - -# LogLevel VERBOSE logs user's key fingerprint on login. -# Needed to have a clear audit log of which keys were used to log in. -SyslogFacility AUTH -LogLevel VERBOSE - -# Use kernel sandbox mechanisms where possible -# Systrace on OpenBSD, Seccomp on Linux, seatbelt on macOS X (Darwin), rlimit elsewhere. -UsePrivilegeSeparation sandbox - -# Handy for keeping network connections alive -TCPKeepAlive yes -ClientAliveInterval 120 - -# Authentication -UsePAM yes -PermitRootLogin without-password -StrictModes yes -PubkeyAuthentication yes -AcceptEnv LANG LC_* - -# Turn off a lot of features -IgnoreRhosts yes -HostbasedAuthentication no -PermitEmptyPasswords no -ChallengeResponseAuthentication no -PasswordAuthentication no -UseDNS no - -# Do not enable sftp -# If you DO enable it, use this line to log which files sftp users read/write -# Subsystem sftp /usr/lib/ssh/sftp-server -f AUTHPRIV -l INFO - -# This makes ansible faster -PrintMotd no -PrintLastLog yes - -# Use only modern host keys -HostKey /etc/ssh/ssh_host_ed25519_key -HostKey /etc/ssh/ssh_host_ecdsa_key - -# Use only modern ciphers -KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256 -Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com -MACs hmac-sha2-256-etm@openssh.com -HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256 -# PubkeyAcceptedKeyTypes accept anything diff --git a/roles/ssh_tunneling/meta/main.yml b/roles/ssh_tunneling/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/ssh_tunneling/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 8a1d4965..259464b4 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -31,42 +31,27 @@ groups: algo home: '/var/jail/{{ item }}' createhome: yes - generate_ssh_key: yes + generate_ssh_key: false shell: /bin/false - ssh_key_type: ecdsa - ssh_key_bits: 256 - ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" - update_password: on_create state: present append: yes with_items: "{{ users }}" + tags: update-users - name: The authorized keys file created - file: - src: '/var/jail/{{ item }}/.ssh/id_ecdsa.pub' - dest: '/var/jail/{{ item }}/.ssh/authorized_keys' - owner: "{{ item }}" - group: "{{ item }}" - state: link + authorized_key: + user: "{{ item }}" + key: "{{ lookup('file', 'configs/' + IP_subject_alt_name + '/pki/public/' + item + '.pub') }}" + state: present + manage_dir: true + exclusive: true with_items: "{{ users }}" + tags: update-users - name: Generate SSH fingerprints shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null register: ssh_fingerprints - - name: Fetch users SSH private keys - fetch: - src: '/var/jail/{{ item }}/.ssh/id_ecdsa' - dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem - flat: yes - with_items: "{{ users }}" - - - name: Change mode for SSH private keys - local_action: file path=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem mode=0600 - with_items: "{{ users }}" - become: false - - name: Fetch the known_hosts file local_action: module: template @@ -80,24 +65,26 @@ src: ssh_config.j2 dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config mode: 0600 - become: no - with_items: - - "{{ users }}" + become: false + tags: update-users + with_items: "{{ users }}" - - name: SSH | Get active system users - shell: > - getent group algo | cut -f4 -d: | sed "s/,/\n/g" - register: valid_users - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + - name: Get active users + getent: + database: group + key: algo + split: ':' + tags: update-users - - name: SSH | Delete non-existing users + - name: Delete non-existing users user: name: "{{ item }}" state: absent remove: yes force: yes - when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ valid_users.stdout_lines | default('null') }}" + when: item not in users + with_items: "{{ getent_group['algo'][2].split(',') }}" + tags: update-users rescue: - debug: var=fail_hint tags: always diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 12f67887..a865dfb4 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,4 +1,42 @@ --- +strongswan_shell: /usr/sbin/nologin +strongswan_home: /var/lib/strongswan +BetweenClients_DROP: true +wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" +wireguard_interface: wg0 +wireguard_network_ipv4: + subnet: 10.19.49.0 + prefix: 24 + gateway: 10.19.49.1 + clients_range: 10.19.49 + clients_start: 2 +wireguard_network_ipv6: + subnet: 'fd9d:bc11:4021::' + prefix: 48 + gateway: 'fd9d:bc11:4021::1' + clients_range: 'fd9d:bc11:4021::' + clients_start: 2 +wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" +wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" +keys_clean_all: false +wireguard_dns_servers: >- + {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} + {{ local_service_ip }} + {% else %} + {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + {% endif %} + +algo_ondemand_cellular: false +algo_ondemand_wifi: false +algo_ondemand_wifi_exclude: '_null' +algo_windows: false +algo_store_cakey: false +algo_local_dns: false +ipv6_support: false +dns_encryption: true +domain: false +subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" +subjectAltName_USER: "{% if '@' in item %}email:{{ item }}{% else %}DNS:{{ item }}{% endif %}" openssl_bin: openssl strongswan_enabled_plugins: - aes @@ -22,8 +60,8 @@ strongswan_enabled_plugins: ciphers: defaults: - ike: aes128gcm16-prfsha512-ecp256! - esp: aes128gcm16-ecp256! + ike: aes256gcm16-prfsha512-ecp384! + esp: aes256gcm16-ecp384! compat: - ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256! - esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256! + ike: aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384! + esp: aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384! diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index f3d19204..ed97d539 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -1,5 +1 @@ --- - -dependencies: - - { role: common, tags: common } - diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index ea1621a2..827bef76 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -9,7 +9,6 @@ - name: Set facts for mobileconfigs set_fact: - proxy_enabled: false PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - name: Build the mobileconfigs @@ -22,25 +21,6 @@ - "{{ PayloadContent.results }}" no_log: True -- name: Build the strongswan app android config - template: - src: sswan.j2 - dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}.sswan - mode: 0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - -- name: Build the android helper html - template: - src: android_html_helper.j2 - dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}_helper.html - mode: 0600 - with_together: - - "{{ users }}" - no_log: True - - name: Build the client ipsec config file template: src: client_ipsec.conf.j2 @@ -57,24 +37,15 @@ with_items: - "{{ users }}" -- name: Create the windows check file - file: - state: touch - path: configs/{{ IP_subject_alt_name }}/.supports_windows - when: Win10_Enabled is defined and Win10_Enabled == "Y" - -- name: Check if the windows check file exists - stat: - path: configs/{{ IP_subject_alt_name }}/.supports_windows - register: supports_windows - - name: Build the windows client powershell script template: src: client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 mode: 0600 - when: Win10_Enabled is defined and Win10_Enabled == "Y" or supports_windows.stat.exists == true - with_items: "{{ users }}" + when: algo_windows + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" - name: Restrict permissions for the local private directories file: diff --git a/roles/vpn/tasks/freebsd.yml b/roles/vpn/tasks/freebsd.yml deleted file mode 100644 index 1dbecd5f..00000000 --- a/roles/vpn/tasks/freebsd.yml +++ /dev/null @@ -1,114 +0,0 @@ ---- - -- name: FreeBSD / HardenedBSD | Get the existing kernel parameters - command: sysctl -b kern.conftxt - register: kern_conftxt - when: rebuild_kernel is defined and rebuild_kernel == "true" - -- name: FreeBSD / HardenedBSD | Set the rebuild_needed fact - set_fact: - rebuild_needed: true - when: item not in kern_conftxt.stdout and rebuild_kernel is defined and rebuild_kernel == "true" - with_items: - - "IPSEC" - - "IPSEC_NAT_T" - - "crypto" - -- name: FreeBSD / HardenedBSD | Make the kernel config - shell: sysctl -b kern.conftxt > /tmp/IPSEC - when: rebuild_needed is defined and rebuild_needed == true - -- name: FreeBSD / HardenedBSD | Ensure the all options are enabled - lineinfile: - dest: /tmp/IPSEC - line: "{{ item }}" - insertbefore: BOF - with_items: - - "options IPSEC" - - "options IPSEC_NAT_T" - - "device crypto" - when: rebuild_needed is defined and rebuild_needed == true - -- name: HardenedBSD | Determine the sources - set_fact: - sources_repo: https://github.com/HardenedBSD/hardenedBSD.git - sources_version: "hardened/{{ ansible_distribution_release.split('.')[0] }}-stable/master" - when: "'Hardened' in ansible_distribution_version" - -- name: FreeBSD | Determine the sources - set_fact: - sources_repo: https://github.com/freebsd/freebsd.git - sources_version: "stable/{{ ansible_distribution_major_version }}" - when: "'Hardened' not in ansible_distribution_version" - -- name: FreeBSD / HardenedBSD | Increase the git postBuffer size - git_config: - name: http.postBuffer - scope: global - value: 1048576000 - -- block: - - name: FreeBSD / HardenedBSD | Fetching the sources... - git: - repo: "{{ sources_repo }}" - dest: /usr/krnl_src - version: "{{ sources_version }}" - accept_hostkey: true - async: 1000 - poll: 0 - register: fetching_sources - - - name: FreeBSD / HardenedBSD | Fetching the sources... - async_status: jid={{ fetching_sources.ansible_job_id }} - when: rebuild_needed is defined and rebuild_needed == true - register: result - until: result.finished - retries: 600 - delay: 30 - rescue: - - debug: var=fetching_sources - - - fail: - msg: "Something went wrong. Check the debug output above." - -- block: - - name: FreeBSD / HardenedBSD | The kernel is being built... - shell: > - mv /tmp/IPSEC /usr/krnl_src/sys/{{ ansible_architecture }}/conf && - make buildkernel KERNCONF=IPSEC && - make installkernel KERNCONF=IPSEC - args: - chdir: /usr/krnl_src - executable: /usr/local/bin/bash - when: rebuild_needed is defined and rebuild_needed == true - async: 1000 - poll: 0 - register: building_kernel - - - name: FreeBSD / HardenedBSD | The kernel is being built... - async_status: jid={{ building_kernel.ansible_job_id }} - when: rebuild_needed is defined and rebuild_needed == true - register: result - until: result.finished - retries: 600 - delay: 30 - rescue: - - debug: var=building_kernel - - - fail: - msg: "Something went wrong. Check the debug output above." - -- name: FreeBSD / HardenedBSD | Reboot - shell: sleep 2 && shutdown -r now - args: - executable: /usr/local/bin/bash - when: rebuild_needed is defined and rebuild_needed == true - async: 1 - poll: 0 - ignore_errors: true - -- name: FreeBSD / HardenedBSD | Enable strongswan - lineinfile: - dest: /etc/rc.conf - regexp: ^strongswan_enable= - line: 'strongswan_enable="YES"' diff --git a/roles/vpn/tasks/ipec_configuration.yml b/roles/vpn/tasks/ipsec_configuration.yml similarity index 100% rename from roles/vpn/tasks/ipec_configuration.yml rename to roles/vpn/tasks/ipsec_configuration.yml diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml index 251335d6..e5b10619 100644 --- a/roles/vpn/tasks/iptables.yml +++ b/roles/vpn/tasks/iptables.yml @@ -19,7 +19,7 @@ owner: root group: root mode: 0640 - when: ipv6_support is defined and ipv6_support == true + when: ipv6_support with_items: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 8e732e1d..bfe929ca 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,33 +1,41 @@ --- - block: - - name: Ensure that the strongswan group exist - group: name=strongswan state=present + - name: Include WireGuard role + include_role: + name: wireguard + tags: wireguard + when: wireguard_enabled and ansible_distribution == 'Ubuntu' - - name: Ensure that the strongswan user exist - user: name=strongswan group=strongswan state=present - - - include: ubuntu.yml + - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - name: Ensure that the strongswan user exist + user: + name: strongswan + group: nogroup + shell: "{{ strongswan_shell }}" + home: "{{ strongswan_home }}" + state: present - name: Install strongSwan package: name=strongswan state=present - - include: ipec_configuration.yml - - include: openssl.yml + - import_tasks: ipsec_configuration.yml + - import_tasks: openssl.yml tags: update-users - - include: distribute_keys.yml - - include: client_configs.yml + - import_tasks: distribute_keys.yml + - import_tasks: client_configs.yml delegate_to: localhost become: no tags: update-users - - meta: flush_handlers - - name: strongSwan started - service: name=strongswan state=started + service: + name: strongswan + state: started + enabled: true + + - meta: flush_handlers rescue: - debug: var=fail_hint tags: always diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 1c3e61bf..3a286be7 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -1,23 +1,29 @@ --- - - block: + - name: Set subjectAltName as a fact + set_fact: + subjectAltName: "{{ subjectAltName_IP }}{% if ipv6_support %},IP:{{ ansible_default_ipv6['address'] }}{% endif %}{% if domain and subjectAltName_DNS %},DNS:{{ subjectAltName_DNS }}{% endif %}" + tags: always + - name: Ensure the pki directory does not exist file: dest: configs/{{ IP_subject_alt_name }}/pki state: absent - when: easyrsa_reinit_existent == True + when: keys_clean_all|bool == True - name: Ensure the pki directories exist file: dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" state: directory recurse: yes + mode: '0700' with_items: - ecparams - certs - crl - newcerts - private + - public - reqs - name: Ensure the files exist @@ -38,14 +44,15 @@ - name: Build the CA pair shell: > - {{ openssl_bin }} ecparam -name prime256v1 -out ecparams/prime256v1.pem && + umask 077; + {{ openssl_bin }} ecparam -name secp384r1 -out ecparams/secp384r1.pem && {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -newkey ec:ecparams/secp384r1.pem + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch - -passout pass:"{{ easyrsa_CA_password }}" && + -passout pass:"{{ CA_password }}" && touch {{ IP_subject_alt_name }}_ca_generated args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" @@ -66,19 +73,20 @@ - name: Build the server pair shell: > + umask 077; {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -newkey ec:ecparams/secp384r1.pem + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && {{ openssl_bin }} ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -days 3650 -batch - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && touch certs/{{ IP_subject_alt_name }}_crt_generated args: @@ -88,19 +96,20 @@ - name: Build the client's pair shell: > + umask 077; {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -newkey ec:ecparams/secp384r1.pem + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ item }}" -batch && {{ openssl_bin }} ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) -days 3650 -batch - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated args: @@ -109,16 +118,31 @@ executable: bash with_items: "{{ users }}" + - name: Create links for the private keys + file: + src: "pki/private/{{ item }}.key" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem" + state: link + force: true + with_items: "{{ users }}" + + - name: Build openssh public keys + openssl_publickey: + path: "configs/{{ IP_subject_alt_name }}/pki/public/{{ item }}.pub" + privatekey_path: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.key" + format: OpenSSH + with_items: "{{ users }}" + - name: Build the client's p12 shell: > + umask 077; {{ openssl_bin }} pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 - -certfile cacert.pem - -passout pass:"{{ easyrsa_p12_export_password }}" + -passout pass:"{{ p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" executable: bash @@ -146,8 +170,8 @@ - name: Revoke non-existing users shell: > {{ openssl_bin }} ca -gencrl - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) - -passin pass:"{{ easyrsa_CA_password }}" + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) + -passin pass:"{{ CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt register: gencrl @@ -162,7 +186,7 @@ shell: > {{ openssl_bin }} ca -gencrl -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }}")) - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -out crl/algo.root.pem when: - gencrl is defined @@ -172,6 +196,8 @@ executable: bash delegate_to: localhost become: no + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" - name: Copy the CRL to the vpn server copy: diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index ccc561b3..6f585441 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -12,7 +12,7 @@ - name: Ubuntu | Enforcing ipsec with apparmor shell: aa-enforce "{{ item }}" - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true with_items: - /usr/lib/ipsec/charon - /usr/lib/ipsec/lookip @@ -44,5 +44,5 @@ - daemon-reload - restart strongswan -- include: iptables.yml +- include_tasks: iptables.yml tags: iptables diff --git a/roles/vpn/templates/android_html_helper.j2 b/roles/vpn/templates/android_html_helper.j2 deleted file mode 100644 index d27528aa..00000000 --- a/roles/vpn/templates/android_html_helper.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ item.0 }} diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 7fde04ab..a45d8e3d 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -6,7 +6,7 @@ conn ikev2-{{ IP_subject_alt_name }} compress=no dpddelay=35s -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +{% if algo_windows %} ike={{ ciphers.compat.ike }} esp={{ ciphers.compat.esp }} {% else %} diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index b984ab10..4ffce674 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,18 +1,198 @@ +#Requires -RunAsAdministrator -function AddAlgoVPN { - certutil -f -importpfx .\{{ item }}.p12 - Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required - Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -Force +<# +.SYNOPSIS +Add or remove the Algo VPN + +.DESCRIPTION +Add or remove the Algo VPN +See the examples for more information + +.PARAMETER Add +Add the VPN to the local system + +.PARAMETER Remove +Remove the VPN from the local system + +.PARAMETER GetInstalledCerts +Retrieve Algo certs, if any, from the system certificate store + +.PARAMETER SaveCerts +Save the Algo certs embedded in this file + +.PARAMETER OutputDirectory +When saving the Algo certs, save to this directory + +.PARAMETER Pkcs12DecryptionPassword +The decryption password for the user's PKCS12 certificate, sometimes called the "p12 password". +Note that this must be passed in as a SecureString, not a regular string. +You can create a secure string with the `Read-Host -AsSecureString` cmdlet. +See the examples for more information. + +.EXAMPLE +client_USER.ps1 -Add + +Adds the Algo VPN + +.EXAMPLE +$p12pass = Read-Host -AsSecureString; client_USER.ps1 -Add -Pkcs12DecryptionPassword $p12pass + +Create a variable containing the PKCS12 decryption password, then use it when adding the VPN. +This can be especially useful when troubleshooting, because you can use the same variable with +multiple calls to client_USER.ps1, rather than having to type the PKCS12 password each time. + +.EXAMPLE +client_USER.ps1 -Remove + +Removes the Algo VPN if installed. + +.EXAMPLE +client_USER.ps1 -GetIntalledCerts + +Show the Algo VPN's installed certificates, if any. + +.EXAMPLE +client_USER.ps1 -SaveCerts -OutputDirectory $Home\Downloads + +Save the embedded CA cert and encrypted user PKCS12 file. +#> +[CmdletBinding(DefaultParameterSetName="Add")] Param( + [Parameter(ParameterSetName="Add")] + [Switch] $Add, + + [Parameter(ParameterSetName="Add")] + [SecureString] $Pkcs12DecryptionPassword, + + [Parameter(Mandatory, ParameterSetName="Remove")] + [Switch] $Remove, + + [Parameter(Mandatory, ParameterSetName="GetInstalledCerts")] + [Switch] $GetInstalledCerts, + + [Parameter(Mandatory, ParameterSetName="SaveCerts")] + [Switch] $SaveCerts, + + [Parameter(ParameterSetName="SaveCerts")] + [string] $OutputDirectory = "$PWD" +) + +$ErrorActionPreference = "Stop" + +$VpnServerAddress = "{{ IP_subject_alt_name }}" +$VpnName = "Algo VPN {{ IP_subject_alt_name }} IKEv2" +$VpnUser = "{{ item.0 }}" +$CaCertificateBase64 = "{{ PayloadContentCA }}" +$UserPkcs12Base64 = "{{ item.1.stdout }}" + +if ($PsCmdlet.ParameterSetName -eq "Add" -and -not $Pkcs12DecryptionPassword) { + $Pkcs12DecryptionPassword = Read-Host -AsSecureString -Prompt "Pkcs12DecryptionPassword" } -function RemoveAlgoVPN { - Get-ChildItem cert:LocalMachine/Root | Where-Object { $_.Subject -match '^CN={{ IP_subject_alt_name }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item - Get-ChildItem cert:LocalMachine/My | Where-Object { $_.Subject -match '^CN={{ item }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item - Remove-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -Force +<# +.SYNOPSIS +Create a temporary directory +#> +function New-TemporaryDirectory { + [CmdletBinding()] Param() + do { + $guid = New-Guid | Select-Object -ExpandProperty Guid + $newTempDirPath = Join-Path -Path $env:TEMP -ChildPath $guid + } while (Test-Path -Path $newTempDirPath) + New-Item -ItemType Directory -Path $newTempDirPath } -switch ($args[0]) { - "Add" { AddAlgoVPN } - "Remove" { RemoveAlgoVPN } - default { Write-Host Usage: $MyInvocation.MyCommand.Name "(Add|Remove)" } +<# +.SYNOPSIS +Retrieve any installed Algo VPN certificates +#> +function Get-InstalledAlgoVpnCertificates { + [CmdletBinding()] Param() + Get-ChildItem -LiteralPath Cert:\LocalMachine\Root | + Where-Object { + $_.Subject -match "^CN=${VpnServerAddress}$" -and $_.Issuer -match "^CN=${VpnServerAddress}$" + } + Get-ChildItem -LiteralPath Cert:\LocalMachine\My | + Where-Object { + $_.Subject -match "^CN=${VpnUser}$" -and $_.Issuer -match "^CN=${VpnServerAddress}$" + } +} + +function Save-AlgoVpnCertificates { + [CmdletBinding()] Param( + [String] $OutputDirectory = $PWD + ) + $caCertPath = Join-Path -Path $OutputDirectory -ChildPath "cacert.pem" + $userP12Path = Join-Path -Path $OutputDirectory -ChildPath "$VpnUser.p12" + # NOTE: We cannot use ConvertFrom-Base64 here because it is not designed for binary data + [IO.File]::WriteAllBytes( + $caCertPath, + [Convert]::FromBase64String($CaCertificateBase64)) + [IO.File]::WriteAllBytes( + $userP12Path, + [Convert]::FromBase64String($UserPkcs12Base64)) + return New-Object -TypeName PSObject -Property @{ + CaPem = $caCertPath + UserPkcs12 = $userP12Path + } +} + +function Add-AlgoVPN { + [Cmdletbinding()] Param() + + $workDir = New-TemporaryDirectory + + try { + $certs = Save-AlgoVpnCertificates -OutputDirectory $workDir + $importPfxCertParams = @{ + Password = $Pkcs12DecryptionPassword + FilePath = $certs.UserPkcs12 + CertStoreLocation = "Cert:\LocalMachine\My" + } + Import-PfxCertificate @importPfxCertParams + $importCertParams = @{ + FilePath = $certs.CaPem + CertStoreLocation = "Cert:\LocalMachine\Root" + } + Import-Certificate @importCertParams + } finally { + Remove-Item -Recurse -Force -LiteralPath $workDir + } + + $addVpnParams = @{ + Name = $VpnName + ServerAddress = $VpnServerAddress + TunnelType = "IKEv2" + AuthenticationMethod = "MachineCertificate" + EncryptionLevel = "Required" + } + Add-VpnConnection @addVpnParams + + $setVpnParams = @{ + ConnectionName = $VpnName + AuthenticationTransformConstants = "GCMAES256" + CipherTransformConstants = "GCMAES256" + EncryptionMethod = "AES256" + IntegrityCheckMethod = "SHA384" + DHGroup = "ECP384" + PfsGroup = "ECP384" + Force = $true + } + Set-VpnConnectionIPsecConfiguration @setVpnParams +} + +function Remove-AlgoVPN { + [CmdletBinding()] Param() + Get-InstalledAlgoVpnCertificates | Remove-Item -Force + Remove-VpnConnection -Name $VpnName -Force +} + +switch ($PsCmdlet.ParameterSetName) { + "Add" { Add-AlgoVPN } + "Remove" { Remove-AlgoVPN } + "GetInstalledCerts" { Get-InstalledAlgoVpnCertificates } + "SaveCerts" { + $certs = Save-AlgoVpnCertificates -OutputDirectory $OutputDirectory + Get-Item -LiteralPath $certs.UserPkcs12, $certs.CaPem + } + default { throw "Unknown parameter set: '$($PsCmdlet.ParameterSetName)'" } } diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 6c5a2d45..086e18af 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -10,7 +10,7 @@ conn %default compress=yes dpddelay=35s -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +{% if algo_windows %} ike={{ ciphers.compat.ike }} esp={{ ciphers.compat.esp }} {% else %} @@ -28,10 +28,10 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if local_dns is defined and local_dns == "Y" %} +{% if algo_local_dns or dns_encryption %} rightdns={{ local_service_ip }} {% else %} - rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support is defined and ipv6_support == "yes" %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} {% endif %} conn ikev2-pubkey diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index ce51ea5a..8a0bb5f6 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -7,13 +7,13 @@ IKEv2 -{% if (OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y') or (OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y') %} +{% if algo_ondemand_wifi or algo_ondemand_cellular %} OnDemandEnabled 1 OnDemandRules -{% if OnDemandEnabled_WIFI_EXCLUDE is defined and OnDemandEnabled_WIFI_EXCLUDE != '_null' %} -{% set WIFI_EXCLUDE_LIST = OnDemandEnabled_WIFI_EXCLUDE.split(',') %} +{% if algo_ondemand_wifi_exclude != '_null' %} +{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|string).split(',') %} Action Disconnect @@ -30,7 +30,7 @@ {% endif %} Action -{% if OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y' %} +{% if algo_ondemand_wifi %} Connect {% else %} Disconnect @@ -42,7 +42,7 @@ Action -{% if OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y' %} +{% if algo_ondemand_cellular %} Connect {% else %} Disconnect @@ -60,9 +60,9 @@ ChildSecurityAssociationParameters DiffieHellmanGroup - 19 + 20 EncryptionAlgorithm - AES-128-GCM + AES-256-GCM IntegrityAlgorithm SHA2-512 LifeTimeInMinutes @@ -81,9 +81,9 @@ IKESecurityAssociationParameters DiffieHellmanGroup - 19 + 20 EncryptionAlgorithm - AES-128-GCM + AES-256-GCM IntegrityAlgorithm SHA2-512 LifeTimeInMinutes @@ -94,7 +94,7 @@ PayloadCertificateUUID {{ pkcs12_PayloadCertificateUUID }} CertificateType - ECDSA256 + ECDSA384 ServerCertificateIssuerCommonName {{ IP_subject_alt_name }} RemoteAddress @@ -124,28 +124,18 @@ Proxies HTTPEnable -{% if proxy_enabled is defined and proxy_enabled == true %} - 1 - HTTPPort - 8118 - HTTPProxy - {{ local_service_ip }} - {% else %} 0 -{% endif %} HTTPSEnable 0 UserDefinedName -{% if proxy_enabled is defined and proxy_enabled == true %} - Algo VPN {{ IP_subject_alt_name }} IKEv2 with proxy - {% else %} Algo VPN {{ IP_subject_alt_name }} IKEv2 -{% endif %} VPNType IKEv2 + Password + {{ p12_export_password }} PayloadCertificateFileName {{ item.0 }}.p12 PayloadContent @@ -187,17 +177,11 @@ PayloadDisplayName -{% if proxy_enabled is defined and proxy_enabled == true %} - {{ IP_subject_alt_name }} IKEv2 with proxy - {% else %} {{ IP_subject_alt_name }} IKEv2 -{% endif %} PayloadIdentifier -{% if proxy_enabled is defined and proxy_enabled == true %} - donut.local.{{ 600000 | random | to_uuid | upper }} - {% else %} donut.local.{{ 500000 | random | to_uuid | upper }} -{% endif %} + PayloadOrganization + Algo VPN PayloadRemovalDisallowed PayloadType diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index e040b184..49c34e2f 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -1,43 +1,94 @@ +#### The mangle table +# This table allows us to modify packet headers +# Packets enter this table first +# *mangle + :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + {% if max_mss is defined %} --A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +-A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} + COMMIT + + +#### The nat table +# This table enables Network Address Translation +# (This is technically a type of packet mangling) +# *nat + :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] --A POSTROUTING -s {{ vpn_network }} -m policy --pol none --dir out -j MASQUERADE + +# Allow traffic from the VPN network to the outside world, and replies +-A POSTROUTING -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -m policy --pol none --dir out -j MASQUERADE + + COMMIT + + +#### The filter table +# The default ipfilter table +# *filter + +# By default, drop packets that are destined for this server :INPUT DROP [0:0] +# By default, drop packets that request to be forwarded by this server :FORWARD DROP [0:0] +# By default, accept any packets originating from this server :OUTPUT ACCEPT [0:0] + +# Accept packets destined for localhost -A INPUT -i lo -j ACCEPT +# Accept any packet from an open TCP connection -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Accept packets using the encapsulation protocol -A INPUT -p esp -j ACCEPT -A INPUT -p ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT --A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) +-A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +# Allow any traffic from the VPN -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT + # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. + +# Accept DNS traffic to the local DNS resolver -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT --A INPUT -d {{ local_service_ip }} -p tcp -m multiport --dport 8080,8118 -j ACCEPT -{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} --A FORWARD -s {{ vpn_network }} -d {{ vpn_network }} -j DROP + +# Drop traffic between VPN clients +{% if BetweenClients_DROP %} +{% set BetweenClientsPolicy = "DROP" %} {% endif %} +-A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} + +# Forward any packet that's part of an established connection -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Drop SMB/CIFS traffic that requests to be forwarded -A FORWARD -p tcp --dport 445 -j DROP +# Drop NETBIOS trafic that requests to be forwarded -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP + +# Forward any IPSEC traffic from the VPN network -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT + +# Forward any traffic from the WireGuard VPN network +{% if wireguard_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network }} -m policy --pol none --dir in -j ACCEPT +{% endif %} + COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 640f6d29..a6d853f2 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -1,59 +1,111 @@ +#### The mangle table +# This table allows us to modify packet headers +# Packets enter this table first +# *mangle + :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + {% if max_mss is defined %} --A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +# MSS is the TCP Max Segment Size +# See rules.v4 for a more complete explanation +-A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} + COMMIT + +#### The nat table +# This table enables Network Address Translation +# (This is technically a type of packet mangling) +# *nat + :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] --A POSTROUTING -s {{ vpn_network_ipv6 }} -m policy --pol none --dir out -j MASQUERADE + +# Allow traffic from the VPN network to the outside world, and replies +-A POSTROUTING -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -m policy --pol none --dir out -j MASQUERADE + COMMIT + +#### The filter table +# The default ipfilter table +# *filter + +# By default, drop packets that are destined for this server :INPUT DROP [0:0] +# By default, drop packets that request to be forwarded by this server :FORWARD DROP [0:0] +# By default, accept any packets originating from this server :OUTPUT ACCEPT [0:0] + +# Create the ICMPV6-CHECK chain and its log chain +# These chains are used later to prevent a type of bug that would +# allow malicious traffic to reach over the server into the private network +# An instance of such a bug on Cisco software is described here: +# https://www.insinuator.net/2016/05/cve-2016-1409-ipv6-ndp-dos-vulnerability-in-cisco-software/ +# other software implementations might be at least as broken as the one in CISCO gear. :ICMPV6-CHECK - [0:0] :ICMPV6-CHECK-LOG - [0:0] + +# Accept packets destined for localhost -A INPUT -i lo -j ACCEPT +# Accept any packet from an open TCP connection -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Accept packets using the encapsulation protocol -A INPUT -p esp -j ACCEPT -A INPUT -m ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT --A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) +-A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT + +# Accept properly formatted Neighbor Discovery Protocol packets -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT + # DHCP in AWS --A INPUT -m state --state NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT +-A INPUT -m conntrack --ctstate NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT + # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. + +# Accept DNS traffic to the local DNS resolver -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT -{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} --A FORWARD -s {{ vpn_network_ipv6 }} -d {{ vpn_network_ipv6 }} -j DROP + +# Drop traffic between VPN clients +{% if BetweenClients_DROP %} +{% set BetweenClientsPolicy = "DROP" %} {% endif %} +-A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} + -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT -# this is so potential malicious traffic can not reach anywhere over the server -# https://www.insinuator.net/2016/05/cve-2016-1409-ipv6-ndp-dos-vulnerability-in-cisco-software/ -# other software implementations might be at least as broken as the one in CISCO gear. +{% if wireguard_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT +{% endif %} + +# Use the ICMPV6-CHECK chain, described above -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " -A ICMPV6-CHECK-LOG -j DROP + COMMIT diff --git a/roles/vpn/templates/sswan.j2 b/roles/vpn/templates/sswan.j2 deleted file mode 100644 index 4fa4fb84..00000000 --- a/roles/vpn/templates/sswan.j2 +++ /dev/null @@ -1,12 +0,0 @@ -{ - "uuid": "{{ 600000 | random | to_uuid }}", - "name": "Algo {{ IP_subject_alt_name }}", - "type": "ikev2-cert", - "remote": { - "addr": "{{ IP_subject_alt_name }}" - }, - "local": { - "p12": "{{ item.1.stdout }}" - }, - "mtu": 1280 -} diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index b658ac08..f71c779e 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -10,16 +10,17 @@ charon { include strongswan.d/charon/*.conf } user = strongswan - group = strongswan + group = nogroup {% if ansible_distribution == 'FreeBSD' %} filelog { - /var/log/charon.log { - time_format = %b %e %T - ike_name = yes - append = no - default = 1 - flush_line = yes - } + charon { + path = /var/log/charon.log + time_format = %b %e %T + ike_name = yes + append = no + default = 1 + flush_line = yes + } } {% endif %} } diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml new file mode 100644 index 00000000..90da64f5 --- /dev/null +++ b/roles/wireguard/defaults/main.yml @@ -0,0 +1,3 @@ +--- +wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/files/50-wireguard-unattended-upgrades b/roles/wireguard/files/50-wireguard-unattended-upgrades new file mode 100644 index 00000000..b1ffc97d --- /dev/null +++ b/roles/wireguard/files/50-wireguard-unattended-upgrades @@ -0,0 +1,4 @@ +// Automatically upgrade packages from these (origin:archive) pairs +Unattended-Upgrade::Allowed-Origins { + "LP-PPA-wireguard-wireguard:${distro_codename}"; +}; diff --git a/roles/wireguard/files/wireguard.sh b/roles/wireguard/files/wireguard.sh new file mode 100644 index 00000000..efcde0e3 --- /dev/null +++ b/roles/wireguard/files/wireguard.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# PROVIDE: wireguard +# REQUIRE: LOGIN +# BEFORE: securelevel +# KEYWORD: shutdown + +. /etc/rc.subr + +name="wg" +rcvar=wg_enable + +command="/usr/local/bin/wg-quick" +start_cmd=wg_up +stop_cmd=wg_down +status_cmd=wg_status +pidfile="/var/run/$name.pid" +load_rc_config "$name" + +: ${wg_enable="NO"} +: ${wg_interface="wg0"} + +wg_up() { + echo "Starting WireGuard..." + /usr/sbin/daemon -cS -p ${pidfile} ${command} up ${wg_interface} +} + +wg_down() { + echo "Stopping WireGuard..." + ${command} down ${wg_interface} +} + +wg_status () { + not_running () { + echo "WireGuard is not running on $wg_interface" && exit 1 + } + /usr/local/bin/wg show wg0 && echo "WireGuard is running on $wg_interface" || not_running +} + +run_rc_command "$1" diff --git a/roles/wireguard/handlers/main.yml b/roles/wireguard/handlers/main.yml new file mode 100644 index 00000000..d13ee31c --- /dev/null +++ b/roles/wireguard/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart wireguard + service: + name: "{{ service_name }}" + state: restarted diff --git a/roles/wireguard/tasks/freebsd.yml b/roles/wireguard/tasks/freebsd.yml new file mode 100644 index 00000000..63e7b48c --- /dev/null +++ b/roles/wireguard/tasks/freebsd.yml @@ -0,0 +1,16 @@ +--- +- name: BSD | WireGuard installed + package: + name: wireguard + state: present + +- set_fact: + service_name: wireguard + tags: always + +- name: BSD | Configure rc script + copy: + src: wireguard.sh + dest: /usr/local/etc/rc.d/wireguard + mode: "0755" + notify: restart wireguard diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml new file mode 100644 index 00000000..33434081 --- /dev/null +++ b/roles/wireguard/tasks/keys.yml @@ -0,0 +1,59 @@ +--- +- name: Delete the lock files + file: + dest: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" + state: absent + when: keys_clean_all|bool == True + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- name: Generate private keys + command: wg genkey + register: wg_genkey + args: + creates: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- block: + - name: Save private keys + copy: + dest: "{{ wireguard_config_path }}/private/{{ item['item'] }}" + content: "{{ item['stdout'] }}" + mode: "0600" + no_log: true + when: item.changed + with_items: "{{ wg_genkey['results'] }}" + delegate_to: localhost + become: false + + - name: Touch the lock file + file: + dest: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" + state: touch + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + when: wg_genkey.changed + +- name: Generate public keys + shell: echo "{{ lookup('file', wireguard_config_path + '/private/' + item) }}" | wg pubkey + register: wg_pubkey + changed_when: false + args: + executable: bash + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- name: Save public keys + copy: + dest: "{{ wireguard_config_path }}/public/{{ item['item'] }}" + content: "{{ item['stdout'] }}" + mode: "0600" + no_log: true + with_items: "{{ wg_pubkey['results'] }}" + delegate_to: localhost + become: false diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml new file mode 100644 index 00000000..fa184fdc --- /dev/null +++ b/roles/wireguard/tasks/main.yml @@ -0,0 +1,85 @@ +--- +- name: Ensure the required directories exist + file: + dest: "{{ wireguard_config_path }}/{{ item }}" + state: directory + recurse: true + with_items: + - private + - public + delegate_to: localhost + become: false + +- name: Include tasks for Ubuntu + include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + tags: always + +- name: Include tasks for FreeBSD + include_tasks: freebsd.yml + when: ansible_distribution == 'FreeBSD' + tags: always + +- name: Generate keys + import_tasks: keys.yml + tags: update-users + +- block: + - block: + - name: WireGuard user list updated + lineinfile: + dest: "{{ wireguard_config_path }}/index.txt" + create: true + mode: "0600" + insertafter: EOF + line: "{{ item }}" + register: lineinfile + with_items: "{{ users }}" + + - set_fact: + wireguard_users: "{{ (lookup('file', wireguard_config_path + 'index.txt')).split('\n') }}" + + - name: WireGuard users config generated + template: + src: client.conf.j2 + dest: "{{ wireguard_config_path }}/{{ item.1 }}.conf" + mode: "0600" + with_indexed_items: "{{ wireguard_users }}" + when: item.1 in users + vars: + index: "{{ item.0 }}" + + - name: Generate QR codes + shell: > + umask 077; + which segno && + segno --scale=5 --output={{ item.1 }}.png \ + "{{ lookup('template', 'client.conf.j2') }}" || true + changed_when: false + with_indexed_items: "{{ wireguard_users }}" + when: item.1 in users + vars: + index: "{{ item.0 }}" + ansible_python_interpreter: "{{ ansible_playbook_python }}" + args: + chdir: "{{ wireguard_config_path }}" + executable: bash + become: false + delegate_to: localhost + + - name: WireGuard configured + template: + src: server.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/wireguard/{{ wireguard_interface }}.conf" + mode: "0600" + notify: restart wireguard + tags: update-users + + +- name: WireGuard enabled and started + service: + name: "{{ service_name }}" + state: started + enabled: true + +- meta: flush_handlers diff --git a/roles/wireguard/tasks/ubuntu.yml b/roles/wireguard/tasks/ubuntu.yml new file mode 100644 index 00000000..c75b8a7b --- /dev/null +++ b/roles/wireguard/tasks/ubuntu.yml @@ -0,0 +1,32 @@ +--- +- name: WireGuard repository configured + apt_repository: + repo: ppa:wireguard/wireguard + state: present + register: result + until: result is succeeded + retries: 10 + delay: 3 + +- name: WireGuard installed + apt: + name: wireguard + state: present + update_cache: true + +- name: WireGuard reload-module-on-update + file: + dest: /etc/wireguard/.reload-module-on-update + state: touch + +- name: Configure unattended-upgrades + copy: + src: 50-wireguard-unattended-upgrades + dest: /etc/apt/apt.conf.d/50-wireguard-unattended-upgrades + owner: root + group: root + mode: 0644 + +- set_fact: + service_name: "wg-quick@{{ wireguard_interface }}" + tags: always diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 new file mode 100644 index 00000000..05bdea00 --- /dev/null +++ b/roles/wireguard/templates/client.conf.j2 @@ -0,0 +1,10 @@ +[Interface] +PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} +Address = {{ wireguard_client_ip }} +DNS = {{ wireguard_dns_servers }} + +[Peer] +PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} +PersistentKeepalive = 25 diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 new file mode 100644 index 00000000..eb77f13a --- /dev/null +++ b/roles/wireguard/templates/server.conf.j2 @@ -0,0 +1,17 @@ +[Interface] +Address = {{ wireguard_server_ip }} +ListenPort = {{ wireguard_port }} +PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} +SaveConfig = false + +{% for u in wireguard_users %} +{% if u in users %} +{% set index = loop.index %} + +[Peer] +# {{ u }} +PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} +AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index }}/128{% endif %} + +{% endif %} +{% endfor %} diff --git a/server.yml b/server.yml new file mode 100644 index 00000000..b6e8340b --- /dev/null +++ b/server.yml @@ -0,0 +1,80 @@ +--- +- name: Configure the server and install required software + hosts: vpn-host + gather_facts: false + tags: algo + become: true + vars_files: + - config.cfg + + roles: + - role: common + tags: common + - role: dns_encryption + when: dns_encryption + tags: dns_encryption + - role: dns_adblocking + when: algo_local_dns + tags: dns_adblocking + - role: wireguard + when: wireguard_enabled + tags: wireguard + - role: vpn + tags: vpn + - role: ssh_tunneling + when: algo_ssh_tunneling + tags: ssh_tunneling + + post_tasks: + - block: + - name: Delete the CA key + local_action: + module: file + path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + state: absent + become: false + when: not algo_store_cakey + + - name: Dump the configuration + local_action: + module: copy + dest: "configs/{{ IP_subject_alt_name }}/config.yml" + content: | + server: {{ 'localhost' if inventory_hostname == 'localhost' else inventory_hostname }} + server_user: {{ ansible_ssh_user }} + {% if algo_provider != "local" %} + ansible_ssh_private_key_file: {{ ansible_ssh_private_key_file|default(SSH_keys.private) }} + {% endif %} + algo_provider: {{ algo_provider }} + algo_server_name: {{ algo_server_name }} + algo_ondemand_cellular: {{ algo_ondemand_cellular }} + algo_ondemand_wifi: {{ algo_ondemand_wifi }} + algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude }} + algo_local_dns: {{ algo_local_dns }} + algo_ssh_tunneling: {{ algo_ssh_tunneling }} + algo_windows: {{ algo_windows }} + algo_store_cakey: {{ algo_store_cakey }} + IP_subject_alt_name: {{ IP_subject_alt_name }} + {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} + become: false + + - name: Create a symlink if deploying to localhost + file: + src: "{{ IP_subject_alt_name }}" + dest: configs/localhost + state: link + force: true + when: inventory_hostname == 'localhost' + + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" + - " {% if algo_store_cakey %}{{ congrats.ca_key_pass }}{% endif %}" + - " {% if algo_provider != 'local' %}{{ congrats.ssh_access }}{% endif %}" + tags: always + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh new file mode 100755 index 00000000..fc7d038e --- /dev/null +++ b/tests/local-deploy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +DEPLOY_ARGS="provider=local server=$LXC_IP ssh_user=ubuntu endpoint=$LXC_IP apparmor_enabled=false ondemand_cellular=true ondemand_wifi=true ondemand_wifi_exclude=test local_dns=true ssh_tunneling=true windows=true store_cakey=true install_headers=false tests=true" + +if [ "${LXC_NAME}" == "docker" ] +then + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook main.yml -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" +else + ansible-playbook main.yml -e "${DEPLOY_ARGS}" --skip-tags apparmor +fi diff --git a/tests/lxd-bridge b/tests/lxd-bridge new file mode 100644 index 00000000..0614e87b --- /dev/null +++ b/tests/lxd-bridge @@ -0,0 +1,16 @@ +USE_LXD_BRIDGE="true" +LXD_BRIDGE="lxdbr0" +UPDATE_PROFILE="true" +LXD_CONFILE="" +LXD_DOMAIN="lxd" +LXD_IPV4_ADDR="10.0.8.1" +LXD_IPV4_NETMASK="255.255.255.0" +LXD_IPV4_NETWORK="10.0.8.0/24" +LXD_IPV4_DHCP_RANGE="10.0.8.2,10.0.8.254" +LXD_IPV4_DHCP_MAX="250" +LXD_IPV4_NAT="true" +LXD_IPV6_ADDR="" +LXD_IPV6_MASK="" +LXD_IPV6_NETWORK="" +LXD_IPV6_NAT="false" +LXD_IPV6_PROXY="true" diff --git a/tests/update-users.sh b/tests/update-users.sh index 8777c82a..ba40bb33 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -2,15 +2,16 @@ set -ex -CAPW=`cat /tmp/ca_password` +USER_ARGS="{ 'server': '$LXC_IP', 'users': ['user1', 'user2'] }" -sed -i 's/- jack$/- jack_test/' config.cfg +if [ "${LXC_NAME}" == "docker" ] +then + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\" -t update-users" +else + ansible-playbook users.yml -e "${USER_ARGS}" -t update-users +fi -ansible-playbook users.yml -e "server_ip=$LXC_IP server_user=root ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" - -cd configs/$LXC_IP/pki/ - -if openssl crl -inform pem -noout -text -in crl/jack.crt | grep CRL +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.crt | grep CRL then echo "The CRL check passed" else @@ -18,7 +19,7 @@ if openssl crl -inform pem -noout -text -in crl/jack.crt | grep CRL exit 1 fi -if openssl x509 -inform pem -noout -text -in certs/jack_test.crt | grep CN=jack_test +if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/user1.crt | grep CN=user1 then echo "The new user exists" else diff --git a/users.yml b/users.yml index 92792085..30e460ae 100644 --- a/users.yml +++ b/users.yml @@ -1,5 +1,4 @@ --- - - hosts: localhost gather_facts: False tags: always @@ -8,27 +7,43 @@ tasks: - block: + - pause: + prompt: "Enter the IP address of your server: (or use localhost for local installation)" + register: _server + when: server is undefined + + - name: Set facts based on the input + set_fact: + algo_server: >- + {% if server is defined %}{{ server }} + {%- elif _server.user_input is defined and _server.user_input != "" %}{{ _server.user_input }} + {%- else %}omit{% endif %} + + - name: Import host specific variables + include_vars: + file: "configs/{{ algo_server }}/config.yml" + + - pause: + prompt: Enter the password for the private CA key + echo: false + register: _ca_password + when: ca_password is undefined + + - name: Set facts based on the input + set_fact: + CA_password: >- + {% if ca_password is defined %}{{ ca_password }} + {%- elif _ca_password.user_input is defined and _ca_password.user_input != "" %}{{ _ca_password.user_input }} + {%- else %}omit{% endif %} + - name: Add the server to the vpn-host group add_host: - hostname: "{{ server_ip }}" - groupname: vpn-host - ansible_ssh_user: "{{ server_user }}" + name: "{{ algo_server }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user|default('root') }}" + ansible_connection: "{% if algo_server == 'localhost' %}local{% else %}ssh{% endif %}" ansible_python_interpreter: "/usr/bin/python2.7" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - easyrsa_CA_password: "{{ easyrsa_CA_password }}" - IP_subject: "{{ IP_subject_alt_name }}" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - - - name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ server_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present - become: false + CA_password: "{{ CA_password }}" rescue: - debug: var=fail_hint tags: always @@ -41,12 +56,12 @@ become: true vars_files: - config.cfg + - "configs/{{ inventory_hostname }}/config.yml" pre_tasks: - block: - - name: Common pre-tasks - include: playbooks/common.yml - tags: always + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml rescue: - debug: var=fail_hint tags: always @@ -54,8 +69,14 @@ tags: always roles: - - { role: ssh_tunneling, tags: always, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - { role: vpn } + - role: common + - role: wireguard + tags: [ 'vpn', 'wireguard' ] + when: wireguard_enabled + - role: vpn + tags: vpn + - role: ssh_tunneling + when: algo_ssh_tunneling post_tasks: - block: diff --git a/roles/cloud-azure/handlers/main.yml b/venvs/.gitinit similarity index 100% rename from roles/cloud-azure/handlers/main.yml rename to venvs/.gitinit