From d3d22fec47dc2e6d780ca5a257d98f58b55df9ec Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 29 Mar 2019 17:51:50 +0300 Subject: [PATCH 01/30] Script to support cloud-init and local easy deploy (#1366) * add the install script to support cloud-init and local one-shot deployments * update travis-ci tests * update docs * enable no_log again * update docs --- .travis.yml | 71 ++++++----- ...-from-script-or-cloud-init-to-localhost.md | 58 +++++++++ install.sh | 115 ++++++++++++++++++ tests/cloud-init.sh | 17 +++ 4 files changed, 232 insertions(+), 29 deletions(-) create mode 100644 docs/deploy-from-script-or-cloud-init-to-localhost.md create mode 100644 install.sh create mode 100755 tests/cloud-init.sh diff --git a/.travis.yml b/.travis.yml index 47a58a95..7a2c67d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,11 @@ --- language: python python: "2.7" -sudo: required -dist: trusty +dist: xenial services: - docker -matrix: - fast_finish: true - addons: apt: sources: @@ -41,35 +37,52 @@ before_cache: - sudo tar cf $HOME/lxc/cache.tar /var/lib/lxd/images/ - sudo chown $USER. $HOME/lxc/cache.tar -env: - - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=18.04 +matrix: + fast_finish: true + include: + - stage: Test + name: local deployment from docker + script: + - docker build -t travis/algo . + - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." + - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge + - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' + - echo -e "#cloud-config\nssh_authorized_keys:\n - $(cat ~/.ssh/id_rsa.pub)" | sudo lxc profile set default user.user-data - + - sudo service lxd restart + - sudo lxc launch ubuntu:18.04 algo + - until host algo.lxd 10.0.8.1 -t A; do sleep 3; done + - export LXC_IP="$(dig algo.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 + - ansible-playbook main.yml --syntax-check + - ./tests/local-deploy.sh + - ./tests/update-users.sh -before_install: - - test "${LXC_NAME}" != "docker" && sudo modprobe wireguard || docker build -t travis/algo . + - stage: Test + name: cloud-init deployment + script: + - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." + - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge + - sudo service lxd restart + - bash tests/cloud-init.sh | sudo lxc profile set default user.user-data - + - sudo lxc profile show default + - sudo lxc launch ubuntu:18.04 algo + - until sudo lxc exec algo -- test -f /var/log/cloud-init-output.log; do echo 'Log file not found, Sleep for 3 seconds'; sleep 3; done + - ( sudo lxc exec algo -- tail -f /var/log/cloud-init-output.log & ) + - | + until sudo lxc exec algo -- test -f /var/lib/cloud/data/result.json; do + echo 'Cloud init is not finished. Sleep for 30 seconds'; + sleep 30; + done + - sudo lxc exec algo -- test -f /opt/algo/configs/localhost/.config.yml -install: - - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." - - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' - - 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: +# 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 main.yml users.yml deploy_client.yml - - ansible-playbook main.yml --syntax-check - - ./tests/local-deploy.sh - - ./tests/update-users.sh notifications: email: false diff --git a/docs/deploy-from-script-or-cloud-init-to-localhost.md b/docs/deploy-from-script-or-cloud-init-to-localhost.md new file mode 100644 index 00000000..57e3ed66 --- /dev/null +++ b/docs/deploy-from-script-or-cloud-init-to-localhost.md @@ -0,0 +1,58 @@ +# Deploy from script or cloud-init + +You can use `install.sh` to prepare the environment and deploy AlgoVPN on the local Ubuntu server in one shot using cloud-init or run the script directly on the server. The script doesn't configure any parameters in your cloud, so it's on your own to configure related [firewall rules](faq.md#what-inbound-ports-are-used), a floating ip address and other resources you may need. + +## Cloud init deployment + +You can copy-paste the snippet below to the user data (cloud-init or startup script) field when creating a new server. For now it is only possible for [DigitalOcean](https://www.digitalocean.com/docs/droplets/resources/metadata/), Amazon [EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) and [Lightsail](https://lightsail.aws.amazon.com/ls/docs/en/articles/lightsail-how-to-configure-server-additional-data-shell-script), [Google Cloud](https://cloud.google.com/compute/docs/startupscript) and [Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/using-cloud-init). + +``` +#!/bin/bash +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +``` +The command will prepare the environment and install AlgoVPN with default parameters. If you want to modify the behaviour you may define additional variables. + +## Variables + +`METHOD` - which method of the deployment to use. Possible values are local and cloud. Default: cloud. The cloud method is intended to use in cloud-init deployments only. If you are not using cloud-init to deploy the server you have to use the local method +`ONDEMAND_CELLULAR` - "Connect On Demand" when connected to cellular networks. Bollean. Default: false +`ONDEMAND_WIFI` - "Connect On Demand" when connected to Wi-Fi. Default: false +`ONDEMAND_WIFI_EXCLUDE` - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand". Comma-separated list. +`WINDOWS` - To support Windows 10 or Linux Desktop clients. Default: false +`STORE_CAKEY` - To retain the CA key. (required to add users in the future, but less secure). Default: false +`LOCAL_DNS` - To install an ad blocking DNS resolver. Default: false +`SSH_TUNNELING` - Enable SSH tunneling for each user. Default: false +`ENDPOINT` - The public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate). It will be gathered automatically for DigitalOcean, AWS, GCE or Azure if the `METHOD` is cloud. Otherwise you need to define this variable according to your public IP address. +`USERS` - list of VPN users. Comma-separated list. +`REPO_SLUG` - Owner and repository that used to get the installation scripts from. Default: trailofbits/algo +`REPO_BRANCH` - Branch for `REPO_SLUG`. Default: master +`EXTRA_VARS` - Additional extra variables. +`ANSIBLE_EXTRA_ARGS` - Any available ansible parameters. ie: `--skip-tags apparmor` + +## Examples + +##### How to customise a cloud-init deployment by variables + +``` +#!/bin/bash +export ONDEMAND_CELLULAR=true +export WINDOWS=true +export SSH_TUNNELING=true +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +``` + +##### How to deploy locally without using cloud-init + +``` +export METHOD=local +export ONDEMAND_CELLULAR=true +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +``` + +##### How to deploy a server using arguments + +The arguments order as per [variables](#variables) above + +``` +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x -s local true false _null true true true true myvpnserver.com +``` diff --git a/install.sh b/install.sh new file mode 100644 index 00000000..248f3784 --- /dev/null +++ b/install.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env sh + +set -ex + +METHOD="${1:-${METHOD:-cloud}}" +ONDEMAND_CELLULAR="${2:-${ONDEMAND_CELLULAR:-false}}" +ONDEMAND_WIFI="${3:-${ONDEMAND_WIFI:-false}}" +ONDEMAND_WIFI_EXCLUDE="${4:-${ONDEMAND_WIFI_EXCLUDE:-_null}}" +WINDOWS="${5:-${WINDOWS:-false}}" +STORE_CAKEY="${6:-${STORE_CAKEY:-false}}" +LOCAL_DNS="${7:-${LOCAL_DNS:-false}}" +SSH_TUNNELING="${8:-${SSH_TUNNELING:-false}}" +ENDPOINT="${9:-${ENDPOINT:-localhost}}" +USERS="${10:-${USERS:-user1}}" +REPO_SLUG="${11:-${REPO_SLUG:-trailofbits/algo}}" +REPO_BRANCH="${12:-${REPO_BRANCH:-master}}" +EXTRA_VARS="${13:-${EXTRA_VARS:-placeholder=null}}" +ANSIBLE_EXTRA_ARGS="${14:-${ANSIBLE_EXTRA_ARGS}}" + +cd /opt/ + +installRequirements() { + apt-get update + apt-get install \ + software-properties-common \ + git \ + build-essential \ + libssl-dev \ + libffi-dev \ + python-dev \ + python-pip \ + python-setuptools \ + python-virtualenv \ + bind9-host \ + jq -y +} + +getAlgo() { + [ ! -d "algo" ] && git clone https://github.com/${REPO_SLUG} algo + cd algo + + git checkout ${REPO_BRANCH} + + python -m virtualenv --python=`which python2` .venv + . .venv/bin/activate + python -m pip install -U pip virtualenv + python -m pip install -r requirements.txt +} + +publicIpFromInterface() { + echo "Couldn't find a valid ipv4 address, using the first IP found on the interfaces as the endpoint." + DEFAULT_INTERFACE="$(ip -4 route list match default | grep -Eo "dev .*" | awk '{print $2}')" + ENDPOINT=$(ip -4 addr sh dev eth0 | grep -w inet | head -n1 | awk '{print $2}' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b') + export ENDPOINT=$ENDPOINT + echo "Using ${ENDPOINT} as the endpoint" +} + +publicIpFromMetadata() { + if curl -s http://169.254.169.254/metadata/v1/vendor-data | grep DigitalOcean >/dev/null; then + PROVIDER="digitalocean" + ENDPOINT="$(curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)" + elif test "$(curl -s http://169.254.169.254/latest/meta-data/services/domain)" = "amazonaws.com"; then + PROVIDER="amazon" + ENDPOINT="$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)" + elif host -t A -W 10 metadata.google.internal 127.0.0.53 >/dev/null; then + PROVIDER="gce" + ENDPOINT="$(curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip")" + elif test "$(curl -s -H Metadata:true 'http://169.254.169.254/metadata/instance/compute/publisher/?api-version=2017-04-02&format=text')" = "Canonical"; then + PROVIDER="azure" + ENDPOINT="$(curl -H Metadata:true 'http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/publicIpAddress?api-version=2017-04-02&format=text')" + fi + + if echo ${ENDPOINT} | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"; then + export ENDPOINT=$ENDPOINT + echo "Using ${ENDPOINT} as the endpoint" + else + publicIpFromInterface + fi +} + +deployAlgo() { + getAlgo + + cd /opt/algo + . .venv/bin/activate + + export HOME=/root + export ANSIBLE_LOCAL_TEMP=/root/.ansible/tmp + export ANSIBLE_REMOTE_TEMP=/root/.ansible/tmp + + ansible-playbook main.yml \ + -e provider=local \ + -e ondemand_cellular=${ONDEMAND_CELLULAR} \ + -e ondemand_wifi=${ONDEMAND_WIFI} \ + -e ondemand_wifi_exclude=${ONDEMAND_WIFI_EXCLUDE} \ + -e windows=${WINDOWS} \ + -e store_cakey=${STORE_CAKEY} \ + -e local_dns=${LOCAL_DNS} \ + -e ssh_tunneling=${SSH_TUNNELING} \ + -e endpoint=$ENDPOINT \ + -e users=$(echo "$USERS" | jq -Rc 'split(",")') \ + -e server=localhost \ + -e ssh_user=root \ + -e "${EXTRA_VARS}" \ + --skip-tags debug ${ANSIBLE_EXTRA_ARGS} | + tee /var/log/algo.log +} + +if test $METHOD = "cloud"; then + publicIpFromMetadata +fi + +installRequirements + +deployAlgo diff --git a/tests/cloud-init.sh b/tests/cloud-init.sh new file mode 100755 index 00000000..2d95c995 --- /dev/null +++ b/tests/cloud-init.sh @@ -0,0 +1,17 @@ +#!/bin/bash +echo "#!/bin/bash +export METHOD=local +export ONDEMAND_CELLULAR=true +export ONDEMAND_WIFI=true +export ONDEMAND_WIFI_EXCLUDE=test1,test2 +export WINDOWS=true +export STORE_CAKEY=true +export LOCAL_DNS=true +export ENDPOINT=algo.lxc +export USERS=user1,user2 +export EXTRA_VARS='install_headers=false tests=true apparmor_enabled=false' +export ANSIBLE_EXTRA_ARGS='--skip-tags apparmor' +export REPO_SLUG=${TRAVIS_PULL_REQUEST_SLUG:-${TRAVIS_REPO_SLUG:-trailofbits/algo}} +export REPO_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH:-master}} + +curl -s https://raw.githubusercontent.com/${TRAVIS_PULL_REQUEST_SLUG:-${TRAVIS_REPO_SLUG}}/${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH}}/install.sh | sudo -E bash -x" From 84bbc0e22ceb1328fd7f71e19effb876ee54e2ca Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 2 Apr 2019 13:21:45 +0300 Subject: [PATCH 02/30] Update ubuntu.yml (#1383) --- roles/common/tasks/ubuntu.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index a37a8c00..c49b8924 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -86,6 +86,7 @@ - iptables-persistent - cgroup-tools - openssl + - gnupg2 sysctl: - item: net.ipv4.ip_forward value: 1 From cf4d5b47a91b35b6c3f58064356c361b01322737 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 6 Apr 2019 13:44:13 +0300 Subject: [PATCH 03/30] IPv6 range to AllowedIPs only when ipv6_support (#1388) --- roles/wireguard/templates/client.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 4c2fc62a..5a4a759f 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -7,6 +7,6 @@ DNS = {{ wireguard_dns_servers }} [Peer] PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + IP_subject_alt_name) }} -AllowedIPs = 0.0.0.0/0, ::/0 +AllowedIPs = 0.0.0.0/0{{ ', ::/0' if ipv6_support else '' }} Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} {{ 'PersistentKeepalive = ' + wireguard_PersistentKeepalive|string if wireguard_PersistentKeepalive > 0 else '' }} From d50a2039a65fedd2d465ac17732ca2aa923425a3 Mon Sep 17 00:00:00 2001 From: David Myers Date: Sat, 6 Apr 2019 06:49:25 -0400 Subject: [PATCH 04/30] Use VULTR_API_CONFIG variable if set (#1374) --- roles/cloud-vultr/tasks/prompts.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index 69978e83..d037bf1a 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -4,7 +4,9 @@ Enter the local path to your configuration INI file (https://trailofbits.github.io/algo/cloud-vultr.html): register: _vultr_config - when: vultr_config is undefined + when: + - vultr_config is undefined + - lookup('env','VULTR_API_CONFIG')|length <= 0 - name: Set the token as a fact set_fact: From 8af0efa623da3f6e39fac9eeb19402bba62fca0f Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Sun, 7 Apr 2019 04:24:31 -0400 Subject: [PATCH 05/30] Update DNS filtering advice in FAQ (#1389) * Update DNS filtering advice in FAQ Updates how to temporarily disable adblocking on IPsec and Wireguard clients separately, and also updates the IPSsec command to avoid `ipsec restart` which [isn't appreciated by systemd](https://bugs.launchpad.net/ubuntu/+source/strongswan/+bug/1287339). * Update faq.md Fix typo --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index db11965d..81184319 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -50,7 +50,7 @@ Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhe ## 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. +You can temporarily disable DNS filtering for all IPsec clients at once with the following 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 `sudo systemctl restart strongswan`. DNS filtering for Wireguard clients has to be disabled on each client device separately by modifying the settings in the app, or by directly modifying the `DNS` setting on the `clientname.conf` file. If all else fails, we recommend deploying a new Algo server without the adblocking feature enabled. ## Wasn't IPSEC backdoored by the US government? From c4ea88000b6022a2e4b5391486b51414653cb7d7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 8 Apr 2019 23:20:34 +0300 Subject: [PATCH 06/30] Refactoring to support roles inclusion (#1365) --- ansible.cfg | 3 +- cloud.yml | 40 +--- config.cfg | 22 +++ input.yml | 195 +++++++++---------- playbooks/rescue.yml | 5 + roles/cloud-azure/tasks/main.yml | 78 ++++---- roles/cloud-digitalocean/tasks/main.yml | 183 +++++++++--------- roles/cloud-ec2/tasks/main.yml | 77 ++++---- roles/cloud-gce/tasks/main.yml | 107 +++++------ roles/cloud-lightsail/tasks/main.yml | 88 ++++----- roles/cloud-openstack/tasks/main.yml | 141 +++++++------- roles/cloud-scaleway/tasks/main.yml | 243 ++++++++++++------------ roles/cloud-vultr/tasks/main.yml | 6 +- roles/common/tasks/main.yml | 48 ++--- roles/common/tasks/ubuntu.yml | 4 +- roles/dns_adblocking/handlers/main.yml | 4 + roles/dns_adblocking/tasks/main.yml | 78 ++++---- roles/local/tasks/main.yml | 11 +- roles/ssh_tunneling/tasks/main.yml | 210 ++++++++++---------- roles/strongswan/defaults/main.yml | 2 - roles/strongswan/tasks/main.yml | 56 +++--- roles/wireguard/defaults/main.yml | 14 -- roles/wireguard/tasks/main.yml | 1 - server.yml | 152 ++++++++------- users.yml | 54 +++--- 25 files changed, 866 insertions(+), 956 deletions(-) create mode 100644 playbooks/rescue.yml diff --git a/ansible.cfg b/ansible.cfg index aef40841..f6b29784 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -4,7 +4,8 @@ pipelining = True retry_files_enabled = False host_key_checking = False timeout = 60 -stdout_callback = full_skip +stdout_callback = default +display_skipped_hosts = no [paramiko_connection] record_host_keys = False diff --git a/cloud.yml b/cloud.yml index 671c7765..310bf23b 100644 --- a/cloud.yml +++ b/cloud.yml @@ -2,48 +2,20 @@ - name: Provision the server hosts: localhost tags: always + become: false vars_files: - config.cfg - pre_tasks: + 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" + - name: Include a provisioning role + include_role: + name: "{{ 'local' if algo_provider == 'local' else 'cloud-' + algo_provider }}" - 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 + - include_tasks: playbooks/rescue.yml diff --git a/config.cfg b/config.cfg index bfcaac68..16411cf0 100644 --- a/config.cfg +++ b/config.cfg @@ -25,6 +25,12 @@ ipsec_enabled: true # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 +# rightsourceip for ipsec +# ipv4 +strongswan_network: 10.19.48.0/24 +# ipv6 +strongswan_network_ipv6: 'fd9d:bc11:4020::/48' + # Deploy WireGuard wireguard_enabled: true wireguard_port: 51820 @@ -33,6 +39,22 @@ wireguard_port: 51820 # See: https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence wireguard_PersistentKeepalive: 0 +# WireGuard network configuration +_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_network_ipv4: "{{ _wireguard_network_ipv4['subnet'] }}/{{ _wireguard_network_ipv4['prefix'] }}" +wireguard_network_ipv6: "{{ _wireguard_network_ipv6['subnet'] }}/{{ _wireguard_network_ipv6['prefix'] }}" + # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission # Unit) than the normal value of 1500 and if you don't reduce the MTU of your diff --git a/input.yml b/input.yml index 5cc60170..f4b155b5 100644 --- a/input.yml +++ b/input.yml @@ -25,115 +25,118 @@ - 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" - block: - pause: prompt: | - Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? - [y/N] - register: _ondemand_cellular - when: ondemand_cellular is undefined + 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: | - Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? - [y/N] - register: _ondemand_wifi - when: ondemand_wifi is undefined - - - pause: - prompt: | - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" - (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) - register: _ondemand_wifi_exclude + Name the vpn server + [algo] + register: _algo_server_name when: - - ondemand_wifi_exclude is undefined - - (ondemand_wifi|default(false)|bool) or - (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) + - server_name is undefined + - algo_provider != "local" + - block: + - pause: + prompt: | + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? + [y/N] + register: _ondemand_cellular + when: ondemand_cellular is undefined + + - pause: + prompt: | + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? + [y/N] + register: _ondemand_wifi + when: ondemand_wifi is undefined + + - pause: + prompt: | + List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" + (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 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 + when: ipsec_enabled - pause: prompt: | - Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) + Do you want to install an ad blocking DNS resolver on this VPN server? [y/N] - register: _windows - when: windows is undefined + register: _local_dns + when: local_dns is undefined - pause: prompt: | - Do you want to retain the CA key? (required to add users in the future, but less secure) + Do you want each user to have their own account for SSH tunneling? [y/N] - register: _store_cakey - when: store_cakey is undefined - when: ipsec_enabled + register: _ssh_tunneling + when: ssh_tunneling is undefined - - pause: - prompt: | - Do you want to install an ad blocking DNS resolver on this VPN server? - [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 - - - 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 | b64encode }} - {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input | b64encode }} - {%- else %}{{ '_null' | b64encode }}{% 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 %} + - 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 | b64encode }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input | b64encode }} + {%- else %}{{ '_null' | b64encode }}{% 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 %} + rescue: + - include_tasks: playbooks/rescue.yml diff --git a/playbooks/rescue.yml b/playbooks/rescue.yml new file mode 100644 index 00000000..4c090cec --- /dev/null +++ b/playbooks/rescue.yml @@ -0,0 +1,5 @@ +--- +- debug: + var: fail_hint + +- fail: diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 113352ce..2c448093 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,47 +1,41 @@ --- +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - name: Include prompts + import_tasks: prompts.yml - - block: - - name: Include prompts - import_tasks: prompts.yml + - 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 %} - - 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 AlgoVPN Server + azure_rm_deployment: + state: present + deployment_name: "{{ algo_server_name }}" + template: "{{ lookup('file', 'deployment.json') }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group_name: "{{ algo_server_name }}" + location: "{{ algo_region }}" + parameters: + sshKeyData: + value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + WireGuardPort: + value: "{{ wireguard_port }}" + vmSize: + value: "{{ cloud_providers.azure.size }}" + imageReferenceSku: + value: "{{ cloud_providers.azure.image }}" + register: azure_rm_deployment - - name: Create AlgoVPN Server - azure_rm_deployment: - state: present - deployment_name: "{{ algo_server_name }}" - template: "{{ lookup('file', 'deployment.json') }}" - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group_name: "{{ algo_server_name }}" - location: "{{ algo_region }}" - parameters: - sshKeyData: - value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - WireGuardPort: - value: "{{ wireguard_port }}" - vmSize: - value: "{{ cloud_providers.azure.size }}" - imageReferenceSku: - value: "{{ cloud_providers.azure.image }}" - register: azure_rm_deployment - - - 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 - - fail: - tags: always + - 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/" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 488ea2d1..93baefef 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,110 +1,105 @@ +--- +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - name: Include prompts + import_tasks: prompts.yml - - block: - - name: Include prompts - import_tasks: prompts.yml + - 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 }}') }}" - - 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 }}') }}" - - - 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 - - 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." - - - name: "Upload the SSH key" + - block: + - name: "Delete the existing Algo SSH keys" digital_ocean: - state: present + state: absent command: ssh - ssh_pub_key: "{{ public_key }}" api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" - register: do_ssh_key + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - name: "Creating a droplet..." + rescue: + - name: Collect the fail error 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 + state: absent + command: ssh api_token: "{{ algo_do_token }}" - ipv6: yes - register: do + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes - - set_fact: - cloud_instance_ip: "{{ do.droplet.ip_address }}" - ansible_ssh_user: root + - debug: var=ssh_keys - - name: Tag the droplet - digital_ocean_tag: - name: "Environment:Algo" - resource_id: "{{ do.droplet.id }}" + - fail: + msg: "Please, ensure that your API token is not read-only." + + - 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 + + - 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 + + - set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" + ansible_ssh_user: root + + - name: Tag the droplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ algo_do_token }}" + state: present + + - block: + - name: "Delete the new Algo SSH key" + digital_ocean: + state: absent + command: ssh api_token: "{{ algo_do_token }}" - state: present + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - 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 + 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 - 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 - - 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 - - fail: - tags: always + - fail: + msg: "Please, ensure that your API token is not read-only." + environment: + PYTHONPATH: "{{ digitalocean_venv }}/lib/python2.7/site-packages/" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index ea3a67a4..ce6532be 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,48 +1,43 @@ +--- +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - name: Include prompts + import_tasks: prompts.yml - - block: - - name: Include prompts - import_tasks: prompts.yml + - 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('.', '-') }}" - - 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('.', '-') }}" + - 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 - - 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 + - import_tasks: encrypt_image.yml + when: encrypted - - import_tasks: encrypt_image.yml - when: encrypted + - 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 %} - - 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 %} + - name: Deploy the stack + import_tasks: cloudformation.yml - - name: Deploy the stack - import_tasks: cloudformation.yml - - - 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 - - fail: - tags: always + - set_fact: + cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ ec2_venv }}/lib/python2.7/site-packages/" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index baa5f469..eb211ee4 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,62 +1,57 @@ +--- +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - name: Include prompts + import_tasks: prompts.yml - - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Network configured + gce_net: + name: "{{ algo_server_name }}" + fwname: "{{ 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_server_name }}" - fwname: "{{ 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 }}" + - 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 - - 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: Set External IP as a fact + set_fact: + external_ip: "{{ gce_eip.address }}" + when: cloud_providers.gce.external_static_ip - - name: Set External IP as a fact - set_fact: - external_ip: "{{ gce_eip.address }}" - when: cloud_providers.gce.external_static_ip + - 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_server_name }}" + tags: + - "environment-algo" + register: google_vm - - 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_server_name }}" - tags: - - "environment-algo" - register: google_vm - - - 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 - - fail: - tags: always + - 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/" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index 21e3d459..c152e5ea 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -1,50 +1,44 @@ +--- +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - name: Include prompts + import_tasks: prompts.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 - - 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 + - set_fact: + cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ lightsail_venv }}/lib/python2.7/site-packages/" diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index 75b3db6d..b8c1181e 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -3,87 +3,80 @@ 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') == "" +- name: Build python virtual environment + import_tasks: venv.yml + - block: - - name: Build python virtual environment - import_tasks: venv.yml + - 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 - - 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: 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: 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 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 images - os_image_facts: - image: "{{ cloud_providers.openstack.image }}" + - name: Gather facts about public networks + os_networks_facts: - - 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 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: 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 - - 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 + - set_fact: + cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ openstack_venv }}/lib/python2.7/site-packages/" diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 87ec1d7f..9ff5124f 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -1,140 +1,133 @@ -- block: - - name: Include prompts - import_tasks: prompts.yml +- name: Include prompts + import_tasks: prompts.yml - - name: Set disk size - set_fact: - server_disk_size: 50000000000 +- 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 server size + set_fact: + server_disk_size: 25000000000 + when: cloud_providers.scaleway.size == "START1-XS" - - name: Check if server exists +- 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://cp-{{ algo_region }}.scaleway.com/servers" + 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_servers + 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: "{{ item.id }}" - no_log: true - when: algo_server_name == item.name - with_items: "{{ scaleway_servers.json.servers }}" + server_id: "{{ algo_instance.json.server.id }}" + when: server_id is not defined - - 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: 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: 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: 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 - - 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 +- set_fact: + cloud_instance_ip: "{{ algo_instance['json']['server']['public_ip']['address'] }}" + ansible_ssh_user: root diff --git a/roles/cloud-vultr/tasks/main.yml b/roles/cloud-vultr/tasks/main.yml index 78e514d0..a1dfa90e 100644 --- a/roles/cloud-vultr/tasks/main.yml +++ b/roles/cloud-vultr/tasks/main.yml @@ -1,3 +1,4 @@ +--- - block: - name: Include prompts import_tasks: prompts.yml @@ -29,8 +30,3 @@ environment: VULTR_API_CONFIG: "{{ algo_vultr_config }}" - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index fcb5af11..4a400881 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,32 +1,26 @@ --- -- block: - - name: Check the system - raw: uname -a - register: OS - tags: - - update-users +- name: Check the system + raw: uname -a + register: OS + tags: + - update-users - - include_tasks: ubuntu.yml - when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' - tags: - - update-users +- include_tasks: ubuntu.yml + when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' + tags: + - update-users - - include_tasks: freebsd.yml - when: '"FreeBSD" in OS.stdout' - tags: - - update-users +- include_tasks: freebsd.yml + when: '"FreeBSD" in OS.stdout' + tags: + - update-users - - name: Sysctl tuning - sysctl: name="{{ item.item }}" value="{{ item.value }}" - when: item.item != "" - with_items: - - "{{ sysctl|default([]) }}" - tags: - - always +- name: Sysctl tuning + sysctl: name="{{ item.item }}" value="{{ item.value }}" + when: item.item != "" + with_items: + - "{{ sysctl|default([]) }}" + tags: + - always - - meta: flush_handlers - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always +- meta: flush_handlers diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index c49b8924..08d37a33 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -97,11 +97,9 @@ - name: Install tools apt: - name: "{{ item }}" + name: "{{ tools|default([]) }}" state: present update_cache: true - with_items: - - "{{ tools|default([]) }}" - name: Install headers apt: diff --git a/roles/dns_adblocking/handlers/main.yml b/roles/dns_adblocking/handlers/main.yml index 98278cef..85cbe321 100644 --- a/roles/dns_adblocking/handlers/main.yml +++ b/roles/dns_adblocking/handlers/main.yml @@ -3,3 +3,7 @@ - name: restart apparmor service: name=apparmor state=restarted + +- name: daemon-reload + systemd: + daemon_reload: true diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 6a44dbee..cd1a54a0 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,52 +1,46 @@ --- -- block: - - name: Dnsmasq installed - package: name=dnsmasq +- name: Dnsmasq installed + package: name=dnsmasq - - name: The dnsmasq directory created - file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup +- name: The dnsmasq directory created + file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup - - include_tasks: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' +- include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include_tasks: freebsd.yml - when: ansible_distribution == 'FreeBSD' +- include_tasks: freebsd.yml + when: ansible_distribution == 'FreeBSD' - - name: Dnsmasq configured - template: - src: dnsmasq.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/dnsmasq.conf" - notify: - - restart dnsmasq +- name: Dnsmasq configured + template: + src: dnsmasq.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/dnsmasq.conf" + notify: + - restart dnsmasq - - name: Adblock script created - template: - src: adblock.sh.j2 - dest: /usr/local/sbin/adblock.sh - owner: root - group: "{{ root_group|default('root') }}" - mode: 0755 +- name: Adblock script created + template: + src: adblock.sh.j2 + dest: /usr/local/sbin/adblock.sh + owner: root + group: "{{ root_group|default('root') }}" + mode: 0755 - - name: Adblock script added to cron - cron: - name: Adblock hosts update - minute: "{{ range(0, 60) | random }}" - hour: "{{ range(0, 24) | random }}" - job: /usr/local/sbin/adblock.sh - user: root +- name: Adblock script added to cron + cron: + name: Adblock hosts update + minute: "{{ range(0, 60) | random }}" + hour: "{{ range(0, 24) | random }}" + job: /usr/local/sbin/adblock.sh + user: root - - name: Update adblock hosts - command: /usr/local/sbin/adblock.sh +- name: Update adblock hosts + command: /usr/local/sbin/adblock.sh - - meta: flush_handlers +- meta: flush_handlers - - name: Dnsmasq enabled and started - service: - name: dnsmasq - state: started - enabled: yes - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always +- name: Dnsmasq enabled and started + service: + name: dnsmasq + state: started + enabled: yes diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index 5803cff9..b690b6b7 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -1,10 +1,3 @@ --- -- block: - - name: Include prompts - import_tasks: prompts.yml - - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always +- name: Include prompts + import_tasks: prompts.yml diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index c52840f6..4ea46808 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -1,120 +1,114 @@ --- +- name: Ensure that the sshd_config file has desired options + blockinfile: + dest: /etc/ssh/sshd_config + marker: '# {mark} ANSIBLE MANAGED BLOCK ssh_tunneling_role' + block: | + Match Group algo + AllowTcpForwarding local + AllowAgentForwarding no + AllowStreamLocalForwarding no + PermitTunnel no + X11Forwarding no + notify: + - restart ssh + +- name: Ensure that the algo group exist + group: name=algo state=present + +- name: Ensure that the jail directory exist + file: + path: /var/jail/ + state: directory + mode: 0755 + owner: root + group: "{{ root_group|default('root') }}" + - block: - - name: Ensure that the sshd_config file has desired options - blockinfile: - dest: /etc/ssh/sshd_config - marker: '# {mark} ANSIBLE MANAGED BLOCK ssh_tunneling_role' - block: | - Match Group algo - AllowTcpForwarding local - AllowAgentForwarding no - AllowStreamLocalForwarding no - PermitTunnel no - X11Forwarding no - notify: - - restart ssh + - name: Ensure that the SSH users exist + user: + name: "{{ item }}" + groups: algo + home: '/var/jail/{{ item }}' + createhome: yes + generate_ssh_key: false + shell: /bin/false + state: present + append: yes + with_items: "{{ users }}" - - name: Ensure that the algo group exist - group: name=algo state=present - - - name: Ensure that the jail directory exist + - block: + - name: Clean up the ssh-tunnel directory file: - path: /var/jail/ + dest: "{{ ssh_tunnels_config_path }}" + state: absent + when: keys_clean_all|bool == True + + - name: Ensure the config directories exist + file: + dest: "{{ ssh_tunnels_config_path }}" state: directory - mode: 0755 - owner: root - group: "{{ root_group|default('root') }}" + recurse: yes + mode: '0700' - - block: - - name: Ensure that the SSH users exist - user: - name: "{{ item }}" - groups: algo - home: '/var/jail/{{ item }}' - createhome: yes - generate_ssh_key: false - shell: /bin/false - state: present - append: yes - with_items: "{{ users }}" + - name: Check if the private keys exist + stat: + path: "{{ ssh_tunnels_config_path }}/{{ item }}.pem" + register: privatekey + with_items: "{{ users }}" - - block: - - name: Clean up the ssh-tunnel directory - file: - dest: "{{ ssh_tunnels_config_path }}" - state: absent - when: keys_clean_all|bool == True + - name: Build ssh private keys + openssl_privatekey: + path: "{{ ssh_tunnels_config_path }}/{{ item.item }}.pem" + passphrase: "{{ p12_export_password }}" + cipher: aes256 + force: false + no_log: true + when: not item.stat.exists + with_items: "{{ privatekey.results }}" + register: openssl_privatekey - - name: Ensure the config directories exist - file: - dest: "{{ ssh_tunnels_config_path }}" - state: directory - recurse: yes - mode: '0700' + - name: Build ssh public keys + openssl_publickey: + path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pub" + privatekey_path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pem" + privatekey_passphrase: "{{ p12_export_password }}" + format: OpenSSH + force: true + no_log: true + when: item.changed + with_items: "{{ openssl_privatekey.results }}" - - name: Check if the private keys exist - stat: - path: "{{ ssh_tunnels_config_path }}/{{ item }}.pem" - register: privatekey - with_items: "{{ users }}" + - name: Build the client ssh config + template: + src: ssh_config.j2 + dest: "{{ ssh_tunnels_config_path }}/{{ item }}.ssh_config" + mode: 0700 + with_items: "{{ users }}" + delegate_to: localhost + become: false - - name: Build ssh private keys - openssl_privatekey: - path: "{{ ssh_tunnels_config_path }}/{{ item.item }}.pem" - passphrase: "{{ p12_export_password }}" - cipher: aes256 - force: false - no_log: true - when: not item.stat.exists - with_items: "{{ privatekey.results }}" - register: openssl_privatekey + - name: The authorized keys file created + authorized_key: + user: "{{ item }}" + key: "{{ lookup('file', ssh_tunnels_config_path + '/' + item + '.pub') }}" + state: present + manage_dir: true + exclusive: true + with_items: "{{ users }}" - - name: Build ssh public keys - openssl_publickey: - path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pub" - privatekey_path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pem" - privatekey_passphrase: "{{ p12_export_password }}" - format: OpenSSH - force: true - no_log: true - when: item.changed - with_items: "{{ openssl_privatekey.results }}" + - name: Get active users + getent: + database: group + key: algo + split: ':' - - name: Build the client ssh config - template: - src: ssh_config.j2 - dest: "{{ ssh_tunnels_config_path }}/{{ item }}.ssh_config" - mode: 0700 - with_items: "{{ users }}" - delegate_to: localhost - become: false - - - name: The authorized keys file created - authorized_key: - user: "{{ item }}" - key: "{{ lookup('file', ssh_tunnels_config_path + '/' + item + '.pub') }}" - state: present - manage_dir: true - exclusive: true - with_items: "{{ users }}" - - - name: Get active users - getent: - database: group - key: algo - split: ':' - - - name: Delete non-existing users - user: - name: "{{ item }}" - state: absent - remove: yes - force: yes - when: item not in users - with_items: "{{ getent_group['algo'][2].split(',') }}" - tags: update-users - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - name: Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users + with_items: "{{ getent_group['algo'][2].split(',') }}" + tags: update-users diff --git a/roles/strongswan/defaults/main.yml b/roles/strongswan/defaults/main.yml index b8969332..de25120e 100644 --- a/roles/strongswan/defaults/main.yml +++ b/roles/strongswan/defaults/main.yml @@ -1,8 +1,6 @@ --- ipsec_config_path: "configs/{{ IP_subject_alt_name }}/ipsec/" ipsec_pki_path: "{{ ipsec_config_path }}/.pki/" -strongswan_network: 10.19.48.0/24 -strongswan_network_ipv6: 'fd9d:bc11:4020::/48' strongswan_shell: /usr/sbin/nologin strongswan_home: /var/lib/strongswan BetweenClients_DROP: true diff --git a/roles/strongswan/tasks/main.yml b/roles/strongswan/tasks/main.yml index 6b9699e9..e59295d8 100644 --- a/roles/strongswan/tasks/main.yml +++ b/roles/strongswan/tasks/main.yml @@ -1,37 +1,31 @@ --- -- block: - - include_tasks: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' +- include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - name: Ensure that the strongswan user exist - user: - name: strongswan - group: nogroup - shell: "{{ strongswan_shell }}" - home: "{{ strongswan_home }}" - state: present +- 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 +- name: Install strongSwan + package: name=strongswan state=present - - import_tasks: ipsec_configuration.yml - - import_tasks: openssl.yml - tags: update-users - - import_tasks: distribute_keys.yml - - import_tasks: client_configs.yml - delegate_to: localhost - become: no - tags: update-users +- import_tasks: ipsec_configuration.yml +- import_tasks: openssl.yml + tags: update-users +- import_tasks: distribute_keys.yml +- import_tasks: client_configs.yml + delegate_to: localhost + become: no + tags: update-users - - name: strongSwan started - service: - name: strongswan - state: started - enabled: true +- name: strongSwan started + service: + name: strongswan + state: started + enabled: true - - meta: flush_handlers - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always +- meta: flush_handlers diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index d0083366..e61c7780 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -3,20 +3,6 @@ wireguard_PersistentKeepalive: 0 wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" wireguard_pki_path: "{{ wireguard_config_path }}/.pki/" 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_network_ipv4: "{{ _wireguard_network_ipv4['subnet'] }}/{{ _wireguard_network_ipv4['prefix'] }}" -wireguard_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 %} diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 235eaa45..4434d091 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -75,7 +75,6 @@ notify: restart wireguard tags: update-users - - name: WireGuard enabled and started service: name: "{{ service_name }}" diff --git a/server.yml b/server.yml index 1ab38346..40326830 100644 --- a/server.yml +++ b/server.yml @@ -2,84 +2,90 @@ - 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: strongswan - when: ipsec_enabled - tags: ipsec - - role: ssh_tunneling - when: algo_ssh_tunneling - tags: ssh_tunneling - - post_tasks: + tasks: - block: - - name: Delete the CA key - local_action: - module: file - path: "{{ ipsec_pki_path }}/private/cakey.pem" - state: absent - become: false - when: - - ipsec_enabled - - not algo_store_cakey + - import_role: + name: common + tags: common - - 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 }} - ipsec_enabled: {{ ipsec_enabled }} - wireguard_enabled: {{ wireguard_enabled }} - {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} - become: false + - import_role: + name: dns_encryption + when: dns_encryption + tags: dns_encryption - - 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' + - import_role: + name: dns_adblocking + when: algo_local_dns + tags: dns_adblocking - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass if algo_ssh_tunneling or ipsec_enabled else '' }}" - - " {{ congrats.ca_key_pass if algo_store_cakey and ipsec_enabled else '' }}" - - " {{ congrats.ssh_access if algo_provider != 'local' else ''}}" - tags: always + - import_role: + name: wireguard + when: wireguard_enabled + tags: wireguard + + - import_role: + name: strongswan + when: ipsec_enabled + tags: ipsec + + - import_role: + name: ssh_tunneling + when: algo_ssh_tunneling + tags: ssh_tunneling + + - block: + - name: Delete the CA key + local_action: + module: file + path: "{{ ipsec_pki_path }}/private/cakey.pem" + state: absent + become: false + when: + - ipsec_enabled + - 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 }} + ipsec_enabled: {{ ipsec_enabled }} + wireguard_enabled: {{ wireguard_enabled }} + {% 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_ssh_tunneling or ipsec_enabled else '' }}" + - " {{ congrats.ca_key_pass if algo_store_cakey and ipsec_enabled else '' }}" + - " {{ congrats.ssh_access if algo_provider != 'local' else ''}}" + tags: always rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - include_tasks: playbooks/rescue.yml diff --git a/users.yml b/users.yml index 3f742946..43473252 100644 --- a/users.yml +++ b/users.yml @@ -47,10 +47,7 @@ ansible_python_interpreter: "/usr/bin/python3" CA_password: "{{ CA_password }}" rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - include_tasks: playbooks/rescue.yml - name: User management hosts: vpn-host @@ -60,37 +57,32 @@ - config.cfg - "configs/{{ inventory_hostname }}/.config.yml" - pre_tasks: + tasks: - block: - name: Local pre-tasks import_tasks: playbooks/cloud-pre.yml become: false - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - roles: - - role: common - - role: wireguard - tags: [ 'vpn', 'wireguard' ] - when: wireguard_enabled - - role: strongswan - when: ipsec_enabled - tags: ipsec - - role: ssh_tunneling - when: algo_ssh_tunneling + - import_role: + name: common - post_tasks: - - block: - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {% if p12.changed %}{{ congrats.p12_pass }}{% endif %}" - tags: always + - import_role: + name: wireguard + when: wireguard_enabled + + - import_role: + name: strongswan + when: ipsec_enabled + tags: ipsec + + - import_role: + name: ssh_tunneling + when: algo_ssh_tunneling + + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {% if p12.changed %}{{ congrats.p12_pass }}{% endif %}" + tags: always rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - include_tasks: playbooks/rescue.yml From d969b8e1b65273ef92a1e386529735ffb6e2acc4 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 9 Apr 2019 08:37:08 -0400 Subject: [PATCH 07/30] Fix 963 again (#1379) * Create charon.conf.j2 Create charon.conf template with mods * Update mobileconfig.j2 Increase client side lifetimes * Update ipsec.conf.j2 Add server-side lifetimes * Add charon.conf --- .../strongswan/tasks/ipsec_configuration.yml | 5 + roles/strongswan/templates/charon.conf.j2 | 365 ++++++++++++++++++ roles/strongswan/templates/ipsec.conf.j2 | 2 + roles/strongswan/templates/mobileconfig.j2 | 4 +- 4 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 roles/strongswan/templates/charon.conf.j2 diff --git a/roles/strongswan/tasks/ipsec_configuration.yml b/roles/strongswan/tasks/ipsec_configuration.yml index ce5b3e5b..af19ed43 100644 --- a/roles/strongswan/tasks/ipsec_configuration.yml +++ b/roles/strongswan/tasks/ipsec_configuration.yml @@ -23,6 +23,11 @@ owner: strongswan group: "{{ root_group|default('root') }}" mode: "0600" + - src: charon.conf.j2 + dest: "strongswan.d/charon.conf" + owner: root + group: "{{ root_group|default('root') }}" + mode: "0644" notify: - restart strongswan diff --git a/roles/strongswan/templates/charon.conf.j2 b/roles/strongswan/templates/charon.conf.j2 new file mode 100644 index 00000000..303c92f6 --- /dev/null +++ b/roles/strongswan/templates/charon.conf.j2 @@ -0,0 +1,365 @@ +# Options for the charon IKE daemon. +charon { + + # Accept unencrypted ID and HASH payloads in IKEv1 Main Mode. + # accept_unencrypted_mainmode_messages = no + + # Maximum number of half-open IKE_SAs for a single peer IP. + # block_threshold = 5 + + # Whether Certificate Revocation Lists (CRLs) fetched via HTTP or LDAP + # should be saved under a unique file name derived from the public key of + # the Certification Authority (CA) to /etc/ipsec.d/crls (stroke) or + # /etc/swanctl/x509crl (vici), respectively. + # cache_crls = no + + # Whether relations in validated certificate chains should be cached in + # memory. + # cert_cache = yes + + # Send Cisco Unity vendor ID payload (IKEv1 only). + # cisco_unity = no + + # Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed. + close_ike_on_child_failure = yes + + # Number of half-open IKE_SAs that activate the cookie mechanism. + # cookie_threshold = 10 + + # Delete CHILD_SAs right after they got successfully rekeyed (IKEv1 only). + # delete_rekeyed = no + + # Delay in seconds until inbound IPsec SAs are deleted after rekeyings + # (IKEv2 only). + # delete_rekeyed_delay = 5 + + # Use ANSI X9.42 DH exponent size or optimum size matched to cryptographic + # strength. + # dh_exponent_ansi_x9_42 = yes + + # Use RTLD_NOW with dlopen when loading plugins and IMV/IMCs to reveal + # missing symbols immediately. + # dlopen_use_rtld_now = no + + # DNS server assigned to peer via configuration payload (CP). + # dns1 = + + # DNS server assigned to peer via configuration payload (CP). + # dns2 = + + # Enable Denial of Service protection using cookies and aggressiveness + # checks. + # dos_protection = yes + + # Compliance with the errata for RFC 4753. + # ecp_x_coordinate_only = yes + + # Free objects during authentication (might conflict with plugins). + # flush_auth_cfg = no + + # Whether to follow IKEv2 redirects (RFC 5685). + # follow_redirects = yes + + # Maximum size (complete IP datagram size in bytes) of a sent IKE fragment + # when using proprietary IKEv1 or standardized IKEv2 fragmentation, defaults + # to 1280 (use 0 for address family specific default values, which uses a + # lower value for IPv4). If specified this limit is used for both IPv4 and + # IPv6. + # fragment_size = 1280 + + # Name of the group the daemon changes to after startup. + # group = + + # Timeout in seconds for connecting IKE_SAs (also see IKE_SA_INIT DROPPING). + half_open_timeout = 5 + + # Enable hash and URL support. + # hash_and_url = no + + # Allow IKEv1 Aggressive Mode with pre-shared keys as responder. + # i_dont_care_about_security_and_use_aggressive_mode_psk = no + + # Whether to ignore the traffic selectors from the kernel's acquire events + # for IKEv2 connections (they are not used for IKEv1). + # ignore_acquire_ts = no + + # A space-separated list of routing tables to be excluded from route + # lookups. + # ignore_routing_tables = + + # Maximum number of IKE_SAs that can be established at the same time before + # new connection attempts are blocked. + # ikesa_limit = 0 + + # Number of exclusively locked segments in the hash table. + # ikesa_table_segments = 1 + + # Size of the IKE_SA hash table. + # ikesa_table_size = 1 + + # Whether to close IKE_SA if the only CHILD_SA closed due to inactivity. + inactivity_close_ike = yes + + # Limit new connections based on the current number of half open IKE_SAs, + # see IKE_SA_INIT DROPPING in strongswan.conf(5). + # init_limit_half_open = 0 + + # Limit new connections based on the number of queued jobs. + # init_limit_job_load = 0 + + # Causes charon daemon to ignore IKE initiation requests. + # initiator_only = no + + # Install routes into a separate routing table for established IPsec + # tunnels. + # install_routes = yes + + # Install virtual IP addresses. + # install_virtual_ip = yes + + # The name of the interface on which virtual IP addresses should be + # installed. + # install_virtual_ip_on = + + # Check daemon, libstrongswan and plugin integrity at startup. + # integrity_test = no + + # A comma-separated list of network interfaces that should be ignored, if + # interfaces_use is specified this option has no effect. + # interfaces_ignore = + + # A comma-separated list of network interfaces that should be used by + # charon. All other interfaces are ignored. + # interfaces_use = + + # NAT keep alive interval. + keep_alive = 25s + + # Plugins to load in the IKE daemon charon. + # load = + + # Determine plugins to load via each plugin's load option. + # load_modular = no + + # Initiate IKEv2 reauthentication with a make-before-break scheme. + # make_before_break = no + + # Maximum number of IKEv1 phase 2 exchanges per IKE_SA to keep state about + # and track concurrently. + # max_ikev1_exchanges = 3 + + # Maximum packet size accepted by charon. + # max_packet = 10000 + + # Enable multiple authentication exchanges (RFC 4739). + # multiple_authentication = yes + + # WINS servers assigned to peer via configuration payload (CP). + # nbns1 = + + # WINS servers assigned to peer via configuration payload (CP). + # nbns2 = + + # UDP port used locally. If set to 0 a random port will be allocated. + # port = 500 + + # UDP port used locally in case of NAT-T. If set to 0 a random port will be + # allocated. Has to be different from charon.port, otherwise a random port + # will be allocated. + # port_nat_t = 4500 + + # Whether to prefer updating SAs to the path with the best route. + # prefer_best_path = no + + # Prefer locally configured proposals for IKE/IPsec over supplied ones as + # responder (disabling this can avoid keying retries due to + # INVALID_KE_PAYLOAD notifies). + # prefer_configured_proposals = yes + + # By default public IPv6 addresses are preferred over temporary ones (RFC + # 4941), to make connections more stable. Enable this option to reverse + # this. + # prefer_temporary_addrs = no + + # Process RTM_NEWROUTE and RTM_DELROUTE events. + # process_route = yes + + # Delay in ms for receiving packets, to simulate larger RTT. + # receive_delay = 0 + + # Delay request messages. + # receive_delay_request = yes + + # Delay response messages. + # receive_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # receive_delay_type = 0 + + # Size of the AH/ESP replay window, in packets. + # replay_window = 32 + + # Base to use for calculating exponential back off, see IKEv2 RETRANSMISSION + # in strongswan.conf(5). + # retransmit_base = 1.8 + + # Maximum jitter in percent to apply randomly to calculated retransmission + # timeout (0 to disable). + # retransmit_jitter = 0 + + # Upper limit in seconds for calculated retransmission timeout (0 to + # disable). + # retransmit_limit = 0 + + # Timeout in seconds before sending first retransmit. + # retransmit_timeout = 4.0 + + # Number of times to retransmit a packet before giving up. + # retransmit_tries = 5 + + # Interval in seconds to use when retrying to initiate an IKE_SA (e.g. if + # DNS resolution failed), 0 to disable retries. + # retry_initiate_interval = 0 + + # Initiate CHILD_SA within existing IKE_SAs (always enabled for IKEv1). + reuse_ikesa = yes + + # Numerical routing table to install routes to. + # routing_table = + + # Priority of the routing table. + # routing_table_prio = + + # Whether to use RSA with PSS padding instead of PKCS#1 padding by default. + # rsa_pss = no + + # Delay in ms for sending packets, to simulate larger RTT. + # send_delay = 0 + + # Delay request messages. + # send_delay_request = yes + + # Delay response messages. + # send_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # send_delay_type = 0 + + # Send strongSwan vendor ID payload + # send_vendor_id = no + + # Whether to enable Signature Authentication as per RFC 7427. + # signature_authentication = yes + + # Whether to enable constraints against IKEv2 signature schemes. + # signature_authentication_constraints = yes + + # The upper limit for SPIs requested from the kernel for IPsec SAs. + # spi_max = 0xcfffffff + + # The lower limit for SPIs requested from the kernel for IPsec SAs. + # spi_min = 0xc0000000 + + # Number of worker threads in charon. + # threads = 16 + + # Name of the user the daemon changes to after startup. + # user = + + crypto_test { + + # Benchmark crypto algorithms and order them by efficiency. + # bench = no + + # Buffer size used for crypto benchmark. + # bench_size = 1024 + + # Number of iterations to test each algorithm. + # bench_time = 50 + + # Test crypto algorithms during registration (requires test vectors + # provided by the test-vectors plugin). + # on_add = no + + # Test crypto algorithms on each crypto primitive instantiation. + # on_create = no + + # Strictly require at least one test vector to enable an algorithm. + # required = no + + # Whether to test RNG with TRUE quality; requires a lot of entropy. + # rng_true = no + + } + + host_resolver { + + # Maximum number of concurrent resolver threads (they are terminated if + # unused). + # max_threads = 3 + + # Minimum number of resolver threads to keep around. + # min_threads = 0 + + } + + leak_detective { + + # Includes source file names and line numbers in leak detective output. + # detailed = yes + + # Threshold in bytes for leaks to be reported (0 to report all). + # usage_threshold = 10240 + + # Threshold in number of allocations for leaks to be reported (0 to + # report all). + # usage_threshold_count = 0 + + } + + processor { + + # Section to configure the number of reserved threads per priority class + # see JOB PRIORITY MANAGEMENT in strongswan.conf(5). + priority_threads { + + } + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is started. + start-scripts { + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is terminated. + stop-scripts { + + } + + tls { + + # List of TLS encryption ciphers. + # cipher = + + # List of TLS key exchange methods. + # key_exchange = + + # List of TLS MAC algorithms. + # mac = + + # List of TLS cipher suites. + # suites = + + } + + x509 { + + # Discard certificates with unsupported or unknown critical extensions. + # enforce_critical = yes + + } + +} diff --git a/roles/strongswan/templates/ipsec.conf.j2 b/roles/strongswan/templates/ipsec.conf.j2 index 68fa3464..7cd27c90 100644 --- a/roles/strongswan/templates/ipsec.conf.j2 +++ b/roles/strongswan/templates/ipsec.conf.j2 @@ -9,6 +9,8 @@ conn %default keyexchange=ikev2 compress=yes dpddelay=35s + lifetime=3h + ikelifetime=12h {% if algo_windows %} ike={{ ciphers.compat.ike }} diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index 6cf0ea13..a8123d58 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -69,7 +69,7 @@ IntegrityAlgorithm SHA2-512 LifeTimeInMinutes - 20 + 1440 DeadPeerDetectionRate Medium @@ -90,7 +90,7 @@ IntegrityAlgorithm SHA2-512 LifeTimeInMinutes - 20 + 1440 LocalIdentifier {{ item.0 }} From 4cb8c6dc2215d40c5b426a7afe979b63e3275b3b Mon Sep 17 00:00:00 2001 From: David Myers Date: Tue, 9 Apr 2019 08:38:18 -0400 Subject: [PATCH 08/30] Consolidate firewall documentation (#1386) --- docs/cloud-do.md | 4 +-- ...-from-script-or-cloud-init-to-localhost.md | 2 +- docs/deploy-to-ubuntu.md | 6 ++-- docs/faq.md | 2 +- docs/firewalls.md | 34 ++++++++++++++++++ docs/images/firewalls.png | Bin 0 -> 115350 bytes 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 docs/firewalls.md create mode 100644 docs/images/firewalls.png diff --git a/docs/cloud-do.md b/docs/cloud-do.md index 25d80a23..c4230a99 100644 --- a/docs/cloud-do.md +++ b/docs/cloud-do.md @@ -96,9 +96,7 @@ For more, see [Scripted Deployment](deploy-from-ansible.md). ## Using the DigitalOcean firewall with Algo -Many cloud providers include the option to configure an external firewall between the Internet and your cloud server. For some providers this is mandatory and Algo will configure it for you, but for DigitalOcean the external firewall is optional. - -An Algo VPN runs its own firewall and doesn't require an external firewall, but you might wish to use the DigitalOcean firewall for example to limit the addresses which can connect to your Algo VPN over SSH, or perhaps to block SSH altogether. +Many cloud providers include the option to configure an external firewall between the Internet and your cloud server. For some providers this is mandatory and Algo will configure it for you, but for DigitalOcean the external firewall is optional. See [AlgoVPN and Firewalls](/docs/firewalls.md) for more information. To configure the DigitalOcean firewall, go to **Networking**, **Firewalls**, and choose **Create Firewall**. diff --git a/docs/deploy-from-script-or-cloud-init-to-localhost.md b/docs/deploy-from-script-or-cloud-init-to-localhost.md index 57e3ed66..7a99d6b2 100644 --- a/docs/deploy-from-script-or-cloud-init-to-localhost.md +++ b/docs/deploy-from-script-or-cloud-init-to-localhost.md @@ -1,6 +1,6 @@ # Deploy from script or cloud-init -You can use `install.sh` to prepare the environment and deploy AlgoVPN on the local Ubuntu server in one shot using cloud-init or run the script directly on the server. The script doesn't configure any parameters in your cloud, so it's on your own to configure related [firewall rules](faq.md#what-inbound-ports-are-used), a floating ip address and other resources you may need. +You can use `install.sh` to prepare the environment and deploy AlgoVPN on the local Ubuntu server in one shot using cloud-init or run the script directly on the server. The script doesn't configure any parameters in your cloud, so it's on your own to configure related [firewall rules](/docs/firewalls.md), a floating ip address and other resources you may need. ## Cloud init deployment diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index f3ba0669..29a54e64 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,6 +1,6 @@ # Local deployment -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. +You can use Algo to configure a local server as an AlgoVPN rather than create and configure a new server on a cloud provider. Install the Algo scripts on your server and follow the normal installation instructions, then choose: ``` @@ -8,4 +8,6 @@ Install to existing Ubuntu 18.04 server (Advanced) ``` Make sure your server is running the operating system specified. -**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. +**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. See [AlgoVPN and Firewalls](/docs/firewalls.md) for more information. + +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/faq.md b/docs/faq.md index 81184319..16a69b32 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -74,4 +74,4 @@ No. ## 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. +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. See [AlgoVPN and Firewalls](/docs/firewalls.md) for more information. diff --git a/docs/firewalls.md b/docs/firewalls.md new file mode 100644 index 00000000..8feb5081 --- /dev/null +++ b/docs/firewalls.md @@ -0,0 +1,34 @@ +# AlgoVPN and Firewalls + +Your AlgoVPN requires properly configured firewalls. The key points to know are: + +* If you deploy to a **cloud** provider all firewall configuration will done automatically. + +* If you perform a **local** installation on an existing server you are responsible for configuring any external firewalls. You must also take care not to interfere with the server firewall configuration of the AlgoVPN. + +## The Two Types of Firewall + +![Firewall Illustration](/docs/images/firewalls.png) + +### Server Firewall + +During installation Algo configures the Linux [Netfilter](https://en.wikipedia.org/wiki/Netfilter) firewall on the server. The rules added are required for AlgoVPN to work properly. The package `netfilter-persistent` is used to load the IPv4 and IPv6 rules files that Algo generates and stores in `/etc/iptables`. The rules for IPv6 are only generated if the server appears to be properly configured for IPv6. The use of conflicting firewall packages on the server such as `ufw` will likely break AlgoVPN. + +### External Firewall + +Most cloud service providers offer a firewall that sits between the Internet and your AlgoVPN. With some providers (such as EC2, Lightsail, and GCE) this firewall is required and is configured by Algo during a **cloud** deployment. If the firewall is not required by the provider then Algo does not configure it. + +External firewalls are not configured when performing a **local** installation, even when using a server from a cloud service provider. + +Any external firewall must be configured to pass the following incoming ports over IPv4 : + +Port | Protocol | Description | Related variables in `config.cfg` +---- | -------- | ----------- | --------------------------------- +22 | TCP | Secure Shell (SSH) | None +500 | UDP | IPsec IKEv2 | `ipsec_enabled` +4500 | UDP | IPsec NAT-T | `ipsec_enabled` +51820 | UDP | WireGuard | `wireguard_enabled`, `wireguard_port` + +If you have chosen to disable either IPsec or WireGuard in `config.cfg` before running `./algo` then the corresponding ports don't need to pass through the firewall. SSH is used when performing a **cloud** deployment and when subsequently modifying the list of VPN users by running `./algo update-users`. + +Even when not required by the cloud service provider, you still might wish to use an external firewall to limit SSH access to your AlgoVPN to connections from certain IP addresses, or perhaps to block SSH access altogether if you don't need it. Every service provider firewall is different so refer to the provider's documentation for more information. diff --git a/docs/images/firewalls.png b/docs/images/firewalls.png new file mode 100644 index 0000000000000000000000000000000000000000..4946bae2bd22821bf37def38216807f33eedc8bd GIT binary patch literal 115350 zcmeFa1yq$=_b-eHiXaL~w_wpACA|$mK|n<|9kQiMy1}iYNa~SpB_-XYG)gHg-5^M( zbaU4O>Z`c%=>5KX#~tJUo-xijdq4YGHRoJ2esiwHlj|}P__!3f7#JA%SFc>YiGhKO zz`)qAgmVBqSqW>C0RO|%y&!!70|VuQw|*NNe5XTPxhai-VSgF}Kp+MN>D|!W8$#fRLwBzzSzutWF+l(BvyBn8#=yYD)4!!?sVIF-0AXs( zcKeR0mNuKM@m=sX28NKW0QhLEZF!r{*4W6zLcms-e)|mp@ELl{PEWV}ilw12y`uDW zx=W_!+H^c@+-w~5BDi#PbVBBLbOdf*7XR8Dd=sYEv$VV`z|L-CW5Z^{#b#=*%g)Kq z&(F?rj{V#@R`3R^g`J7zZCh3o3x=IezV&li+X7*(f7epq)PxS|_qLX)m8CE}Jv7kw ze|F|+sju^&kxVSUE(9r}fxlZ}J@d*7g`5cE{wrnZHtkrmXvyotW0$T^|ymv=w@ z&uBZXE}0tNHP^PV0L?^rzBc*s+3xpc&Gogx`k-M%IKRIC$lnJ9Sb3C2E5EF1zl*u6ja5XpvoX4QQ2@7mDOku_fav;KGHa2N*T7{Zvjb?T4i zw9^KsocrLPt^#VYpMb!VA45p~&>yZGYIC2n<}YYl+MEmr2*Dk*pKhx){u9^To0x-)wYrC90~Dd;CtR8=<}zPs^1~<7~AK`|;$E z=81RKDTS+Jmd)}CG2~o2(J3h@t}Dss$#@KIb!a}oZ}4T5Yj}9*cyNbntGWYgnA2kW z=un8MOuBBJi9x3y1tp(}YR8@D(hVYO6VJtcOs2XVmp?sjOm*29w;T?$iOsPXhL5Q? z&gRn!H}&|?;7wOx;!R7`h0Zw7E82>Jr|GxfK3j^CC|&!sF%pE|B?{>8jBRr;?aQV>q^>-+Oy=-_V|RizwUd#_K~4~1FX0Dz_dimJFu4bwTFsmh3R{a z0zw!m-u=8WLiok`@YLD~vUJ@Pe)=D{yd#P6a&++0iPh9eY^|2jxQ=3MdOC{-cr>zH z!}**uA^3eq;zG?Og9WU{Fs81nHOt8g^H7_=KhaEVa^V%u`MmLzyx_EpI!RaTYj|2S zG<}G!5l%k_ zYvQ#PO(TJ(Y%R0AJz-l>d!3T|4AT-mZcB)n9-n8z)!t~dMib-RDpN4=t8vN*ta3Kq zbJ9U??jTHf@8xkD##HKL%CU0@@%it|#pjYf^t*sFRlg;5oh4A*L#C$IYe`FmZi zRlBW?rK8av(yuxXKPJ19-a(F|y#4b5t?Nmhp3e57?j4r~HuTEp49fGzZ=`O;lW!P3 zvf04IGvF;bp=2xG=C0dYzQiyjj4#Tk zMh`)Epe(m$ByK4xesbfiBsZdmxRU}}`RK-o-Nl-jDgETaD4lr}xp5QU>a6nin-%%w zJ;KS&Hcg%)yis1O;u`9Sp%4|ZjB>Hu6V3(QdXQ36G0v1X4d|p7 zT_xS1n#dRqcZj`fI{bMzSEqNE$M(4vw$wfaDbbCr0*P#kyqu4(JNYtt3? zmHGf7DZ%8nch-ND0kpRsEHM~D+wjit!`|2 z#-!JY2zEQp)#xuyw3kizJ9$&`u^Fd*IISPyw0Jd2!pFowl*W1aljY{ha9rM0E^|$o zVD>9xUCO!bH9#uzUpMTpoQZntEQ*EDDGNLP%gUQr>PRbCRdK&ZHUc zXK?=XFDjJ|78!sUh4GzE zpqa%UZ61Z*)2u$R^xiV?&wUZSGr)bBoadf*{qr_t=D-nq+{Ms%`_KI7TdJe18b1oXXti+T&mK?q1|aS|20uZ2vEV{;x17augHV=?bc5V(xYJ|7cd*H#Pl@ zyyh2I2XQsFsF;Ses9Mxy40sEYhztnL8eYB6Y54J`|DQ|b2o4hRGa<0Zt-{G-ebJ<$!Z9Fb3^eo(ND5$BqS$?GDm=w#i(=wrfEy{+; z^DWthJjuXn=vVqO>Q@JH^4P3Rwo&tLppg4T&!IzpsiDvu>^B2E8G87X^pl>LxQH@=N+nPi>z>)uy7H90TlM|5gK zgQg0WdQ=1WdTa+W-r+KTo;9qooq2n(asN@F>u_U^p1HIGp@;eR6`?LoJsaf!i+PBc@P)YM{5kSiI%uXA-fm1|pbF?Mr9#Be1wZ9n-2V3EUmzIfY22TBc8_@u5CmK5{@iiMlx4Enns zuCa{^<{2hAVt_-X>iDH>bf)BJ-NO7O+STj1^9@oKE15c>4pTX!fe3HLL|(FKJEi?H zzzX%YeX~L(;+mgdlfOd#>SKNv$-~S@5bnj&uJ;`rH8yonSsi3|30lQ1oxnex#j!OT zP!tNBqNjyh8`E)$YU#m5#!@A6beNnBW$uY6fia#|)#ozjKRx8uZ-};>{cr}aGTB18 zX>PNn-PgbhN7cGrdlV>z#HgYAgky<}5|K`$y1l$q01qAW2 zh+@gFVH6!l7Z$yu_|6Yv$Ts!mjcFH`)k=lJf7IxNNRij=XprE6v+}a%I^6lQ5}()qKR(`mmg%g0;E8+C+NeBT5s+Mbbo9$*;-< z3Gfhon(t)!;X^$k=9c}e1~uoA2axlEMB-hJ?DSVL2{HjPtZvF{&=j8m1!o(JovE*9 zwXp~%bH`Kk3l>_W%#yP$Kb#7PzI22?I=gE2&^+_qwaM%_cIPi18~$?limxBHT^)^+ za}4-CiBC+gNA6O;F+-Xk6P6c>`xFMB=$LJYXze3btEY`DYJE1cK00^>yYT|o>S zTJA-ufU1VJsJH0m@_c&4MpA#vP+XX`vN9xZoE21ZsKV;n<78Yq*Pw81jjC;ZWv=nL ztVj^Fv1sf*KeOz%Y9;n&mKPWfwP-jay7D1oGOH7w%i>e~l=%EomqlZb!*rN+kKOPI z!2{Ia^iUHs9a*|M{{lMA_|@m@kfF;ul}hBs54MYCDWOEPsnfWM{`|;Cr$vWBKLr`l z^-o9fI{%@ytiJ#$n`%GN#WqA^XYc0X{4*cNlQz=g!-rV1+O+fo)U03WYt9YTNa==} ziLWkZ7lqNdtbbAMu5(^YhXQ}6l+xftZci?erJOoHV`M?UJM$5Q*CNd-*psS(ol@)t zmJT+zroYVs*+NG62KnbXK z7-x4k3Am)nQ%tgyk})=9_=Y*^lXm1qXqbWy94o^0~f{ zGId`$cj&McvQbu4cw?o`g=Vaz-sP*|hDif|)3E>MZ`|5QYHTg}>b6<_5PCQBv8Gq; z6T!8l8jfFx5;ftHfLls26BnvN1Z}1wyYrg)raq&b`j1CksbbAm^?pP6HgImZPByi0 z)nKZp`Hp3pU7q(Apv{#p)xkjr&PgPU4m&mv2d2FwBy;IQ9*k8`w~JvZl0TDt z*R)yjU}87iN#pS1IyDRX5(JqdKQU^C%zj$w@(SKl+t=T~JQLw)UiQUtkXhqZpoZZP zZ>Q1oo>~9wXbBp-=%g`?&DnsoIq|T|ef1h!tF7Dn7@#t2!e2^{kG_fvmn1yQ1%M}R z=k;YGW;TC#O)Gt_QE5|qIbd%JDrXb;bHG3i(tO>w;M`f(y#|TNBtvRuWyGTZY16P3(JFO= z^e3Cp^mHV#1zXpEaL&j}58OJD@M*3(q37b>R6%fcyTNyYRCE3J_8++tVv?)xa_Oup zZ4&=Y`GEqW;PYl$!RNFk1=eRuswCIvBDSto&^Q=(V@7MptRTy$cI@nj;d1cTN3dKj{I9YQ(|1;Vcv1$1wqpmJ7 zC6_7t{uJ;vm6n|+()(T5%8NKS5apg}F9oOGAb=;5Svd>VKd|=FcFQ`jD79cw`BTs> zTsE)beqa-#+tebq)qbQka>|8444v7bL-NV28MRU#Ozv6JSh9Lj48vXx(RJDQEWY3P zq7$?=$X;T~if&l8(tEHRTYDa`QLr&-Iz6U-Bk^|B>Xc1UXEQa6VvD%Vb$JCFO^Fn* zslNBBIjS$z&*f%|PP-TCRy4NS+0flmu;EZ=w_&L}k~&cUMe?&36nVD7eJ-he$;8m>yv6@gecok#5G@9u0fF}Yn5vrmjDn_Uj zh#|LK5{28$t5_|T-0f}#x2V~4r=9yUB_E>9Ih?y+B99liTWKPs!&WhMXB`H-_!b4D z-v%;l@G9Tzdw5*ty%&TajE)`Y8}BemuQ6}!&szbYGh5E3-p5}Gb)5|C>L@3}H`wYH zle6cdyaf_C>OI&C&+~=H&$S_Z7Vy7BBvKl^zL2sxmYAw8-+eY?EFp1dY-`fgWju3J z(@BGf(x_>@MkK(X=xBfToxI7cs-RLXB_OkTK&rg86r3O@rWxvLgk>NP)gHK@q?^LPM(Q`k-|#Y^ z-vD5d)U@+bUSqdSuUexp!u6dOZ^v1ygBA-assG3q`7sg2FNl0<*>JhDt;I(dS~XJd zRye|wcx|dVE20&XS&|-`yEjXx>&N)PD|H5I5gt8LTXWg3$NB;a=VRV=RS}P!LU0T> z0;3+3yMMA$Ih@Z_ee8$`bJh6tNqCZHvxbyh5>-2OzH+d9YfDdZutm{ob83TN?@FyD zz`6asUZg%hn^#7nLIf9{OjM0A+#kf?H>Dt}E|d=TE!UD0S?_Ta?wx9{w=y->(}qG{ zuF*TqJfLMsQVn6HWL94c&=RYK&3T#SZ*&VXK2q{=^={N_h`-s%G8KJXmqM zVHDM%n?7XfNIUVm46Ar>K;}GeS%Y?HaSOQex9l)HX&X zq22fMf^%=2&brhKOm6~fkN+S&2UAEwx4H->Qda0osnLRZT?l6cTuIySVx&^m@%H4Z z^w#v@t+k@95WLtPoe<-jOYY9?FL}bUwQ37i#}Z#T_`RQhBI`(B6T}r4yD9#uO=P8b z|IurZ^HgJH7{Yn$WS6m!S%oKz3uYPDaI zY+~Hxf0>%xY2kNg+xbMQW06dr(%}6mWc$G_g$&(^6Egut>!(`laLmVha0LT;Q^--c zebYeyyLWpO*FmaohIHRu;Iqk&^(^v<-IKGns#G*P2gmXfkc*Q^wk^`pEl<#;U3o9j z0HPTmcj?shS^-yB(^Rqz9y6=R=JLR3C37lE(oHypn=iFtSxBb7j!>uKzH=c7hsyJT zTiD>^vOZwcEK(Q36G1;Rs-H0efN+$!br!We6q2Wla2CS0OeC#q31*EF*kuAMjwSN>!jr0std*a1e$mImPE%?K54 z(T}|0-uQIXr#5nfBl z4u-Q$1=w~ec+*bP4vnTRYn>Tg=2e3!A9g(11)RfSTL+ZuMt%pkjMyS2J2O*Iht!EN6L6G*p*r$-9^_+|TeDY1YAuSJ9 zIGKvI;Mhn{YvE$%MUK{#ys2QwLqEOr231q$Zgi09F03pBe%#Zp%dBE>OuS&JXlsKJ zQ)wWTyEr*$juGDO0n2D${mekO{L+I>@*uV4^_1RrPoV30wH1~<{ID2ojp<$yTTFd= zOq=p%6=CE;)u~%CR~bv@f+el1a6C-V#$>fAPQ*`(r+ETC)9z4emuwN;AWY#$I&Ed7 zK*RjW|G@Q%zjA)&AbQzJ1}m#Gs5GAf%W%@bFIVipJL`XS6ERJW0|;! zqv%anYEv+-roF@@JjzTqC+o5*r~k3bEGaEGnRee}LRID9V4!0)6PKM!McDys1ljs~MOk0k%$Ecv#-r^+V;JlU4wSXj9!n z$k*Yl=70d2`GA(VD^7n7h};@Ke|T=OfJmG(bsV6~S=Z6Mtl7g?v{Iv*PPlFk#c-0y zz_NFgCU0)%ekrGH1i`*)HHCQup&psafH>?ruNSSTNOIBUYWsvp9fR9sqQ-{7ZTTkP zP8SdmWdX-LIy--m)z!WxToxETTgW#`scY|hr;6OVQ@ZZWe>0e#fD7~7xHQ-vgps71 zF^pL%KEy`M*jnN_AgB-t0}<`0Cs*y+Wcc~Q>B6-JyWy~as%iG9BK`)A4RKW;8(z$J z>5Op94dD9!Q$tg4m-l9g$4xsxEsKCEg7r8v#*#mMaZZDzciHThuwxd{*+eR#71J<% z_sIGN;;4`fK=6PbZ%Td_T@VC<7|X*Q83@8;*-v~|=yXhFFN#2F=nrB%Wwsd*y7?ya z!YAq>niH>US>+oEEcZQV1RHO~$a+dB>z$WFQf7;Cjv=YK?QQ!BRE7CUP4%+aQbPDq zk!8)44>+(+vkGd9m3B`*xH6*}@DKI-OP{n$NelbfkzBCa*btl1_T~1D{o;o{3WHN*r~FX;x=8W>C*pNTL&P*Cao!tg3xFMCUQm9v&UkY* zH8n=~*u*sZeRhPysIBwXlFL@B;T%d?C#Ek?o!L@j`J<0>(>Sh2nSFAg?6gDgT7ydO zYKs;rZUjo$%Ef{mzPUC)yQK(czmIx%cUyf#KbNVr;=iw>0D?+YXH#;N)@NMir0drF z_gxT_QwDN87SSCJ_!+86!4%BS-XL`~0cS6mzqwkiDp{t4py?F0N|@FhvF#qHlna`Y-XFhD(Oba)olL5NhBNt)=~5=>u=fwF?w#UhY1sekMwR|a<*I1dY$luf2EH$eRfQ7Pz zZ&ZV|4i{kGg5#D~A7HA$%^*A*E-fpva_2U2Z^L?^N{otm_AHP*jj{ppnqELTVPjx% z7!OOz8M!zE<7&MEla({Z6h?Ud9>`-zdDvJS->FXryV0^?U;$v16H44^U!u%tKwQ*l&M{nin%nkSt;S@lQZG(2Ngm-vQ0HF z@>R-rB&-7;rn*}TfV!huohHmxsTt>Vs+Sb8&xO#p@lZwwb-&E_>m_a9EVpAAGEL&9 z*q2x*GB3Kmm>gJh6@$4bz-z6j{&b|hwB~FY~joYT;1%ap1 zD<6vmZHxX*SQ(CFj-c+es4T3g~&M>0Hi)`6l<~i#I=T1up-;zdUF&*FMj*?%H z5uEfj$^TLgfvR!Z{z|b!BJkspos}y@7K{67mtpO1hUy}E$Kiu09t&O^<0vm)NvP1J zUD!ZazbrI_#}J+pLI~pr*kte)lX33`kj}v=4aTTo{+q9QG>2J%(tFj=@&#pYhIOVj zq@%3Be^UWSk;xgFr8PkLV0D*`1=s8&B>;#yUncm^#%6p*n$BW4C#g-lc7W@W#{;-C z*nyu#&+HE?`r?${ZQU8u126M#Hr!gS5QQ5k(`@fJy?EGWQ4sI^qKWwLjPzY>EvxDY zNkuVhmz_ww(w=EHP#$AT^F`>O0qv49Fy&QK z8l%z8bEC&2mM^c$EoFTGww5V1L)yxyAWV%oE1XzPs?&V{UY#>xBa4znVOufWDIXJ}%s$l%)NOqsu~~hM#=Va=%9-HmT}UsPxERqvon0&|-BB=TH|%@_H6hh!X8d zL5zXAc={J@qReae+}=<)+-bU?Agy zcezJCYnaqvfSt}lc(YzNl=g$qd-{otOA{uMZPP0ia-d z9UvgX?M=?)?mRM;IiIb_3$I|oFZWzmJe9?*$L}l$l-3RPYi6nxp5)pH`S)4`TVXam zL22dftd#@m$(w~|z0Zh*@VE>_;CX||yEA9{FII4F(+0$&#+3(O#kzW)I&l5u=X?ecyn zgN-C2TYj6VE@jj7*N)bw%3MhW)+0OUn&5SDP`S$lyoB(iKYqTapE5$bx`C(@&xcmD zkUKF25;EZ26{ZxzRYq5aBc`+UGA*KeDe<+CICn$`edWwqFZcBU*E}5Z^r*Ym#t|{& zepyUDn$=qN05ET;;pdDnS%k($DU;R%flZarkm^W#ImgashiHrm)CCObF*oIZ8Nh z1AyZgD9p?;T+r#dTY^uVn3CONPdv9)r3!)?b1^34^ibv|sdC`*)phv+Xfy6@TaVy< z5r+7bR3c`79XX!Fb?tGYI_6}1#r&2J5s*70Ut;)pg-FFuH~Xb`awTG+L@O8I_U0D4 zAarC^r>WNkc0gb}>G`+?mb`2TFKd8?ta!|t?zG$_)rS_tMLVodUtGfNyh0Jevh_|fk`?dC$Ni!_v#RkmuD$18cC z;!8eV_ZTE#3?8+a1_8O0ynrM$YIQSiU8A0H!M!CYkxF?3Z$zaQoSF@}uG8beC z0%wJ8OQ7|-+poRFkz~a$2RTYoybFN)D25I@pm6R>t(3S=m zXxCnEfTWw;cBZ}!YsNr?~JJ2_NaPC-FI>8Z@3y~c23oNS$%I^^lwdA)aI)m)Q8iA}`ch^lli zGYEz@l#+Vp8ITaS$`pI7onsv+OUUi7Hh6Bxk`sd~D;UBmRTdpV(#q%7;c%wB>lIg- zhg6dU$W-nI#6G5Vely=>_M{jj<9(r&=|7>^j|q*OK@_$qz91x2b@yb zXW5nSWHb3-R4XPg1H&N=;nyg|PY_hWF3299QL-e4V)t71q-&O9%Q?BEX1@NVBF;}i zoxin|TsQQY`%sR@j?eN3CD_PC0yp_)rWlYiL>X1JN@^#3@K}?eQ{sgZTni6h|T%_OW~Y1AnbUI>+MPvUl1oKRKQAoqhGs`2+}u7%1;jI1HF z-`s9P`|`{%&~iN*sM}5j@^g$Hg}u&e+UYlqtOMe7 zgTPl&^w(e{;|d||-2z^f6eNQx0U)z9roF@*o>2v;Cc1yaQ?pu(w8r!>WhV;4Du_+D zQ%bL&?>Kpj8c84A=WPly@-?e(;#d5})4Oz&E{k6sBvs9wB7nbjJEm}DZg@CFKEw>9 zEQqt30V!Z5Aob=1Wy2uMYpAk<>3$~G$uj)Hxv=TzxNv(Wn^Xu2$;pT81mxwhh-RHn zZ5aVzm<-`(W$6xpeOQ!fIYMzUTT!a#a~T1~YW!RKwG6PzHr5RZDNr|+IznQqk8ut@KyhWYYgj}9wMtqziy8Y5iky(gdOOc1(b}a&XvUyKcn3qD$I*6C%K)UTX*_^i5-~EthnK zAosNH@dcs#Up9fW>#ye07}Dbl0*kz~d>Q^Hur3HR+)p8s_ofb%P7ayf*?($)dS$h> zNZmRD=N1irn)ww8K6xm-I%D;bS@XAO?v$fH|KP%&|I}; z8L`6W+3jGqc;=X!%V{;!f(51{0<#15nXhKqZ89_cev`DOt3QoN*mcfs_Op8d|9~e~ zLmo&JLM1Ek{O}r2*jp%JT*l-8`DTme9hQyKjTIt`C{^cVa#Up{$lZh|T7#g1WOWUI zybLzU+zgn~2&*@3J-i_=xBRkF_zu}kacxq#< zcZ;p66H0K_Qkpfm^YeiWtNVq{2!KO2C;-~eua2iK@n=A14IoS~r&+%Qm2LB;j`GMe z4?m<{*A{pi47GD5OqFQkhJMCljyJ=@RBpqr@7$S?nl0zyai*&v@zL&@Qn$}q|`vW zJ!5^gDo)Wg>BcC4%j`@R-oLTI@-F8cfY5m;?G&aZ_TdW3*vtq~2x_WHv(%G`E*~2Z z0!S;~AoGTVQD?9;Apw*{j>Yk1pm$%UaX++zH}Du;z@jj{Y~F}C=T;25QP$=0z-ia++Hb=G%u1&j1?nMik|Tf(6(;7 zn#4kdoNX`eR({lOv?Pg~AdIBx%|_ifUC#nrwCJi$x0OOfi5qGu+gzKME_(ivifMt> zsQ%RXyjh);VwZCrpVnT2@-hL@O>Fn&jk;J+FqTo*TV-T7ylv})Fi{3J6Yd}^7y)38 z(uf-%hYy~RmV~w-gPXhuw<)nxgYZ&=axey*c-sulT=ZtV( zkGTfAQq{pJPetNMAgeMU!3>Y7vKlOyG&W^6;&QIPaBy%;qF4`$vP{G10TnWkFk1d_ zb+!CRt)fnl_I9#EYkaZiW>OnzXHs1-Ph!%Q^#~sj_FX!0-tyhUL)#Svbs}pR~Yr8B4xM_8tS3xY_8n^6`0pSu5wnp%Vd|JVj@xCv^+5AR1tj{z6 zw{e{u949wS?`FonQ_4GdCfrs_X$gV10E$9WSlAto?R#F3%uQ7!4dp`8H;d>Vj;IbN8X;{DKEUG2Vl7e^YEr*Az#>-?R7bY zVf1n$pAz}MRSVtJ1X`UHOQn<~$tea0gioXHD%FdmZsaPJ>cDFqg$ltmvXjS^jh@>B zFpCtE)gkW!mX`P!Gl2!juQFjpEoS#?V)@YkAFF^zzKujE>&i4M!5k=9h=21=(UebU zzW!1p2pD|n?*Jj6qKY=j4mhHRKbFF{4k$`i2Q7a^V+0bb%i7&rYpiP_w6-6 zUc&3t%$ntukp(e|NWLK(4P+oHO*ba8wr7gJX^x?7J#5-hx4T3yb`1*paqcJOrJo`e zok1Rr23UmO=Xt8~%w#JHBg5-{S@vz zy9&{a2N$P2iSU})*K!;*>IS(;N+#;;m0C9 z@^Kbloe`r&R6|*I`DWl!^u2Qr%`_o&1f^K>xxp$L_!TW9prjH}qaw$SDm^yZWl_hP z^trutLyehKZ_$y-RG1jk0}-+!sSlh7$tw0D#~3O68pE>CPK1we1GP=pGvKf-H3|9S z^m{h*APna|`I4wvezY74(v4kC;^V0jE@XF5sYz=otgr=}z16+h9xsE1wW||8*qq|7 zWSF20i&cEKf~T>3k+*Z_%vd~BlP|4`i>@~AYd-%lG4dJ1c9kXx zyl}moukeP|Q!O(`2!8{0lDfIzNXK?Ik7!@`LM({oPzec8;p4Vql@VF_oYAfmvXFO9 z8H>qz@dXJSBk>7bkT?MnH5wP*Md0_;;S94g)PGly(1v! z)0@_MFmVI&fEnRf$-W6rEe}T3J5nLb+6{6;jBp(1ah2ZSa&PL8e!;YG5LPQlHe(?c z;j!VG)gza<3fT`BZi2ARy#;m5D#kC^<%@?#G}!eo6|1-H9YMU3=H;bhBWgUYH<7=J z9PCN)dShIy<@n$!{-6>s8%c(v0zA)0ePPC!8#U>aL*7!;!sA&TCJEfz##0ap)j7Rh z{S3kM;f}K(=D}IR#Mi_&SUly{D3o$9g<;Y&DCKE( z;Yy9E43zz+05yx3a+iTz#sF_Cd4{wzn?3D4J!6)oSLBD+Zi^H_{adk~e&?VPyNfeY z8RQ5?Z&vsTHBeMU+5Y);kpkDSoS;^=L2Vh8Pvlfvu+ZjEO-KO!-Mb*YXU<2bsC*bl_UZKvN4D}YlPB?})Y`F0~nkx0x9><_2j2I4o-6lRZ^hIh}e z;_9b!nUWX6#AVQ6p0FycF%{-K?e1o%EcwS^bw`Am@JbiGcgiSs2eis3K!qjoSjWk4 z#VbF&5cvrc^2JJ~M<1c-6o(*Q9PAw)$;zMW2Xpk!Ico^;e{dGq} zPa-F=pfh#zAhRL>`Iy^r2XsUNxPCkfa{p%5cJJCS`nzwjNuxKGdPPC0lJ2Lcg$v21 zG5U?MF(00vVXq#aF6JHFIFy4<|NN@Tx1U&s!jjyA@Jl@}h->P6u2?X5u32pm6V&=Ps#rvI@+xrN63&MgiowPLFh}`zo9R?)Lijvfv}Abj2mCFZ|q^50?p zOSJz=+W+u5d78w090mg9tdu}%Au2avl7kC1-q=p_a#!F zx(l66k(1#+Weh4=^e5U-u65^q)|DGxW)i{w;&?G#JtHQAxaSp6vCO)Tf9D#!oXRac z_xm{i`l0wWDY)xsA`V%%%yd(qem3r!MM*!WiX4om@U^%M-qsmU{ z&hFk1sDZYT#|_af4yl8awCUd*^3jtQ-6H9p9tZcgFg&|}rfuTP9>mq?ZAb5Fhj6wvRX98_u87kn^hxxxKhO)Gv86g({d{m){BYAWtVlQ z@7_Av;gGi_B4nzwIv)`sU1axObzG(P+jV2b^+e~P6Rg=FI^s6&$lwNfuowI|1*c(= z#?Un)ygnBt(26S%3FIc0me@69;09w(&sikO!|R%^v*t?1YMU5^mDJLASVNBs2_4;o z0x_uA-MEFT`vgog9xBcP?$1)zs<`JjYkHpYyAV{7CZbXiw90bi2%bSL_Z$YYYh?@J*yZ5!)KsMhNM6L#xnf!Qvx{p{dbNT1~MOP$NVQ$eWCf(|o7{??coB@4Rc{UrL|2dmcuZM;aV zME_hyG!-GM0r%cd-O zui-(Cw%1%=!7lCEzO#5jbe({^qpm&n>|81Aqotw5PHE zi_LkM6Ntp63FPQ+s*rcHxLzI8BTK<%9=h-2`yzKf6mt-gW!g$gvGBZp%T};nJeIg^ zRKr*So6;jvKmNofm3g*GjA_@eeeCrFb_t69AAXcZr*VE&U(gfeCjdlRyb-Y*Kobn?=EFBOQ_usx zpYSxfxR-(c_Tqo$v3E*u?>_1Y01NSNTOQxdj9=OF?TO|aTowVMJi!(naHYjyI92> zr2)4yvYm&H3~*Fqt+yObKWOsiCth7oEC$t7y}Ev&)GxAbJciU*PW^9T0w}RgA!uz> zh7#Wd_s%k@C;BpyrT^nD^ygHHjKYR48G>$L#Cz`vtA6h@-h8^#SAjW(>L(fem5o0< z0XKR)KO-v>Vw83jy4R)=IOP#7pq7`2S*PWg8#PC)}@vcJgMT=#%ssnb|B-N?N4 z!7o$V&inF#Kc5XO(taN^0&W`f^Df$+FS7hlQjrw}s{6u1w`q>wcw*f4{h5c^!M-aQ zx1(hJX_yQs0U)))^J_MT!wB1-Izl|Dp#3#RZA?)K{CdL}S0UtM41R$Y%%@dtT-2P&7WJ1V>((sd^GR~|fkdhy~# zKFbSO#L>)WRZE$1dFRK7#pbmnu(9N)U>_5efFtSGdi$^hW=H;teX&ST>s1f)kU{OJV0y?2p>&XfEc@qWTTUfS1|aj>*>4;u8;&`XB(JI$%& z;blo|zfBj3(aW>XPVvdERl!uT<2}h0OpZL-^~1k?4U@hHiY*+(a`u?Oo{Moy1oVAl zZoz?m;(N9mF~WSvDcH$ex$t-Vya$$ugDCe4d2r&d0^PL zHptua?b62=^!~qe15Z&UpHaS_ywnrO5u1IW?k_h^q!f;?pC5L9t>$-oDRC^_L7Lm;rnJzXN{TCqK&ezmT*e4rXR%wkE_`i;JG-0tG;=$ZQ?6 zK%$jPOMK+ZcP~@SUZ|czZwoR60ZS*RPJMd#L(fSw%y>O z_1|e33`{j8ph*>+7jmrsQVjJnVdjZT#Ygs-un}@-Naqk z+vG#iVLH}f?bG7fO^h3mG19(n{TQA8hh4#B*6pTQd&fsh8e|*ZyO6Y_u^|J) zh0efaUsST}m&nV=c9u?mywBnEIliom|DpT#C%TJ(IfU;}?$}3KiXEbI5`az)#7`XC#V0V6YjQLgkL7pG^k2WiIQB6i4jk7K zM$0r#0=<;8BuZaQw;aMPQ77jq$6SOBcSwgrSG;r$g9^UeUK8vGQosq*4nR9J7IE7Q zlDlZ0xv1pWfl#`OWR0munYTNXPd*&w)C@NF z!SdO?3xCkoXc+**0y>-lhB*G0nV%NWZodH&3 z{>scAfg`mt9p^I*8|x9$r_a#}O5}dLna&SK*89!I(9dq>6|62?SX8ZVFEdCW0TiTJ z>Ckob?Fj^tCoVlIH5r~D-(xK}bRclSIFg8-rI7+!^73iT9O}YRtcK z8+E!(@xE!{1@F7S7m}Pu{&tOC#o)5~{pwP$7kVgmLF@F~@hZR;mHJG*lZi$?9QZ$Eg* zC6PN`J92qjDk2}?Mn^}#2bV?-f{T#-Kb1VTsZZ?7K&wR2=>2!ZdRAo6pRWLC?z6AR z%gev1&=J}pYnV(QxYEE6T)-w9Zrgt-2i&hL(7j-G0WDq7?0*>~Tv{%LnDda9z>U#u zJa(i5bM+wZ>PCF;bM8MBMPw~|eFDc2xCHZxSaq$7&t2E;;R$i{=;Vsd=T)&DDQ}NQtrF}n*O`w6CO`Bb&c^b7lkox9lMc`yGc|M{4snxgNaOmpQ zv6iFWEY3>arTJ-s-vrlE7}y`RX`W&CKV|t>w}Q?c^TUm<0EEJWgGc?rsUU&X(H;4S z{$=6-Sqo6+ z^z_0sRnk2e_#amO{Hat708Xlb{5WXaJv+qB$9u)%!#;fxWs3GAZ98}y5&%Z6=m4V= zI42)-JUW4%THufJ4DDCBoW*M0E0wBx_54Dbzg5kkYr}ev=w)7Z0{(p2Qmk{CJQ2=* zC!GhgTP!wrV2^uh_ogZGaH@89S}F@BzMjO7ZjZr8N%S^n$e)ST<=}aW)Zij``FO6* zbsfAcV*3t@V>}EoZW!h?zau>}Pe3xH3wL+-8DYSg%t{}AzxxNMm6~KEe_u0iNTl}M zKre~Xnsu>R(vBW1E&{!w@xx$7*vAlg4vTwe`WHCdqHCg9oR^SVhapD5N&$s0U#obu zdvq+~$a6Z^=?|chd>lFg1X7LpvNfB}mkVv%KnpB@q%SX4s%TyHt1|u89ndQv)8l~f z{#h8l_6jnuL&c4i2YLcCcN#pTE4GowxpH{7RMMFPR7c#T!v9;p-_*e`(}RT^;^yIb zvLuT@M139{yn6NO6RPufVMnM1PP)3f-c(Xz1b9K)A`Ct{uqS%QC%-+M)jLm%PIOUsfTnQ z0@MWP%8wDj!4$8Oll?$At8_sMUxkE}R0Ua8;IJgAum6fTzsXEs2n6CSK2!GKPhyyZ zi;+No0?KqAdm|5YONM1&m)H6}?47jS;P=vUbCoNrs>EoSnV6DZYu~u>Y#}fr!W5f5b2cWWwLa!d&fkyKRR?B%3uVQB1|gUAD@2LxeQP5R$`yukyZjZ(79G+RqX~2H!p8xLyS~R zQc`uDTbs2Z8JD1-3b^U?q>_@-q(ET%jf+tzr?NJ|ALvf404>F7^1Q_lLh@}KWbMg% z<~S}j{MP&1@<@XXcI&^XPYSm5~L9X4D#c)St1LiO@PP zzerL_7w7hUCC}9P^3<6#Rqn*hpMB+IPI$JHuwzv0Xt5oajvCLCivL_+yB8TJeV_fb z-;*A^Jy0zq@3E1O*)49Cn#xBDRX~x*DjcvdZS}E5^B1R1ol0|9 zGI^DlXgDXQoT2xOh?qFD{nn`Qk>W!?Dx-Ue#bEtVgkq)^w~;H-RBmc}$xn;w$I1No z{*9nA+2E|r&;Kh1v9T-@KUrIcrFoQLzMe2k)<{l^Y2pPm7i>3z6#RQMz~ z{Nq<$eQo>;xDS)$e-ZZHK~1h*_wcb`qp1i;w}6QB-UEn82O;#1q4(a4pn_7RcR~#W z2)#&GkzPVax=0Bv1Zg3Jz<1;OJm)#*_s=(zaYkmu3~l%(X9To%@I6{MbWZ||BY`lFH;K2TR=v{pp=rD z{yl70icSvRH&wQ|)X^g(H}8Yod-UTIQ&-l>CpGju6}*ymTu8*-e>8FiIzd{d8c4R^ zsSEft@&%{0w4*quO{%q(O3C}<(%qo!=!_BdYgTJYYOykWmGb48;->+k@bg{}M9)U* zvJ<8^Bu$jB1o`90J)U7(rDQXvuPJl_WSy0n%E2F&xiN-n{dv^*s}=NPpAoJfAD{S0 z){iVwdm|8Apj{fHfVmPqo>u>*g27qVrnl%R;>O5PodW)ENR-00ZG^`D~ z;D51r06?+x{mATp!@&Glq;v;m_B2 zJ)4uRrx4A~c@mmJwPViM8pi@pVub0j`a9di&X@q>^0dITmRek6j*Ln$l|8Xe-#oxf z3%7W@=a>;*T;w^qvp#??A|#%VL5!V6oin#JyF&Z?t!f8=SJVw6ql5-QB;~oBdlYzFq!T5^o13=VdY2-TunIWIc6lg^An@)O0WT9wC!ueDa+nOZ^Lx+6)vVyaVyxu!ms=Mfx8!bPyde#DkSw*VoFMXiFRp0eZ3H@`=bSlh={1#YLK>fa4-o-FbZB@ zg#e^z8Dd3|ST1SwOx4r#xr$id=k0RkTg>Tc)9DHE%m{C8MHSVJ_-jT2Kol9)0npd1 zQugwHNKQcOxSuHa0=Sb3=Gr01!*Xt;$EG41w9qy)V%pCKQ$m!81X}P{K2Fj3#oO}d zFTslC@W$Q|DaT)ku4exxSdPXu9XU5mA3hM*pL9)YaOPU8a=NnOo1h*KcoAUEJTCEP!~}WlEdb zQ0}n(a}zxtUP6e#&AWkuQV9ZqG&j4EMg##pF?{xu92cto#L~N_5@~nY{B56YHqmOG1C_4*1cfTE9*R?>{P zseZ{GK2O?~*hqcY znoRy-(bt9fs#v7->*w(WBS_|{gFC|B~AYN~@{_cCL+;Iz1?tT-Z#w0Q3pE9{$+GWa3StoCB;$MWd z^JQO-Iq6XuOdQoYa`z|LpK}#Xv|&W&N1Uk_S-Fh}VFge4 z(MEceBrh*MLgkmrT;nbBNU>ezPWT@_&Es`QM8qxqdWSUNzSPc^&NX{itRp`?lr1o9 za6%tRi1J=-ikF5!-U_7K_T7Yd73yZ$N56kFne3SS!q%afdUK%bdFmfYAOxTRt{s{G zor{O~=9d{e~u=D%9Er zi=aeTZ+}q9$$-Ofoxi8jH&akOq};|2Cl0#v;K=gHr=vpd zAe@Rn|Q zd*$;YCz_~M36Z8h{jSFb2o|)L0?Ap@?>$L^8s_)Z1II#o8RM7pKKt??{0rS@N4TR9v}1^jk3KN(+?`bw$cGe$V>WEU|R~f88RJvkrM| zq)u6F!~RQ?dm6XLYM^ng^1V>*Ko!0wOP?kKMr-E)gH07dDWF7pq5hyqzI@K}M6z6b z&fB8GUMlPV!O2y3hS~bq+1VjA`%lqz05C0C&<7@0e=~j2p7*`a=^uG+eGdqK&8M7y z{TFe4m;E^|!%rWsZ(2Dsf||^opxbB1;>*%`k4Z+@YSE6`A0~LGE-pR{DblO9Ww~gn zl0uhGyP}un%+madtl0czR`zE>v~(v(za=;XLftxsPlxvg&u_NVGB`?bGX^$7&Q}F3 ztG{Z=p=}VbrO)iJ17lnOnU|=#4RFQ$1f7c&oA+zxhDd z+RL<>4SlGm*wHBk*Y6E?LlH8-Z2Aakka79}_OmY$@WLv#9v`Fo1n;8b5H}OKblw28z zxb}J#2Oz#2yfn?o*44&SiiShwfA_HsqS_=?t)=aN^D*8pmjlAp7& z72XbPq3Ir6+%LbU#o#bq5R9eYPdAyXrthO5UEO&)Kf@;=DJokovC+@j|22CZ*V?-8 z$tL3WDk7G$cM0;|>I4;g<*fmlpIy-K#Ofe{e_(tVO5>ZGuT(mF25}8uZ-h^M3*29M zwR*|jIZ>gt(ey6G-9lYIXPSBc%(S)>2T|+qxY2g}Ukzgs_Rui=xR?3D_3EK_Rm!m` z3h&eUOs=HO-)&BF7sA%S3raB<_b@1H{z()m?)3E%-QL&PkuP_qS$m# zk>5rgm45Bq{iY6qT(*Cm+01uS8VTequ;y1B2vrOopB1Gd)3`67b4=j7nLDLO{)T1y zVZvhyktyVmM4=xS0tz%qk`RyhRTIk6qk43 zCzL1ItG6loWgvdsqw(^IBm zYZ0G9osZC$Ie`UNOe!u_U#u0Zl*pl$$ma>lmBTe$ncXhuZ^OgrfVA=|#@AG_L6WsOp zl>;8V^wez;ukBQanOen?QmJg?{KrOzcuYI@#bSs8)ApeF)YKn$)Y9Dl&dWg33-RLk z(ym>*OdSsXFKirQak1MW=_YdI8FRXBC$7|xW*ZfIOp@kQQtg|rV^h3Q(^2tnt)JigH5;VIIJ0SB(lKhGv!VX zpbM1Np(k!K!!+{ghmGmDAhmf%P|;qfN(f^tk{>C$aj?1QLtr<*&MP^)m7k5lE}p>_ z?Y=KSeE&_^{~+?{!ypVBB6KphpE{nO5vt8Q!pJ;_7&?>p|M4Vq*l)d}sbO#1H2N<& zTl?|LxJ*%R<{pS(Q-6z(sIaPjBz%rG6b*Nv~kP{n-0l?+zus3CM{R(8rKDMMFU_v?1lQtZVY=g3fhQ@f&Q*|Y!_=5wFk-m($)7-rF{fQvb7%sg^X|9&t=ZdPTqymYa|Ab9% zq+bDAsLaRv7t!tym-s?nS{6k6^dS4Fxeabof6w_|aME4fAKy}2NAW$y3lKxK^%oQ? zqm@zKe<{9=qFqo@aiNl#lAe8;@@fRgf`&&lgRB5QwEQD0Y|W$OaA}$~_0_6^{fQSc z=L#i$rIA$Km&%Y)kmPvGJ(v^hR#bQ3n@taAOIhSr<#Jw z|Bw(DwBUz~H#$`GW8Rs|K3l}P;wQEFu-uoEvoKz$)ANLdB@t31wL!^2YkyL!9iB@9 z4KTvQ;RL8uU_cax`3MTE(i?s?9G%d{g$%GQKV9E8?Wv<^f5ac2X<5RxR!_eZs~m(* z9r9J$@u(SWH-4MiWbS3a-FSW~)kBUv(eDqEP_OmN4?EsQocg4-DmT3bByc^k*3aI` zYTHlR+mFpJ7zwEVpnnvo-F0-zh`o{#q~pLuJL;B$mn{MTxS=D4VDt2M2=Vzg?e_x` zM*AIfcSV5A0k4T_k>BKIg&CB$9x81)K3fT*HRDEbxgrWmp^8p2D#$anbxDJ> zAl%rfa6YuC-*3FHDC)>(Ce0?QgOAHoK!c2Pb&viSd9F9osPvMSLywA_i4=!)od`2S`)Z(!hBVE1AZ7iYE|I;}?z`xw1$Q)M+To`2hd{8#_38pi2;6(eW zreSR0-ts=SZEFlD;5F@USwPilD$8AxfHFuByw{{X;z8md7k(`@$aMi@R78T^_KHhe z@Y@KH%a+^HC~`a7XauTRJCezAk>7Uuo-U06l_*DOF4DlrAD$Y;-Sjn5`d7KN6o|6}9NOohIR1o#{1WTkW>mR~-JNt_1b%J@G0a zD#s-zCfYzj_(ByX;@K!+J%xaW!s4cCvLqopcz|$ zW$?gS0b@z&tC`uE%hr@bwojsHwvGnJ+UR&bz`LQ#e1$m2v9?vPDE|-NAj|+Xb;B>^ zL}j)O)5AEz<{b_YfQ~zoh0WNrZ}2F1k|4%!tGz{-9FI%P+;C)c6LTt!FQLu__k_>H zC**;nAN?Tw-mFidFDj@&O?Oy}o)A0>y!$I*Mw52$tDFE1e9-j<>D8dXO<@MYnOIX= z33M?tsNwXO&>Rq^HN})L71gbp;B>e_UVD2$c)0$ziTD0oX}*^Np`}jU6zcfwUUOZF zkd(D53SB?cjQPcz5-B+hpAbfKnn11n=BSNI3fGSAJA~#_PpkK1strhc2I3x747)?z z^KI6R-Y);!Hud0>w4)ots_%F>LLLKIU#$Ca8yI z&0+fKE%k#rE|I00OV|Q1!Re{d)_wPp;(q$T5V6{C89L;F-cuQ%j@n-^)+k#jY@UDE zRH46S0Y8F2+~2|2Ai0^aEMOVWP=O5pjs&kGC}Pv~OOePa)h^y{sCs7AFd1UUG*-S1P0 zvHc&0ZL_Xo2JZzQKKwd@sgUgkxh$Dl4CbzmI-H?w{mzH~!C?Ww-t02qs;f5pdRa7R zPLx8i(ot4F7}yo0?B~}Mb)Wb7$q|};b#--g{9psa_k3EulDF9#QxAK_#AEh`@|Z*@ z`w&4fY*&)aG!MQI+r>P3($^ODF*PT&))p2$IWljVd3mc=7u`KutyQ*$@|GRYj)lm& zq{Ku$3=vY>CRkH5eqAy%4fmTvc0tGZE5{G#=O99KxI6;Q1aTeudPEaj>O{OHJvC2- zT3(}Hr}qr-7gcX?FZ$yFf8YTCs$4_fzq^ruW;bl&&g8+;5u2r;#0Epxc%OZ9Up<~) zz34G^s#eVEESbPrDo9d*&jcNhDW%gM*Q#epJhlI@VnymS%|)f&#)s2UR8qP1d5g1? zuCTsp(mE>a%{S7D4YJzme95Rrq`lY<3Had%0!M7y6piKxJW>8p6{t?K<@O99S`GLE ze}nCr;?OLH-)1HqT`KYUwI`Jj1K)jiZ`^h&`}pL0ZM?jB+f0!fF6^K954{5S7t-AJG;N3LBP?!c3@cFRD8?#WXt{rr|Ykt9Y52Oo5@>tz7 zeSt_f0D~b!p8NazmOzt1-X!4q)OkOam-lF^ZG;D=M!opsc!Q~O%a)myRT;Z8kKln_ z<9By+1EA7u6RQOFCGd|BQz}#THvBrO9WhmN?b!_`?P2K?h_W)3i!PGR&Q4Af!1t4! zaZTGZQ^)i09lxhPp*7rZ121O?1qEJ|r=!i@&NQNK1={9bbDv$~lanW3Taj*)obya& zr^Y{1gtI*vS&OQ*RVR$(qc@cfom5qjpWdx7@6&NEt~G|f+!qs0!^(>wG8(U^3x1&= zdZ}@LtC9LJAY(u>(}wGLh3xt6l)I#cNEDuHU~3T}Pu^|s%&^#EN;`;(tWSl8|I?LE7ux0Im$*uGOyX{ zu5eccojq-*Lez_!e_>`{k(>`Ov3vi^EQ$FS&eyAQ@69sp0U;Nq`p+zT>FO_*U0~mt zqNSy!3Yj;s@W#-bkMyhU4a_?VwNIUq)bf&1@Z5S*Iw8Ff>8z4Ws>U~ASIGexo=|2` zrzGSwXY>AMEWPL>dDZ)B2+6O?3|lu{Hu6!8sPz-f&+K6GDKnTF7#bh{T6}CfK~vZWS0PF`?C) zK%Ep1LQ~53Q{hS7DzfRc-?q&!+jpo|`W~#0@Hx&1Xf%ghxo!}mI$!@-h*a=N1{VEc ztBqNISzRDR$l9l>5w5Y9?XMqGX22fpNY`8Q;V=`dqHy%i(CJfD?pwJIp9RjZ0c#W5 zItoh}7{!fI?bpAe7fOwxTU*(qMGaAet|{h1XD0cdNYDJI-a@JVb@j)US`Lex-Q*03 zO~3BK*(4l@WG5u4%oPwfX5D{XrSa|1ex%s;n>w&!GIf7?f4~7gG|QCFeM`P(&vQti9(rvYb@R!TV4 zOATX4SSNnXP>MV)u65L}KdoaZX-ln9Kg>GTV_qm5YVBmcdNz-5&ZhpiNF>d1RY0%7 zW+X3FRU-%(3H*VT9v+ebtSaaSrnDC}HO<_avqvAI=NiFe^de^3G}nNC z($~;L=DF=jd&WVn7P}}Z_Oy=hrm))?8R;yA{$XV%?HcKuTeA|o$?>0>Ih!he= z$Bd%sO6?2EYgL{lBa*{zYev!j+~!@oN^?|*%y^c|q$}?^?@A(0w?$Vlt<~vJ4B{e{ zk5$C24Qie)@xcXRwefq1S61MAhl1;=nRBXc+#uws(Sw^G+YY5O2JrP8KG6b#LOny_ zj0q=w`47Jvx*Ju(q2mu-=vZqkvzGgTC9Na~hk@sQ{^QG)788=pMq6 zrYhf})P`pOEaQ@uW-!yd;n41?Rc^rgdF zJ3r?ihJN>DWbpN8&+In+pXgNz+%v`ho^jJh1GU`d=ge=F+jsWXQP8>_d~kW7z{=06 zpyZY|0;MkmZfU6O_$RHh=}y9=4diTrs!o9$NpW0rJw-&>Idy zm*}#D0G))pXSY%Zi=Ne06Wd@et%h26jH!tami#IL3(pqS9z}gT(0H3F#(B`o3gy_? z$UPr1E$H6jn1xT%Ik!A4)vc%xO$G3srl#iIBp&mpFKKT~GD$j>W$2VRI2LO;ngro2 z*#jDZs+j!^996zF;3J4oD3KDgJOz{<#H-@4Ouzvnw+KN&gWC=%(6@4`n%dUhS-~sB zo^%sO7NS1$29b~Lul$Y&y-pR_{xScA@~$MSmFk1+vDqqIds zqKXJxv>`hry|1WOU(%o?-!F!yqMLauzb3pO$nDEP?%RSz+p!g!1rVElixJr=o)pf> z#vjsid$ce&v-YN#o!oG3P$2X4J4duCU+A;&T#-jxA>YFTzjVrF z|5Bh=t4MVcK5lGKb2G`B40qg4+E@l7{^MX%yL@uKe|FT+-V3adH7EFg{ZR#3z35zW z4p^o$4{W&G6*WfOW&Sscj(wx!l3^B_uyretUAtVkpSEj&eUURfmeM!OFjkDzw^GP) zR|N{7qm!zWVK;W301I{lq!$e- z4!?KqBBJy)1N8t!aeu=Z{X79@IUN!ah?rL$sE4+rjB!vbhHQiSlj0h8{q>BJ(8qpR z4R*^SbR+Fgy5uHldZxm13&n~tYJFbfzGznHBHG7 zyGnIZ%k&sHtU|`N`lTxien6rJ*s_#DJaw-;UMg`F$MjL{3RBqzYU{fHLNY_4;c8qz zG|ptB)lldofH%F1+gGm+aOeEH-9@_P>OvsanB8Y;=1ux6PAy$PF^q(_M%70fK5p9= zPR_(7mgHXBky-~!&(L9Q+5JX*6VlZBpdLi}qz%{@wzpk_tbfNdU&1pZv^5-=z0v-- z@djcO2Ml;KOfpX4GJaNASU4%L61UBsKD&-rK*|O0?d_GpN2QK`UhxHXL9V9TmsfpD zLrW&wMwmpOrvi@S;5DcUp`xL|A#cIfn@daymUR|@(D+uX#=vQPb# zi;gh_Va@w<8J2fAsWnK;Q6o%>SEUt^gJt0DJfKxi-ic}f4zBduKa0+9#h)40%mKAX6)vQ&r{nYWXM6Pcv6!;j>~g|F7LbEuF(t^FF~43G z2vm)(mZZNy3|qdg zQ5e_mOaJNfx!_I#Ji^8P${BWIT=n|>mO#&lbefoFiJ;|Y@7o-YA9tje1e~4X@|JpS zgj*RAR6i z=(87KBOpiFw&*&$_lAg3IJn=wb3iVVsu$6)@XYk|7c~O=+8)bPiar$17_{=+-cvOk zIQTIU?*L+*Gk9%Ka*!pYeake{UdV;+4itolN)r;9y7lx#Jnz~ZJ9b4eP)6~zXyV9Q z6S1zYq8fJ)a)lWiPs}_Y(B>5MGgi~~S>j8?OUqBpkkFfl*fc(kuvGZAw-io=z5Jc( zA6i+g^v&710pi2PWbHX{@Kf|_tQrws2Z1N1nhd4?$O#{k-l&l~k*nERsyMRm2X>?q z2DKJD9L=9M?2A<`ULD!!Yb}-bgNaYs7NZ?ca(;yKCft3FZ_jX=Dhi~IX&Mij(a;C6 zSVt5*DO5sm)*b^C)5;0h$u^A(M0*{NjG4--%J>A(5_@JnV6JnA1#HW~L^AJg3LBP>yMNm#(FCQTJyYDVW&ZoXnJqTLgc1lKS?luMNd@OyeMvhjp> zy#BCj3yR5mONQbuMPWGn<#gt>@m==PJnE|?%Ml)+geUv43(H+fuhY+` zJ<iy__;1q70xU8L1g4z^o;V@t)QcgVhBz8 z*qf0;NQH+{n@&=|>u6_BjqV&Cv7=;6yl4iL6cRZ}PFi;>U*}xhLtxSM%WP5LY>5J` z((Qepz&Z%t#@j}4h`GWpvneNw?NQK;#kWx1T)hZ68IL?s9- z*`EsnHLhXY781q+gG^>ljfyk)|JA+Ql=B53d<%1O9&C=iXSY;Si&;-xnQ!uR?@`MT zQgw45#1OZ@xV=dtDwly|uOld(6`HZe4!DRBw>*Fy_5Gnl#ATJ)m>bD0M75XKTCoeW zcdfRc%BW!Mhov+Jk;s%3(;)o|n?1W$38m{SWQ@luaLF7#L*WyaK431(68N1*QD}k< z=&MLwev;1PTc5@yIn0zkm2nFyM@%j6{R~t8*(?QOw>o+WmC3cN@_=8oU$p;I{}YXk zQ68tYo;!S-$#in;p7nzax0963S^ubqytjv?_=v}kcIN49>X1#tN|ZQxtO7(EL?lsR8^K>*Mx@p}3Cjh=3Ed-n1FM!g6GU zkkNfW5>IFR;eN|t$sLd-_yB``cKIhp>g_;vz^YP={NX%!oNXhhiSe5MUeO2qhGc+J z#W?x)YgM<{jv{{P#En>+-5_o^K2@Gl@;eh!ts(O8%)D0m&6ck9xp_$aEO?FN`sVnq zDSfbG&S1c#Z4UpWs%#fvH2SD^9X+3Ig8h8D_@3O)n<%h6q=yox910L+f3-UM6=LH6 z^A4yPhiVG$appcLRX4393PhvQ{egPW?lC7{h;V)TBS6zDL3Uv?58Y|fCFhEej=84T zvpp%)94pI2XSKxnW9UP%!ba~%Kxhbq0p`-A>SdONaN&o$sFfj_MeGJ~eI-Y5732nZ zUR>cH5f;cq>Q_AsHV?O^4994?FJIJVd^W=mroLP-VXG?%>Qso=@TBYc^8z--Uu%E~ zeb-5p zX4I$|5gAzshbuY(>*&3(3&Z*)6ttU#4uqE*!D}RnDvGYHr17ejT^wN1{o^oBM?URF zoj#?FS>+Qo=%!y#_E5<@zCf(~!KU`(jKMA4M)wAKpF^of(L*$565Zs78dstXO!?SY zRQbY4lgma}6^?pY)1MV6Uh*CRs4g0wEeD!fJH)o|SeJdO5Tj~Q39|905!>1BV^N9V zDuEZqB8<9UdH+D)ndj2y3M}I<@AGT1b^Yz^(V!Z3$&rl04V2#~%m>^}wvpFdiaYPET$%SfR0oMdqXyVIME!)i5=%sOM zSsIH-z{jqnNn7W`$!LzL_55;1H#_)dRf_19YQh^lGDZTsM4u&Io0;-&9^^*<8dej; zTmr17xi=Xw;?=qW2UK?c=F4NbWR4S`$HFr?op(~VYGMJNpnp@gpCQ)_DpOB{&|0VJ z^7Mz!3R#ZCeyP3^$&eH(HFIvRV6HM>sKYUgbOM~t)#cw%9{@(4Zo$6baTIO_NU2+3 z%%@jHG~7ZV1Vm>>K)DS2s2bhMsoHOv@Bh2FKrHvr4*^W-eUk`77Aoz%+Fz@mU6+~r zwN%_o=~HY4qDAwd^+&crzG&K>_Ey1y)@Rj2imvg^ z50b2z%yO8)l<%9%uF#Suwp@Ka`KV8|ZGop_X_eheEd2xbRuSaNGvu1~JH1)KxbVa@ zg0Ns1TZy(4bKGjU)J;zcs4Id0`U%HY z;+!%W$HB&a!|r-X`^i;NLiAV=etdC8SDX6eo)NcmkV*2kcebubFb%Amq%hEP)G zd~mIzFu3^`1XeuKd)oX7Fu10c6tz*_k)7cQl`)k%B__|U;0&w~k+e{Y1pk68sQnXS|a6oKyqe*nOs?U{u3U+xaZ z!+qaqt;kmi#gL%JpTPs-eOT(&_3NoCz}ea0He|cXn1)#?rQ^8 z&t7}M?Dvh7O-!;+fnCw@!ixd?1&iW7^^x8AI=W?g4H|Zl0oJ#xn3|D^ALDG()elsmPlDG-idUG4s@86k&GyFTb!y6kJ}gm4OLP*~J=Ms%jO?BEzO9 zIJ;#|UWp9iBIhDRj|)$VPWpP=5cOi(2&5RUtwx=86}9cP^{E)3=3Na&66&^^0JHq+ z%yf!4Ou&obRM9&7rt889W^1Ktg8aRpWFr=Y_h$OpY9o10kjEM>J?oB87Zdnd^z{WI zjM|QF4E3UW__U8u-J|6~GES=^asXI$(x+A~O_}G0ks1phjlnNXu&bX|Ll8RYErhy^ z7o{gecFs;vc5I7(kxiu_qobKs`Ddb)gX|{2`zH4`Ib_CA+$3T4EUIpp9sj!`xm)8N=ln8Kpl8Czhu%-|6_~WAZ`;TWi6Fi;~yAark5r;v05W!+^ zr8ho3ebXCzMSn-otJBA}bzLBb@io#9MDPigxKK1vV{#;U)l%$pb*&aR4jGg#WPKqy?b%?V^IV#pTJP6ZUdN%SThyT~Wz9 zjiRu<^7cDb8&e%D9?I3BZ$e^RS}I>X^WOgGb#H%pw+u(nbSz#fE1?1k2Hs@` z&l=E)f#(4xUQ)t_>6hOfB{W$pn1W@-v}}1o29E2-7g{ZPv~?94f?AhpuSPFYx-tK$ zKkM&(KzSP&ny_|XbcOt1>dTU@PP^=-8N+{nB{1)6Xwm{i68ToUbvz=|2`u4T@7&)_ zr;bavXBnmzIs-fTA71mdo}$YOkte>qTmh>u8rob9y5%ll9RBz!~+ z3OphrZCT(YaQQdXU05$}@y{RZQ6v7!GhA1f&12t7>0tt$Bn7M{arf5850>L^)$M04 zp+v)|v?+TIfj*ANy)~SX|JM=Oz1*~N!z)1*4!&QjjB{dK32JV}gaj<6zI?*p&*5;g zatq(Q7*A(%5_)PmjXAwWN3O&8Y55Rvs_giEx*_JA9#C4qQY2nKz6m#l`bXRR+|$d# z1YaHXmwQ`gP^eV|jkRue9Ys9Z-pX`(is^B}&CGH41Hz{uwvUP;5ijzfB#A_q!RU!p z2KEZBGnA=OM}z_LeB9|F(#J$sUMjh5QShm)^Et0J*g5b}%r|hBJqEAPy~Z&8_*|f3 z+R-Y?kZ2mHE}a$&zXQPzfMwqFrs4}Txb(S0=&VZT&m^YvZ-fL5LDFsneDaWg3%Yf- z^h4(gUN?J%L|XjX=)dM-8f)eh6uba7qf;laee;}wkm|3ktX$2!+~|+paRYRx>t)%~ z>D2N?W>Xt|5MEM$`yuv)S-|5pehZ?bqtk^SM(_B@hLPOKYFbzvpHoiehy(V@KL=3S z?0jSQuXmFbrrhnvo!kXwWgJw^u|7}()#{&b-(TuTRZOE9wIA&rb$G_#B^qn{Jf|_< z^H=JdDc!vhH~tB~b^iNAbNU$whhzxRd$-*}-PIl#XAttyNXs?Jmt-jmv}X0}%cQ$0 zeG;ZxY%zx2bs8H39S`!b|oFr8ah6ppKDe(TBzBV0q4`OM`S znzMxVcPB-bh(uaZGRc*$GiPdb!y2EDiZ6=&QsX4l5bnRL`h2I4osZ*g^}?$j)K}z2V?3GzzQeRp)?fh+)kjX zXghhf(~33mXCACKDOHz2WqsXDIEjk(AC*XcI;IZ#o0L0jKJ`o9PD=ZSq)mA>Ss699UXIe0lKn;W|AU;6hab9RtRotj0gPe%Rr%U5-6O?0mW zj@swFKK9>z>=i%ZMcLng0{0`Zu>B$6>Uy~RDoZJxuu%kky)jxSgDNmEFo*y^BA9~9 z|80B2OtI$c%~Q{*Eb&BrN}(BLwR_|3d5p#+n63iCo@Wh{gT8dPKS0IACJcX zrm!A@t>`oLY(Dyw4`4_l#3owPMQF8Fc6`y_p-9DIsO?7I-lR*w+<(u+&fT945zXKA zpnp!xpL5fqkdxsCrubc85;btYRCil5O~Y^h;5(U3#_&*~PoKv|AolBIY1fvoGp%9$!JL;{PA_t*}BDH$wKNgktJ@m&d?)#Ptylt?rl*DbtqO{pzd=< z4AV;)s1LDtskuN$CNmjPF7cuU5eEHTf~cP`~OIL1atdCb22 z^+6fdAe;FB3oxm*@IK2kpi5XXhEdWEI197g{f6j|U%VGud{d=@LBNi@hk#E=+1<>s z=-umIlh0xzj^eVi5FdqziHXs|M4e5iSs&h(+^7qDVrLge=+G?cxal1Cf$Ev@=T2Kr zjAJ4{E5}1d#EIy6bcGqgN-K+lIU>($Y~>lz!(S%Dw=_9)ZRNeat!+r@+( z-^t14&Fd$88n58~mcgUyH#QXC8u!=5hZGRJ6v3O3E`opjzko@1IbJQ`)oiX1OFIXd zIuoHkB15@jb_*lU5tx|4DXK%A0&4w~*tQT=e-r(Cl<2%GS~fVEj8jbLM{R*?o8PYL zvBi-0XRF=8CaD;qA0JAFpFO$xiw9c~xe$J1BWkU5N+)tXSwy|@Ay+D{@vhu_Zync9c=1LI&+b_MNY8`n zo8D%-;4*~eBtW6iA+~GvO1T_L$Jo=Gm=wq2m>`UdRV!(q;`URgXn}RXHjW%Q@0oAE z>O`#)qwF8?x4JTS`eEnljvNE2WmZG=q02)f?edt~pvl)A-lAl`oSr=zDnbU*pzn^6 zZW#!#5B4_=Zex>q5dtCQ;soZ-&)ifVMS;4T5$m}9eyXJ%Uh=13J*;)TWbBOWJ%>!+ zHdqIg%n34Sa1oUoYYMNT(mwpQ44{50Z-P^?dupMEjrPCoY;yYW8oRvJ{~b2*5MaH; zG5(%N(0_8*uI*)DQjT7GkmPsK(Wp-z!@#(HF2StS)DOVS+0;i}iI-MZR?N^db+EyS zLPa4k%N?q|xxu^hLSwbG+d_{YU(q#keV-g-^e)YfYqOB0!Eqg!y!QpDZ6A`QeZ4<1 zF`@U_}+y5 znW}lF^-q5XZOE_!?1`pF{7E3Mm}ZsjYf;#yILy)Bqs>j|(cQ$KGaDmIFaPs7T%zA# z1M$XNDRuuzOn<`6nf%Y+HRn5?R99xnzDCg1`xY9zMtHC=zprjMsXbz~ovEM=$Ulnr zm~P&yFFp8*6VW%x(X^hj;ew(m_=mY38gn*apCX#;xbfbKhg{ru>>MT9$PCpSq=WPs z_xjx8Dm3p;jqAu>b?L2w+b}F%lhxjYD26yP4>jq2%vxDh^H4jfSbwD%gz1~j9c+I! zC@CO7(yKI(Oq&8boHi(wZHQ(qYO!ga`C0w>|0C=xfU4};t_4v+5jcp_ARW>z-QC?S z-Q6V}(p}Oe-Hp=SAT8Y?9RmO6(dW(Yoqxu0I5UoV_I>Xw)>_xPm_%94AuXb+8?T7E zBQ;X|RtoV4>lJ)$w<%daYwda?gs$SRGC-zR9&`QhnpKj0TN^An#vkx_7M^pVwstuD zvIj=u$mOP|v7FM7{!<$37wlv?a)E|yO;kf}Na9Niwl8Tn8MWQkEWQod*Be4`@m7jl+J>{9VH;IYPz#T)ORo$edlV?nr-tfBK`uXcV73rZR@tsPR~(LqFZ)J0`HH;*r-hzc5) zm+CB@G5LYSI7Qd+`k}fbs&Ip4v#DK?UWVGYt(3mwjQOr~mqzruzA1D*(cFJX;Nh7b zy@Sq^M9*tIDGIVG`;#?)WWf;H+k1Glkd>I@yCgFhM{RI#^_}K;)LJZzRD2MV8_N}Y zzM)X0XuQ&N=`>9Lu8s~>tBBxRgEk5p4x0mf00K7eT+NxMDLpGEiDXKf>tS<1MFm|J zIggCT(GMQCoBi6JfdPK~Y}lP7W(rYVmB{305T=?mnjtHif-HXm3Vh`1Nd>mM*kHnLAZ?IjzB7V9+2eHfM0?&Z;^0r zveQz08>DQKfb81WKY)bFL6E0tcpE$-ElbNR)bkS(ncwt_h`)_3V_e@8-9kE4ZXPM= zm_E1QI;s^B64W2U^N?D;^1fBZ4)R&Qv`PDYo`X`{vP$vs@yT=%z}@XL=)M92tvGgt z0G0pN!XM}X&Rvr9uRJ@4QrYl~^W|h^G3r= zq#9z=IiOa*Sb#>an3FEGwTLb-=RigxdNO2BSt_ayANBN1AP_kcQy_@-zHhQfau0nF z^OMZQMqE*=Vb(}4#+%?Wt7*Csv}!$h3mMOJKQ%|AWVN&1m+N-YrG;)&{G^6M4yfV+ zi&5z^QK_aoSJI`nB@YMu2o8o2q@>5kWj3<^QIh@NAAfK_&@F z6-3iDDJ&r|Ie)2Gs=04Zn;WVFh77;gTH?B#@1Zm3ii=3J*zYRJIjM#h!CKs09ESf~ z?bciCi8LC|eF==OmfMs&KI4BI=H?(ye(t~Ka7Jx(((9+86igZOsM`zV{fX3K_Yy!=z=W4&vuQ-dP$@nF(~V_sdRWNQdtqJN)7ik z($c=F)mY*>w;iZY4SaKI5Uwzp@IB95PLnU#I%=#@-~;>vvINp`jA(6G`Z(_~;u>bX z#@my=#+gU`b*5XdKBLK{zYr7o-5dP>e!mmqmW>G!%TSS=088b^V4M#EX$2;j0f*yu zdp4cR;t-4^?Hd%u*^Fnc`8kCYUwFRRwbr;>rQ;*L;ZR2y^I{;#Gp5ku#?irvf1aRzEvYR{_A)2IN9e*5wR*O+?0hpy+|1qHdl>1asR>muTb$E1Exh>oeKn zexnPtqg&&7h#expOzsU(?csl;pro8k;h!#63wZrbk(+-4c$Q6(=Qf9ME2;~Cd!cD5 zo@@VIx?OZx$${dk`5)amD-zny-e1d-^-aH zE9c6^Fx@?Z6r0(^g%AHJ@l;hs+RMitlbcKM5clJK4XMw-b1bI|zN%EWrz>=0*sM_~ z$CNG(LURSt<%c=UpE+T$cgp^LPu20bX>;GjC-Hk6etak(?QOH=?(bs#UNiAWQWJCdpz*# znyxmIt+#XHGfk&Ul5EZRKyRSn+kPBntBA!wBA(y{e3}t~+|a`4LeM;3{up09&Lf$P zaNmjFnejQ3xzNt{QCP;yxiE1F-lDIMBW@-y^67X!75viFF^&yjl%IB0<+OkJ z{mLrA`8KFVp8dxe|LaE#p|+@^vD91hHx$G96GkD6k8BX8P<2I5?EAd4XfTy8UQ=9ma(7)2Gg99)zd9*>pi$v0f5iNTKQXC4H20s@LCCjYzxV^?> zWo>}aGc2C1^+tcMv(6$j9tm?bZ=sCZQI(ZsCKR-D^j)!X1~%)ayc|kcw5iq9J_BIQ zJwic20Zs^O*lhg7sm(y@V5@3QRNU#(v4tXR7N$iVRO{VYDW2~78%kBDiP&EnVd(UN zV*8w6H%Iual|6|_KnoDHD-|VcmQr>2{IXA$PSn!=u2d&_mPY4RX2ZkyMik><@0-7s zJ(k{FHJ_W%t^D>Ff9kdRpjIu!{`LA!lWm5K;?F=YBc0tslZdl@#2(G#N)Jq%rrBD7 z`1<&_6y%~J#^`@?*bh#S;}#6NIR5oa|Lx~CbLhC77uwEF&&wQ(U2ooOwjcMI=eu4R zD3kZk&&5~&4jn%JAawX_NHbsYcbdy$9S+z)p+G(yE;c_z)X{nVDkir}tM;lv97dDM z=)hx->ur*8*{V6ab@~tkmQaU-d35*d)1SgtMbC19jjJlFg(-$@GizE*P@Al5HQOt_ zzNK%Uu-UYqlI$h~z@ujd(RO~QC|^;qw!`LdVYs{O=5nFfsrpou;i!eIC)#(IX+9@o z{mXZ`jIX^ta-iA6#Q#*X;0l@5fm_+FX&KF=ug`iVZ@uqzv<^8Gk=@JkiHze8 zIIP>t7xM8jJC1q2tHxRWS%ca1NCsW+GnU@{Nm}cM<{%27AcZ_d|K%4A+ zP{U@a|N8!ocUK_UwCBryG3IZwa_g?qYk|KHHz^F9SJF%O5jzUwWIziC6_VTDAx1?- zW#(vGh(EW2lmw~i>Is+%K2wm5#RY|Fb#;swI;}~56R5#dclP{u>Kj0-fWqN&D*efS zv2|e~qDQtP0hF(F5}{<@)?QFImC0<+@4xR`n$8x9wz|98oU=UMxduf+^Q@*GMzs98 zJ&9C?)s3WbO#VD6GOInecw>#I)8@j6`fEMC2nPK}yeTPpuV+Z4tL&ed7>8^xJl390 zO8BHY!BbFFB4gY${h2!lZ@Lmc_AoLqTXBoU^=am8b`rr@H72Ig?{@+^#OvbT5kuLH z1@nKM6|xZuRKJgNTO2OtWY>0TEw$hJpsQr5jrX7Uddbml+hztW6pquG#^EY1CPOvk znV6UuW+QI4)WYF%w&T|Y%bIFUK~DZ{w%QbrtO-305*Qjf;Tye(GSJo0kwDyL&m?9& z#GWyKhJkF(YT7=-WPO{$YEPaipQLXo_(imr17K@{x5bPMX_UF0#ZaX6LbO8P>f)y_?PkrsUQ6V^qjppcz-;E{CZiLGP_eH- z3gEhieqDdn@G?Ztq~F^;f^(mps7wA+QbV<}@E`2v8r1R=o8M&r_mMC__1C0(RcqBv zN%B@S^sjy!wlCuP_@OAiBef(f^iTKd7W0s+PupNd%1lg5%B)uffxBk{K6^CEIDV$M zu&_}~z9hdXD4Kk-*_jDCAScO|Eu&*f$)lWer=ebz@+)c8TeF&=YU%0iwNC1ruZ}x4 zJv&^&jhb43pB~QO3*c239SjDFhNQ_Vq5Y{qG~27^G3#p>2e^gmd}=SZUWn=YDhW8^ z6&_w9YzuEq^_E%AMBLs=(VXwwHN0bX;pWDo+nsP!TOKZoyj?&nBh3?U4wNofAE!6ht(dM=RIHf`LaW zFe3(jWx#NcuE@D|E_PsPk|adB&NzA8JvsoM-zXE=&sFA=N?ylC@zXP*x@bL>0{%>r zT60HMCW})#C>cHSqJFxwh_BvHa5GMOWEKzztKHPQ5scWJ@p-edNV_L;I(UD-6dcef zdOI>Y=>Ov&09Y2~wa(@F?D!K}t<9o2Z2g(N?OooQIYjpV6^GmN>+!ll5Fx} zjdOYm`>wla%A0c@dC_1|g@bH7W@bNMKYaT&nmpHa%lXj4Z*r2mR#_Ktfx@+lsfa|b z7t3vIxP{NKP3_qrc;*{KsCYGvb;&M2*GBfk{*<-eyk5EkKzx|;?0=kx*Dz9u{(H9e;CWMNgBuFwEa^7djbS8|a(M>%P6680in=xe_cM zzv{>=B32B@5ZujYtGu({S(;TDjY0zy>@>Jc&XNL1)|14Hoa?VWaR`jydX_rDl# z!~W%QtnE&DX^TK!zag7dL^-g*%34i9Csl67iM@J~|KjxV2DkT<-{1ED=`-3mw(1wL zKMCZ&PoV^Eq$H0J?KW4kwXu2eIY6D2>Wi&MQiw&e|<uV4!v zKbI#r5tE?g{dR`y6k?p}e`%3wZL{w$C?VYRijROGg5h)NxPv~sy+CLF3oE=_SlKMq z8U}qGnMZxGY#;263kt{CX-uAnSnchCq1z38{=6KPP)0%u_~NJM0$o*P-|sgXcM>L9 zCv>3xTyteO8j1Q?n5F*-6|||rh#ogjMx8huhqEpX75_dMZ;^+1@2u`_rU)1@o&w@e zw1(QNfEwWC^-;oQfF@J=W=X>nVAFBp8xIl7dB?z7;)CF`&p69(H@_-9uyMt&MDQ}|#5C!<%QgoD7 zU{osZM?B?XFsF_WUncPxDYL9JI`HcE^RME3FMuw*B^iP z%&-qVGc2OZxTN#h&SdU$1bRBU5in*hheh%1B?gR$5|xh>J+E(JGLt!-M7~K^f_sJp zqzr&yE(#IO0Lo>Jx&HR`>?5rabpNp^%9puEA;?cL1=RCC1)5L4$-rU74Sob67jW*2 z@b*Fuz!dm-(45a1=jV0aDNn=ycIiB&H_#+j-xr$k!+L&CA8(*dk6B4XnyHDYbcG%> z0@Wq#ih3dHvNf#{y$(CF%f-v!<+&(`C ziOrF)ubDh3`mCQccQLhK*ZdBAKRm&$UiIuA7#3`@+m=On^TtJi#V|zs=*L%KF)==) z{0n!z>$6?Z4X|o=f5OhKQwk>7I9~6k)$4*E^p4)HH4p#t^B7drR;ODsfk@MsyfXkg z(7C-n%f-@LRj~@;dR0a`+@HB{BW;b}O=Irh#LF%#gwS3t)#6oW+<=L{653}J{{0g= z&9zy6v25mugqnQtF-G{cm81Bej23^vGTw6F?jrjB5a+85a)o*eaCLq~lXAGp*Xz1G zQm>Uke4b; zg4Y!&oEg$-UH`q?`ezvp%LM&%s51et^@^EyMePaZ>!J9+-V_ifz+q8FnF$4Wv2a-| zHlc&@byl@zIR-$MN<=8Bs_vvdHCKgr8v>4?%;~4@V9=;R^z@r}N;Uu0?r;JQj)iyM zcuQXnh`mh2i=qUrhMHxwA3y~&KlI0iVH9MuYn5q3fumltHx!4A2=Jr9c4IJ_9L&$d z@b4+*&Xa~*;4+}-`3nf*s|3+ja$O-a;sic!pSX(tUQ42@mWM7`do544)L8w z9{F~6vW9XnkRzLuNO7Nsi&^WAA_weK{C!g3W+D9Vn-ve|TXhK6KT92^HaPpl4%fIT zRqfB4*aio3CX08wL>O_I`;EiiQY$VaBV(9Ff4;Zh3sl||WFo&}Vg$OvEuQ+oZP3IA z6&8{so;#SwgSZOJd|uw(TnL$c$x`mFR&=XWw6m)sjzjsGT^l|~lI{k2_|1XDj}sH} z;g5QpuYk&BCH?*viKwV3yGi?uBrPp1nAY|qD=8kjUS~Ri3mpI8E2^MbWs4{)^l}>J zBSLfGaCjU?12mj9EG4<(3i}0blZV_>L|+y&41x6^brg%mu{B-sD>L{oSRrBM1e;=b zp5i(l7qSjGIT0F0O`V{hMw5BZShd`r$OJ#J@(*LO;VZrl z?FFEUiWVh*Kw_(nkpzoiUQXujdi-0@eZh$y!yG=!H-Cx?dJw)oUTxVP@Q83z$pZTf z2Gd_m=Cf$3YtM+VB$NKMv;Vl@;&03LzSHRw8{iY*wfj!ZWDg^6F+RQA6KP*oVRHM3 zgxZl1r@G#Kyqv{t?`NTg7tih+5lFgVPC*ET3`+(5Ve*5rn;q>?FZ(l{j4tNpk0GLu zhU37NZa1rBPZPBovr%>2PWKU@>$SiJ|BE&I^7K$8UpgD=>`dPKd#S~Q=s*-yN>$$% zRCRBEbRbz~%gs6X*$$ z1iVZg`XL$XeSik9+|nx{1mjLYM(2Aa)MPZ&LxqVNCPVU>jD@$xU*#!&nM^@G+u4c@ zLaJn9!hq9Y-F?9^>0s@{UT^+PT6Xbtg%1KD?;e_Z)kUy*gR2m1cj(sDzp0ad-D>FM z#=b%~n~`G^V@AIXUpN@S+in9&S>YQQK3RhvzmhW% zLFa~2v3M$s;S!BT6TO%aEvfHdHOOlaq>Gq~!NR1x#RhwULw1@%(}jS;|4jcsN@3pY zsd~jTwfc%+6EF8ID(SjRZwJDK|D4{onlC}_*(;rwkMdq|yXEk3dl5HUi4-;3cKBqe z8Jglc-UD;HnXF@iq+J#nRunpCGAH4BM!qV@{NpHVS%KF!tagSk1=3-zez=*N8mv;dt4-?JO=QsNM{;GPpewzPpY zJENsqTvevil8s7aSnD4!ZPEh21_sis<4gefj$vAnek@w;=5@DZQlhdk%wl8U-LYb^ zjB&Z`%g%(z9S^&{vah5X)d-WidE2_A!y^1eG4nwSte6Kfd04LD)as$LNJN}=&BpEL z^9x82+yIN|L4nzX$g+v4hQ>w&B>U0lW_s!+k z)65W1YWNCTzvRlP$%R#5yIN^~?cTE8EH?4LuP(-gh`P;MpWCIVx59PaAk-1f7cU`C z|Gu$q-d?|2CsYCnT8emRbBnTnHKhe2c86hZ(!3CPd<5(vSYljNX+Uz_TWoGzJit$> ziVLDSYS=9)s$EEp4Ge?@J6KGmvU)oc)!~8C0SU~frJu*7y)zei?#p&i*PgLcWjua# zwVBQq)2Q~AeRD8b$+9-tq5wO(=6td-e#XnkCu4e)ei=g501Kdf`+|dQQT4yG!9Ox47qN7n zN8J6vk>l(;S~^H&!{9ds_mXc34t*SvkO&ywj#!K9Lv801oZBlhROJ4CW~g`$e z$`@h^PbjoFp1IxnEW~Lk@(sx``=LF&tlC0v8!0e7H7=C)85zv;&)wJCp?VOY7?5^TU|9YKg*V+Tf(I+CPt?(>O2=+77KtOF{?3GgJH-No@lI zpT0X(B1KHh+t)u~sn$b9Zg7OW(+IGB5FRNkEq*VRbqP>ZY^12D%+Kw3IhKf2URVa} zY&uP*BpP6moXb7P%+!pXDH|PYH{psm4yo;t{xG&CI-#Jj+UAP8>`ND2;nbTk9_yh| zm?iNvUtV`~Rg0Sc`pIUdUv^3Di$qJ_@PnQN+LvtUE|51lIz}b!Cd>>azo}0r?jKQN zR9^|g*`YbY6q=BKgp{WiEzj0w6cv}J4I?8x6Sw9@nHHS8JnpBiW_Y>6H1iz^FHOPD zcrGrbZ1FinS2VCPiyexj6xZ_?q_p*};atcXjLa83vt$?hdIGkgYJc^4g5+J;>aQNd z2{ijofGEROm)1y)X4i2#Y`$0lyIb2d92fkSfKdX~ZQzd~M0yuo&5}>lBRjo_2{**rq>;aHY1v6**{5h`gIIB;LvK?s13I3)wF^ zZ`Rx4c8V<|Er#kh?atLwcf8H1s2Hod>loqVk3+U86kd$Vo?STPg7+12+{Val(V3Tsqf?v(~1 zhdHp;Thp6NI4{j-a%4GUhRr3fSMS)ae@9*V@na5`^i9s-5U-`hPL@!R!me~;Y#2eh zM^N3I1-Fy>7rsfW*tnMcwV)o277XWp#Vhp&`v9oQ-<5t_1UCryjQcZ;)9P|k5oEmL zquIRi5vSYnMDQ z!H`G(1@eXBUsA7smW13^NZ;k^o*{m5dW5aD8(7H+WZRBILe63JWOyGxez6>oowwtq za)&{R7dI?7r=`&1eB9r9;JY5y=xwQGL)9T~?k8RkhezD-`aLF9EfDlw;(GVZin`TZ z@TJ_(TnT$JVqZ|yKbt$Qy;|aDS#bdcxg6p%r6z7}Sza+>%KD`?qXaKl7^Py>oS!D- z<=X98$C58;os8#disK4~*eIFig9E8LV+2yDi>V=IOhjrTGE^6{{1JVy>d-r1zV%LG zO1tm0>S{M9cD)PeuR}nGm7BIJ@tt@c6eQSp^i{HGlS-{XzDu$Q=2TE*++(Ht!}r_p zX&HyLe5tH_xo6daa6*+Wmly(Sj$1gW^~B*al3CaWnqJaG1KF2?sIOi;KoD(~umdUY zUxDK4>uNX`0^~zq;}@}dwy%ee9gVJ>H8^N<#kYax_Ox$+^OE~L)A!CFUq9lNq)5ER zWHM_5Qrm57$NKtuB%a(>zd99Q_5%foRVJ*4Z07_PYy6F&)S~xVeeBeAKcG*y$DdUU zl5*bc(a*cQ8&5<3sq_91B=V0Z=Je4koA&;$2aMe&_m6I*n_JyFPU|^O*;q0RG9@K) z3*X?ht(i8n1&{7LYm+&+h22_!Dq{qZ-;?;Q@j$xzJ>m6TBej1sJv_CU3)GX>Pwv=r zKI5}K< zY>sEF?fc$MJp7ZfBsSJZw^HRNhxgpQ-XF=y_){&VRIpvv`$L{H9=N~Y3}+ExjASzEp5(iZd?sCP9?_OEsuirxd6kwrl?53 zFW?Kan#eF^0ZIzo@y`C|O5K>u`PWJV-@hw20k{F)Q58C)Qj*HEr>qCg20ApCK2wU*P^{H2=B z4BiE#)zMK=cDp^{Poy}`P&8e(b6gx6r~0T{f6)PUDgdNrm5~1f+tEgb%B5w7Goxq1 zv>A0TlUH~YvSjj$v`f<`q94~%Ha@cpF^Lj9`b%=~=Po7*Ru}6)LyJQ_HAV84Hxcw$ zdSKrmDRrJ)BPUU}Y%g~$=wVab&u5@w3)z7HD0v(pLbps=DXXJ4n)tSHHzAa&RaTmj zYL9)qa&j?9;(mSMx8sY+$+=Wz6+8~qVL4Ct11;4kzw;+X53=%n;;5$=Li)PjsQnzo zu1u%1*l&C_jI~Sq9K{qYa3m>i(YsM8`GQ2pHKS~KU+FfG+nP)A(sik>e^0KpBl?s- zv4c&WctC??O&%^T!aHrcp}XqcUY37#T{keHgYc(EJ)#2rQ*;0LrrN8>jAk@BSvP8+ z?RYRK>Rz3;?TjJm37uy#XA1`Evqno1xg)UucO_B?heuH{&fIqPc#wH1)O@bSnS!Qy zJ2$!NcKuubG~Za%E*!1j{X@nBN*^>Jm6FWEDvP)MQ}!QaB5m<-%Hs)Q zkWFIHDl;MuS0*9};Qdl7JkO)n^JR$HIPrMp@kR3EYrF9)6t9{h?AG!e z_INU`-3BzOAp(sKS+XDUzfiurpw7tDmW?iCcSa82x`Z)a6(p$9zzi72li$Xc6-40K z4O+F@`Y6R%gAl9y8>d2I0GJJY65s7okS16pmlRfb>sOUceo=#VGC)7<8~AGg7Gf3r zL$+%kbPa)@CzyaSLI|Z=jnOuYS-*w`C+NQ7`qWbJ-QNSTgfFV5(^s89{0@`=srQQq zzAT2oxBXG;^|SAz3UM^L}!7}-l|OmBbDF({=fV#`RBwtdiV!+n8aT7h8i<> zZF|gbslo7e4L$WQ4AAj(!j5~r!_za*_Y#o?4;{@DotIeMJECL4f4B0lgb8Rj#<2Qq zI@}~C`J0x+aXo~n%(Kb^h6=WW0|6i?`H4U9)~v9i0(r&9Nfu5L?Eu1J^}}m}d^Usc zSNmX9; z`3kH4=U*d`92z?+HTbYk{ImHP4g116<^n{|nay~Nil0>bR}UBeF9Qw_s{_JY)h zJ}6-8EIAnoNS;_K)j%tBe!WQAnxemzKW{@$AMv4NLz0KVY9;b9x&F$mQjdhi zY=H$hLRrh>bkDo@Yug-BRLBCSq^P>*%c8+HH}3Nq!#zE1mBXAxIWNnWlw^&8Y~#y1 zN9eM^oRwBRu5P@2|8mY+dnNT89qNm$(F5Sb0VM()r5ElYWJ7 zr-TL^>Gw)QL@;-~I_l0;%WC?qPp6O43Vgl<6IXZ}Z>y^Z$?)hcR{&g9cfC_Y3n;Um z6!fn=+y^C7F|l|zc^~|B#Ub-KpU{Gv(v{vZYR2LvNLS|wq-zF>${>!R`?B!Gk$dCB zXqUf#9WhHW5zD@)KlUWYrt^!;644v$3HzonbMBstklj9VtCdCp zBYF#zD5B|$DZNfR4dfhz8+RbB|WYD~|`gVP%g;d`tHa^y|+LpWnlRg5Os9LNgt&AknKy{-|%vRec5-p#2wcG-34@ ziA(|^ztW7K_j!*8%rgT$TtW#H|Br#Ol*9?ruY(QtyEMShv*$LOyTLFbF3x}H{_aBc z+NGXk-TY_Vd!deI#nQT!=Yy~5W$I^XeL}hIDQ)6QxGzjMpmnYtKX-V<8ezjjc)Fg*$&{I3KUe%B@bZ>+AJ+OWmd58J zP!4^yPT3ulP19|>J)dJF5ubYrvX8fruK3I_=bBF?8MP{y>;DMi`y{46o9+OZXw{zj zqLP$9UITHC`uX)4ZpVYao!Z{3{$Z~BrUMp_i^iqxxJEV7p@fxem&>DNm&RG!lG@SE z*auD*D113J)I1U*Y{w>%me-}YZhgbK?v&XMz&uBf94ZZd2LNd)J7^V#f`DvI(lu&y z(m~60pD`vbF0F(Le*r8*G2X==dJVM)dQY#6|+`q*ux zFu^fkgW|@Lb!smNCMnn}n{=tpnm{s@by8mAsXEs?#V>7u$EtEg(Op3j#=8(bsNck4 zKWckCi*-g!dgq7qx; zl}Y#R%mr7w+K|f%psrKody0HkuN^^ zwM21yf?SISdOkHR)vt z?_*KAwx%!f{cJs6YbyE1Fx}n%!E(sw z)(QCsYFu=5V@jKw+mgnEZ$R@bbx7h~Pu_*%y_fRdwkl6tiutJZ3B)*9SXT46y}PKi zr7)+^6WmlZb&6fcLZwLfrwk>N>Vr6>W2y=t86{lo3ePU^D?i-1Q zN1pc=o)IS^hq_l_7qkE$@A6^mJgYru87<`6 zM-0S_6GAOLyaFxOSS-(xjVQc`pou{BsL4Uw6Q2-65IX>}_x?n%wZ2OcTv&g98tsooXbe)k+2i z`9{-j%ScCaEeOs`lAG|K%ljL`Of0`zHdj~_!E(CcAavLb?a5X|F!$r<|9qCe%ra4l zX)kDA^32DO7lu;(Aao zd2w>y1qdcKhM8Om%m6yWCpJF`hXT*>`54?^3Ro(CR?T?Fhr43Kcffsq>r})gxuIktg~Rhl746&b{r(ILLi>tk9!b z!|OmTj>06c^Myg8pZ`$$lmN2k?|p}%Op z1|tP#Rm#IE=|Hn=Zu?88)NnY>MIkj##ATc7?q@iHJ(K0)o?MA9epgjEptT0lIObaF zMt5m;{@ujJ?Dq$$l*NiZ6fX>@$Qp3-~*kK zeFC#(T3qY^?s$2v<&q}7cBHsIfncatoreo@8Z3*(eqK|PI#^v{*tcSWIkY2k&NjDa zKeXdgK}kUYZ&Pw9rc7baQf-J^7!nL0kn+8V?c3@q)>^`Stf6WIgVEb>dZ)ug5a+bn z>SxQZ74mc5j7OAnnV021*2XTaALEw-NY8oFx3yW(f0+R(1N{&Krej}(b zFrZ6(yh78!1qaa$eml+yU`{!WZh;$@3aWzSvoT67hnWv9lM?T4z?nuyN3V{wkm!mV z^Uo7KgGc)@vIwA8-JQ-Lz5vPRag=HsWeOX+;ixSLuSUrts+LYRhaJD;OJVnQ{?$%` zaQz06{m5&hZx)P=1vJRMEdc+W`XW5zU30&LW5VbZO#G1;T8@i(TU8f(0#wBUz2p^* z>r?>d%+{E@Ugt2sasD<@^NE6ln0QCw!+&Y{>Ll8dy4?0|^hR&P4xH>Jvsh%^I`$pv z9{+)Q82Guz5|++a&5bTZ*#>^u5Z~GU;L0zy9k?8qW}62B5p5hO>z?S-*_u|r{0KV(o2xy%9y3l)%~1mSrS zQnBdyr_>1X%Uw}>2TK|~C9L6&6~D;vitBAkEMTi_HNwQ?E2fx+I2_1t_boAE8OB_z zZ8@B&^H-ai+PR<|^#2)gzmxFN6~E{72*YUBEIgi{SN{cSYO3lY5F$at6pG0_lyloG zvkmZyWaT@mFW>f8}YA^Q3IQC}SN!AZ+rHRY=}w&k%7Y?1}cl zWc(XyO?ctf; zN<-Fq-eJys77^i?FBX)Cm2BpMMteG9Ea~FAIfn`{;#c*>iXlg^@VQ0=+^z*#VOQ5>9P9uWtkq*U2b7y0m<7XG7^X8B zSG@!2x|<;*?-cJT!5zwqBD$=4=-A@`_yFu&b?R}!aNlgg+AynXoa9BE^8K()b^8j~ z`ghYj{OO@>_|h7R-dTXW_?lbX@alz3dNif$1$Wo!NvvLl#p}N>9I5xwqg2$qb!czE zO$xcPipUaw_Yb5PWAkc7JIQ+w}6gBl^oT1XOfAq z`2~R!+!vM>=t^K)29KbLpYkuLDGZ7C8Cb07u>!*467CgFP-IZTr1_hY9oyGkRhX+M zdYg|#xMA0PCv`nSWX(#{eQh)YerChULdP=3deGW}XiGB96_jDjSACfLo@3)J8EbF* z9F^nWUI5fx!#q*kZy`vdKY+l#8fCqOs_+E$0KbdfekaCZsZFbE?|^0ci^|6bKS5h1 zU!?9sll$0axHr^pWoBk3;-<{Tlbn-N z(-IaZ7Msfh3sm;G$o`3W@TLQ|D&fTIVW~S5J!-8``?=!056nXdexcaj!ppdPVc+~= ziFeJ?L|x)rW01%`>~T^_GCtH0*!W^LbOrtTB}hbTR$*3m`Tf!%Ub!vtH~Ry%fG+;5 zTW7h5+kJfHm|e<0S}PV<=}>g6ZWYHk8(KO*b2j}t?mw#u>Dl)~)%0SgG(fSjJ}5?6 z2mDG>;9x$rX?>s|A~tJwGXPVAaXl`-HuNrQ%s*W(sOswAL*&SYJ6hYOpSx_wz5ATw zWJ&ci>uAiCx4OPdufJ=qe}(@F*icNFD#9e(1E>z(FQF#3wE0|hpB?fu$Nbk>si(b4 zC|Lo@Lvloeqb=SSZ5G0Hmv=23QzbqW`b0CCP8qJP#;NLtg1dM1_IvK&)$io-yS%x4 z|ByPBYMX;CC>xjiG?jEM4(oe_dyY;LtuMVrwXMyb7*r1nc+`pBD!C$muY4z+*XJC` z-3EzZ*f?%5?bS$_GEO2MNQrv?L?`2{^39X^j`w)TM-XVR042ipEzk;c(&69Z`qza9 zXJzQm1qTNo-^0xeavzL~I8gUiC~0b1)K*_93Sg0Sp@Wh6_A;Gz8l7)2r#q8kw#G5$ zLkG?-bSBoFdt*5-I#{~kX>;{mZdO95ALbQJy9gGAZ!~0Oq3b=#p=Ig$>Tc8~NS!|{ z`%~ukP;)nZLX+Egm4`*M>f#{qv&%f<+6J|mik#P$6JF+G{wz~6=&%BnT8^x*pkrcn z_3(pkjH;T7UrJ8Dwb?ShzNTA7dkk;PB0d%UW9-tDk1Gq$uxkF$Rc~qFz~fKoo?KTE zc|Z{!hWFm;cGC~Bw3lIXD2Y3d^+xBVt`$T=J${OIzJB0eImG&6Wn|`iAgJgbS`F(m zcV2Oin7W@`nB`W>-?|)@NAIG}d8pT*=|@&(4AbosqF`~>jro-LQp8(t4%qg4HFr5z z6v?;(?c@uFFoSnXTX9Nqn{XM7BKT~G8Fc?TY)zsnPX_7(@(_g8x}Zl;78ruyQE%|2V6lubH! z8q=mA^q}Mq;3x~ofHFP@a!UjXMn;93Fb#NKH$1|V?=|Lgb`sQ0w44x9DyDRBK&0vRQ(*s2*%p?TfPkR-bj#*m(l>qujVy7Dkg^8$k$^8*zitU0HGOBel`)0yd^IAJy#Ax`cPk;wl%O) zUJWyC zAMZFJOtR`+31u@W&)1 zC!4DF=|YAztiC=;`s&9lN6Gv&;g9PGBtZcxJWR zRC&CUpEaT9O(<2bM+R99H&-gXB(>H?YI?!eq%-)n6Ts7cf<8XcVNq?9nwS({_N-Al zv!O6pRG(VaCr0NZq|g?}ugGp(OW%h#62jbzKIG5&{*D3QgJ zOSk3SIODQVJ>wZQ*3%k}%k#FwlW7%|l+3OwRK#bzvV1VIUjPS)#~92aA6z%KfFx7g zjAjdHrgS}J@@ctz{6vV$S`aQ#yh+BPB77P;?h~`MVKjXb63lU%-m5em-n=eIY zG>O@WL4CCfnh&5>ND9#f)5yjM-p>%Qy!5w_)bH4ww(_b+zszNPJppC3_BN%uW@TF+^h1Di1ZxZqj10kI8Jirvf z;K*;Udm;%&TRlDVhSKqw;;!*G)GPFZh?|)Dnhf{*F%OR~4&Hq;b=)L4NO<3`1n_mb zvyOzF4u0NENm$cS+4p#FJ}aiW7GGZoI6bJQomYZY+Mjj9Lp}NS6f#TBWAH%+a!&1p zSfD5PmWku2LgneVj1{bh!~0AIBJNB~m3tL6L?(E!*qN{VGBR?N`~iMb)l26>87c&>yGy+}-SPN!5z+dQ zDk%aaBVBRVT1BXL(4xDYHEf?dKa6wQ*x2laU`?^vZT&daozi*sN7VR_*c!o&gd`j) z-tonvvg)*4Z#E3h2i-QfMf|gCtYtR)*#|)^%mT6{#?BT?{OK}bRx0#?n?)eoH`GYX);=3 z56hZILLlEdPI6sapND`bVXkBPz`|97tmjy^h==P?(ZDJxkhO~;;_&p}HQVQ5PM1H{ z9h>`H@`G?EGm8OLf-g_@!<3Eg%yZwuIHS4D5PI$BomW*;vH>d#H`bT8QlqQW%=m;& zSH3GykK=Z(05>VD{GR^?uRmYy1olR(W zxTsNCA0IO!0g1xUIhe0^+AXf#vA+PzUbtBXQ{Go<*3Z0K`IHzRuT+Xz*^v+%8+&#g z8S-2TCh`o!TBp;)V@jjR{_9ro!$Q>YS<3$!ka(L9=Ub>IY-o^D0H>SlnEENfYRQV} z`Oe30y{0g^G5lE)_GogkCvI=OAK{M>*Sw@Ubgn;79Ve-mdNdd+hX=KU_m;{dDpP&_ zWT5YW@B(>F^HYKeL!N5}hkdUF(M5}<=dJyCM*>)B^u5aX6&QK?v2SK^eqtPK4=El> z=UPHRodmQGrY;5nRi=MtO>RQ=Gxr{jzR@fp)1hdvEe@yZ$0C&tXRny=MAVGLHLAfe z)y2L({P6|FX8QcxD@dNor&Je$_*Q$`p`W#@Mr`r3edKG!8iu4@q2l7)Zh7J{g{P~L zJDTQ2Jh$eKrM2pHddz*Fu;e_FL$&dw)ik~N(%pk<3{$<|++KQq*M5JHLh{g5Z$W_d zM7StYT(92{E)dqa(4%$>K}2Tn@>HmX_63rUaes9MLO$Gs5$z1krIxy5(2cHwyfx{O zQii<*^vMX$V`BW760bFYzb-OuZk=aelNt5Yn$RofYwH0t+Xbls$@m|^Xyfr7Z%qnq zLvH_he}0cNdQp1+d(m08(D4EJ2oAb<=Nk>zyC;>ioA_KQY4n`d(k!sZM_%0W$|$+^ zNx5(|dHRw(%A^av>}@?7!fhX*qnu4ax=H$&*3VY2B7;Q=+TPGQmQ|D%3#RSZ9h+gX zgL36@v1B5_SowAQq*sp8o~_emp7$Ps^ttX`OqN(ky`m)eoO7;`i$=8tvq-etfcVLT zb2UHANt|g7q*%A#nIJ31tdHM(VGZQISzsc&g(gNY4A1o^on7;_O{|ORdRh&l!c>Kp z7S_E1MD=vbKR-L}oKdoLvsg1^cC2E2R~^<3`WRi>gB|nP(k+^MqrE_ajns$VM*>6g zpq4(VV;ca%+ZhDifh?hLU0`{L#%MCJeeFWlH~jy&d&{`0viA>kL{JnJP)b@55s>aa ziU`uu($bBzGzUjSTBTd*63Ig&N=Zw@p(Lfd>#mI>$mq=fH}`X2-FHVkXYaMvlk17^ z!@IzjXWTh$6Ae?xdN2Z-ncc6l>UTuG<>Pj&C9xj{`Th(g^ND((p7V8dkk1nqLu^hj zuf`#?K`Y4GpJ8q9)P_G_vDbf|AkV~_fuE|sURiLToc|h0eDU7$RWXX618SA`wOpDV zXL<8mEN-;0jk#Krfp@S}B8h*3dp^YnF^7|S60|GWS zQinU%)o=2LJS$vXFSa(~H)mTfU%nxGj}~7>Pb(fjq>pFpoeGAnZ0l7yOd0Y}h3?^= z4WE~6+Dt4AVT5(81S$c5hP`fYgXStvyqB8$;kW4rzmCX{Dz(-5ZFR|pZ~`lITAt6==rL@`f|V1;FQR4 zmoQ?ax`}anwKUALKN%DA>?8_2uaBw!K_I#f&y_cFE;|cN&!*m=KlkJ=IpP++(SgK@ z;ZRcl3!}QoR~$POr(94#(_VWcNtJVDMqOPU3^cZV{N%Ern1C+){34O3w7^ys8*@^^ zYdq!&5MJKX@GWxQvXWC{9`D})m6n!YpW$!oMOa;}@bb$7!=;@8oI~GGP!O6&KZ}y^wiQQya+>=3>E?ia;2nvxEO1To z`LO&gYRk=0#+N)+sBhb4QQX?1zBe+upFTBIf?`ELfdMjHUgzej!^m7#v7xC|MnbKi zwH-f0o0m-D+I$$W*_5;GhC@$A{d$UROB-|(=qww?)gvKB^ts8i#+^e~hU+iM-5zKFjucgw1cB(7Sd05!U=&Nar$1uyjG z*0ODg-FMsX(Fyq4XOh*48jg*PO_*L+Cn{M-S6knW<_g-zYoLneEVn%j0Pr@*_iXoV zCMe+;2GEmt#l+mbnmd>q)w2o1Ss7X-6-WuM!Won_;b9!9p0(5#1N!n&bq|ojgP+lF zpx^}$lqV$-R!0_Gv*;wu6I>u3k27|y`K+bgSOo3GT@d1FGJn}CewH9pTDa28o6pwK z#Q?$&sP`54L3Zm~;!XTyQA@fDNf4+~jO(`1+IQF0xBCYI)W$oYKZ`9}S5nfC z=4OFuuSc2FhVe&fH(VV&e0-Bh?=o&LggZcRe+0%QDHq$M-k7*@JGP(9xHAn`1!0~! zK>cdR>eU%u07wK%o3P#_amm*2-FJB5p35y%L#2K9{$#mHH$zx;8u#S43=7MuAg9K1 zh6r1Zvq-aRVuBCzoH%KhF|eXvnU+kgTB)6N>6S>4@(Y5Yw{qr zrmD-3zmzT_uz3M;WYzTVwMxWZp*l=6Y%&5p?rfL5#x

EO4u9rieTJKjC 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

-pj`s-l)1gB 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 F^M655onHU| literal 0 HcmV?d00001 From 4ea1dcdf5a40f3aa731354cdceae89b92083792c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 10 Apr 2019 12:20:00 +0300 Subject: [PATCH 09/30] Update deploy-from-script-or-cloud-init-to-localhost.md --- ...eploy-from-script-or-cloud-init-to-localhost.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/deploy-from-script-or-cloud-init-to-localhost.md b/docs/deploy-from-script-or-cloud-init-to-localhost.md index 7a99d6b2..368db5a0 100644 --- a/docs/deploy-from-script-or-cloud-init-to-localhost.md +++ b/docs/deploy-from-script-or-cloud-init-to-localhost.md @@ -19,15 +19,15 @@ The command will prepare the environment and install AlgoVPN with default parame `ONDEMAND_WIFI` - "Connect On Demand" when connected to Wi-Fi. Default: false `ONDEMAND_WIFI_EXCLUDE` - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand". Comma-separated list. `WINDOWS` - To support Windows 10 or Linux Desktop clients. Default: false -`STORE_CAKEY` - To retain the CA key. (required to add users in the future, but less secure). Default: false -`LOCAL_DNS` - To install an ad blocking DNS resolver. Default: false +`STORE_CAKEY` - To retain the CA key. (required to add users in the future, but less secure). Default: false. +`LOCAL_DNS` - To install an ad blocking DNS resolver. Default: false. `SSH_TUNNELING` - Enable SSH tunneling for each user. Default: false `ENDPOINT` - The public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate). It will be gathered automatically for DigitalOcean, AWS, GCE or Azure if the `METHOD` is cloud. Otherwise you need to define this variable according to your public IP address. -`USERS` - list of VPN users. Comma-separated list. -`REPO_SLUG` - Owner and repository that used to get the installation scripts from. Default: trailofbits/algo -`REPO_BRANCH` - Branch for `REPO_SLUG`. Default: master -`EXTRA_VARS` - Additional extra variables. -`ANSIBLE_EXTRA_ARGS` - Any available ansible parameters. ie: `--skip-tags apparmor` +`USERS` - list of VPN users. Comma-separated list. +`REPO_SLUG` - Owner and repository that used to get the installation scripts from. Default: trailofbits/algo. +`REPO_BRANCH` - Branch for `REPO_SLUG`. Default: master. +`EXTRA_VARS` - Additional extra variables. +`ANSIBLE_EXTRA_ARGS` - Any available ansible parameters. ie: `--skip-tags apparmor`. ## Examples From 1c7e1dc331142918d7b9324c217850467cc45b46 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 13 Apr 2019 11:53:45 +0200 Subject: [PATCH 10/30] Move `Delete the CA key` task to the appropriate role (#1393) --- roles/strongswan/tasks/openssl.yml | 10 ++++++++++ server.yml | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index 694bb83c..ffaa7062 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -209,3 +209,13 @@ - gencrl.changed notify: - rereadcrls + +- name: Delete the CA key + local_action: + module: file + path: "{{ ipsec_pki_path }}/private/cakey.pem" + state: absent + become: false + when: + - ipsec_enabled + - not algo_store_cakey diff --git a/server.yml b/server.yml index 40326830..349150cb 100644 --- a/server.yml +++ b/server.yml @@ -37,16 +37,6 @@ tags: ssh_tunneling - block: - - name: Delete the CA key - local_action: - module: file - path: "{{ ipsec_pki_path }}/private/cakey.pem" - state: absent - become: false - when: - - ipsec_enabled - - not algo_store_cakey - - name: Dump the configuration local_action: module: copy From 8f10647ec1bb4f09ffca00e315fb250ef60306de Mon Sep 17 00:00:00 2001 From: wtgtybhertgeghgtwtg Date: Wed, 17 Apr 2019 03:57:53 -0600 Subject: [PATCH 11/30] fix: get public IP from default interface (#1396) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 248f3784..ed385e73 100644 --- a/install.sh +++ b/install.sh @@ -50,7 +50,7 @@ getAlgo() { publicIpFromInterface() { echo "Couldn't find a valid ipv4 address, using the first IP found on the interfaces as the endpoint." DEFAULT_INTERFACE="$(ip -4 route list match default | grep -Eo "dev .*" | awk '{print $2}')" - ENDPOINT=$(ip -4 addr sh dev eth0 | grep -w inet | head -n1 | awk '{print $2}' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b') + ENDPOINT=$(ip -4 addr sh dev $DEFAULT_INTERFACE | grep -w inet | head -n1 | awk '{print $2}' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b') export ENDPOINT=$ENDPOINT echo "Using ${ENDPOINT} as the endpoint" } From a1117ecf0a25dde057b43c898d76e8f4f02139e7 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Wed, 17 Apr 2019 07:53:41 -0400 Subject: [PATCH 12/30] Update Adblock lists (#1394) Uses the Unified hosts file from @StevenBlack available [here](https://github.com/StevenBlack/hosts). This encompasses the Ad Away, MVPS, and Malware Domain lists, deleting duplicates for us, and also adds a bunch more. --- config.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config.cfg b/config.cfg index 16411cf0..a652749f 100644 --- a/config.cfg +++ b/config.cfg @@ -70,9 +70,7 @@ reduce_mtu: 0 # 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://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" - "https://hosts-file.net/ad_servers.txt" # Enable DNS encryption. From 505538bcbb0f0e8907b9ac0cf5cc4511dfdae7dc Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Wed, 17 Apr 2019 11:44:58 -0400 Subject: [PATCH 13/30] Update README.md (#1380) Add mention of Wireguard SSID exclusion ability. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4016c85..40da8bd5 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,13 @@ WireGuard is used to provide VPN services on Apple devices. Algo generates a Wir On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the iOS App Store. Then, use the WireGuard app to scan the QR code or AirDrop the configuration file to the device. -On macOS Mojave or later, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1451685025?mt=12) app from the Mac App Store. WireGuard will appear in the menu bar once you run the app. Click on the WireGuard icon, choose **Import tunnel(s) from file...**, then select the appropriate WireGuard configuration file. Enable "Connect on Demand" by editing the tunnel configuration in the WireGuard app. +On macOS Mojave or later, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1451685025?mt=12) app from the Mac App Store. WireGuard will appear in the menu bar once you run the app. Click on the WireGuard icon, choose **Import tunnel(s) from file...**, then select the appropriate WireGuard configuration file. + +On either iOS or macOS, you can enable "Connect on Demand" and/or exclude certain trusted Wi-Fi networks (such as your home or work) by editing the tunnel configuration in the WireGuard app. (Algo can't do this automatically for you.) Installing WireGuard is a little more complicated on older version of macOS. See [Using macOS as a Client with WireGuard](docs/client-macos-wireguard.md). -If you prefer to use the built-in IPSEC VPN on Apple devices, then see [Using Apple Devices as a Client with IPSEC](docs/client-apple-ipsec.md). +If you prefer to use the built-in IPSEC VPN on Apple devices, or need "Connect on Demand" or excluded Wi-Fi networks automatically configured, then see [Using Apple Devices as a Client with IPSEC](docs/client-apple-ipsec.md). ### Android Devices @@ -247,4 +249,4 @@ All donations support continued development. Thanks! * Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. * We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. -Algo is licensed and distributed under the AGPLv3. If you want to distribute a closed-source modification or service based on Algo, then please consider purchasing an exception . As with the methods above, this will help support continued development. \ No newline at end of file +Algo is licensed and distributed under the AGPLv3. If you want to distribute a closed-source modification or service based on Algo, then please consider purchasing an exception . As with the methods above, this will help support continued development. From a60d49f5fc427938dbbb292120bd52ecc33948b1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 19 Apr 2019 10:57:31 +0200 Subject: [PATCH 14/30] Update deploy-from-script-or-cloud-init-to-localhost.md --- docs/deploy-from-script-or-cloud-init-to-localhost.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deploy-from-script-or-cloud-init-to-localhost.md b/docs/deploy-from-script-or-cloud-init-to-localhost.md index 368db5a0..6070562c 100644 --- a/docs/deploy-from-script-or-cloud-init-to-localhost.md +++ b/docs/deploy-from-script-or-cloud-init-to-localhost.md @@ -8,7 +8,7 @@ You can copy-paste the snippet below to the user data (cloud-init or startup scr ``` #!/bin/bash -curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x ``` The command will prepare the environment and install AlgoVPN with default parameters. If you want to modify the behaviour you may define additional variables. @@ -38,7 +38,7 @@ The command will prepare the environment and install AlgoVPN with default parame export ONDEMAND_CELLULAR=true export WINDOWS=true export SSH_TUNNELING=true -curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x ``` ##### How to deploy locally without using cloud-init @@ -46,7 +46,7 @@ curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | s ``` export METHOD=local export ONDEMAND_CELLULAR=true -curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x ``` ##### How to deploy a server using arguments @@ -54,5 +54,5 @@ curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | s The arguments order as per [variables](#variables) above ``` -curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo bash -x -s local true false _null true true true true myvpnserver.com +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x -s local true false _null true true true true myvpnserver.com ``` From 1e35753aa27ca06269035d0e05d780f0f9fd9f2a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 23 Apr 2019 12:36:12 +0200 Subject: [PATCH 15/30] Update openssl.yml (#1403) --- roles/strongswan/tasks/openssl.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index ffaa7062..fd38611a 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -151,6 +151,23 @@ with_items: "{{ users }}" register: p12 + - name: Build the client's p12 with the CA cert included + shell: > + umask 077; + {{ openssl_bin }} pkcs12 + -in certs/{{ item }}.crt + -inkey private/{{ item }}.key + -export + -name {{ item }} + -out private/{{ item }}_ca.p12 + -certfile cacert.pem + -passout pass:"{{ p12_export_password }}" + args: + chdir: "{{ ipsec_pki_path }}" + executable: bash + with_items: "{{ users }}" + register: p12 + - name: Copy the p12 certificates copy: src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" From b7a448350a6c2fc47fbcde242d3e2807dc40da73 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Fri, 26 Apr 2019 00:54:37 -0400 Subject: [PATCH 16/30] Update cloud-vultr.md (#1406) * Update cloud-vultr.md More fleshed-out instructions for generating an API key and saving the file. Also notes the default ansible behavior of looking for the file in `~/.vultr.ini`. * Update README.md --- README.md | 1 + docs/cloud-vultr.md | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40da8bd5..ef6bfbfc 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ After this process completes, the Algo VPN server will contain only the users li - Configure [Azure](docs/cloud-azure.md) - Configure [DigitalOcean](docs/cloud-do.md) - Configure [Google Cloud Platform](docs/cloud-gce.md) + - Configure [Vultr](docs/cloud-vultr.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 18.04](docs/deploy-to-ubuntu.md) server diff --git a/docs/cloud-vultr.md b/docs/cloud-vultr.md index 3448e773..25291b12 100644 --- a/docs/cloud-vultr.md +++ b/docs/cloud-vultr.md @@ -1,8 +1,15 @@ ### Configuration file -You need to create a configuration file in INI format with your api key (https://my.vultr.com/settings/#settingsapi) +Algo requires an API key from your Vultr account in order to create a server. The API key is generated by going to your Vultr settings at https://my.vultr.com/settings/#settingsapi, and then selecting "generate new API key" on the right side of the box labeled "API Key". + +Algo can read the API key in several different ways. Algo will first look for the file containing the API key in the environment variable $VULTR_API_CONFIG if present. You can set this with the command: `export VULTR_API_CONFIG=/path/to/vultr.ini`. Probably the simplest way to give Algo the API key is to create a file titled `.vultr.ini` in your home directory by typing `nano ~/.vultr.ini`, then entering the following text: ``` [default] key = ``` +where you've cut-and-pasted the API key from above into the `` field (no brackets). + +When Algo asks `Enter the local path to your configuration INI file +(https://trailofbits.github.io/algo/cloud-vultr.html):` if you hit enter without typing anything, Algo will look for the file in `~/.vultr.ini` by default. + From feb009144855123bf3b89ff033a9b0d4e61870f7 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 26 Apr 2019 00:56:38 -0400 Subject: [PATCH 17/30] Update Linux WireGuard client instructions (#1407) --- docs/client-linux-wireguard.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/client-linux-wireguard.md b/docs/client-linux-wireguard.md index 3430959c..52f6e85a 100644 --- a/docs/client-linux-wireguard.md +++ b/docs/client-linux-wireguard.md @@ -2,38 +2,54 @@ ## 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: +To connect to your AlgoVPN using [WireGuard](https://www.wireguard.com) from Ubuntu Server, first install WireGuard: ```shell # Add the WireGuard repository: sudo add-apt-repository ppa:wireguard/wireguard -# Update the list of available packages (not necessary on Bionic): +# Update the list of available packages (not necessary on Bionic or later): 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.) +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. +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`. Each WireGuard client you connect to your AlgoVPN must use a different 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: +## Configure DNS + +### Ubuntu 18.04 (Bionic) + +If your client is running Bionic (or another Linux that uses `systemd-resolved` for DNS but does not have `resolvectl` or `resolvconf` installed) 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. +### Ubuntu 18.10 (Cosmic) or 19.04 (Disco) + +If your client is running Cosmic or Disco (or another Linux that uses `systemd-resolved` for DNS and has `resolvectl` but *not* `resolvconf` installed) you can either edit the config file as shown above for Bionic or run the following command once: + +``` +sudo ln -s /usr/bin/resolvectl /usr/bin/resolvconf +``` + +### Other Linux Distributions + +On other Linux distributions you might need to install the `openresolv` package. + ## 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: +# Linux client: sudo install -o root -g root -m 600 .conf /etc/wireguard/wg0.conf # Start the WireGuard VPN: @@ -42,14 +58,14 @@ sudo systemctl start wg-quick@wg0 # Check that it started properly: sudo systemctl status wg-quick@wg0 -# Verify the connection to the Algo VPN: +# Verify the connection to the AlgoVPN: sudo wg -# See that your client is using the IP address of your Algo VPN: +# See that your client is using the IP address of your AlgoVPN: 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`). +If your Linux distribution does not use `systemd` you can bring up WireGuard with `sudo wg-quick up wg0`. From 25513cf9259646fc2207869fe853c74b7355e4a3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 26 Apr 2019 17:48:28 +0200 Subject: [PATCH 18/30] Refactoring, Linting and additional tests (#1397) * Refactoring, Linting and additional tests * Vultr: Undefined variable and deprecation notes fix * Travis-CI enable linters * Azure: Update python requirements * Update main.yml * Update install.sh * Add missing roles to ansible-lint * Linting for skipped roles * add .ansible-lint config --- .ansible-lint | 3 + .travis.yml | 82 +++++++++++-------- algo | 23 +++--- deploy_client.yml | 38 ++------- input.yml | 49 ++++++----- install.sh | 39 ++++----- main.yml | 11 +++ playbooks/cloud-pre.yml | 29 ++++--- playbooks/rescue.yml | 3 +- requirements.txt | 2 +- roles/client/handlers/main.yml | 1 - roles/client/tasks/main.yml | 12 ++- roles/client/tasks/systems/CentOS.yml | 4 +- roles/client/tasks/systems/Debian.yml | 7 +- roles/client/tasks/systems/Fedora.yml | 4 +- roles/client/tasks/systems/Ubuntu.yml | 7 +- roles/cloud-azure/tasks/main.yml | 4 +- roles/cloud-azure/tasks/venv.yml | 39 +++++---- roles/cloud-digitalocean/tasks/main.yml | 6 +- .../cloud-ec2/files/{stack.yml => stack.yaml} | 0 roles/cloud-ec2/tasks/cloudformation.yml | 2 +- roles/cloud-ec2/tasks/main.yml | 2 +- roles/cloud-gce/tasks/prompts.yml | 2 +- roles/cloud-lightsail/tasks/prompts.yml | 2 +- roles/cloud-openstack/tasks/main.yml | 2 +- roles/cloud-scaleway/tasks/main.yml | 2 +- roles/cloud-scaleway/tasks/prompts.yml | 2 +- roles/cloud-vultr/tasks/main.yml | 10 +-- roles/cloud-vultr/tasks/prompts.yml | 2 +- roles/common/handlers/main.yml | 3 - roles/common/tasks/facts.yml | 16 ++-- roles/common/tasks/freebsd.yml | 4 +- roles/common/tasks/main.yml | 3 +- roles/common/tasks/ubuntu.yml | 13 +-- roles/common/tasks/unattended-upgrades.yml | 2 +- roles/dns_adblocking/tasks/main.yml | 1 + roles/dns_adblocking/tasks/ubuntu.yml | 6 +- roles/dns_encryption/tasks/ubuntu.yml | 4 +- roles/local/tasks/prompts.yml | 6 +- roles/ssh_tunneling/tasks/main.yml | 2 +- roles/strongswan/handlers/main.yml | 2 +- roles/strongswan/tasks/client_configs.yml | 7 +- .../strongswan/tasks/ipsec_configuration.yml | 9 +- roles/strongswan/tasks/openssl.yml | 15 ++-- roles/strongswan/tasks/ubuntu.yml | 11 +-- .../strongswan/templates/client_ipsec.conf.j2 | 4 +- roles/wireguard/defaults/main.yml | 2 +- roles/wireguard/tasks/freebsd.yml | 3 +- roles/wireguard/tasks/keys.yml | 7 +- roles/wireguard/tasks/ubuntu.yml | 3 +- server.yml | 9 +- tests/algo.conf | 1 + tests/cloud-init.sh | 5 +- tests/ipsec-client.sh | 23 ++++++ tests/local-deploy.sh | 6 +- tests/lxd-bridge | 4 +- tests/pre-deploy.sh | 30 +++++++ tests/ssh-tunnel.sh | 15 ++++ tests/update-users.sh | 14 ++-- tests/wireguard-client.sh | 21 +++++ users.yml | 10 ++- 61 files changed, 405 insertions(+), 245 deletions(-) create mode 100644 .ansible-lint rename roles/cloud-ec2/files/{stack.yml => stack.yaml} (100%) create mode 100644 tests/algo.conf create mode 100755 tests/ipsec-client.sh create mode 100755 tests/pre-deploy.sh create mode 100755 tests/ssh-tunnel.sh create mode 100755 tests/wireguard-client.sh diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 00000000..ddfa4ba1 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,3 @@ +skip_list: + - '204' +verbosity: 1 diff --git a/.travis.yml b/.travis.yml index 7a2c67d8..e799b05b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,15 +8,14 @@ services: addons: apt: - sources: + sources: &default_sources - sourceline: 'ppa:ubuntu-lxc/stable' - sourceline: 'ppa:wireguard/wireguard' - packages: + packages: &default_packages - python-pip - lxd - expect-dev - debootstrap - - shellcheck - tree - bridge-utils - dnsutils @@ -25,7 +24,12 @@ addons: - libffi-dev - python-dev - linux-headers-$(uname -r) - - wireguard-dkms + - wireguard + - libxml2-utils + - crudini + - fping + - strongswan + - libstrongswan-standard-plugins cache: directories: @@ -37,39 +41,54 @@ before_cache: - sudo tar cf $HOME/lxc/cache.tar /var/lib/lxd/images/ - sudo chown $USER. $HOME/lxc/cache.tar +custom_scripts: + provisioning: &provisioning + - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' + - sudo ./tests/pre-deploy.sh + - 'sed -i "s/^reduce_mtu:\s0$/reduce_mtu: 20/" config.cfg' + tests: &tests + - sudo ./tests/wireguard-client.sh + - sudo env "PATH=$PATH" ./tests/ipsec-client.sh + - sudo ./tests/ssh-tunnel.sh + matrix: fast_finish: true include: - - stage: Test - name: local deployment from docker + - stage: Tests + name: code checks and linters + addons: + apt: + packages: + - shellcheck script: - - docker build -t travis/algo . - - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." - - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge - - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' - - echo -e "#cloud-config\nssh_authorized_keys:\n - $(cat ~/.ssh/id_rsa.pub)" | sudo lxc profile set default user.user-data - - - sudo service lxd restart - - sudo lxc launch ubuntu:18.04 algo - - until host algo.lxd 10.0.8.1 -t A; do sleep 3; done - - export LXC_IP="$(dig algo.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 + - shellcheck algo install.sh - ansible-playbook main.yml --syntax-check + - ansible-lint -v roles/*/*/*.yml playbooks/*.yml *.yml + + - stage: Deploy + name: local deployment from docker + addons: + apt: + sources: *default_sources + packages: *default_packages + env: DEPLOY=docker + before_install: *provisioning + before_script: + - docker build -t travis/algo . - ./tests/local-deploy.sh - ./tests/update-users.sh + script: *tests - - stage: Test + - stage: Deploy name: cloud-init deployment - script: - - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." - - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge - - sudo service lxd restart - - bash tests/cloud-init.sh | sudo lxc profile set default user.user-data - - - sudo lxc profile show default - - sudo lxc launch ubuntu:18.04 algo + addons: + apt: + sources: *default_sources + packages: *default_packages + env: DEPLOY=cloud-init + before_install: *provisioning + before_script: - until sudo lxc exec algo -- test -f /var/log/cloud-init-output.log; do echo 'Log file not found, Sleep for 3 seconds'; sleep 3; done - ( sudo lxc exec algo -- tail -f /var/log/cloud-init-output.log & ) - | @@ -78,11 +97,10 @@ matrix: sleep 30; done - sudo lxc exec algo -- test -f /opt/algo/configs/localhost/.config.yml - -# 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 main.yml users.yml deploy_client.yml + - sudo lxc exec algo -- tar zcf /root/algo-configs.tar -C /opt/algo/configs/ . + - sudo lxc file pull algo/root/algo-configs.tar ./ + - sudo tar -C ./configs -zxf algo-configs.tar + script: *tests notifications: email: false diff --git a/algo b/algo index 07a2875c..260c0e65 100755 --- a/algo +++ b/algo @@ -4,19 +4,20 @@ set -e if [ -z ${VIRTUAL_ENV+x} ] then - 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 + ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" + if [ -f "$ACTIVATE_SCRIPT" ] + then + # shellcheck source=/dev/null + source "$ACTIVATE_SCRIPT" + else + echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" + exit 1 + fi fi case "$1" in - update-users) PLAYBOOK=users.yml; ARGS="${@:2} -t update-users";; - *) PLAYBOOK=main.yml; ARGS=${@} ;; + update-users) PLAYBOOK=users.yml; ARGS=( "${@:2}" -t update-users ) ;; + *) PLAYBOOK=main.yml; ARGS=( "${@}" ) ;; esac -ansible-playbook ${PLAYBOOK} ${ARGS} +ansible-playbook ${PLAYBOOK} "${ARGS[@]}" diff --git a/deploy_client.yml b/deploy_client.yml index 21fd7709..8ee87670 100644 --- a/deploy_client.yml +++ b/deploy_client.yml @@ -1,5 +1,7 @@ +--- - name: Configure the client hosts: localhost + become: false vars_files: - config.cfg @@ -8,9 +10,10 @@ add_host: name: "{{ client_ip }}" groups: client-host - ansible_ssh_user: "{{ ssh_user }}" + ansible_ssh_user: "{{ 'root' if client_ip == 'localhost' else ssh_user }}" vpn_user: "{{ vpn_user }}" - server_ip: "{{ server_ip }}" + IP_subject_alt_name: "{{ server_ip }}" + ansible_python_interpreter: "/usr/bin/python3" - name: Configure the client and install required software hosts: client-host @@ -18,33 +21,6 @@ become: true vars_files: - config.cfg - - roles/vpn/defaults/main.yml - - pre_tasks: - - name: Get the OS - raw: uname -a - register: distribution - - - name: Modify the server name fact - set_fact: - IP_subject_alt_name: "{{ server_ip }}" - - - name: Ubuntu Xenial | Install prerequisites - raw: > - test -x /usr/bin/python2.7 || - sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 && - sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - changed_when: false - when: "'ubuntu' in distribution.stdout|lower" - - - name: Fedora 25 | Install prerequisites - raw: > - test -x /usr/bin/python2.7 || - sudo dnf install python2 -y && - sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && - rpm -ql python2-dnf || dnf install python2-dnf -y - changed_when: false - when: "'fedora' in distribution.stdout|lower" - + - roles/strongswan/defaults/main.yml roles: - - { role: client, tags: ['client'] } + - role: client diff --git a/input.yml b/input.yml index f4b155b5..d7d6aecd 100644 --- a/input.yml +++ b/input.yml @@ -26,11 +26,12 @@ tasks: - block: - - pause: + - name: Region prompt + pause: prompt: | What provider would you like to use? {% for p in providers_map %} - {{ loop.index }}. {{ p['name']}} + {{ loop.index }}. {{ p['name'] }} {% endfor %} Enter the number of your desired provider @@ -41,7 +42,8 @@ set_fact: algo_provider: "{{ provider | default(providers_map[_algo_provider.user_input|default(omit)|int - 1]['alias']) }}" - - pause: + - name: VPN server name prompt + pause: prompt: | Name the vpn server [algo] @@ -50,21 +52,24 @@ - server_name is undefined - algo_provider != "local" - block: - - pause: + - name: Cellular On Demand prompt + pause: prompt: | Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? [y/N] register: _ondemand_cellular when: ondemand_cellular is undefined - - pause: + - name: Wi-Fi On Demand prompt + pause: prompt: | Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? [y/N] register: _ondemand_wifi when: ondemand_wifi is undefined - - pause: + - name: Trusted Wi-Fi networks prompt + pause: prompt: | List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) @@ -74,14 +79,16 @@ - (ondemand_wifi|default(false)|bool) or (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) - - pause: + - name: Compatible ciphers prompt + 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: + - name: Retain the CA key prompt + pause: prompt: | Do you want to retain the CA key? (required to add users in the future, but less secure) [y/N] @@ -89,14 +96,16 @@ when: store_cakey is undefined when: ipsec_enabled - - pause: + - name: DNS adblocking prompt + pause: prompt: | Do you want to install an ad blocking DNS resolver on this VPN server? [y/N] register: _local_dns when: local_dns is undefined - - pause: + - name: SSH tunneling prompt + pause: prompt: | Do you want each user to have their own account for SSH tunneling? [y/N] @@ -107,36 +116,38 @@ 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 %} + {%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input|length > 0 -%} + {%- 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']) }} + {%- elif _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']) }} + {%- elif _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 | b64encode }} - {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input | b64encode }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input|length > 0 -%} + {{ _ondemand_wifi_exclude.user_input | b64encode }} {%- else %}{{ '_null' | b64encode }}{% 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']) }} + {%- elif _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']) }} + {%- elif _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']) }} + {%- elif _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']) }} + {%- elif _store_cakey.user_input %}{{ booleans_map[_store_cakey.user_input] | default(defaults['store_cakey']) }} {%- else %}false{% endif %} rescue: - - include_tasks: playbooks/rescue.yml + - include_tasks: playbooks/rescue.yml diff --git a/install.sh b/install.sh index ed385e73..5c7cf504 100644 --- a/install.sh +++ b/install.sh @@ -36,12 +36,11 @@ installRequirements() { } getAlgo() { - [ ! -d "algo" ] && git clone https://github.com/${REPO_SLUG} algo + [ ! -d "algo" ] && git clone "https://github.com/${REPO_SLUG}" -b "${REPO_BRANCH}" algo cd algo - - git checkout ${REPO_BRANCH} - - python -m virtualenv --python=`which python2` .venv + + python -m virtualenv --python="$(command -v python2)" .venv + # shellcheck source=/dev/null . .venv/bin/activate python -m pip install -U pip virtualenv python -m pip install -r requirements.txt @@ -50,27 +49,23 @@ getAlgo() { publicIpFromInterface() { echo "Couldn't find a valid ipv4 address, using the first IP found on the interfaces as the endpoint." DEFAULT_INTERFACE="$(ip -4 route list match default | grep -Eo "dev .*" | awk '{print $2}')" - ENDPOINT=$(ip -4 addr sh dev $DEFAULT_INTERFACE | grep -w inet | head -n1 | awk '{print $2}' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b') + ENDPOINT=$(ip -4 addr sh dev "$DEFAULT_INTERFACE" | grep -w inet | head -n1 | awk '{print $2}' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b') export ENDPOINT=$ENDPOINT echo "Using ${ENDPOINT} as the endpoint" } publicIpFromMetadata() { if curl -s http://169.254.169.254/metadata/v1/vendor-data | grep DigitalOcean >/dev/null; then - PROVIDER="digitalocean" ENDPOINT="$(curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)" elif test "$(curl -s http://169.254.169.254/latest/meta-data/services/domain)" = "amazonaws.com"; then - PROVIDER="amazon" ENDPOINT="$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)" elif host -t A -W 10 metadata.google.internal 127.0.0.53 >/dev/null; then - PROVIDER="gce" ENDPOINT="$(curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip")" elif test "$(curl -s -H Metadata:true 'http://169.254.169.254/metadata/instance/compute/publisher/?api-version=2017-04-02&format=text')" = "Canonical"; then - PROVIDER="azure" ENDPOINT="$(curl -H Metadata:true 'http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/publicIpAddress?api-version=2017-04-02&format=text')" fi - if echo ${ENDPOINT} | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"; then + if echo "${ENDPOINT}" | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"; then export ENDPOINT=$ENDPOINT echo "Using ${ENDPOINT} as the endpoint" else @@ -82,23 +77,25 @@ deployAlgo() { getAlgo cd /opt/algo + # shellcheck source=/dev/null . .venv/bin/activate export HOME=/root export ANSIBLE_LOCAL_TEMP=/root/.ansible/tmp export ANSIBLE_REMOTE_TEMP=/root/.ansible/tmp + # shellcheck disable=SC2086 ansible-playbook main.yml \ -e provider=local \ - -e ondemand_cellular=${ONDEMAND_CELLULAR} \ - -e ondemand_wifi=${ONDEMAND_WIFI} \ - -e ondemand_wifi_exclude=${ONDEMAND_WIFI_EXCLUDE} \ - -e windows=${WINDOWS} \ - -e store_cakey=${STORE_CAKEY} \ - -e local_dns=${LOCAL_DNS} \ - -e ssh_tunneling=${SSH_TUNNELING} \ - -e endpoint=$ENDPOINT \ - -e users=$(echo "$USERS" | jq -Rc 'split(",")') \ + -e "ondemand_cellular=${ONDEMAND_CELLULAR}" \ + -e "ondemand_wifi=${ONDEMAND_WIFI}" \ + -e "ondemand_wifi_exclude=${ONDEMAND_WIFI_EXCLUDE}" \ + -e "windows=${WINDOWS}" \ + -e "store_cakey=${STORE_CAKEY}" \ + -e "local_dns=${LOCAL_DNS}" \ + -e "ssh_tunneling=${SSH_TUNNELING}" \ + -e "endpoint=$ENDPOINT" \ + -e "users=$(echo "$USERS" | jq -Rc 'split(",")')" \ -e server=localhost \ -e ssh_user=root \ -e "${EXTRA_VARS}" \ @@ -106,7 +103,7 @@ deployAlgo() { tee /var/log/algo.log } -if test $METHOD = "cloud"; then +if test "$METHOD" = "cloud"; then publicIpFromMetadata fi diff --git a/main.yml b/main.yml index faf4c2d1..c1c14abd 100644 --- a/main.yml +++ b/main.yml @@ -1,4 +1,15 @@ --- +- hosts: localhost + become: false + tasks: + - name: Verify Ansible meets Drupal VM's version requirements. + assert: + that: "ansible_version.full is version('2.7.10', '==')" + msg: > + Ansible version is {{ ansible_version.full }}. + You must update the requirements to use this version of Algo. + Try to run python -m pip install -U -r requirements.txt + - name: Include prompts playbook import_playbook: input.yml diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index 53de7fab..f25dafa0 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -1,7 +1,7 @@ --- -- name: Display the invocation environment - local_action: - module: shell +- block: + - name: Display the invocation environment + shell: > ./algo-showenv.sh \ 'algo_provider "{{ algo_provider }}"' \ 'algo_ondemand_cellular "{{ algo_ondemand_cellular }}"' \ @@ -13,17 +13,20 @@ 'wireguard_enabled "{{ wireguard_enabled }}"' \ 'dns_encryption "{{ dns_encryption }}"' \ > /dev/tty - tags: debug + tags: debug -- name: Install the requirements - local_action: - module: pip - state: latest - name: - - pyOpenSSL - - jinja2==2.8 - - segno - tags: always + - name: Install the requirements + pip: + state: latest + name: + - pyOpenSSL + - jinja2==2.8 + - segno + tags: + - always + - skip_ansible_lint + delegate_to: localhost + become: false - name: Generate the SSH private key openssl_privatekey: diff --git a/playbooks/rescue.yml b/playbooks/rescue.yml index 4c090cec..375e0e6f 100644 --- a/playbooks/rescue.yml +++ b/playbooks/rescue.yml @@ -2,4 +2,5 @@ - debug: var: fail_hint -- fail: +- name: Fail the installation + fail: diff --git a/requirements.txt b/requirements.txt index 38f36dac..60c89a08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -ansible==2.5.2 +ansible==2.7.10 diff --git a/roles/client/handlers/main.yml b/roles/client/handlers/main.yml index 84c893a1..33b013f1 100644 --- a/roles/client/handlers/main.yml +++ b/roles/client/handlers/main.yml @@ -1,4 +1,3 @@ --- - - name: restart strongswan service: name=strongswan state=restarted diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index e3b1634d..a2be9550 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -8,13 +8,21 @@ package: name="{{ item }}" state=present with_items: - "{{ prerequisites }}" + register: result + until: result is succeeded + retries: 10 + delay: 3 - name: Install strongSwan package: name=strongswan state=present + register: result + until: result is succeeded + retries: 10 + delay: 3 - name: Setup the ipsec config template: - src: "roles/vpn/templates/client_ipsec.conf.j2" + src: "roles/strongswan/templates/client_ipsec.conf.j2" dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.conf" mode: '0644' with_items: @@ -24,7 +32,7 @@ - name: Setup the ipsec secrets template: - src: "roles/vpn/templates/client_ipsec.secrets.j2" + src: "roles/strongswan/templates/client_ipsec.secrets.j2" dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.secrets" mode: '0600' with_items: diff --git a/roles/client/tasks/systems/CentOS.yml b/roles/client/tasks/systems/CentOS.yml index aeb495e5..68929dfa 100644 --- a/roles/client/tasks/systems/CentOS.yml +++ b/roles/client/tasks/systems/CentOS.yml @@ -1,6 +1,6 @@ --- - -- set_fact: +- name: Set OS specific facts + set_fact: prerequisites: - epel-release configs_prefix: /etc/strongswan diff --git a/roles/client/tasks/systems/Debian.yml b/roles/client/tasks/systems/Debian.yml index 2566c076..36873c09 100644 --- a/roles/client/tasks/systems/Debian.yml +++ b/roles/client/tasks/systems/Debian.yml @@ -1,5 +1,6 @@ --- - -- set_fact: - prerequisites: [] +- name: Set OS specific facts + set_fact: + prerequisites: + - libstrongswan-standard-plugins configs_prefix: /etc diff --git a/roles/client/tasks/systems/Fedora.yml b/roles/client/tasks/systems/Fedora.yml index 6bc13ef4..f4805785 100644 --- a/roles/client/tasks/systems/Fedora.yml +++ b/roles/client/tasks/systems/Fedora.yml @@ -1,6 +1,6 @@ --- - -- set_fact: +- name: Set OS specific facts + set_fact: prerequisites: - libselinux-python configs_prefix: /etc/strongswan diff --git a/roles/client/tasks/systems/Ubuntu.yml b/roles/client/tasks/systems/Ubuntu.yml index 2566c076..36873c09 100644 --- a/roles/client/tasks/systems/Ubuntu.yml +++ b/roles/client/tasks/systems/Ubuntu.yml @@ -1,5 +1,6 @@ --- - -- set_fact: - prerequisites: [] +- name: Set OS specific facts + set_fact: + prerequisites: + - libstrongswan-standard-plugins configs_prefix: /etc diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 2c448093..70682288 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -9,14 +9,14 @@ - 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'] }} + {%- elif _algo_region.user_input %}{{ azure_regions[_algo_region.user_input | int -1 ]['name'] }} {%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} - name: Create AlgoVPN Server azure_rm_deployment: state: present deployment_name: "{{ algo_server_name }}" - template: "{{ lookup('file', 'deployment.json') }}" + template: "{{ lookup('file', role_path + '/files/deployment.json') }}" secret: "{{ secret }}" tenant: "{{ tenant }}" client_id: "{{ client_id }}" diff --git a/roles/cloud-azure/tasks/venv.yml b/roles/cloud-azure/tasks/venv.yml index cbadf8de..bc32c6f9 100644 --- a/roles/cloud-azure/tasks/venv.yml +++ b/roles/cloud-azure/tasks/venv.yml @@ -10,23 +10,32 @@ 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 + - azure-cli-core==2.0.35 + - azure-cli-nspkg==3.0.2 + - azure-common==1.1.11 + - azure-mgmt-batch==4.1.0 + - azure-mgmt-compute==2.1.0 + - azure-mgmt-containerinstance==0.4.0 + - azure-mgmt-containerregistry==2.0.0 + - azure-mgmt-containerservice==3.0.1 + - azure-mgmt-dns==1.2.0 + - azure-mgmt-keyvault==0.40.0 + - azure-mgmt-marketplaceordering==0.1.0 + - azure-mgmt-monitor==0.5.2 + - azure-mgmt-network==1.7.1 + - azure-mgmt-nspkg==2.0.0 + - azure-mgmt-rdbms==1.2.0 + - azure-mgmt-resource==1.2.2 + - azure-mgmt-sql==0.7.1 + - azure-mgmt-storage==1.5.0 + - azure-mgmt-trafficmanager==0.50.0 + - azure-mgmt-web==0.32.0 + - azure-nspkg==2.0.0 + - azure-storage==0.35.1 - 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 + - azure-keyvault==1.0.0a1 + - azure-graphrbac==0.40.0 state: latest virtualenv: "{{ azure_venv }}" virtualenv_python: python2.7 diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 93baefef..b6010306 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -10,7 +10,7 @@ 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'] }} + {%- elif _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 }}') }}" @@ -22,7 +22,7 @@ api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys - until: ssh_keys.changed != true + until: not ssh_keys.changed retries: 10 delay: 1 @@ -83,7 +83,7 @@ api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys - until: ssh_keys.changed != true + until: not ssh_keys.changed retries: 10 delay: 1 diff --git a/roles/cloud-ec2/files/stack.yml b/roles/cloud-ec2/files/stack.yaml similarity index 100% rename from roles/cloud-ec2/files/stack.yml rename to roles/cloud-ec2/files/stack.yaml diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml index 27977203..126c5318 100644 --- a/roles/cloud-ec2/tasks/cloudformation.yml +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -6,7 +6,7 @@ stack_name: "{{ stack_name }}" state: "present" region: "{{ algo_region }}" - template: roles/cloud-ec2/files/stack.yml + template: roles/cloud-ec2/files/stack.yaml template_parameters: InstanceTypeParameter: "{{ cloud_providers.ec2.size }}" PublicSSHKeyParameter: "{{ lookup('file', SSH_keys.public) }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index ce6532be..44eebc9f 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -9,7 +9,7 @@ - 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'] }} + {%- elif _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('.', '-') }}" diff --git a/roles/cloud-gce/tasks/prompts.yml b/roles/cloud-gce/tasks/prompts.yml index b054cc9e..dfedecf0 100644 --- a/roles/cloud-gce/tasks/prompts.yml +++ b/roles/cloud-gce/tasks/prompts.yml @@ -63,5 +63,5 @@ - 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 ] }} + {%- elif _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-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml index 1c98c5ac..be3c3e15 100644 --- a/roles/cloud-lightsail/tasks/prompts.yml +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -57,5 +57,5 @@ - 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'] }} + {%- elif _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-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index b8c1181e..bce01579 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -1,7 +1,7 @@ --- - 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') == "" + when: lookup('env', 'OS_AUTH_URL')|length <= 0 - name: Build python virtual environment import_tasks: venv.yml diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 9ff5124f..3e9a39b8 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -84,7 +84,7 @@ organization: "{{ organization_id }}" name: "{{ algo_server_name }}" image: "{{ image_id }}" - commercial_type: "{{cloud_providers.scaleway.size }}" + commercial_type: "{{ cloud_providers.scaleway.size }}" enable_ipv6: true boot_type: local tags: diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml index 22c3f1aa..b481880d 100644 --- a/roles/cloud-scaleway/tasks/prompts.yml +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -30,5 +30,5 @@ 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'] }} + {%- elif _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 index a1dfa90e..03b23743 100644 --- a/roles/cloud-vultr/tasks/main.yml +++ b/roles/cloud-vultr/tasks/main.yml @@ -1,16 +1,16 @@ --- -- block: - - name: Include prompts - import_tasks: prompts.yml +- name: Include prompts + import_tasks: prompts.yml +- block: - name: Upload the SSH key - vr_ssh_key: + vultr_ssh_key: name: "{{ SSH_keys.comment }}" ssh_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" register: ssh_key - name: Creating a server - vr_server: + vultr_server: name: "{{ algo_server_name }}" hostname: "{{ algo_server_name }}" os: "{{ cloud_providers.vultr.os }}" diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index d037bf1a..1245b719 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -54,5 +54,5 @@ 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'] }} + {%- elif _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/handlers/main.yml b/roles/common/handlers/main.yml index 06f5080d..6b369267 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -20,8 +20,5 @@ ifconfig lo100 inet {{ local_service_ip }} netmask 255.255.255.255 && ifconfig lo100 inet6 FCAA::1/64; echo $? -- name: save iptables - shell: service netfilter-persistent save - - name: restart iptables service: name=netfilter-persistent state=restarted diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 6e79bfce..c064d7b3 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -1,26 +1,26 @@ --- - block: - name: Generate password for the CA key - local_action: - module: shell - openssl rand -hex 16 + command: 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())]))' + 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 + delegate_to: localhost - name: Define facts set_fact: p12_export_password: "{{ p12_password|default(p12_password_generated.stdout) }}" tags: update-users -- set_fact: +- name: Set facts + set_fact: CA_password: "{{ CA_password.stdout }}" IP_subject_alt_name: "{{ IP_subject_alt_name }}" @@ -31,5 +31,5 @@ - name: Check size of MTU set_fact: - reduce_mtu: "{% if reduce_mtu|int == 0 and ansible_default_ipv4['mtu']|int < 1500 %}{{ 1500 - ansible_default_ipv4['mtu']|int }}{% else %}{{ reduce_mtu|int }}{% endif %}" + reduce_mtu: "{{ 1500 - ansible_default_ipv4['mtu']|int if reduce_mtu|int == 0 and ansible_default_ipv4['mtu']|int < 1500 else reduce_mtu|int }}" tags: always diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 81362632..e0d54c16 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -17,7 +17,8 @@ - name: Gather additional facts import_tasks: facts.yml -- set_fact: +- name: Set OS specific facts + set_fact: config_prefix: "/usr/local/" strongswan_shell: /usr/sbin/nologin strongswan_home: /var/empty @@ -73,5 +74,6 @@ shell: > kldstat -n ipfw.ko || kldload ipfw ; sysctl net.inet.ip.fw.enable=0 && bash /etc/rc.firewall && sysctl net.inet.ip.fw.enable=1 + changed_when: false - meta: flush_handlers diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 4a400881..02330967 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,6 +2,7 @@ - name: Check the system raw: uname -a register: OS + changed_when: false tags: - update-users @@ -17,7 +18,7 @@ - name: Sysctl tuning sysctl: name="{{ item.item }}" value="{{ item.value }}" - when: item.item != "" + when: item.item with_items: - "{{ sysctl|default([]) }}" tags: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 08d37a33..114b4180 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -25,8 +25,7 @@ ignore_errors: true - name: Wait until SSH becomes ready... - local_action: - module: wait_for + wait_for: port: 22 host: "{{ inventory_hostname }}" search_regex: OpenSSH @@ -34,6 +33,7 @@ timeout: 320 when: reboot_required is defined and reboot_required.stdout == 'required' become: false + delegate_to: localhost when: algo_provider != "local" - name: Include unatteded upgrades configuration @@ -65,18 +65,21 @@ - meta: flush_handlers - name: Check apparmor support - shell: apparmor_status + command: apparmor_status ignore_errors: yes + changed_when: false register: apparmor_status -- set_fact: +- name: Set fact if apparmor enabled + set_fact: apparmor_enabled: true when: '"profiles are in enforce mode" in apparmor_status.stdout' - name: Gather additional facts import_tasks: facts.yml -- set_fact: +- name: Set OS specific facts + set_fact: tools: - git - screen diff --git a/roles/common/tasks/unattended-upgrades.yml b/roles/common/tasks/unattended-upgrades.yml index d0beae0a..582c1ebe 100644 --- a/roles/common/tasks/unattended-upgrades.yml +++ b/roles/common/tasks/unattended-upgrades.yml @@ -2,7 +2,7 @@ - name: Install unattended-upgrades apt: name: unattended-upgrades - state: latest + state: present - name: Configure unattended-upgrades template: diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index cd1a54a0..49cfa164 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -36,6 +36,7 @@ - name: Update adblock hosts command: /usr/local/sbin/adblock.sh + changed_when: false - meta: flush_handlers diff --git a/roles/dns_adblocking/tasks/ubuntu.yml b/roles/dns_adblocking/tasks/ubuntu.yml index ffc88876..33b62f23 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|default(false)|bool == true + when: apparmor_enabled|default(false)|bool notify: - restart dnsmasq - name: Ubuntu | Enforce the dnsmasq AppArmor policy - shell: aa-enforce usr.sbin.dnsmasq - when: apparmor_enabled|default(false)|bool == true + command: aa-enforce usr.sbin.dnsmasq + when: apparmor_enabled|default(false)|bool tags: ['apparmor'] - name: Ubuntu | Ensure that the dnsmasq service directory exist diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index f9cd7ee0..76f0e159 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -12,7 +12,7 @@ - name: Install dnscrypt-proxy apt: name: dnscrypt-proxy - state: latest + state: present update_cache: true - name: Configure unattended-upgrades @@ -37,7 +37,7 @@ command: aa-enforce usr.bin.dnscrypt-proxy changed_when: false tags: apparmor - when: apparmor_enabled|default(false)|bool == true + when: apparmor_enabled|default(false)|bool - name: Ubuntu | Ensure that the dnscrypt-proxy service directory exist file: diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml index a12b8807..9df53f4f 100644 --- a/roles/local/tasks/prompts.yml +++ b/roles/local/tasks/prompts.yml @@ -10,7 +10,7 @@ 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 }} + {%- elif _algo_server.user_input %}{{ _algo_server.user_input }} {%- else %}localhost{% endif %} - pause: @@ -26,7 +26,7 @@ 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 }} + {%- elif _algo_ssh_user.user_input %}{{ _algo_ssh_user.user_input }} {%- else %}root{% endif %} - pause: @@ -40,5 +40,5 @@ set_fact: IP_subject_alt_name: >- {% if endpoint is defined %}{{ endpoint }} - {%- elif _endpoint.user_input is defined and _endpoint.user_input != "" %}{{ _endpoint.user_input }} + {%- elif _endpoint.user_input %}{{ _endpoint.user_input }} {%- else %}{{ cloud_instance_ip }}{% endif %} diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 4ea46808..437fa47f 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -42,7 +42,7 @@ file: dest: "{{ ssh_tunnels_config_path }}" state: absent - when: keys_clean_all|bool == True + when: keys_clean_all|bool - name: Ensure the config directories exist file: diff --git a/roles/strongswan/handlers/main.yml b/roles/strongswan/handlers/main.yml index 6e7968f4..5f35003f 100644 --- a/roles/strongswan/handlers/main.yml +++ b/roles/strongswan/handlers/main.yml @@ -2,7 +2,7 @@ service: name=strongswan state=restarted - name: daemon-reload - shell: systemctl daemon-reload + systemd: daemon_reload=true - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/strongswan/tasks/client_configs.yml b/roles/strongswan/tasks/client_configs.yml index de4ff0f8..145f29da 100644 --- a/roles/strongswan/tasks/client_configs.yml +++ b/roles/strongswan/tasks/client_configs.yml @@ -1,8 +1,13 @@ --- - name: Register p12 PayloadContent - shell: cat private/{{ item }}.p12 | base64 + shell: | + set -o pipefail + cat private/{{ item }}.p12 | + base64 register: PayloadContent + changed_when: false args: + executable: bash chdir: "{{ ipsec_pki_path }}" with_items: "{{ users }}" diff --git a/roles/strongswan/tasks/ipsec_configuration.yml b/roles/strongswan/tasks/ipsec_configuration.yml index af19ed43..d75a93ca 100644 --- a/roles/strongswan/tasks/ipsec_configuration.yml +++ b/roles/strongswan/tasks/ipsec_configuration.yml @@ -32,8 +32,13 @@ - restart strongswan - name: Get loaded plugins - shell: > - find {{ config_prefix|default('/') }}etc/strongswan.d/charon/ -type f -name '*.conf' -exec basename {} \; | cut -f1 -d. + shell: | + set -o pipefail + find {{ config_prefix|default('/') }}etc/strongswan.d/charon/ -type f -name '*.conf' -exec basename {} \; | + cut -f1 -d. + changed_when: false + args: + executable: bash register: strongswan_plugins - name: Disable unneeded plugins diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index fd38611a..b813e61c 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -2,14 +2,19 @@ - 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 %}" + subjectAltName: >- + {{ subjectAltName_IP }} + {%- if ipv6_support -%},IP:{{ ansible_default_ipv6['address'] }}{%- endif -%} + {%- if domain and subjectAltName_DNS -%},DNS:{{ subjectAltName_DNS }}{%- endif -%} tags: always + - debug: var=subjectAltName + - name: Ensure the pki directory does not exist file: dest: "{{ ipsec_pki_path }}" state: absent - when: keys_clean_all|bool == True + when: keys_clean_all|bool - name: Ensure the pki directories exist file: @@ -182,7 +187,7 @@ awk '{print $5}' | sed 's/\/CN=//g' args: - chdir: "{{ ipsec_pki_path}}" + chdir: "{{ ipsec_pki_path }}" register: valid_certs - name: Revoke non-existing users @@ -228,11 +233,11 @@ - rereadcrls - name: Delete the CA key - local_action: - module: file + file: path: "{{ ipsec_pki_path }}/private/cakey.pem" state: absent become: false + delegate_to: localhost when: - ipsec_enabled - not algo_store_cakey diff --git a/roles/strongswan/tasks/ubuntu.yml b/roles/strongswan/tasks/ubuntu.yml index 41b58352..afaffa38 100644 --- a/roles/strongswan/tasks/ubuntu.yml +++ b/roles/strongswan/tasks/ubuntu.yml @@ -1,18 +1,19 @@ --- - -- set_fact: +- name: Set OS specific facts + set_fact: strongswan_additional_plugins: [] - name: Ubuntu | Install strongSwan apt: name: strongswan - state: latest + state: present update_cache: yes install_recommends: yes - name: Ubuntu | Enforcing ipsec with apparmor - shell: aa-enforce "{{ item }}" - when: apparmor_enabled|default(false)|bool == true + command: aa-enforce "{{ item }}" + when: apparmor_enabled|default(false)|bool + changed_when: false with_items: - /usr/lib/ipsec/charon - /usr/lib/ipsec/lookip diff --git a/roles/strongswan/templates/client_ipsec.conf.j2 b/roles/strongswan/templates/client_ipsec.conf.j2 index a45d8e3d..e44f9498 100644 --- a/roles/strongswan/templates/client_ipsec.conf.j2 +++ b/roles/strongswan/templates/client_ipsec.conf.j2 @@ -1,4 +1,4 @@ -conn ikev2-{{ IP_subject_alt_name }} +conn algovpn-{{ IP_subject_alt_name }} fragmentation=yes rekey=no dpdaction=clear @@ -16,7 +16,7 @@ conn ikev2-{{ IP_subject_alt_name }} right={{ IP_subject_alt_name }} rightid={{ IP_subject_alt_name }} - rightsubnet=0.0.0.0/0 + rightsubnet={{ rightsubnet | default('0.0.0.0/0') }} rightauth=pubkey leftsourceip=%config diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index e61c7780..4c7f17f3 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -5,7 +5,7 @@ wireguard_pki_path: "{{ wireguard_config_path }}/.pki/" wireguard_interface: wg0 keys_clean_all: false wireguard_dns_servers: >- - {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} + {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool %} {{ 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 %} diff --git a/roles/wireguard/tasks/freebsd.yml b/roles/wireguard/tasks/freebsd.yml index 63e7b48c..15bc1f5b 100644 --- a/roles/wireguard/tasks/freebsd.yml +++ b/roles/wireguard/tasks/freebsd.yml @@ -4,7 +4,8 @@ name: wireguard state: present -- set_fact: +- name: Set OS specific facts + set_fact: service_name: wireguard tags: always diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml index e13a015a..0deaef65 100644 --- a/roles/wireguard/tasks/keys.yml +++ b/roles/wireguard/tasks/keys.yml @@ -3,7 +3,7 @@ file: dest: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" state: absent - when: keys_clean_all|bool == True + when: keys_clean_all|bool with_items: - "{{ users }}" - "{{ IP_subject_alt_name }}" @@ -39,7 +39,10 @@ when: wg_genkey.changed - name: Generate public keys - shell: echo "{{ lookup('file', wireguard_pki_path + '/private/' + item) }}" | wg pubkey + shell: | + set -o pipefail + echo "{{ lookup('file', wireguard_pki_path + '/private/' + item) }}" | + wg pubkey register: wg_pubkey changed_when: false args: diff --git a/roles/wireguard/tasks/ubuntu.yml b/roles/wireguard/tasks/ubuntu.yml index c75b8a7b..603c065a 100644 --- a/roles/wireguard/tasks/ubuntu.yml +++ b/roles/wireguard/tasks/ubuntu.yml @@ -27,6 +27,7 @@ group: root mode: 0644 -- set_fact: +- name: Set OS specific facts + set_fact: service_name: "wg-quick@{{ wireguard_interface }}" tags: always diff --git a/server.yml b/server.yml index 349150cb..5c9b183c 100644 --- a/server.yml +++ b/server.yml @@ -38,8 +38,7 @@ - block: - name: Dump the configuration - local_action: - module: copy + copy: dest: "configs/{{ IP_subject_alt_name }}/.config.yml" content: | server: {{ 'localhost' if inventory_hostname == 'localhost' else inventory_hostname }} @@ -59,8 +58,12 @@ IP_subject_alt_name: {{ IP_subject_alt_name }} ipsec_enabled: {{ ipsec_enabled }} wireguard_enabled: {{ wireguard_enabled }} - {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} + {% if tests|default(false)|bool %} + ca_password: {{ CA_password }} + p12_password: {{ p12_export_password }} + {% endif %} become: false + delegate_to: localhost - name: Create a symlink if deploying to localhost file: diff --git a/tests/algo.conf b/tests/algo.conf new file mode 100644 index 00000000..a93d4206 --- /dev/null +++ b/tests/algo.conf @@ -0,0 +1 @@ +dhcp-host=algo,10.0.8.100 diff --git a/tests/cloud-init.sh b/tests/cloud-init.sh index 2d95c995..ca182cd5 100755 --- a/tests/cloud-init.sh +++ b/tests/cloud-init.sh @@ -7,8 +7,9 @@ export ONDEMAND_WIFI_EXCLUDE=test1,test2 export WINDOWS=true export STORE_CAKEY=true export LOCAL_DNS=true -export ENDPOINT=algo.lxc -export USERS=user1,user2 +export SSH_TUNNELING=true +export ENDPOINT=10.0.8.100 +export USERS=desktop,user1,user2 export EXTRA_VARS='install_headers=false tests=true apparmor_enabled=false' export ANSIBLE_EXTRA_ARGS='--skip-tags apparmor' export REPO_SLUG=${TRAVIS_PULL_REQUEST_SLUG:-${TRAVIS_REPO_SLUG:-trailofbits/algo}} diff --git a/tests/ipsec-client.sh b/tests/ipsec-client.sh new file mode 100755 index 00000000..d2c3f548 --- /dev/null +++ b/tests/ipsec-client.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +xmllint --noout ./configs/10.0.8.100/ipsec/apple/user1.mobileconfig + +ansible-playbook deploy_client.yml \ + -e client_ip=localhost \ + -e vpn_user=desktop \ + -e server_ip=10.0.8.100 \ + -e rightsubnet='172.16.0.1/32' + +ipsec up algovpn-10.0.8.100 + +ipsec statusall + +ipsec statusall | grep -w ^algovpn-10.0.8.100 | grep -w ESTABLISHED + +fping -t 900 -c3 -r3 -Dse 10.0.8.100 172.16.0.1 + +host google.com 172.16.0.1 + +echo "IPsec tests passed" diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index fc7d038e..99bf5c21 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,11 +2,11 @@ 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" +DEPLOY_ARGS="provider=local server=10.0.8.100 ssh_user=ubuntu endpoint=10.0.8.100 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" ] +if [ "${DEPLOY}" == "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" + 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 root: /root/.ssh && chmod -R 600 /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 index 0614e87b..ddc59d2a 100644 --- a/tests/lxd-bridge +++ b/tests/lxd-bridge @@ -1,7 +1,7 @@ USE_LXD_BRIDGE="true" LXD_BRIDGE="lxdbr0" UPDATE_PROFILE="true" -LXD_CONFILE="" +LXD_CONFILE="/etc/default/algo.conf" LXD_DOMAIN="lxd" LXD_IPV4_ADDR="10.0.8.1" LXD_IPV4_NETMASK="255.255.255.0" @@ -13,4 +13,4 @@ LXD_IPV6_ADDR="" LXD_IPV6_MASK="" LXD_IPV6_NETWORK="" LXD_IPV6_NAT="false" -LXD_IPV6_PROXY="true" +LXD_IPV6_PROXY="false" diff --git a/tests/pre-deploy.sh b/tests/pre-deploy.sh new file mode 100755 index 00000000..764eb673 --- /dev/null +++ b/tests/pre-deploy.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +sysctl net.ipv6.conf.all.disable_ipv6=0 + +tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." +cp -f tests/lxd-bridge /etc/default/lxd-bridge +cp -f tests/algo.conf /etc/default/algo.conf + +if [[ "$DEPLOY" == "cloud-init" ]]; then + bash tests/cloud-init.sh | lxc profile set default user.user-data - +else + echo -e "#cloud-config\nssh_authorized_keys:\n - $(cat ~/.ssh/id_rsa.pub)" | lxc profile set default user.user-data - +fi + +systemctl restart lxd-bridge.service lxd-containers.service lxd.service + +lxc profile set default raw.lxc lxc.aa_profile=unconfined +lxc profile set default security.privileged true +lxc profile show default +lxc launch ubuntu:18.04 algo + +ip addr + +until dig A +short algo.lxd @10.0.8.1 | grep -vE '^$' > /dev/null; do + sleep 3 +done + +lxc list diff --git a/tests/ssh-tunnel.sh b/tests/ssh-tunnel.sh new file mode 100755 index 00000000..39f6ecc0 --- /dev/null +++ b/tests/ssh-tunnel.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PASS=$(grep ^p12_password: configs/10.0.8.100/.config.yml | awk '{print $2}') + +ssh-keygen -p -P ${PASS} -N '' -f configs/10.0.8.100/ssh-tunnel/desktop.pem + +ssh -o StrictHostKeyChecking=no -D 127.0.0.1:1080 -f -q -C -N desktop@10.0.8.100 -i configs/10.0.8.100/ssh-tunnel/desktop.pem + +git config --global http.proxy 'socks5://127.0.0.1:1080' + +git clone -vv https://github.com/trailofbits/algo /tmp/ssh-tunnel-check + +echo "SSH tunneling tests passed" diff --git a/tests/update-users.sh b/tests/update-users.sh index c083994c..d957787d 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -2,11 +2,11 @@ set -ex -USER_ARGS="{ 'server': '$LXC_IP', 'users': ['user1', 'user2'] }" +USER_ARGS="{ 'server': '10.0.8.100', 'users': ['desktop', 'user1', 'user2'] }" -if [ "${LXC_NAME}" == "docker" ] +if [ "${DEPLOY}" == "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" + 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 root: /root/.ssh && chmod -R 600 /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 @@ -15,7 +15,7 @@ fi # IPsec # -if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/crl/phone.crt | grep CRL +if sudo openssl crl -inform pem -noout -text -in configs/10.0.8.100/ipsec/.pki/crl/phone.crt | grep CRL then echo "The CRL check passed" else @@ -23,7 +23,7 @@ if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/crl/ exit 1 fi -if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/certs/user1.crt | grep CN=user1 +if sudo openssl x509 -inform pem -noout -text -in configs/10.0.8.100/ipsec/.pki/certs/user1.crt | grep CN=user1 then echo "The new user exists" else @@ -35,7 +35,7 @@ fi # WireGuard # -if sudo test -f configs/$LXC_IP/wireguard/user1.conf +if sudo test -f configs/10.0.8.100/wireguard/user1.conf then echo "WireGuard: The new user exists" else @@ -47,7 +47,7 @@ fi # SSH tunneling # -if sudo test -f configs/$LXC_IP/ssh-tunnel/user1.ssh_config +if sudo test -f configs/10.0.8.100/ssh-tunnel/user1.ssh_config then echo "SSH Tunneling: The new user exists" else diff --git a/tests/wireguard-client.sh b/tests/wireguard-client.sh new file mode 100755 index 00000000..7dac2a32 --- /dev/null +++ b/tests/wireguard-client.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +crudini --set configs/10.0.8.100/wireguard/user1.conf Interface Table off + +wg-quick up configs/10.0.8.100/wireguard/user1.conf + +wg + +ifconfig user1 + +ip route add 172.16.0.1/32 dev user1 + +fping -t 900 -c3 -r3 -Dse 10.0.8.100 172.16.0.1 + +wg | grep "latest handshake" + +host google.com 172.16.0.1 + +echo "WireGuard tests passed" diff --git a/users.yml b/users.yml index 43473252..e33f04ec 100644 --- a/users.yml +++ b/users.yml @@ -7,7 +7,8 @@ tasks: - block: - - pause: + - name: Server address prompt + pause: prompt: "Enter the IP address of your server: (or use localhost for local installation)" register: _server when: server is undefined @@ -16,14 +17,15 @@ set_fact: algo_server: >- {% if server is defined %}{{ server }} - {%- elif _server.user_input is defined and _server.user_input != "" %}{{ _server.user_input }} + {%- elif _server.user_input %}{{ _server.user_input }} {%- else %}omit{% endif %} - name: Import host specific variables include_vars: file: "configs/{{ algo_server }}/.config.yml" - - pause: + - name: CA password prompt + pause: prompt: Enter the password for the private CA key echo: false register: _ca_password @@ -35,7 +37,7 @@ 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 }} + {%- elif _ca_password.user_input %}{{ _ca_password.user_input }} {%- else %}omit{% endif %} - name: Add the server to the vpn-host group From faa4b9a8da735b088e5263d095042dc2d721f8bb Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Sat, 27 Apr 2019 06:59:26 -0400 Subject: [PATCH 19/30] Automatically create cloud firewall rules for installs onto Vultr (#1400) * Update main.yml * Change module names and add IPv6 firewall rules Uses guide at https://www.renemoser.net/blog/2018/03/19/vultr-firewalling-with-ansible/ written by Rene Moser. * change vultr to vr * add ip_version to firewall rules * add SSH access rules * Use variable for wireguard port * update module names for ansible 2.7 * Fix trailing whitespaces * Try to fix trailing whitespaces again --- roles/cloud-vultr/tasks/main.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/roles/cloud-vultr/tasks/main.yml b/roles/cloud-vultr/tasks/main.yml index 03b23743..79b51df8 100644 --- a/roles/cloud-vultr/tasks/main.yml +++ b/roles/cloud-vultr/tasks/main.yml @@ -9,6 +9,27 @@ ssh_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" register: ssh_key + - name: Creating a firewall group + vultr_firewall_group: + name: "{{ algo_server_name }}" + + - name: Creating firewall rules + vultr_firewall_rule: + group: "{{ algo_server_name }}" + protocol: "{{ item.protocol }}" + port: "{{ item.port }}" + ip_version: "{{ item.ip }}" + cidr: "{{ item.cidr }}" + with_items: + - { protocol: tcp, port: 22, ip: v4, cidr: "0.0.0.0/0" } + - { protocol: tcp, port: 22, ip: v6, cidr: "::/0" } + - { protocol: udp, port: 500, ip: v4, cidr: "0.0.0.0/0" } + - { protocol: udp, port: 500, ip: v6, cidr: "::/0" } + - { protocol: udp, port: 4500, ip: v4, cidr: "0.0.0.0/0" } + - { protocol: udp, port: 4500, ip: v6, cidr: "::/0" } + - { protocol: udp, port: "{{ wireguard_port }}", ip: v4, cidr: "0.0.0.0/0" } + - { protocol: udp, port: "{{ wireguard_port }}", ip: v6, cidr: "::/0" } + - name: Creating a server vultr_server: name: "{{ algo_server_name }}" @@ -16,6 +37,7 @@ os: "{{ cloud_providers.vultr.os }}" plan: "{{ cloud_providers.vultr.size }}" region: "{{ algo_vultr_region }}" + firewall_group: "{{ algo_server_name }}" state: started tag: Environment:Algo ssh_key: "{{ ssh_key.vultr_ssh_key.name }}" From b526f738817dea239f43d84f225d9ef12e868e53 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 29 Apr 2019 04:40:20 -0400 Subject: [PATCH 20/30] Update troubleshooting.md - regions not available (#1414) Changes the "region not available" question to reflect Algo behavior since #976. Also addresses #1413. Adds a couple of quote marks to the Ubuntu error question, which disappeared for some reason. --- docs/troubleshooting.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 5bd0f84a..4901f496 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,7 +18,7 @@ First of all, check [this](https://github.com/trailofbits/algo#features) and ens * [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") + * [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) @@ -153,7 +153,9 @@ You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home ### The region you want is not available -You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. +Algo downloads the regions from the supported cloud providers (other than Microsoft Azure) listed in the first menu using APIs. If the region you want isn't available, the cloud provider has probably taken it offline for some reason. You should investigate further with your cloud provider. + +If there's a specific region you want to install to in Microsoft Azure that isn't available, you should [file an issue](https://github.com/trailofbits/algo/issues/new), give us information about what region is missing, and we'll add it. ### AWS: SSH permission denied with an ECDSA key @@ -269,7 +271,7 @@ sudo rm -rf /etc/wireguard/*.lock ``` Then immediately re-run `./algo`. -### Ubuntu Error: "unable to write 'random state" when generating CA password +### Ubuntu Error: "unable to write 'random state'" when generating CA password When running Algo, you received an error like this: From d6a1fb91bddfec25a690041c244ef3191fe9860f Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 1 May 2019 11:51:06 +0200 Subject: [PATCH 21/30] WIP: Facts definition fix (#1415) Facts definition fix --- input.yml | 18 +++++++++--------- playbooks/cloud-pre.yml | 4 +++- roles/local/tasks/prompts.yml | 28 ++++++++++++++-------------- users.yml | 8 ++++---- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/input.yml b/input.yml index d7d6aecd..fa4984b2 100644 --- a/input.yml +++ b/input.yml @@ -26,7 +26,7 @@ tasks: - block: - - name: Region prompt + - name: Cloud prompt pause: prompt: | What provider would you like to use? @@ -122,11 +122,11 @@ {{ _server | regex_replace('(?!\.)(\W|_)', '-') }} algo_ondemand_cellular: >- {% if ondemand_cellular is defined %}{{ ondemand_cellular | bool }} - {%- elif _ondemand_cellular.user_input %}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }} + {%- elif _ondemand_cellular.user_input is defined %}{{ 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 %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} + {%- elif _ondemand_wifi.user_input is defined %}{{ 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 | b64encode }} @@ -135,19 +135,19 @@ {%- else %}{{ '_null' | b64encode }}{% endif %} algo_local_dns: >- {% if local_dns is defined %}{{ local_dns | bool }} - {%- elif _local_dns.user_input %}{{ booleans_map[_local_dns.user_input] | default(defaults['local_dns']) }} + {%- elif _local_dns.user_input is defined %}{{ 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 %}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }} + {%- elif _ssh_tunneling.user_input is defined %}{{ 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 %}{{ booleans_map[_windows.user_input] | default(defaults['windows']) }} + {%- elif _windows.user_input is defined %}{{ 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 %}{{ booleans_map[_store_cakey.user_input] | default(defaults['store_cakey']) }} - {%- else %}false{% endif %} + {% if ipsec_enabled %}{%- if store_cakey is defined %}{{ store_cakey | bool }} + {%- elif _store_cakey.user_input is defined %}{{ booleans_map[_store_cakey.user_input] | default(defaults['store_cakey']) }} + {%- else %}false{% endif %}{% endif %} rescue: - include_tasks: playbooks/rescue.yml diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index f25dafa0..710702cd 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -4,12 +4,14 @@ shell: > ./algo-showenv.sh \ 'algo_provider "{{ algo_provider }}"' \ + {% if ipsec_enabled %} 'algo_ondemand_cellular "{{ algo_ondemand_cellular }}"' \ 'algo_ondemand_wifi "{{ algo_ondemand_wifi }}"' \ 'algo_ondemand_wifi_exclude "{{ algo_ondemand_wifi_exclude }}"' \ + 'algo_windows "{{ algo_windows }}"' \ + {% endif %} '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 diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml index 9df53f4f..fa085ecd 100644 --- a/roles/local/tasks/prompts.yml +++ b/roles/local/tasks/prompts.yml @@ -13,21 +13,21 @@ {%- elif _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" +- block: + - 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 -- name: Set the facts - set_fact: - ansible_ssh_user: >- - {% if ssh_user is defined %}{{ ssh_user }} - {%- elif _algo_ssh_user.user_input %}{{ _algo_ssh_user.user_input }} - {%- else %}root{% endif %} + - name: Set the facts + set_fact: + ansible_ssh_user: >- + {% if ssh_user is defined %}{{ ssh_user }} + {%- elif _algo_ssh_user.user_input %}{{ _algo_ssh_user.user_input }} + {%- else %}root{% endif %} + when: cloud_instance_ip != "localhost" - pause: prompt: | diff --git a/users.yml b/users.yml index e33f04ec..9d2b21e3 100644 --- a/users.yml +++ b/users.yml @@ -40,6 +40,10 @@ {%- elif _ca_password.user_input %}{{ _ca_password.user_input }} {%- else %}omit{% endif %} + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml + become: false + - name: Add the server to the vpn-host group add_host: name: "{{ algo_server }}" @@ -61,10 +65,6 @@ tasks: - block: - - name: Local pre-tasks - import_tasks: playbooks/cloud-pre.yml - become: false - - import_role: name: common From 6b33d09d9fc81ec2e2b1fdb4e47df676f400b2e2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 3 May 2019 09:55:45 +0200 Subject: [PATCH 22/30] Scaleway modules (#1410) * Scaleway modules * Update docs --- docs/deploy-from-ansible.md | 1 - library/scaleway_compute.py | 619 +++++++++++++++++++++ roles/cloud-scaleway/tasks/image_facts.yml | 10 - roles/cloud-scaleway/tasks/main.yml | 155 ++---- roles/cloud-scaleway/tasks/prompts.yml | 13 +- 5 files changed, 657 insertions(+), 141 deletions(-) create mode 100644 library/scaleway_compute.py delete mode 100644 roles/cloud-scaleway/tasks/image_facts.yml diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 9816f0bd..f062a04f 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -230,7 +230,6 @@ Possible options can be gathered via cli `aws lightsail get-regions` Required variables: - [scaleway_token](https://www.scaleway.com/docs/generate-an-api-token/) -- [scaleway_org](https://cloud.scaleway.com/#/billing) - region Possible regions: diff --git a/library/scaleway_compute.py b/library/scaleway_compute.py new file mode 100644 index 00000000..8aa6ce48 --- /dev/null +++ b/library/scaleway_compute.py @@ -0,0 +1,619 @@ +#!/usr/bin/python +# +# Scaleway Compute management module +# +# Copyright (C) 2018 Online SAS. +# https://www.scaleway.com +# +# 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: scaleway_compute +short_description: Scaleway compute management module +version_added: "2.6" +author: Remy Leone (@sieben) +description: + - "This module manages compute instances on Scaleway." +extends_documentation_fragment: scaleway + +options: + + enable_ipv6: + description: + - Enable public IPv6 connectivity on the instance + default: false + type: bool + + boot_type: + description: + - Boot method + default: bootscript + choices: + - bootscript + - local + + image: + description: + - Image identifier used to start the instance with + required: true + + name: + description: + - Name of the instance + + organization: + description: + - Organization identifier + required: true + + state: + description: + - Indicate desired state of the instance. + default: present + choices: + - present + - absent + - running + - restarted + - stopped + + tags: + description: + - List of tags to apply to the instance (5 max) + required: false + default: [] + + region: + description: + - Scaleway compute zone + required: true + choices: + - ams1 + - EMEA-NL-EVS + - par1 + - EMEA-FR-PAR1 + + commercial_type: + description: + - Commercial name of the compute node + required: true + choices: + - ARM64-2GB + - ARM64-4GB + - ARM64-8GB + - ARM64-16GB + - ARM64-32GB + - ARM64-64GB + - ARM64-128GB + - C1 + - C2S + - C2M + - C2L + - START1-XS + - START1-S + - START1-M + - START1-L + - X64-15GB + - X64-30GB + - X64-60GB + - X64-120GB + + wait: + description: + - Wait for the instance to reach its desired state before returning. + type: bool + default: 'no' + + wait_timeout: + description: + - Time to wait for the server to reach the expected state + required: false + default: 300 + + wait_sleep_time: + description: + - Time to wait before every attempt to check the state of the server + required: false + default: 3 +''' + +EXAMPLES = ''' +- name: Create a server + scaleway_compute: + name: foobar + state: present + image: 89ee4018-f8c3-4dc4-a6b5-bca14f985ebe + organization: 951df375-e094-4d26-97c1-ba548eeb9c42 + region: ams1 + commercial_type: VC1S + tags: + - test + - www + +- name: Destroy it right after + scaleway_compute: + name: foobar + state: absent + image: 89ee4018-f8c3-4dc4-a6b5-bca14f985ebe + organization: 951df375-e094-4d26-97c1-ba548eeb9c42 + region: ams1 + commercial_type: VC1S +''' + +RETURN = ''' +''' + +import datetime +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six.moves.urllib.parse import quote as urlquote +from ansible.module_utils.scaleway import SCALEWAY_LOCATION, scaleway_argument_spec, Scaleway + +SCALEWAY_COMMERCIAL_TYPES = [ + + # Virtual ARM64 compute instance + 'ARM64-2GB', + 'ARM64-4GB', + 'ARM64-8GB', + 'ARM64-16GB', + 'ARM64-32GB', + 'ARM64-64GB', + 'ARM64-128GB', + + # Baremetal + 'C1', # ARM64 (4 cores) - 2GB + 'C2S', # X86-64 (4 cores) - 8GB + 'C2M', # X86-64 (8 cores) - 16GB + 'C2L', # x86-64 (8 cores) - 32 GB + + # Virtual X86-64 compute instance + 'START1-XS', # Starter X86-64 (1 core) - 1GB - 25 GB NVMe + 'START1-S', # Starter X86-64 (2 cores) - 2GB - 50 GB NVMe + 'START1-M', # Starter X86-64 (4 cores) - 4GB - 100 GB NVMe + 'START1-L', # Starter X86-64 (8 cores) - 8GB - 200 GB NVMe + 'X64-15GB', + 'X64-30GB', + 'X64-60GB', + 'X64-120GB', +] + +SCALEWAY_SERVER_STATES = ( + 'stopped', + 'stopping', + 'starting', + 'running', + 'locked' +) + +SCALEWAY_TRANSITIONS_STATES = ( + "stopping", + "starting", + "pending" +) + + +def fetch_state(compute_api, server): + compute_api.module.debug("fetch_state of server: %s" % server["id"]) + response = compute_api.get(path="servers/%s" % server["id"]) + + if response.status_code == 404: + return "absent" + + if not response.ok: + msg = 'Error during state fetching: (%s) %s' % (response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + try: + compute_api.module.debug("Server %s in state: %s" % (server["id"], response.json["server"]["state"])) + return response.json["server"]["state"] + except KeyError: + compute_api.module.fail_json(msg="Could not fetch state in %s" % response.json) + + +def wait_to_complete_state_transition(compute_api, server): + wait = compute_api.module.params["wait"] + if not wait: + return + wait_timeout = compute_api.module.params["wait_timeout"] + wait_sleep_time = compute_api.module.params["wait_sleep_time"] + + start = datetime.datetime.utcnow() + end = start + datetime.timedelta(seconds=wait_timeout) + while datetime.datetime.utcnow() < end: + compute_api.module.debug("We are going to wait for the server to finish its transition") + if fetch_state(compute_api, server) not in SCALEWAY_TRANSITIONS_STATES: + compute_api.module.debug("It seems that the server is not in transition anymore.") + compute_api.module.debug("Server in state: %s" % fetch_state(compute_api, server)) + break + time.sleep(wait_sleep_time) + else: + compute_api.module.fail_json(msg="Server takes too long to finish its transition") + + +def create_server(compute_api, server): + compute_api.module.debug("Starting a create_server") + target_server = None + response = compute_api.post(path="servers", + data={"enable_ipv6": server["enable_ipv6"], + "boot_type": server["boot_type"], + "tags": server["tags"], + "commercial_type": server["commercial_type"], + "image": server["image"], + "name": server["name"], + "organization": server["organization"]}) + + if not response.ok: + msg = 'Error during server creation: (%s) %s' % (response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + try: + target_server = response.json["server"] + except KeyError: + compute_api.module.fail_json(msg="Error in getting the server information from: %s" % response.json) + + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + + return target_server + + +def restart_server(compute_api, server): + return perform_action(compute_api=compute_api, server=server, action="reboot") + + +def stop_server(compute_api, server): + return perform_action(compute_api=compute_api, server=server, action="poweroff") + + +def start_server(compute_api, server): + return perform_action(compute_api=compute_api, server=server, action="poweron") + + +def perform_action(compute_api, server, action): + response = compute_api.post(path="servers/%s/action" % server["id"], + data={"action": action}) + if not response.ok: + msg = 'Error during server %s: (%s) %s' % (action, response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + wait_to_complete_state_transition(compute_api=compute_api, server=server) + + return response + + +def remove_server(compute_api, server): + compute_api.module.debug("Starting remove server strategy") + response = compute_api.delete(path="servers/%s" % server["id"]) + if not response.ok: + msg = 'Error during server deletion: (%s) %s' % (response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + wait_to_complete_state_transition(compute_api=compute_api, server=server) + + return response + + +def present_strategy(compute_api, wished_server): + compute_api.module.debug("Starting present strategy") + changed = False + query_results = find(compute_api=compute_api, wished_server=wished_server, per_page=1) + + if not query_results: + changed = True + if compute_api.module.check_mode: + return changed, {"status": "A server would be created."} + + target_server = create_server(compute_api=compute_api, server=wished_server) + else: + target_server = query_results[0] + + if server_attributes_should_be_changed(compute_api=compute_api, target_server=target_server, + wished_server=wished_server): + changed = True + + if compute_api.module.check_mode: + return changed, {"status": "Server %s attributes would be changed." % target_server["id"]} + + server_change_attributes(compute_api=compute_api, target_server=target_server, wished_server=wished_server) + + return changed, target_server + + +def absent_strategy(compute_api, wished_server): + compute_api.module.debug("Starting absent strategy") + changed = False + target_server = None + query_results = find(compute_api=compute_api, wished_server=wished_server, per_page=1) + + if not query_results: + return changed, {"status": "Server already absent."} + else: + target_server = query_results[0] + + changed = True + + if compute_api.module.check_mode: + return changed, {"status": "Server %s would be made absent." % target_server["id"]} + + # A server MUST be stopped to be deleted. + while not fetch_state(compute_api=compute_api, server=target_server) == "stopped": + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + response = stop_server(compute_api=compute_api, server=target_server) + + if not response.ok: + err_msg = 'Error while stopping a server before removing it [{0}: {1}]'.format(response.status_code, + response.json) + compute_api.module.fail_json(msg=err_msg) + + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + + response = remove_server(compute_api=compute_api, server=target_server) + + if not response.ok: + err_msg = 'Error while removing server [{0}: {1}]'.format(response.status_code, response.json) + compute_api.module.fail_json(msg=err_msg) + + return changed, {"status": "Server %s deleted" % target_server["id"]} + + +def running_strategy(compute_api, wished_server): + compute_api.module.debug("Starting running strategy") + changed = False + query_results = find(compute_api=compute_api, wished_server=wished_server, per_page=1) + + if not query_results: + changed = True + if compute_api.module.check_mode: + return changed, {"status": "A server would be created before being run."} + + target_server = create_server(compute_api=compute_api, server=wished_server) + else: + target_server = query_results[0] + + if server_attributes_should_be_changed(compute_api=compute_api, target_server=target_server, + wished_server=wished_server): + changed = True + + if compute_api.module.check_mode: + return changed, {"status": "Server %s attributes would be changed before running it." % target_server["id"]} + + server_change_attributes(compute_api=compute_api, target_server=target_server, wished_server=wished_server) + + current_state = fetch_state(compute_api=compute_api, server=target_server) + if current_state not in ("running", "starting"): + compute_api.module.debug("running_strategy: Server in state: %s" % current_state) + changed = True + + if compute_api.module.check_mode: + return changed, {"status": "Server %s attributes would be changed." % target_server["id"]} + + response = start_server(compute_api=compute_api, server=target_server) + if not response.ok: + msg = 'Error while running server [{0}: {1}]'.format(response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + return changed, target_server + + +def stop_strategy(compute_api, wished_server): + compute_api.module.debug("Starting stop strategy") + query_results = find(compute_api=compute_api, wished_server=wished_server, per_page=1) + + changed = False + + if not query_results: + + if compute_api.module.check_mode: + return changed, {"status": "A server would be created before being stopped."} + + target_server = create_server(compute_api=compute_api, server=wished_server) + changed = True + else: + target_server = query_results[0] + + compute_api.module.debug("stop_strategy: Servers are found.") + + if server_attributes_should_be_changed(compute_api=compute_api, target_server=target_server, + wished_server=wished_server): + changed = True + + if compute_api.module.check_mode: + return changed, { + "status": "Server %s attributes would be changed before stopping it." % target_server["id"]} + + server_change_attributes(compute_api=compute_api, target_server=target_server, wished_server=wished_server) + + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + + current_state = fetch_state(compute_api=compute_api, server=target_server) + if current_state not in ("stopped",): + compute_api.module.debug("stop_strategy: Server in state: %s" % current_state) + + changed = True + + if compute_api.module.check_mode: + return changed, {"status": "Server %s would be stopped." % target_server["id"]} + + response = stop_server(compute_api=compute_api, server=target_server) + compute_api.module.debug(response.json) + compute_api.module.debug(response.ok) + + if not response.ok: + msg = 'Error while stopping server [{0}: {1}]'.format(response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + return changed, target_server + + +def restart_strategy(compute_api, wished_server): + compute_api.module.debug("Starting restart strategy") + changed = False + query_results = find(compute_api=compute_api, wished_server=wished_server, per_page=1) + + if not query_results: + changed = True + if compute_api.module.check_mode: + return changed, {"status": "A server would be created before being rebooted."} + + target_server = create_server(compute_api=compute_api, server=wished_server) + else: + target_server = query_results[0] + + if server_attributes_should_be_changed(compute_api=compute_api, + target_server=target_server, + wished_server=wished_server): + changed = True + + if compute_api.module.check_mode: + return changed, { + "status": "Server %s attributes would be changed before rebooting it." % target_server["id"]} + + server_change_attributes(compute_api=compute_api, target_server=target_server, wished_server=wished_server) + + changed = True + if compute_api.module.check_mode: + return changed, {"status": "Server %s would be rebooted." % target_server["id"]} + + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + + if fetch_state(compute_api=compute_api, server=target_server) in ("running",): + response = restart_server(compute_api=compute_api, server=target_server) + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + if not response.ok: + msg = 'Error while restarting server that was running [{0}: {1}].'.format(response.status_code, + response.json) + compute_api.module.fail_json(msg=msg) + + if fetch_state(compute_api=compute_api, server=target_server) in ("stopped",): + response = restart_server(compute_api=compute_api, server=target_server) + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + if not response.ok: + msg = 'Error while restarting server that was stopped [{0}: {1}].'.format(response.status_code, + response.json) + compute_api.module.fail_json(msg=msg) + + return changed, target_server + + +state_strategy = { + "present": present_strategy, + "restarted": restart_strategy, + "stopped": stop_strategy, + "running": running_strategy, + "absent": absent_strategy +} + + +def find(compute_api, wished_server, per_page=1): + compute_api.module.debug("Getting inside find") + # Only the name attribute is accepted in the Compute query API + url = 'servers?name=%s&per_page=%d' % (urlquote(wished_server["name"]), per_page) + response = compute_api.get(url) + + if not response.ok: + msg = 'Error during server search: (%s) %s' % (response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + search_results = response.json["servers"] + + return search_results + + +PATCH_MUTABLE_SERVER_ATTRIBUTES = ( + "ipv6", + "tags", + "name", + "dynamic_ip_required", +) + + +def server_attributes_should_be_changed(compute_api, target_server, wished_server): + compute_api.module.debug("Checking if server attributes should be changed") + compute_api.module.debug("Current Server: %s" % target_server) + compute_api.module.debug("Wished Server: %s" % wished_server) + debug_dict = dict((x, (target_server[x], wished_server[x])) + for x in PATCH_MUTABLE_SERVER_ATTRIBUTES + if x in target_server and x in wished_server) + compute_api.module.debug("Debug dict %s" % debug_dict) + + try: + return any([target_server[x] != wished_server[x] + for x in PATCH_MUTABLE_SERVER_ATTRIBUTES + if x in target_server and x in wished_server]) + except AttributeError: + compute_api.module.fail_json(msg="Error while checking if attributes should be changed") + + +def server_change_attributes(compute_api, target_server, wished_server): + compute_api.module.debug("Starting patching server attributes") + patch_payload = dict((x, wished_server[x]) + for x in PATCH_MUTABLE_SERVER_ATTRIBUTES + if x in wished_server and x in target_server) + response = compute_api.patch(path="servers/%s" % target_server["id"], + data=patch_payload) + if not response.ok: + msg = 'Error during server attributes patching: (%s) %s' % (response.status_code, response.json) + compute_api.module.fail_json(msg=msg) + + wait_to_complete_state_transition(compute_api=compute_api, server=target_server) + + return response + + +def core(module): + region = module.params["region"] + wished_server = { + "state": module.params["state"], + "image": module.params["image"], + "name": module.params["name"], + "commercial_type": module.params["commercial_type"], + "enable_ipv6": module.params["enable_ipv6"], + "boot_type": module.params["boot_type"], + "tags": module.params["tags"], + "organization": module.params["organization"] + } + module.params['api_url'] = SCALEWAY_LOCATION[region]["api_endpoint"] + + compute_api = Scaleway(module=module) + + changed, summary = state_strategy[wished_server["state"]](compute_api=compute_api, wished_server=wished_server) + module.exit_json(changed=changed, msg=summary) + + +def main(): + argument_spec = scaleway_argument_spec() + argument_spec.update(dict( + image=dict(required=True), + name=dict(), + region=dict(required=True, choices=SCALEWAY_LOCATION.keys()), + commercial_type=dict(required=True, choices=SCALEWAY_COMMERCIAL_TYPES), + enable_ipv6=dict(default=False, type="bool"), + boot_type=dict(default="bootscript"), + state=dict(choices=state_strategy.keys(), default='present'), + tags=dict(type="list", default=[]), + organization=dict(required=True), + wait=dict(type="bool", default=False), + wait_timeout=dict(type="int", default=300), + wait_sleep_time=dict(type="int", default=3), + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/roles/cloud-scaleway/tasks/image_facts.yml b/roles/cloud-scaleway/tasks/image_facts.yml deleted file mode 100644 index 41269845..00000000 --- a/roles/cloud-scaleway/tasks/image_facts.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- 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 index 3e9a39b8..d7aae795 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -1,133 +1,46 @@ - 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 +- block: + - name: Gather Scaleway organizations facts + scaleway_organization_facts: - 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 }} + scaleway_image_facts: + region: "{{ algo_region }}" - - name: Set image id as a fact - include_tasks: image_facts.yml - with_items: "{{ scaleway_images['results'] }}" - loop_control: - loop_var: outer_item + - name: Set cloud specific facts + set_fact: + organization_id: "{{ scaleway_organization_facts[0]['id'] }}" + images: >- + [{% for i in scaleway_image_facts -%} + {% if i.name == cloud_providers.scaleway.image and + i.arch == cloud_providers.scaleway.arch -%} + '{{ i.id }}'{% if not loop.last %},{% endif %} + {%- endif -%} + {%- endfor -%}] - 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 + scaleway_compute: + name: "{{ algo_server_name }}" + enable_ipv6: true + boot_type: local + state: running + image: "{{ images[0] }}" + organization: "{{ organization_id }}" + region: "{{ algo_region }}" + commercial_type: "{{ cloud_providers.scaleway.size }}" + wait: true + tags: + - Environment:Algo + - AUTHORIZED_KEY={{ lookup('file', SSH_keys.public)|regex_replace(' ', '_') }} 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 + until: algo_instance.msg.public_ip + retries: 3 + delay: 3 + environment: + SCW_TOKEN: "{{ algo_scaleway_token }}" - set_fact: - cloud_instance_ip: "{{ algo_instance['json']['server']['public_ip']['address'] }}" + cloud_instance_ip: "{{ algo_instance.msg.public_ip.address }}" ansible_ssh_user: root diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml index b481880d..be4743fb 100644 --- a/roles/cloud-scaleway/tasks/prompts.yml +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -4,13 +4,9 @@ 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 + when: + - scaleway_token is undefined + - lookup('env','SCW_TOKEN')|length <= 0 - pause: prompt: | @@ -26,8 +22,7 @@ - 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_scaleway_token: "{{ scaleway_token | default(_scaleway_token.user_input) | default(lookup('env','SCW_TOKEN'), true) }}" algo_region: >- {% if region is defined %}{{ region }} {%- elif _algo_region.user_input %}{{ scaleway_regions[_algo_region.user_input | int -1 ]['alias'] }} From 826a2c5036b1f7d407c4c4faba5827a8cc8c6fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Sun, 12 May 2019 11:21:55 +0200 Subject: [PATCH 23/30] Add documentation about Scaleway credentials (#1419) --- README.md | 1 + docs/cloud-scaleway.md | 9 +++++++++ roles/cloud-scaleway/tasks/prompts.yml | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/cloud-scaleway.md diff --git a/README.md b/README.md index ef6bfbfc..327bdf1f 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,7 @@ After this process completes, the Algo VPN server will contain only the users li - Configure [Azure](docs/cloud-azure.md) - Configure [DigitalOcean](docs/cloud-do.md) - Configure [Google Cloud Platform](docs/cloud-gce.md) + - Configure [Scaleway](docs/cloud-scaleway.md) - Configure [Vultr](docs/cloud-vultr.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server diff --git a/docs/cloud-scaleway.md b/docs/cloud-scaleway.md new file mode 100644 index 00000000..7e6a02ae --- /dev/null +++ b/docs/cloud-scaleway.md @@ -0,0 +1,9 @@ +### Configuration file + +Algo requires an API key from your Scaleway account to create a server. +The API key is generated by going to your Scaleway credentials at [https://console.scaleway.com/account/credentials](https://console.scaleway.com/account/credentials), and then selecting "Generate new token" on the right side of the box labeled "API Tokens". + +Enter this token when Algo prompts you for the `auth token`. +This information will be pass as the `algo_scaleway_token` variable when asked for in the Algo prompt. + +Your organization ID is also on this page: https://console.scaleway.com/account/credentials diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml index be4743fb..7e371d21 100644 --- a/roles/cloud-scaleway/tasks/prompts.yml +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -1,7 +1,7 @@ --- - pause: prompt: | - Enter your auth token (https://www.scaleway.com/docs/generate-an-api-token/) + Enter your auth token (https://trailofbits.github.io/algo/cloud-scaleway.html) echo: false register: _scaleway_token when: From bcf2008b8d7c1cd34c5871356e0af2197bf88e19 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 13 May 2019 03:33:22 -0400 Subject: [PATCH 24/30] Update deploy-from-script-or-cloud-init-to-localhost.md (#1433) I was going to add this onto the existing PR for docs update, but it turned out to be a little more involved and require some testing of actual deployment. --- ...-from-script-or-cloud-init-to-localhost.md | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/deploy-from-script-or-cloud-init-to-localhost.md b/docs/deploy-from-script-or-cloud-init-to-localhost.md index 6070562c..2e4308ab 100644 --- a/docs/deploy-from-script-or-cloud-init-to-localhost.md +++ b/docs/deploy-from-script-or-cloud-init-to-localhost.md @@ -1,33 +1,33 @@ # Deploy from script or cloud-init -You can use `install.sh` to prepare the environment and deploy AlgoVPN on the local Ubuntu server in one shot using cloud-init or run the script directly on the server. The script doesn't configure any parameters in your cloud, so it's on your own to configure related [firewall rules](/docs/firewalls.md), a floating ip address and other resources you may need. +You can use `install.sh` to prepare the environment and deploy AlgoVPN on the local Ubuntu server in one shot using cloud-init, or run the script directly on the server after it's been created. The script doesn't configure any parameters in your cloud, so it's on your own to configure related [firewall rules](/docs/firewalls.md), a floating ip address and other resources you may need. The output of the install script (including the p12 and CA passwords) and user config files will be installed into the `/opt/algo` directory. ## Cloud init deployment -You can copy-paste the snippet below to the user data (cloud-init or startup script) field when creating a new server. For now it is only possible for [DigitalOcean](https://www.digitalocean.com/docs/droplets/resources/metadata/), Amazon [EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) and [Lightsail](https://lightsail.aws.amazon.com/ls/docs/en/articles/lightsail-how-to-configure-server-additional-data-shell-script), [Google Cloud](https://cloud.google.com/compute/docs/startupscript) and [Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/using-cloud-init). +You can copy-paste the snippet below to the user data (cloud-init or startup script) field when creating a new server. For now it is only possible for [DigitalOcean](https://www.digitalocean.com/docs/droplets/resources/metadata/), Amazon [EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html) and [Lightsail](https://lightsail.aws.amazon.com/ls/docs/en/articles/lightsail-how-to-configure-server-additional-data-shell-script), [Google Cloud](https://cloud.google.com/compute/docs/startupscript), [Azure](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/using-cloud-init) and [Vultr](https://my.vultr.com/startup/), although Vultr doesn't [officially support cloud-init](https://www.vultr.com/docs/getting-started-with-cloud-init). ``` #!/bin/bash curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x ``` -The command will prepare the environment and install AlgoVPN with default parameters. If you want to modify the behaviour you may define additional variables. +The command will prepare the environment and install AlgoVPN with the default parameters below. If you want to modify the behavior you may define additional variables. ## Variables -`METHOD` - which method of the deployment to use. Possible values are local and cloud. Default: cloud. The cloud method is intended to use in cloud-init deployments only. If you are not using cloud-init to deploy the server you have to use the local method -`ONDEMAND_CELLULAR` - "Connect On Demand" when connected to cellular networks. Bollean. Default: false -`ONDEMAND_WIFI` - "Connect On Demand" when connected to Wi-Fi. Default: false -`ONDEMAND_WIFI_EXCLUDE` - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand". Comma-separated list. -`WINDOWS` - To support Windows 10 or Linux Desktop clients. Default: false -`STORE_CAKEY` - To retain the CA key. (required to add users in the future, but less secure). Default: false. -`LOCAL_DNS` - To install an ad blocking DNS resolver. Default: false. -`SSH_TUNNELING` - Enable SSH tunneling for each user. Default: false -`ENDPOINT` - The public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate). It will be gathered automatically for DigitalOcean, AWS, GCE or Azure if the `METHOD` is cloud. Otherwise you need to define this variable according to your public IP address. -`USERS` - list of VPN users. Comma-separated list. +`METHOD` - which method of the deployment to use. Possible values are local and cloud. Default: cloud. The cloud method is intended to use in cloud-init deployments only. If you are not using cloud-init to deploy the server you have to use the local method. +`ONDEMAND_CELLULAR` - "Connect On Demand" when connected to cellular networks. Boolean. Default: false. +`ONDEMAND_WIFI` - "Connect On Demand" when connected to Wi-Fi. Default: false. +`ONDEMAND_WIFI_EXCLUDE` - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand". Comma-separated list. +`WINDOWS` - To support Windows 10 or Linux Desktop clients. Default: false. +`STORE_CAKEY` - To retain the CA key. (required to add users in the future, but less secure). Default: false. +`LOCAL_DNS` - To install an ad blocking DNS resolver. Default: false. +`SSH_TUNNELING` - Enable SSH tunneling for each user. Default: false. +`ENDPOINT` - The public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate). It will be gathered automatically for DigitalOcean, AWS, GCE, Azure or Vultr if the `METHOD` is cloud. Otherwise you need to define this variable according to your public IP address. +`USERS` - list of VPN users. Comma-separated list. Default: user1. `REPO_SLUG` - Owner and repository that used to get the installation scripts from. Default: trailofbits/algo. -`REPO_BRANCH` - Branch for `REPO_SLUG`. Default: master. -`EXTRA_VARS` - Additional extra variables. -`ANSIBLE_EXTRA_ARGS` - Any available ansible parameters. ie: `--skip-tags apparmor`. +`REPO_BRANCH` - Branch for `REPO_SLUG`. Default: master. +`EXTRA_VARS` - Additional extra variables. +`ANSIBLE_EXTRA_ARGS` - Any available ansible parameters. ie: `--skip-tags apparmor`. ## Examples @@ -46,6 +46,7 @@ curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | s ``` export METHOD=local export ONDEMAND_CELLULAR=true +export ENDPOINT=[your server's IP here] curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x ``` @@ -54,5 +55,5 @@ curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | s The arguments order as per [variables](#variables) above ``` -curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x -s local true false _null true true true true myvpnserver.com +curl -s https://raw.githubusercontent.com/trailofbits/algo/master/install.sh | sudo -E bash -x -s local true false _null true true true true myvpnserver.com phone,laptop,desktop ``` From 515494e90eb73d3ceb47d72a6a7669c865517445 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 15 May 2019 19:33:07 +0200 Subject: [PATCH 25/30] Update config.cfg --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index a652749f..1e12c8d0 100644 --- a/config.cfg +++ b/config.cfg @@ -142,13 +142,13 @@ cloud_providers: digitalocean: size: s-1vcpu-1gb image: "ubuntu-18-04-x64" + ec2: # 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: From 3ce92f9feecbd053924163fd73735595ee0ee5b6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 16 May 2019 07:17:00 +0200 Subject: [PATCH 26/30] Update deploy-from-ansible.md Closes #1434 --- docs/deploy-from-ansible.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index f062a04f..8d2180d7 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -114,7 +114,8 @@ Additional variables: "ec2:DescribeImages", "ec2:DescribeKeyPairs", "ec2:DescribeRegions", - "ec2:ImportKeyPair" + "ec2:ImportKeyPair", + "ec2:CopyImage" ], "Resource": [ "*" From de88211fb95fd9b73b366d864e8d5944c0e3bf88 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 16 May 2019 13:28:59 +0200 Subject: [PATCH 27/30] Update config.cfg Closes #1435 --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 1e12c8d0..c5393ac3 100644 --- a/config.cfg +++ b/config.cfg @@ -8,7 +8,7 @@ users: - laptop - desktop -# NOTE: You must "escape" any usernames with leading 0's, like "000dan" +# NOTE: You must "escape" any usernames with leading 0's or containing only numbers, like "000dan" or "123" ### Advanced users only below this line ### From 638a355196edcd4b13095aaef2f7d470a6e4c069 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Thu, 16 May 2019 08:04:57 -0400 Subject: [PATCH 28/30] Update config.cfg (#1436) * Update config.cfg Reflects fixes in #1434 and #1435. * Update config.cfg --- config.cfg | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/config.cfg b/config.cfg index c5393ac3..2d2f8577 100644 --- a/config.cfg +++ b/config.cfg @@ -1,15 +1,14 @@ --- -# This is the list of user to generate. +# This is the list of users to generate. # Every device must have a unique username. # You can generate up to 250 users at one time. +# Usernames with leading 0's or containing only numbers should be escaped in double quotes, e.g. "000dan" or "123". users: - phone - laptop - desktop -# NOTE: You must "escape" any usernames with leading 0's or containing only numbers, like "000dan" or "123" - ### Advanced users only below this line ### # If True re-init all existing certificates. Boolean @@ -145,10 +144,6 @@ cloud_providers: ec2: # 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 encrypted: false size: t2.micro image: From 38ebe4893dcee0c48a34c45d8bd2e73ccfba1511 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Thu, 16 May 2019 15:01:01 -0400 Subject: [PATCH 29/30] Update docs (#1430) * Point additional docs to index.md * Update index.md Moves existing links from readme.md over to update this separate (previously out-of-date, redundant) page. * Update documented Ansible roles * Fix broken links in index.md * Complete index.md As a general rule all docs should be linked to from the index file. No? * Update SSH access instructions * Clarify SSH access instructions * Delete setup-roles.md * Update deploy-from-ansible.md Change header, insert text from setup-roles.md * Remove link to setup-roles from index.md * Fix typos * Update deploy-from-ansible.md Document other `--skip-tags` options, as well as examples for Vultr and Scaleway variables. * Update deploy-from-ansible.md Added region examples for AWS and Lightsail. Happy to add other examples if people have experience with other providers. --- README.md | 50 ++++++++++--------------------------- docs/deploy-from-ansible.md | 49 ++++++++++++++++++++---------------- docs/index.md | 16 +++++++++--- docs/setup-roles.md | 28 --------------------- 4 files changed, 53 insertions(+), 90 deletions(-) delete mode 100644 docs/setup-roles.md diff --git a/README.md b/README.md index 327bdf1f..0f094930 100644 --- a/README.md +++ b/README.md @@ -72,15 +72,15 @@ That's it! You will get the message below when the server deployment process com 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. ``` - "\"#----------------------------------------------------------------------#\"", - "\"# Congratulations! #\"", - "\"# Your Algo server is running. #\"", - "\"# 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 172.16.0.1 #\"", - "\"# The p12 and SSH keys password is XXXXXXXX #\"", - "\"#----------------------------------------------------------------------#\"", + "# Congratulations! #" + "# Your Algo server is running. #" + "# 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 172.16.0.1 #" + "# The p12 and SSH keys password for new users is XXXXXXXX #" + "# The CA key password is XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX #" + "# Shell access: ssh -i configs/algo.pem root@xxx.xxx.xx.xx #" ``` ## Configure the VPN Clients @@ -166,16 +166,14 @@ Use the example command below to start an SSH tunnel by replacing `user` and `ip ## SSH into Algo Server -To SSH into the Algo server for administrative purposes you can use the example command below by replacing `ip` with your own: +Your Algo server is configured for key-only SSH access for administrative purposes. Open the Terminal app, `cd` into the `algo-master` directory where you originally downloaded Algo, and then use the command listed on the success message: - `ssh root@ip -i ~/.ssh/algo.pem` + `ssh -i configs/algo.pem user@ip` -If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. +where `user` is either `root` or `ubuntu` as listed on the success message, and `ip` is the IP address of your Algo server. If you find yourself regularly logging into the server then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. `ssh-add ~/.ssh/algo > /dev/null 2>&1` -Note the admin username is `ubuntu` instead of `root` on providers other than Digital Ocean. - ## Adding or Removing Users If you chose to save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. @@ -187,29 +185,7 @@ If you chose to save the CA certificate during the deploy process, then Algo's o After this process completes, the Algo VPN server will contain only the users listed in the `config.cfg` file. ## Additional Documentation - -* Setup instructions - - Documentation for available [Ansible roles](docs/setup-roles.md) - - Deploy from [Fedora Workstation (26)](docs/deploy-from-fedora-workstation.md) - - Deploy from [RedHat/CentOS 6.x](docs/deploy-from-redhat-centos6.md) - - Deploy from [Windows](docs/deploy-from-windows.md) - - Deploy from [Ansible](docs/deploy-from-ansible.md) directly -* 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) - - Setup Apple devices to use [IPSEC](docs/client-apple-ipsec.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) - - Configure [Scaleway](docs/cloud-scaleway.md) - - Configure [Vultr](docs/cloud-vultr.md) -* Advanced Deployment - - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.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) +* [Deployment instructions, cloud provider setup instructions, and further client setup instructions available here.](docs/index.md) * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 8d2180d7..f26de00d 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -1,10 +1,10 @@ -# Scripted Deployment +# Deployment from Ansible Before you begin, make sure you have installed all the dependencies necessary for your operating system as described in the [README](../README.md). You can deploy Algo non-interactively by running the Ansible playbooks directly with `ansible-playbook`. -`ansible-playbook` accepts "tags" via the `-t` or `TAGS` options. You can pass tags as a list of comma separated values. Ansible will only run plays (install roles) with the specified tags. +`ansible-playbook` accepts "tags" via the `-t` or `TAGS` options. You can pass tags as a list of comma separated values. Ansible will only run plays (install roles) with the specified tags. You can also use the `--skip-tags` option to skip certain parts of the install, such as `iptables` (overwrite iptables rules), `ipsec` (install strongSwan), `wireguard` (install Wireguard). `ansible-playbook` accepts variables via the `-e` or `--extra-vars` option. You can pass variables as space separated key=value pairs. Algo requires certain variables that are listed below. @@ -23,25 +23,25 @@ ansible-playbook main.yml -e "provider=digitalocean do_token=token" ``` -See below for more information about providers and extra variables +See below for more information about variables and roles. ### 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_cellular` (Optional) VPN On Demand when connected to cellular networks with IPsec. Default: false +- `ondemand_wifi` - (Optional. See `ondemand_wifi_exclude`) VPN On Demand when connected to WiFi networks with IPsec. 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 clients, 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 +If any of the above variables are unspecified, ansible will ask the user to input them. ### Ansible roles -Roles can be activated by specifying an extra variable `provider` +Cloud roles can be activated by specifying an extra variable `provider`. Cloud roles: @@ -55,13 +55,25 @@ Cloud roles: Server roles: -- role: vpn +- role: strongswan + * Installs [strongSwan](https://www.strongswan.org/) + * Enables AppArmor, limits CPU and memory access, and drops user privileges + * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user + * Bundles the appropriate certificates into Apple mobileconfig profiles and Powershell scripts for each user - role: dns_adblocking + * Installs 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 - role: dns_encryption + * Installs [dnscrypt-proxy](https://github.com/jedisct1/dnscrypt-proxy) + * Constrains dnscrypt-proxy with AppArmor and cgroups CPU and memory limitations - role: ssh_tunneling + * Adds a restricted `algo` group with no shell access and limited SSH forwarding options + * Creates one limited, local account and an SSH public key for each user - role: wireguard + * Installs a [Wireguard](https://www.wireguard.com/) server, with a startup script, and automatic checks for upgrades + * Creates wireguard.conf files for Linux clients as well as QR codes for Apple/Android clients -Note: The `vpn` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: +Note: The `strongswan` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: - ondemand_wifi: true - ondemand_wifi_exclude: HomeNet,OfficeWifi @@ -91,9 +103,9 @@ Possible options can be gathered calling to https://api.digitalocean.com/v2/regi Required variables: -- aws_access_key +- aws_access_key: `AKIA...` - aws_secret_key -- region +- region: e.g. `us-east-1` Possible options can be gathered via cli `aws ec2 describe-regions` @@ -180,8 +192,8 @@ Required variables: Required variables: -- [vultr_config](https://trailofbits.github.io/algo/cloud-vultr.html) -- [region](https://api.vultr.com/v1/regions/list) +- [vultr_config](https://trailofbits.github.io/algo/cloud-vultr.html): /path/to/.vultr.ini +- [region](https://api.vultr.com/v1/regions/list): e.g. `Chicago`, `'New Jersey'` ### Azure @@ -197,9 +209,9 @@ Required variables: Required variables: -- aws_access_key +- aws_access_key: `AKIA...` - aws_secret_key -- region +- region: e.g. `us-east-1` Possible options can be gathered via cli `aws lightsail get-regions` @@ -231,12 +243,7 @@ Possible options can be gathered via cli `aws lightsail get-regions` Required variables: - [scaleway_token](https://www.scaleway.com/docs/generate-an-api-token/) -- region - -Possible regions: - -- ams1 -- par1 +- region: e.g. ams1, par1 ### OpenStack diff --git a/docs/index.md b/docs/index.md index 84f07185..02214052 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,29 @@ # Algo VPN documentation -* Setup instructions - - Documentation for available [Ansible roles](setup-roles.md) +* Deployment instructions - Deploy from [Fedora Workstation (26)](deploy-from-fedora-workstation.md) - Deploy from [RedHat/CentOS 6.x](deploy-from-redhat-centos6.md) - Deploy from [Windows](deploy-from-windows.md) - - Deploy from [Ansible](deploy-from-ansible.md) directly + - Deploy from a [Docker container](deploy-from-docker.md) + - Deploy from [Ansible](deploy-from-ansible.md) non-interactively + - Deploy onto a [cloud server at time of creation](deploy-from-script-or-cloud-init-to-localhost.md) * Client setup - Setup [Android](client-android.md) clients - Setup [Generic/Linux](client-linux.md) clients with Ansible -* Cloud setup + - Setup Ubuntu clients to use [WireGuard](client-linux-wireguard.md) + - Setup Apple devices to use [IPSEC](client-apple-ipsec.md) + - Setup Macs running macOS 10.13 or older to use [Wireguard](client-macos-wireguard.md) + - Manual Windows 10 client setup for [IPSEC](client-windows.md) +* Cloud provider setup + - Configure [Amazon EC2](cloud-amazon-ec2.md) - Configure [Azure](cloud-azure.md) - Configure [DigitalOcean](cloud-do.md) + - Configure [Google Cloud Platform](cloud-gce.md) - Configure [Vultr](cloud-vultr.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.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) +* [Firewalls](firewalls.md) * [Troubleshooting](troubleshooting.md) diff --git a/docs/setup-roles.md b/docs/setup-roles.md deleted file mode 100644 index 1523d181..00000000 --- a/docs/setup-roles.md +++ /dev/null @@ -1,28 +0,0 @@ -# Ansible Roles - -## Required roles - -* **Common** - * Installs several required packages and software updates, then reboots if necessary - * Configures network interfaces, and enables packet forwarding on them -* **VPN** - * Installs [strongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges - * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user - * Bundles the appropriate certificates into Apple mobileconfig profiles for each user - * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) - -## Optional roles - -* **Security Enhancements** - * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied - * Modify features like core dumps, kernel parameters, and SUID binaries to limit possible attacks - * Enhances SSH with modern ciphers and seccomp, and restricts access to old or unwanted features like X11 forwarding and SFTP -* **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 From 5904546a483d9f2ed3daa571cb952db5ba1df62a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 17 May 2019 14:49:29 +0200 Subject: [PATCH 30/30] Randomly generated IP address for the local dns resolver (#1429) * generate service IPs dynamically * update cloud-init tests * exclude ipsec and wireguard ranges from the random service ip * Update docs * @davidemyers: update wireguard docs for linux * Move to netaddr filter * AllowedIPs fix * WireGuard IPs fix --- config.cfg | 21 +++--------- docs/client-linux-wireguard.md | 32 +++---------------- docs/faq.md | 2 +- main.yml | 14 ++++++-- requirements.txt | 1 + roles/common/handlers/main.yml | 2 +- roles/common/tasks/freebsd.yml | 2 +- .../common/templates/10-algo-lo100.network.j2 | 2 +- roles/common/templates/rules.v6.j2 | 2 +- .../dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- roles/wireguard/defaults/main.yml | 8 +++-- roles/wireguard/templates/server.conf.j2 | 3 +- tests/cloud-init.sh | 2 +- tests/ipsec-client.sh | 2 ++ tests/local-deploy.sh | 2 +- tests/update-users.sh | 2 +- tests/wireguard-client.sh | 2 ++ 17 files changed, 42 insertions(+), 59 deletions(-) diff --git a/config.cfg b/config.cfg index 2d2f8577..181ae022 100644 --- a/config.cfg +++ b/config.cfg @@ -39,20 +39,8 @@ wireguard_port: 51820 wireguard_PersistentKeepalive: 0 # WireGuard network configuration -_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_network_ipv4: "{{ _wireguard_network_ipv4['subnet'] }}/{{ _wireguard_network_ipv4['prefix'] }}" -wireguard_network_ipv6: "{{ _wireguard_network_ipv6['subnet'] }}/{{ _wireguard_network_ipv6['prefix'] }}" +wireguard_network_ipv4: 10.19.49.0/24 +wireguard_network_ipv6: fd9d:bc11:4021::/48 # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission @@ -99,8 +87,9 @@ dns_servers: - 2606:4700:4700::1111 - 2606:4700:4700::1001 -# IP address for the local dns resolver -local_service_ip: 172.16.0.1 +# Randomly generated IP address for the local dns resolver +local_service_ip: "{{ '172.16.0.1' | ipmath(1048573 | random(seed=algo_server_name + ansible_fqdn)) }}" +local_service_ipv6: "{{ 'fd00::1' | ipmath(1048573 | random(seed=algo_server_name + ansible_fqdn)) }}" # Your Algo server will automatically install security updates. Some updates # require a reboot to take effect but your Algo server will not reboot itself diff --git a/docs/client-linux-wireguard.md b/docs/client-linux-wireguard.md index 52f6e85a..96455e18 100644 --- a/docs/client-linux-wireguard.md +++ b/docs/client-linux-wireguard.md @@ -1,18 +1,18 @@ -# Using Ubuntu Server as a Client with WireGuard +# Using Ubuntu as a Client with WireGuard ## Install WireGuard -To connect to your AlgoVPN using [WireGuard](https://www.wireguard.com) from Ubuntu Server, first install WireGuard: +To connect to your AlgoVPN using [WireGuard](https://www.wireguard.com) from Ubuntu, first install WireGuard: ```shell # Add the WireGuard repository: sudo add-apt-repository ppa:wireguard/wireguard -# Update the list of available packages (not necessary on Bionic or later): -sudo apt update +# Update the list of available packages (not necessary on 18.04 or later): +sudo apt update # Install the tools and kernel module: -sudo apt install wireguard +sudo apt install wireguard openresolv ``` For installation on other Linux distributions, see the [Installation](https://www.wireguard.com/install/) page on the WireGuard site. @@ -21,28 +21,6 @@ For installation on other Linux distributions, see the [Installation](https://ww 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`. Each WireGuard client you connect to your AlgoVPN must use a different config file. Choose one of these files and copy it to your Linux client. -## Configure DNS - -### Ubuntu 18.04 (Bionic) - -If your client is running Bionic (or another Linux that uses `systemd-resolved` for DNS but does not have `resolvectl` or `resolvconf` installed) 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. - -### Ubuntu 18.10 (Cosmic) or 19.04 (Disco) - -If your client is running Cosmic or Disco (or another Linux that uses `systemd-resolved` for DNS and has `resolvectl` but *not* `resolvconf` installed) you can either edit the config file as shown above for Bionic or run the following command once: - -``` -sudo ln -s /usr/bin/resolvectl /usr/bin/resolvconf -``` - -### Other Linux Distributions - -On other Linux distributions you might need to install the `openresolv` package. - ## Configure WireGuard Finally, install the config file on your client as `/etc/wireguard/wg0.conf` and start WireGuard: diff --git a/docs/faq.md b/docs/faq.md index 16a69b32..5e59b63f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -50,7 +50,7 @@ Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhe ## Can DNS filtering be disabled? -You can temporarily disable DNS filtering for all IPsec clients at once with the following 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 `sudo systemctl restart strongswan`. DNS filtering for Wireguard clients has to be disabled on each client device separately by modifying the settings in the app, or by directly modifying the `DNS` setting on the `clientname.conf` file. If all else fails, we recommend deploying a new Algo server without the adblocking feature enabled. +You can temporarily disable DNS filtering for all IPsec clients at once with the following workaround: SSH to your Algo server (using the 'shell access' command printed upon a successful deployment), edit `/etc/ipsec.conf`, and change `rightdns=` to `rightdns=8.8.8.8`. Then run `sudo systemctl restart strongswan`. DNS filtering for Wireguard clients has to be disabled on each client device separately by modifying the settings in the app, or by directly modifying the `DNS` setting on the `clientname.conf` file. If all else fails, we recommend deploying a new Algo server without the adblocking feature enabled. ## Wasn't IPSEC backdoored by the US government? diff --git a/main.yml b/main.yml index c1c14abd..45aae582 100644 --- a/main.yml +++ b/main.yml @@ -2,11 +2,19 @@ - hosts: localhost become: false tasks: - - name: Verify Ansible meets Drupal VM's version requirements. + - name: Ensure the requirements installed + debug: + msg: "{{ '' | ipaddr }}" + ignore_errors: true + no_log: true + register: ipaddr + + - name: Verify Ansible meets Algo VPN requirements. assert: - that: "ansible_version.full is version('2.7.10', '==')" + that: + - ansible_version.full is version('2.7.10', '==') + - not ipaddr.failed msg: > - Ansible version is {{ ansible_version.full }}. You must update the requirements to use this version of Algo. Try to run python -m pip install -U -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 60c89a08..b79aa5fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ ansible==2.7.10 +netaddr diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 6b369267..ebbe91ad 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -18,7 +18,7 @@ ifconfig lo100 destroy || true && ifconfig lo100 create && ifconfig lo100 inet {{ local_service_ip }} netmask 255.255.255.255 && - ifconfig lo100 inet6 FCAA::1/64; echo $? + ifconfig lo100 inet6 {{ local_service_ipv6 }}/128; echo $? - name: restart iptables service: name=netfilter-persistent state=restarted diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index e0d54c16..9dbfb189 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -54,7 +54,7 @@ block: | cloned_interfaces="lo100" ifconfig_lo100="inet {{ local_service_ip }} netmask 255.255.255.255" - ifconfig_lo100_ipv6="inet6 FCAA::1/64" + ifconfig_lo100_ipv6="inet6 {{ local_service_ipv6 }}/128" notify: - restart loopback bsd diff --git a/roles/common/templates/10-algo-lo100.network.j2 b/roles/common/templates/10-algo-lo100.network.j2 index 87280511..ccdca7e6 100644 --- a/roles/common/templates/10-algo-lo100.network.j2 +++ b/roles/common/templates/10-algo-lo100.network.j2 @@ -4,4 +4,4 @@ Name=lo [Network] Description=lo:100 Address={{ local_service_ip }}/32 -Address=FCAA::1/64 +Address={{ local_service_ipv6 }}/128 diff --git a/roles/common/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 index 12bed2b4..adb59f5d 100644 --- a/roles/common/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -83,7 +83,7 @@ COMMIT # 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 +-A INPUT -d {{ local_service_ipv6 }}/128 -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }} diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index c52b6b9c..1857c55b 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -116,7 +116,7 @@ group=nogroup #except-interface= # Or which to listen on by address (remember to include 127.0.0.1 if # you use this.) -listen-address=127.0.0.1,FCAA::1,{{ local_service_ip }} +listen-address=127.0.0.1,{{ local_service_ipv6 }},{{ local_service_ip }} # If you want dnsmasq to provide only DNS service on an interface, # configure it as shown above, and then use the following line to # disable DHCP and TFTP on it. diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 4c7f17f3..e0c82f51 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -10,5 +10,9 @@ wireguard_dns_servers: >- {% 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 %} -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 %}" +wireguard_client_ip: >- + {{ wireguard_network_ipv4 | ipaddr(index|int+2) }} + {{ ',' + wireguard_network_ipv6 | ipaddr(index|int+2) if ipv6_support else '' }} +wireguard_server_ip: >- + {{ wireguard_network_ipv4 | ipaddr('1') }} + {{ ',' + wireguard_network_ipv6 | ipaddr('1') if ipv6_support else '' }} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index 247c7d2f..46c280de 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -11,7 +11,6 @@ SaveConfig = false [Peer] # {{ u }} PublicKey = {{ lookup('file', wireguard_pki_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 %} - +AllowedIPs = {{ wireguard_network_ipv4 | ipaddr(index|int+1) | ipv4('address') }}/32{{ ',' + wireguard_network_ipv6 | ipaddr(index|int+1) | ipv6('address') + '/128' if ipv6_support else '' }} {% endif %} {% endfor %} diff --git a/tests/cloud-init.sh b/tests/cloud-init.sh index ca182cd5..e6d3209a 100755 --- a/tests/cloud-init.sh +++ b/tests/cloud-init.sh @@ -10,7 +10,7 @@ export LOCAL_DNS=true export SSH_TUNNELING=true export ENDPOINT=10.0.8.100 export USERS=desktop,user1,user2 -export EXTRA_VARS='install_headers=false tests=true apparmor_enabled=false' +export EXTRA_VARS='install_headers=false tests=true apparmor_enabled=false local_service_ip=172.16.0.1' export ANSIBLE_EXTRA_ARGS='--skip-tags apparmor' export REPO_SLUG=${TRAVIS_PULL_REQUEST_SLUG:-${TRAVIS_REPO_SLUG:-trailofbits/algo}} export REPO_BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-${TRAVIS_BRANCH:-master}} diff --git a/tests/ipsec-client.sh b/tests/ipsec-client.sh index d2c3f548..c64ca533 100755 --- a/tests/ipsec-client.sh +++ b/tests/ipsec-client.sh @@ -21,3 +21,5 @@ fping -t 900 -c3 -r3 -Dse 10.0.8.100 172.16.0.1 host google.com 172.16.0.1 echo "IPsec tests passed" + +ipsec down algovpn-10.0.8.100 diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index 99bf5c21..7699469d 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,7 +2,7 @@ set -ex -DEPLOY_ARGS="provider=local server=10.0.8.100 ssh_user=ubuntu endpoint=10.0.8.100 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" +DEPLOY_ARGS="provider=local server=10.0.8.100 ssh_user=ubuntu endpoint=10.0.8.100 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 local_service_ip=172.16.0.1" if [ "${DEPLOY}" == "docker" ] then diff --git a/tests/update-users.sh b/tests/update-users.sh index d957787d..8c76ba1d 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -2,7 +2,7 @@ set -ex -USER_ARGS="{ 'server': '10.0.8.100', 'users': ['desktop', 'user1', 'user2'] }" +USER_ARGS="{ 'server': '10.0.8.100', 'users': ['desktop', 'user1', 'user2'], 'local_service_ip': '172.16.0.1' }" if [ "${DEPLOY}" == "docker" ] then diff --git a/tests/wireguard-client.sh b/tests/wireguard-client.sh index 7dac2a32..46b4603a 100755 --- a/tests/wireguard-client.sh +++ b/tests/wireguard-client.sh @@ -19,3 +19,5 @@ wg | grep "latest handshake" host google.com 172.16.0.1 echo "WireGuard tests passed" + +wg-quick down configs/10.0.8.100/wireguard/user1.conf