From ec9fe778216b6c1f4e25d4eb27f95d6d07677457 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 20 Nov 2018 19:20:24 +0100 Subject: [PATCH 01/55] BSD StrongSwan fixes (#1207) --- roles/common/tasks/freebsd.yml | 2 ++ roles/vpn/defaults/main.yml | 2 ++ roles/vpn/tasks/main.yml | 8 ++++++++ roles/vpn/templates/strongswan.conf.j2 | 15 ++++++++------- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index dc52931c..dda5dcf9 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,6 +1,8 @@ --- - set_fact: config_prefix: "/usr/local/" + strongswan_shell: /usr/sbin/nologin + strongswan_home: /var/empty root_group: wheel ssh_service_name: sshd apparmor_enabled: false diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 8e044f29..a865dfb4 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,4 +1,6 @@ --- +strongswan_shell: /usr/sbin/nologin +strongswan_home: /var/lib/strongswan BetweenClients_DROP: true wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" wireguard_interface: wg0 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 27be701a..bfe929ca 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -9,6 +9,14 @@ - 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: Install strongSwan package: name=strongswan state=present diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index 7fcf9ef4..f71c779e 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -13,13 +13,14 @@ charon { group = nogroup {% if ansible_distribution == 'FreeBSD' %} filelog { - /var/log/charon.log { - time_format = %b %e %T - ike_name = yes - append = no - default = 1 - flush_line = yes - } + charon { + path = /var/log/charon.log + time_format = %b %e %T + ike_name = yes + append = no + default = 1 + flush_line = yes + } } {% endif %} } From 458f2f11d7dccf3395664f67620ba702b5a6de9b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 22 Nov 2018 19:04:37 +0100 Subject: [PATCH 02/55] dnscrypt-proxy apparmor fix (#1210) ## Description Apparmor profile for dnscrypt-proxy didn't work at all ## Motivation and Context Fixes #1155 ## How Has This Been Tested? Deployed to DigitalOcean, checked that the dnscrypt-proxy binary is in enforce mode ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] I have read the **CONTRIBUTING** document. - [x] My code follows the code style of this project. - [x] All new and existing tests passed. --- .../files/apparmor.profile.dnscrypt-proxy | 18 ++++++++++++------ roles/dns_encryption/tasks/ubuntu.yml | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy index a2e51639..7e900bc5 100644 --- a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -1,6 +1,6 @@ #include -/usr/sbin/dnscrypt-proxy { +/usr/bin/dnscrypt-proxy { #include #include #include @@ -12,12 +12,18 @@ capability setuid, capability sys_resource, - /etc/dnscrypt-proxy.toml r, + /etc/dnscrypt-proxy/** r, + /usr/bin/dnscrypt-proxy mr, + /tmp/public-resolvers.md* rw, + + /tmp/*.tmp w, + owner /tmp/*.tmp r, + + /run/systemd/notify rw, + /lib/x86_64-linux-gnu/ld-*.so mr, + @{PROC}/sys/kernel/hostname r, + @{PROC}/sys/net/core/somaxconn r, /etc/ld.so.cache r, - /usr/sbin/dnscrypt-proxy mr, - /usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv r, /usr/local/lib/{@{multiarch}/,}libldns.so* mr, /usr/local/lib/{@{multiarch}/,}libsodium.so* mr, - /run/dnscrypt-proxy.pid rw, - /run/systemd/notify rw, } diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 13ba1709..89515ddb 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -27,14 +27,14 @@ - name: Ubuntu | Unbound profile for apparmor configured copy: src: apparmor.profile.dnscrypt-proxy - dest: /etc/apparmor.d/usr.sbin.dnscrypt-proxy + dest: /etc/apparmor.d/usr.bin.dnscrypt-proxy owner: root group: root mode: 0600 notify: restart dnscrypt-proxy - name: Ubuntu | Enforce the dnscrypt-proxy AppArmor policy - command: aa-enforce usr.sbin.dnscrypt-proxy + command: aa-enforce usr.bin.dnscrypt-proxy changed_when: false tags: apparmor when: apparmor_enabled|default(false)|bool == true From 8a42c29422582ce578ad393294f11b6666652369 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 22 Nov 2018 19:04:58 +0100 Subject: [PATCH 03/55] on-build python venvs (#1199) --- .gitignore | 2 + Dockerfile | 2 +- README.md | 2 +- config.cfg | 3 + playbooks/cloud-pre.yml | 10 ++ requirements.txt | 14 +- roles/cloud-azure/defaults/main.yml | 1 + roles/cloud-azure/tasks/main.yml | 73 +++++---- roles/cloud-azure/tasks/venv.yml | 32 ++++ roles/cloud-digitalocean/defaults/main.yml | 2 + roles/cloud-digitalocean/tasks/main.yml | 176 +++++++++++---------- roles/cloud-digitalocean/tasks/venv.yml | 13 ++ roles/cloud-ec2/defaults/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 66 ++++---- roles/cloud-ec2/tasks/venv.yml | 15 ++ roles/cloud-gce/defaults/main.yml | 2 + roles/cloud-gce/tasks/main.yml | 94 +++++------ roles/cloud-gce/tasks/venv.yml | 15 ++ roles/cloud-lightsail/defaults/main.yml | 2 + roles/cloud-lightsail/tasks/main.yml | 76 +++++---- roles/cloud-lightsail/tasks/venv.yml | 15 ++ roles/cloud-openstack/defaults/main.yml | 2 + roles/cloud-openstack/tasks/main.yml | 130 +++++++-------- roles/cloud-openstack/tasks/venv.yml | 13 ++ users.yml | 10 ++ venvs/.gitinit | 0 26 files changed, 466 insertions(+), 305 deletions(-) create mode 100644 roles/cloud-azure/tasks/venv.yml create mode 100644 roles/cloud-digitalocean/defaults/main.yml create mode 100644 roles/cloud-digitalocean/tasks/venv.yml create mode 100644 roles/cloud-ec2/tasks/venv.yml create mode 100644 roles/cloud-gce/defaults/main.yml create mode 100644 roles/cloud-gce/tasks/venv.yml create mode 100644 roles/cloud-lightsail/defaults/main.yml create mode 100644 roles/cloud-lightsail/tasks/venv.yml create mode 100644 roles/cloud-openstack/defaults/main.yml create mode 100644 roles/cloud-openstack/tasks/venv.yml create mode 100644 venvs/.gitinit diff --git a/.gitignore b/.gitignore index b632022a..de4fd233 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ inventory_users *.kate-swp env .DS_Store +venvs/* +!venvs/.gitinit diff --git a/Dockerfile b/Dockerfile index c2476ae1..6fa1d0fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY . . RUN chmod 0755 /algo/algo-docker.sh # Because of the bind mounting of `configs/`, we need to run as the `root` user -# This may break in cases where user namespacing is enabled, so hopefully Docker +# This may break in cases where user namespacing is enabled, so hopefully Docker # sorts out a way to set permissions on bind-mounted volumes (`docker run -v`) # before userns becomes default # Note that not running as root will break if we don't have a matching userid diff --git a/README.md b/README.md index 282de737..8737d5da 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua ```bash $ python -m virtualenv --python=`which python2` env && source env/bin/activate && - python -m pip install -U pip && + python -m pip install -U pip virtualenv && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. diff --git a/config.cfg b/config.cfg index 03f439e9..7f46aa54 100644 --- a/config.cfg +++ b/config.cfg @@ -13,6 +13,9 @@ users: # If True re-init all existing certificates. Boolean keys_clean_all: False +# Clean up cloud python environments +clean_environment: false + vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index b40f6c85..338e70dd 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -14,6 +14,16 @@ 'dns_encryption "{{ dns_encryption }}"' \ > /dev/tty +- name: Install the requirements + local_action: + module: pip + state: latest + name: + - pyOpenSSL + - jinja2==2.8 + - segno + tags: always + - name: Generate the SSH private key openssl_privatekey: path: "{{ SSH_keys.private }}" diff --git a/requirements.txt b/requirements.txt index 4d40c39b..38f36dac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1 @@ -setuptools>=11.3 -SecretStorage < 3 -ansible[azure]==2.5.2 -dopy==0.3.5 -boto>=2.5 -boto3 -apache-libcloud -six -pyopenssl -jinja2==2.8 -shade -pycrypto -segno +ansible==2.5.2 diff --git a/roles/cloud-azure/defaults/main.yml b/roles/cloud-azure/defaults/main.yml index cd5301d2..dbd82f36 100644 --- a/roles/cloud-azure/defaults/main.yml +++ b/roles/cloud-azure/defaults/main.yml @@ -1,4 +1,5 @@ --- +azure_venv: "{{ playbook_dir }}/configs/.venvs/azure" _azure_regions: > [ { diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 27e2defc..38adc741 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,43 +1,48 @@ --- - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.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 %} + - block: + - name: Include prompts + import_tasks: prompts.yml - - name: Create AlgoVPN Server - azure_rm_deployment: - state: present - deployment_name: "AlgoVPN-{{ algo_server_name }}" - template: "{{ lookup('file', 'deployment.json') }}" - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group_name: "AlgoVPN-{{ algo_server_name }}" - parameters: - AlgoServerName: - value: "{{ algo_server_name }}" - sshKeyData: - value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - location: - value: "{{ algo_region }}" - WireGuardPort: - value: "{{ wireguard_port }}" - vmSize: - value: "{{ cloud_providers.azure.size }}" - imageReferenceSku: - value: "{{ cloud_providers.azure.image }}" - register: azure_rm_deployment + - 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: - cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" - ansible_ssh_user: ubuntu + - name: Create AlgoVPN Server + azure_rm_deployment: + state: present + deployment_name: "AlgoVPN-{{ algo_server_name }}" + template: "{{ lookup('file', 'deployment.json') }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group_name: "AlgoVPN-{{ algo_server_name }}" + parameters: + AlgoServerName: + value: "{{ algo_server_name }}" + sshKeyData: + value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + location: + value: "{{ algo_region }}" + WireGuardPort: + value: "{{ wireguard_port }}" + vmSize: + value: "{{ cloud_providers.azure.size }}" + imageReferenceSku: + value: "{{ cloud_providers.azure.image }}" + register: azure_rm_deployment + - set_fact: + cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ azure_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-azure/tasks/venv.yml b/roles/cloud-azure/tasks/venv.yml new file mode 100644 index 00000000..cbadf8de --- /dev/null +++ b/roles/cloud-azure/tasks/venv.yml @@ -0,0 +1,32 @@ +--- +- name: Clean up the environment + file: + dest: "{{ azure_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - packaging + - requests[security] + - azure-mgmt-compute>=2.0.0,<3 + - azure-mgmt-network>=1.3.0,<2 + - azure-mgmt-storage>=1.5.0,<2 + - azure-mgmt-resource>=1.1.0,<2 + - azure-storage>=0.35.1,<0.36 + - azure-cli-core>=2.0.12,<3 + - msrest==0.4.29 + - msrestazure==0.4.31 + - azure-mgmt-dns>=1.0.1,<2 + - azure-mgmt-keyvault>=0.40.0,<0.41 + - azure-mgmt-batch>=4.1.0,<5 + - azure-mgmt-sql>=0.7.1,<0.8 + - azure-mgmt-web>=0.32.0,<0.33 + - azure-mgmt-containerservice>=2.0.0,<3.0.0 + - azure-mgmt-containerregistry>=1.0.1 + - azure-mgmt-rdbms==1.2.0 + - azure-mgmt-containerinstance==0.4.0 + state: latest + virtualenv: "{{ azure_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-digitalocean/defaults/main.yml b/roles/cloud-digitalocean/defaults/main.yml new file mode 100644 index 00000000..34ba5f86 --- /dev/null +++ b/roles/cloud-digitalocean/defaults/main.yml @@ -0,0 +1,2 @@ +--- +digitalocean_venv: "{{ playbook_dir }}/configs/.venvs/digitalocean" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index aca66b7b..488ea2d1 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,102 +1,108 @@ - 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: Build python virtual environment + import_tasks: venv.yml - 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 + - name: Include prompts + import_tasks: prompts.yml - rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes + - name: Set additional facts + set_fact: + algo_do_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ do_regions[_algo_region.user_input | int -1 ]['slug'] }} + {%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - - debug: var=ssh_keys + - block: + - name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - fail: - msg: "Please, ensure that your API token is not read-only." + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes - - name: "Upload the SSH key" - digital_ocean: - state: present - command: ssh - ssh_pub_key: "{{ public_key }}" - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: do_ssh_key + - debug: var=ssh_keys - - name: "Creating a droplet..." - digital_ocean: - state: present - command: droplet - name: "{{ 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 + - fail: + msg: "Please, ensure that your API token is not read-only." - - set_fact: - cloud_instance_ip: "{{ do.droplet.ip_address }}" - ansible_ssh_user: root + - 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: Tag the droplet - digital_ocean_tag: - name: "Environment:Algo" - resource_id: "{{ do.droplet.id }}" - api_token: "{{ algo_do_token }}" - state: present + - 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 - - 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 + - set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" + ansible_ssh_user: root - rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes + - name: Tag the droplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ algo_do_token }}" + state: present - - debug: var=ssh_keys + - 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 - - fail: - msg: "Please, ensure that your API token is not read-only." + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + + - fail: + msg: "Please, ensure that your API token is not read-only." + environment: + PYTHONPATH: "{{ digitalocean_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-digitalocean/tasks/venv.yml b/roles/cloud-digitalocean/tasks/venv.yml new file mode 100644 index 00000000..80e85b9f --- /dev/null +++ b/roles/cloud-digitalocean/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ digitalocean_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: dopy + version: 0.3.5 + virtualenv: "{{ digitalocean_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 8060eb72..12b3f19d 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -4,3 +4,4 @@ encrypted: "{{ cloud_providers.ec2.encrypted }}" ec2_vpc_nets: cidr_block: 172.16.0.0/16 subnet_cidr: 172.16.254.0/23 +ec2_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 64dbfcd4..ea3a67a4 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,40 +1,46 @@ - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.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('.', '-') }}" + - block: + - name: Include prompts + import_tasks: prompts.yml - - 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 + - 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('.', '-') }}" - - import_tasks: encrypt_image.yml - when: encrypted + - name: Locate official AMI for region + ec2_ami_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + owners: "{{ cloud_providers.ec2.image.owner }}" + region: "{{ algo_region }}" + filters: + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" + register: ami_search - - 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 %} + - import_tasks: encrypt_image.yml + when: encrypted - - name: Deploy the stack - import_tasks: cloudformation.yml + - name: Set the ami id as a fact + set_fact: + ami_image: >- + {% if ami_search_encrypted.image_id is defined %}{{ ami_search_encrypted.image_id }} + {%- elif search_crypt.images is defined and search_crypt.images|length >= 1 %}{{ (search_crypt.images | sort(attribute='creation_date') | last)['image_id'] }} + {%- else %}{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}{% endif %} - - set_fact: - cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" - ansible_ssh_user: ubuntu + - 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 diff --git a/roles/cloud-ec2/tasks/venv.yml b/roles/cloud-ec2/tasks/venv.yml new file mode 100644 index 00000000..be2eeced --- /dev/null +++ b/roles/cloud-ec2/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ ec2_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ ec2_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-gce/defaults/main.yml b/roles/cloud-gce/defaults/main.yml new file mode 100644 index 00000000..d771cc8f --- /dev/null +++ b/roles/cloud-gce/defaults/main.yml @@ -0,0 +1,2 @@ +--- +gce_venv: "{{ playbook_dir }}/configs/.venvs/gce" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 8af6ff87..e04b3d80 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,54 +1,60 @@ - block: - - name: Include prompts - import_tasks: prompts.yml - - - name: Network configured - gce_net: - name: "algo-net-{{ algo_server_name }}" - fwname: "algo-net-{{ algo_server_name }}-fw" - allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" - state: "present" - mode: auto - src_range: 0.0.0.0/0 - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" + - name: Build python virtual environment + import_tasks: venv.yml - block: - - name: External IP allocated - gce_eip: + - name: Include prompts + import_tasks: prompts.yml + + - name: Network configured + gce_net: + name: "algo-net-{{ algo_server_name }}" + fwname: "algo-net-{{ algo_server_name }}-fw" + allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" + state: "present" + mode: auto + src_range: 0.0.0.0/0 + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + + - 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: "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 }}" - name: "{{ algo_server_name }}" - region: "{{ algo_region.split('-')[0:2] | join('-') }}" - state: present - register: gce_eip + metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' + network: "algo-net-{{ algo_server_name }}" + tags: + - "environment-algo" + register: google_vm - - name: Set External IP as a fact - set_fact: - external_ip: "{{ gce_eip.address }}" - when: cloud_providers.gce.external_static_ip - - - name: "Creating a new instance..." - gce: - instance_names: "{{ algo_server_name }}" - zone: "{{ algo_region }}" - external_ip: "{{ external_ip | default('ephemeral') }}" - machine_type: "{{ cloud_providers.gce.size }}" - image: "{{ cloud_providers.gce.image }}" - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" - metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ algo_server_name }}" - tags: - - "environment-algo" - register: google_vm - - - set_fact: - cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" - ansible_ssh_user: ubuntu + - set_fact: + cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ gce_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-gce/tasks/venv.yml b/roles/cloud-gce/tasks/venv.yml new file mode 100644 index 00000000..078efe5b --- /dev/null +++ b/roles/cloud-gce/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ gce_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - apache-libcloud + - pycrypto + state: latest + virtualenv: "{{ gce_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-lightsail/defaults/main.yml b/roles/cloud-lightsail/defaults/main.yml new file mode 100644 index 00000000..06ae0ee9 --- /dev/null +++ b/roles/cloud-lightsail/defaults/main.yml @@ -0,0 +1,2 @@ +--- +lightsail_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index 29342af9..21e3d459 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -1,41 +1,47 @@ - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.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 + - block: + - name: Include prompts + import_tasks: prompts.yml - - set_fact: - cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" - ansible_ssh_user: ubuntu + - 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 diff --git a/roles/cloud-lightsail/tasks/venv.yml b/roles/cloud-lightsail/tasks/venv.yml new file mode 100644 index 00000000..9816fea1 --- /dev/null +++ b/roles/cloud-lightsail/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ lightsail_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ lightsail_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-openstack/defaults/main.yml b/roles/cloud-openstack/defaults/main.yml new file mode 100644 index 00000000..3bec06b2 --- /dev/null +++ b/roles/cloud-openstack/defaults/main.yml @@ -0,0 +1,2 @@ +--- +openstack_venv: "{{ playbook_dir }}/configs/.venvs/openstack" diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index 8fb1e6b0..75b3db6d 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -4,77 +4,83 @@ when: lookup('env', 'OS_AUTH_URL') == "" - 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: Build python virtual environment + import_tasks: venv.yml - - 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 } + - 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: Keypair created - os_keypair: - state: "{{ state|default('present') }}" - name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" - public_key_file: "{{ SSH_keys.public }}" - register: os_keypair + - 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: Gather facts about flavors - os_flavor_facts: - ram: "{{ cloud_providers.openstack.flavor_ram }}" + - 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 images - os_image_facts: - image: "{{ cloud_providers.openstack.image }}" + - name: Gather facts about flavors + os_flavor_facts: + ram: "{{ cloud_providers.openstack.flavor_ram }}" - - name: Gather facts about public networks - os_networks_facts: + - name: Gather facts about images + os_image_facts: + image: "{{ cloud_providers.openstack.image }}" - - 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: Gather facts about public networks + os_networks_facts: - - 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 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: 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: 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'] }}" - - set_fact: - cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" - ansible_ssh_user: ubuntu + - 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 diff --git a/roles/cloud-openstack/tasks/venv.yml b/roles/cloud-openstack/tasks/venv.yml new file mode 100644 index 00000000..e2c4f86a --- /dev/null +++ b/roles/cloud-openstack/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ openstack_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: shade + state: latest + virtualenv: "{{ openstack_venv }}" + virtualenv_python: python2.7 diff --git a/users.yml b/users.yml index bb934946..30e460ae 100644 --- a/users.yml +++ b/users.yml @@ -58,6 +58,16 @@ - config.cfg - "configs/{{ inventory_hostname }}/config.yml" + pre_tasks: + - block: + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always + roles: - role: common - role: wireguard diff --git a/venvs/.gitinit b/venvs/.gitinit new file mode 100644 index 00000000..e69de29b From 79d46b9c8b0e9a04b356f8acd78d6af3c82d4fcb Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 26 Nov 2018 10:58:34 -0500 Subject: [PATCH 04/55] Add p12 password back to mobileconfigs (#1218) --- roles/vpn/templates/mobileconfig.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 54614fd4..8a0bb5f6 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -134,6 +134,8 @@ IKEv2 + Password + {{ p12_export_password }} PayloadCertificateFileName {{ item.0 }}.p12 PayloadContent From 1a3c1754f64b8578a96942d8b4398815dd605d09 Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Mon, 26 Nov 2018 22:09:33 -0800 Subject: [PATCH 05/55] Add "unable to write 'random state'" resolution (#1219) I ran into the same issue as #1058, and the solution worked. This PR generalizes the solution and adds it to the troubleshooting documentation, making it easier to resolve for future users. --- docs/troubleshooting.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6e910218..e9335947 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,6 +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") * [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) @@ -268,6 +269,23 @@ sudo rm -rf /etc/wireguard/*.lock ``` Then immediately re-run `./algo`. +### Ubuntu Error: "unable to write 'random state" when generating CA password + +When running Algo, you received an error like this: + +``` +TASK [common : Generate password for the CA key] *********************************************************************************************************************************************************** +fatal: [xxx.xxx.xxx.xxx -> localhost]: FAILED! => {"changed": true, "cmd": "openssl rand -hex 16", "delta": "0:00:00.024776", "end": "2018-11-26 13:13:55.879921", "msg": "non-zero return code", "rc": 1, "start": "2018-11-26 13:13:55.855145", "stderr": "unable to write 'random state'", "stderr_lines": ["unable to write 'random state'"], "stdout": "xxxxxxxxxxxxxxxxxxx", "stdout_lines": ["xxxxxxxxxxxxxxxxxxx"]} +``` + +This happens when your user does not have ownership of the `$HOME/.rnd` file, which is a seed for randomization. To fix this issue, give your user ownership of the file with this command: + +``` +sudo chown $USER:$USER $HOME/.rnd +``` + +Now, run Algo again. + ## Connection Problems Look here if you deployed an Algo server but now have a problem connecting to it with a client. From ef62de1eeb445e884643f2d1de5be34fd9807181 Mon Sep 17 00:00:00 2001 From: jxn Date: Thu, 29 Nov 2018 07:11:26 -0600 Subject: [PATCH 06/55] fix typo in powershell execution in windows client set up doc (#1224) --- docs/client-windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 77ba3c6f..53b62f22 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -8,7 +8,7 @@ To install automatically, use the generated user Powershell script. 2. Open Powershell as Administrator. 3. Run the following command: ```powershell -powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 Add +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add ``` 4. The command has help information available. To view its full help, run this from Powershell: ```powershell From cc37d11ece4eba985fd9484a86e35a3bd3faa934 Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 3 Dec 2018 09:33:36 -0500 Subject: [PATCH 07/55] Run adblock.sh at a random time (#1227) --- roles/dns_adblocking/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index b276d355..6a44dbee 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -30,8 +30,8 @@ - name: Adblock script added to cron cron: name: Adblock hosts update - minute: 10 - hour: 2 + minute: "{{ range(0, 60) | random }}" + hour: "{{ range(0, 24) | random }}" job: /usr/local/sbin/adblock.sh user: root From d097f4e6a080c700d7a9d9fe81497b785613658c Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 3 Dec 2018 12:32:23 -0500 Subject: [PATCH 08/55] Increase memory limit for dnsmasq (#1228) * Increase memory limit for dnsmasq * Increase memory limit for dnsmasq further --- roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 index 98cbbddb..30e5359b 100644 --- a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 +++ b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 @@ -1,4 +1,5 @@ [Service] -MemoryLimit=16777216 +MemoryHigh=128M +MemoryMax=192M CPUAccounting=true -CPUQuota=5% +CPUQuota=20% From 378df4f832f23088c542c0d0702788a7193c497b Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 5 Dec 2018 00:57:13 -0500 Subject: [PATCH 09/55] docs/gce: Fix typos, clarify instructions (#1239) --- docs/cloud-gce.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cloud-gce.md b/docs/cloud-gce.md index fe43c43a..c8467655 100644 --- a/docs/cloud-gce.md +++ b/docs/cloud-gce.md @@ -1,12 +1,12 @@ # Google Cloud Platform setup -Follow the [installation instructions](https://cloud.google.com/sdk/) to have the CLI commands to interact with Google. +* Follow the [`gcloud` installation instructions](https://cloud.google.com/sdk/) -After creating an account and installing, login in on your account using `gcloud init` +* Log into your account using `gcloud init` ### Creating a project -The recommendation on GCP is to group resources on **Projets**, so we will create one project to put our VPN server and service account restricted to it. +The recommendation on GCP is to group resources into **Projects**, so we will create a new project for our VPN server and use a service account restricted to it. ```bash ## Create the project to group the resources @@ -38,4 +38,4 @@ gcloud services enable compute.googleapis.com **Attention:** take care of the `configs/gce.json` file, which contains the credentials to manage your Google Cloud account, including create and delete servers on this project. -There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md) +There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md). From e0326c41c9b94a6977e894224968f9e1e3cd8d6d Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Fri, 7 Dec 2018 14:41:19 -0500 Subject: [PATCH 10/55] Add info about modifying blacklists (#1236) # Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: # /usr/local/sbin/adblock.sh # If you load very large blocklists, you may also have to modify resource limits: # /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf --- config.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config.cfg b/config.cfg index 7f46aa54..088ed012 100644 --- a/config.cfg +++ b/config.cfg @@ -35,6 +35,11 @@ wireguard_port: 51820 # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 +# Algo will use the following lists to block ads. You can add new block lists +# after deployment by modifying the line starting "BLOCKLIST_URLS=" at: +# /usr/local/sbin/adblock.sh +# If you load very large blocklists, you may also have to modify resource limits: +# /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf adblock_lists: - "http://winhelp2002.mvps.org/hosts.txt" - "https://adaway.org/hosts.txt" From 0327c4fba410c7913d404185824360dff08aa7b0 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 7 Dec 2018 14:41:39 -0500 Subject: [PATCH 11/55] Note that WireGuard configs cannot be shared (#1238) --- config.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config.cfg b/config.cfg index 088ed012..168c359c 100644 --- a/config.cfg +++ b/config.cfg @@ -1,7 +1,10 @@ --- -# Add as many users as you want for your VPN server here. -# Credentials will be generated for each one. +# Add up to 250 users here. +# For each user, configuration files will be generated for both an IPsec +# connection and a WireGuard connection. Multiple client devices can share an +# IPsec configuration but WireGuard clients must each use a unique +# WireGuard configuration. users: - dan - jack From a285008c29c7ab6966f22e5f3670fbe1c97d37d8 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 7 Dec 2018 14:42:17 -0500 Subject: [PATCH 12/55] Update local install instructions (#1148) * Update local install instructions * Update deploy-to-ubuntu.md --- docs/deploy-to-ubuntu.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 6d6db62b..f3ba0669 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,20 +1,11 @@ # Local deployment -It is possible to download the Algo scripts to your own Ubuntu 18.04 server and run the scripts locally. +You can use Algo to configure a local server as an Algo VPN rather than create and configure a new server on a cloud provider. -In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 18.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. - -tl;dr: - -```shell -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible python-pip build-essential python-dev libssl-dev libffi-dev -pip install virtualenv -pip install --upgrade pip -git clone https://github.com/trailofbits/algo -cd algo -python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt -./algo +Install the Algo scripts on your server and follow the normal installation instructions, then choose: ``` +Install to existing Ubuntu 18.04 server (Advanced) +``` +Make sure your server is running the operating system specified. -**Warning**: Algo is intended to be run on a standalone server. If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). Other changes are also made, which can break other services running on your server (web, mail, etc.). +**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. From 9ffd936ead57f6c5ae5d00288a5f74cc68289ca1 Mon Sep 17 00:00:00 2001 From: "Federico G. Schwindt" Date: Mon, 10 Dec 2018 05:57:15 +0000 Subject: [PATCH 13/55] Fix ipv4 address missing on reboot (#1245) --- roles/common/tasks/freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index dda5dcf9..9f200189 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -42,7 +42,7 @@ block: | cloned_interfaces="lo100" ifconfig_lo100="inet {{ local_service_ip }} netmask 255.255.255.255" - ifconfig_lo100="inet6 FCAA::1/64" + ifconfig_lo100_ipv6="inet6 FCAA::1/64" notify: - restart loopback bsd tags: From a460d9d3d2aa32c51778dac2d2cd08e170d93a97 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 10 Dec 2018 16:37:36 +0100 Subject: [PATCH 14/55] Fixes #1246 --- roles/cloud-vultr/tasks/prompts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index 84e0cfd9..69978e83 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -2,7 +2,7 @@ - pause: prompt: | Enter the local path to your configuration INI file - (https://github.com/trailofbits/algo/docs/cloud-vultr.md): + (https://trailofbits.github.io/algo/cloud-vultr.html): register: _vultr_config when: vultr_config is undefined From af8d417751c333170be1cddb88050d8d973099b4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 18 Dec 2018 13:59:25 +0100 Subject: [PATCH 15/55] IPv6 forwarding fixes (#1256) --- roles/common/tasks/freebsd.yml | 2 +- roles/common/tasks/main.yml | 1 + roles/common/tasks/ubuntu.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 9f200189..78f47397 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -21,7 +21,7 @@ sysctl: - item: net.inet.ip.forwarding value: 1 - - item: net.inet6.ip6.forwarding + - item: "{{ 'net.inet6.ip6.forwarding' if ipv6_support else none }}" value: 1 tags: - always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 73e6783f..21d51a46 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -15,6 +15,7 @@ - name: Sysctl tuning sysctl: name="{{ item.item }}" value="{{ item.value }}" + when: item.item != "" with_items: - "{{ sysctl|default([]) }}" tags: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 9c6e6a5b..6dbc6335 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -114,7 +114,7 @@ value: 1 - item: net.ipv4.conf.all.forwarding value: 1 - - item: net.ipv6.conf.all.forwarding + - item: "{{ 'net.ipv6.conf.all.forwarding' if ipv6_support else none }}" value: 1 tags: - always From d762c896088992914a74e34f40805d66af400f70 Mon Sep 17 00:00:00 2001 From: Izzy Gomez Date: Thu, 20 Dec 2018 02:46:37 -0700 Subject: [PATCH 16/55] Fix typo in deploy-from-ansible.md. (#1261) --- docs/deploy-from-ansible.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 946c045b..f2809e0c 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -34,7 +34,7 @@ See below for more information about providers and extra variables - `ondemand_wifi_exclude` (Required if `ondemand_wifi` set) - WiFi networks to exclude from using the VPN. Comma-separated values - `local_dns` - (Optional) Enable a DNS resolver. Default: false - `ssh_tunneling` - (Optional) Enable SSH tunneling for each user. Default: false -- `windows` - (Optional) Enables compatible ciphers and key exchange to support Windows clietns, less secure. Default: false +- `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 From a9146f0a037cb7535347616c6cad8f5c8e98efc9 Mon Sep 17 00:00:00 2001 From: David Myers Date: Thu, 20 Dec 2018 04:47:24 -0500 Subject: [PATCH 17/55] Document DigitalOcean firewall (#1257) --- docs/cloud-do.md | 108 ++++++++++++++++++++++-------------- docs/images/do-firewall.png | Bin 0 -> 123960 bytes 2 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 docs/images/do-firewall.png diff --git a/docs/cloud-do.md b/docs/cloud-do.md index 675754a9..25d80a23 100644 --- a/docs/cloud-do.md +++ b/docs/cloud-do.md @@ -18,70 +18,94 @@ You will be returned to the **Tokens/Keys** tab, and your new key will be shown Copy or note down the hash that shows below the name you entered, as this will be necessary for the steps below. This value will disappear if you leave this page, and you'll need to regenerate it if you forget it. -## Using DigitalOcean with Algo (command) +## Using DigitalOcean with Algo (interactive) -These steps are for people who run Algo using Docker or using the "algo" command. +These steps are for those who run Algo using Docker or using the `./algo` command. -First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. +Choose DigitalOcean as your provider: ``` - What provider would you like to use? +What provider would you like to use? 1. DigitalOcean 2. Amazon Lightsail 3. Amazon EC2 - 4. Microsoft Azure - 5. Google Compute Engine - 6. Scaleway - 7. OpenStack (DreamCompute optimised) - 8. Install to existing Ubuntu 18.04 server - + 4. Vultr + 5. Microsoft Azure + 6. Google Compute Engine + 7. Scaleway + 8. OpenStack (DreamCompute optimised) + 9. Install to existing Ubuntu 18.04 server (Advanced) + Enter the number of your desired provider -: 1 -``` - -Next you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (don't worry if don't see any output, as the key input is hidden by Algo). - -``` -Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): -[pasted values will not be displayed] : +1 ``` -You will be prompted for the server name to enter. Feel free to leave this as the default ("algo.local") if you are not certain how this will affect your setup. +Enter a name for your server. Leave this as the default if you are not certain how this will affect your setup: ``` Name the vpn server: -[algo.local]: +[algo]: ``` -After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. +After several prompts related to Algo features you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (you won't see any output as the key is not echoed by Algo): ``` - What region should the server be located in? - 1. Amsterdam (Datacenter 2) - 2. Amsterdam (Datacenter 3) - 3. Frankfurt - 4. London - 5. New York (Datacenter 1) - 6. New York (Datacenter 2) - 7. New York (Datacenter 3) - 8. San Francisco (Datacenter 1) - 9. San Francisco (Datacenter 2) - 10. Singapore - 11. Toronto - 12. Bangalore -Enter the number of your desired region: -[7]: 11 +Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): + (output is hidden): ``` -You will then be asked the remainder of the setup questions. +Finally you will be asked the region in which you wish to setup your new Algo server. This list is dynamic and can change based on availability of resources. Enter the number next to name of the region: -## Using DigitalOcean with Algo (via Ansible) +``` +What region should the server be located in? + 1. ams3 Amsterdam 3 + 2. blr1 Bangalore 1 + 3. fra1 Frankfurt 1 + 4. lon1 London 1 + 5. nyc1 New York 1 + 6. nyc3 New York 3 + 7. sfo2 San Francisco 2 + 8. sgp1 Singapore 1 + 9. tor1 Toronto 1 + +Enter the number of your desired region +[6] +: +9 +``` -If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_token`. +## Using DigitalOcean with Algo (scripted) -For example, +If you are using Ansible directly to run Algo you will need to pass the API Token as `do_token`. For example: - ansible-playbook deploy.yml -e 'provider=digitalocean do_token=my_secret_token' +```shell +ansible-playbook main.yml -e "provider=digitalocean + server_name=algo + ondemand_cellular=true + ondemand_wifi=true + local_dns=false + ssh_tunneling=false + windows=false + store_cakey=true + region=nyc3 + do_token=token" +``` -Where "my_secret_token" is your API Token. For more references see [deploy-from-ansible](deploy-from-ansible.md) +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. + +To configure the DigitalOcean firewall, go to **Networking**, **Firewalls**, and choose **Create Firewall**. + +Configure your **Inbound Rules** as follows: + +![Inbound Rules](/docs/images/do-firewall.png) + +Leave the **Outbound Rules** at their defaults. + +Under **Apply to Droplets** enter the tag `Environment:Algo` to apply this firewall to all current and future Algo VPNs you create. diff --git a/docs/images/do-firewall.png b/docs/images/do-firewall.png new file mode 100644 index 0000000000000000000000000000000000000000..26c9e5866cd2c35283b361805563fb2d3823d4e3 GIT binary patch literal 123960 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%0A*n}ri6OqTb_MHnCJr?*J*bfg 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 literal 0 HcmV?d00001 From 1172b02cbce97781767475cf8a2dc62d548e289a Mon Sep 17 00:00:00 2001 From: Michael Schubert Date: Thu, 20 Dec 2018 15:20:39 +0100 Subject: [PATCH 18/55] README: fix small typos (#1262) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8737d5da..0e87e361 100644 --- a/README.md +++ b/README.md @@ -173,13 +173,13 @@ Note the admin username is `ubuntu` instead of `root` on providers other than Di ## Adding or Removing Users -If you chose the save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. +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. 1. Update the `users` list in your `config.cfg` 2. Open a terminal, `cd` to the algo directory, and activate the virtual environment with `source env/bin/activate` 3. Run the command: `./algo update-users` -After this process completes, the Algo VPN server will contains only the users listed in the `config.cfg` file. +After this process completes, the Algo VPN server will contain only the users listed in the `config.cfg` file. ## Additional Documentation From ac2d27767c99edd40732241bcb37b94ffc05ff01 Mon Sep 17 00:00:00 2001 From: David Myers Date: Thu, 20 Dec 2018 09:21:04 -0500 Subject: [PATCH 19/55] Replace 'max_mss' with 'reduce_mtu' (#1253) --- config.cfg | 17 +++-- docs/troubleshooting.md | 80 ++++++++++++++++++++---- roles/common/tasks/facts.yml | 5 ++ roles/vpn/templates/rules.v4.j2 | 4 +- roles/vpn/templates/rules.v6.j2 | 6 +- roles/wireguard/templates/client.conf.j2 | 2 + 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/config.cfg b/config.cfg index 168c359c..b0c7756d 100644 --- a/config.cfg +++ b/config.cfg @@ -24,15 +24,14 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true wireguard_port: 51820 -# MSS is the TCP Max Segment Size -# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation -# This appears to be necessary on (at least) Google Cloud, -# however, some routers also require a change to this parameter -# See also: -# - https://github.com/trailofbits/algo/issues/216 -# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu -# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan -#max_mss: 1316 +# 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 +# VPN tunnel some network connections will hang. Algo will attempt to set this +# automatically based on your server, but if connections hang you might need to +# adjust this yourself. +# See: https://github.com/trailofbits/algo/blob/master/docs/troubleshooting.md#various-websites-appear-to-be-offline-through-the-vpn +reduce_mtu: 0 # StrongSwan log level # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e9335947..fa0472f2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -320,22 +320,76 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network ### Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. Different networks may require the MTU within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. +This issue appears occasionally due to issues with [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) size. Different networks may require the MTU to be within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. -Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +If either your Internet service provider or your chosen cloud service provider use an MTU smaller than the normal value of 1500 you can use the `reduce_mtu` option in the file `config.cfg` to correspondingly reduce the size of the VPN tunnels created by Algo. Algo will attempt to automatically set `reduce_mtu` based on the MTU found on the server at the time of deployment, but it cannot detect if the MTU is smaller on the client side of the connection. -E.g., On Linux (client -- Ubuntu 18.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: -``` -$ ping -M do -s 1500 www.google.com -PING www.google.com (74.125.22.147) 1500(1528) bytes of data. -ping: local error: Message too long, mtu=1438 -``` -Then, set the MTU size on your network adapter (wlan0 or eth0): -``` -$ sudo ifconfig wlan0 mtu 1438 -``` +If you change `reduce_mtu` you'll need to deploy a new Algo VPN. -You can also set the `max_mss` variable to a new value in config.cfg, and then redeploy your server rather than reconfigure the current one in-place. +To determine the value for `reduce_mtu` you should examine the MTU on your Algo VPN server's primary network interface (see below). You might algo want to run tests using `ping`, both on a local client *when not connected to the VPN* and also on your Algo VPN server (see below). Then take the smallest MTU you find (local or server side), subtract it from 1500, and use that for `reduce_mtu`. An exception to this is if you find the smallest MTU is your local MTU at 1492, typical for PPPoE connections, then no MTU reduction should be necessary. + +#### Check the MTU on the Algo VPN server + +To check the MTU on your server, SSH in to it, run the command `ifconfig`, and look for the MTU of the main network interface. For example: +``` +ens4: flags=4163 mtu 1460 +``` +The MTU shown here is 1460 instead of 1500. Therefore set `reduce_mtu: 40` in `config.cfg`. Algo should do this automatically. + +#### Determine the MTU using `ping` + +When using `ping` you increase the payload size with the "Don't Fragment" option set until it fails. The largest payload size that works, plus the `ping` overhead of 28, is the MTU of the connection. + +##### Example: Test on your Algo VPN server (Ubuntu) +``` +$ ping -4 -s 1432 -c 1 -M do github.com +PING github.com (192.30.253.112) 1432(1460) bytes of data. +1440 bytes from lb-192-30-253-112-iad.github.com (192.30.253.112): icmp_seq=1 ttl=53 time=13.1 ms + +--- github.com ping statistics --- +1 packets transmitted, 1 received, 0% packet loss, time 0ms +rtt min/avg/max/mdev = 13.135/13.135/13.135/0.000 ms + +$ ping -4 -s 1433 -c 1 -M do github.com +PING github.com (192.30.253.113) 1433(1461) bytes of data. +ping: local error: Message too long, mtu=1460 + +--- github.com ping statistics --- +1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms +``` +In this example the largest payload size that works is 1432. The `ping` overhead is 28 so the MTU is 1432 + 28 = 1460, which is 40 lower than the normal MTU of 1500. Therefore set `reduce_mtu: 40` in `config.cfg`. + +##### Example: Test on a macOS client *not connected to your Algo VPN* +``` +$ ping -c 1 -D -s 1464 github.com +PING github.com (192.30.253.113): 1464 data bytes +1472 bytes from 192.30.253.113: icmp_seq=0 ttl=50 time=169.606 ms + +--- github.com ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = 169.606/169.606/169.606/0.000 ms + +$ ping -c 1 -D -s 1465 github.com +PING github.com (192.30.253.113): 1465 data bytes + +--- github.com ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +``` +In this example the largest payload size that works is 1464. The `ping` overhead is 28 so the MTU is 1464 + 28 = 1492, which is typical for a PPPoE Internet connection and does not require an MTU adjustment. Therefore use the default of `reduce_mtu: 0` in `config.cfg`. + +#### Change the client MTU without redeploying the Algo VPN + +If you don't wish to deploy a new Algo VPN (which is required to incorporate a change to `reduce_mtu`) you can change the client side MTU of WireGuard clients and Linux IPsec clients without needing to make changes to your Algo VPN. + +For WireGuard on Linux, or macOS (when installed with `brew`), you can specify the MTU yourself in the client configuration file (typically `wg0.conf`). Refer to the documentation (see `man wg-quick`). + +For WireGuard on iOS and Android you can change the MTU in the app. + +For IPsec on Linux you can change the MTU of your network interface to match the required MTU. For example: +``` +sudo ifconfig eth0 mtu 1440 +``` +To make the change take affect after a reboot, on Ubuntu 18.04 and later edit the relevant file in the `/etc/netplan` directory (see `man netplan`). ### Clients appear stuck in a reconnection loop diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 29ee3f55..235e3ac9 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -28,3 +28,8 @@ set_fact: ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" tags: always + +- 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 %}" + tags: always diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 49c34e2f..1b487e6d 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -10,8 +10,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if max_mss is defined %} --A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% if reduce_mtu|int > 0 %} +-A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} {% endif %} COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index a6d853f2..6095e211 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -10,10 +10,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if max_mss is defined %} -# MSS is the TCP Max Segment Size -# See rules.v4 for a more complete explanation --A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% if reduce_mtu|int > 0 %} +-A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} {% endif %} COMMIT diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 05bdea00..2aa2b3de 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -2,6 +2,8 @@ PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} Address = {{ wireguard_client_ip }} DNS = {{ wireguard_dns_servers }} +{% if reduce_mtu|int > 0 %}MTU = {{ 1420 - reduce_mtu|int }} +{% endif %} [Peer] PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} From 0e324bf748885d3b13e4154a3ebf0a757158da7e Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Wed, 2 Jan 2019 19:23:37 -0500 Subject: [PATCH 20/55] Update README.md (#1286) Adds Wireguard to the first line. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e87e361..1ea7d616 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) [![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) -Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. +Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC and Wireguard VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. ## Features From f619a260d40cd782c11672a854b05f4b1605c0e2 Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 2 Jan 2019 19:24:18 -0500 Subject: [PATCH 21/55] Sync list of supported cloud hosts (#1278) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea7d616..490bbb9d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack, or your own Ubuntu 18.04 LTS server ## Anti-features @@ -29,7 +29,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or an OpenStack based cloud hosting. +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon Lightsail](https://aws.amazon.com/lightsail/), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/), and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or other OpenStack-based cloud hosting. 2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. From 0ceb4da0f0be1c6b01355fa506ac89ff49e2127b Mon Sep 17 00:00:00 2001 From: Angel Montes de Oca Date: Mon, 7 Jan 2019 23:48:05 -0800 Subject: [PATCH 22/55] Include Algo generated password (#1272) I change a line to Include the Algo generated password so the users do not need to manually enter the password when installing on Windows 10 computers. --- roles/vpn/templates/client_windows.ps1.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4ffce674..4a846f35 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -85,7 +85,7 @@ $CaCertificateBase64 = "{{ PayloadContentCA }}" $UserPkcs12Base64 = "{{ item.1.stdout }}" if ($PsCmdlet.ParameterSetName -eq "Add" -and -not $Pkcs12DecryptionPassword) { - $Pkcs12DecryptionPassword = Read-Host -AsSecureString -Prompt "Pkcs12DecryptionPassword" + $Pkcs12DecryptionPassword = ConvertTo-SecureString '{{ p12_export_password }}' -asplaintext -force } <# From 99cc4305f5b13c30855747e91d2d7b619abd13dc Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 8 Jan 2019 08:53:35 +0100 Subject: [PATCH 23/55] Update deploy-from-ansible.md --- docs/deploy-from-ansible.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index f2809e0c..9d3d3f29 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -179,7 +179,7 @@ Required variables: Required variables: -- [vultr_config](https://github.com/trailofbits/algo/docs/cloud-vultr.md) +- [vultr_config](https://trailofbits.github.io/algo/cloud-vultr.html) - [region](https://api.vultr.com/v1/regions/list) ### Azure From 0a88db7551006fc0add324eec0514d738feb666a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 8 Jan 2019 08:57:40 +0100 Subject: [PATCH 24/55] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 490bbb9d..fa78fd23 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, #### Ubuntu Server 18.04 example -1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan +1. `sudo apt-get install strongswan libstrongswan-standard-plugins`: install strongSwan 2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` 3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//pki/private/.key` 4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//pki/cacert.pem` From e28fe006a217207cd8e190fa937b06bc7015bb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selim=20=C5=9Eumlu?= Date: Tue, 15 Jan 2019 06:23:48 +0300 Subject: [PATCH 25/55] Update deploy-from-windows.md (#1296) Updating the tutorial according to latest Windows 10 and Ubuntu changes --- docs/deploy-from-windows.md | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/deploy-from-windows.md b/docs/deploy-from-windows.md index 33fe1b92..62472b47 100644 --- a/docs/deploy-from-windows.md +++ b/docs/deploy-from-windows.md @@ -1,11 +1,8 @@ -# Windows client prerequisites +# Windows client prerequisite -Before run Algo, you have to have: +* 64-bit Windows 10 (Anniversary update or later version) -* Windows 10 (Anniversary update or later version) -* 64-bit installation (can't run on 32-bit systems) - -Once you verify your system is 64-bit and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': +Once you verify your system is 64-bit (32-bit is not supported) and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': 1. Open 'Settings' 2. Click 'Update & Security', then click the 'For developers' option on the left. @@ -15,22 +12,16 @@ Wait a minute for Windows to install a few things in the background (it will eve 1. Click on 'Programs' 2. Click on 'Turn Windows features on or off' -3. Scroll down and check 'Windows Subsystem for Linux (Beta)', and then click OK. +3. Scroll down and check 'Windows Subsystem for Linux', and then click OK. +4. The subsystem will be installed, then Windows will require a restart. +5. Restart Windows and then [install Ubuntu from the Windows Store](https://www.microsoft.com/p/ubuntu/9nblggh4msv6). +6. Run Ubuntu from the Start menu. It will take a few minutes to install. It will have you create a separate user account for the Linux subsystem. Once that's done, you will finally have Ubuntu running somewhat integrated with Windows. -The subsystem will be installed, then Windows will require a reboot. Reboot, then open up the start menu and enter 'bash' (to open up 'Bash' installation in a new command prompt). Fill out all the questions (it will have you create a separate user account for the Linux subsystem), and once that's all done (it takes a few minutes to install), you will finally have Ubuntu running on your Windows laptop, somewhat integrated with Windows. Install additional packages: ```shell -sudo apt-get update && sudo apt-get install \ - git \ - build-essential \ - libssl-dev \ - libffi-dev \ - python-dev \ - python-pip \ - python-setuptools \ - python-virtualenv -y +sudo apt-get update && sudo apt-get install git build-essential libssl-dev libffi-dev python-dev python-pip python-setuptools python-virtualenv -y ``` Clone the Algo repository: From 0da3ecac6231e8decd59b8b27016a31275dddc7d Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 19 Jan 2019 05:39:08 +0100 Subject: [PATCH 26/55] IPv6 fix (#1302) --- roles/common/tasks/freebsd.yml | 16 ++++++---------- roles/common/tasks/main.yml | 7 ++++--- roles/common/tasks/ubuntu.yml | 13 +++---------- users.yml | 1 + 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 78f47397..70ebe8fa 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,4 +1,10 @@ --- +- name: Gather facts + setup: + +- name: Gather additional facts + import_tasks: facts.yml + - set_fact: config_prefix: "/usr/local/" strongswan_shell: /usr/sbin/nologin @@ -23,17 +29,11 @@ value: 1 - item: "{{ 'net.inet6.ip6.forwarding' if ipv6_support else none }}" value: 1 - tags: - - always - -- setup: - name: Install tools package: name="{{ item }}" state=present with_items: - "{{ tools|default([]) }}" - tags: - - always - name: Loopback included into the rc config blockinfile: @@ -45,8 +45,6 @@ ifconfig_lo100_ipv6="inet6 FCAA::1/64" notify: - restart loopback bsd - tags: - - always - name: Enable the gateway features lineinfile: dest=/etc/rc.conf regexp='^{{ item.param }}.*' line='{{ item.param }}={{ item.value }}' @@ -59,8 +57,6 @@ - { param: natd_flags, value: '"-dynamic -m"' } notify: - restart ipfw - tags: - - always - name: FreeBSD | Activate IPFW shell: > diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 21d51a46..a777eae6 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -6,12 +6,13 @@ - 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' - - - name: Gather additional facts - import_tasks: facts.yml + tags: + - update-users - name: Sysctl tuning sysctl: name="{{ item.item }}" value="{{ item.value }}" diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 6dbc6335..37d469e8 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -71,8 +71,6 @@ dest: /etc/systemd/network/10-algo-lo100.network notify: - restart systemd-networkd - tags: - - always - name: systemd services enabled and started systemd: @@ -83,12 +81,8 @@ with_items: - systemd-networkd - systemd-resolved - tags: - - always - meta: flush_handlers - tags: - - always - name: Check apparmor support shell: apparmor_status @@ -99,6 +93,9 @@ apparmor_enabled: true when: '"profiles are in enforce mode" in apparmor_status.stdout' +- name: Gather additional facts + import_tasks: facts.yml + - set_fact: tools: - git @@ -116,15 +113,11 @@ value: 1 - item: "{{ 'net.ipv6.conf.all.forwarding' if ipv6_support else none }}" value: 1 - tags: - - always - name: Install tools package: name="{{ item }}" state=present with_items: - "{{ tools|default([]) }}" - tags: - - always - name: Install headers apt: diff --git a/users.yml b/users.yml index 30e460ae..64422638 100644 --- a/users.yml +++ b/users.yml @@ -62,6 +62,7 @@ - block: - name: Local pre-tasks import_tasks: playbooks/cloud-pre.yml + become: false rescue: - debug: var=fail_hint tags: always From 07b95808c389b34cdc0407929ffcdf2ebec83734 Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 23 Jan 2019 01:12:43 -0500 Subject: [PATCH 27/55] Document using WireGuard on iOS (#1266) --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa78fd23..1a14dafb 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,27 @@ Certificates and configuration files that users will need are placed in the `con ### Apple Devices -**Send users their Apple Profile.** Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. +Apple devices can connect to an Algo VPN via IPsec using their built-in IPsec support or via WireGuard by installing WireGuard client software. -**Turn on the VPN.** On iOS, connect to the VPN by opening Settings and clicking the toggle next to "VPN" near the top of the list. On macOS, connect to the VPN by opening System Preferences -> Network, finding Algo VPN in the left column and clicking "Connect." On macOS, check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. +#### Install WireGuard -**Managing On-Demand VPNs.** If you enabled "On Demand", the VPN will connect automatically whenever it is able. On iOS, you can turn off "On Demand" by clicking the (i) next to the entry for Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "On Demand" by opening the Network Preferences, finding Algo VPN in the left column, and unchecking the box for "Connect on demand." +On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the App Store. For each user you defined, Algo generated a WireGuard configuration file `wireguard/.conf` and a corresponding QR code image `wireguard/.png`. Either AirDrop the configuration file to the iOS device or use the WireGuard app to scan the QR code. To use "Connect On Demand" with WireGuard enable it by editing the configuration in the WireGuard app. + +Until the WireGuard app for macOS is ready, installing WireGuard on macOS is a little more complicated. See [Using MacOS as a Client with WireGuard](docs/client-macos-wireguard.md). + +#### Configure IPsec + +Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. + +#### Enable the VPN + +On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. + +#### Managing "Connect On Demand" + +If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". + +On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. ### Android Devices From bb7f84163d3d9cad9aab6a6001f6a95f90e55301 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 23 Jan 2019 07:14:37 +0100 Subject: [PATCH 28/55] Fixes #1305 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1a14dafb..45b4f4ab 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,6 @@ Depending on the platform, you may need one or multiple of the following files. * cacert.pem: CA Certificate * user.mobileconfig: Apple Profile * user.p12: User Certificate and Private Key (in PKCS#12 format) -* user.sswan: Android strongSwan Profile * ipsec_user.conf: strongSwan client configuration * ipsec_user.secrets: strongSwan client configuration * windows_user.ps1: Powershell script to help setup a VPN connection on Windows From 5a1a6b369a76c5993458b1f3097da4298b019935 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 24 Jan 2019 13:11:34 +0100 Subject: [PATCH 29/55] Update deploy-from-ansible.md (#1307) --- docs/deploy-from-ansible.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 9d3d3f29..361272d3 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -202,6 +202,28 @@ Required variables: Possible options can be gathered via cli `aws lightsail get-regions` +#### Minimum required IAM permissions for deployment: + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LightsailDeployment", + "Effect": "Allow", + "Action": [ + "lightsail:GetInstance", + "lightsail:CreateInstances", + "lightsail:OpenInstancePublicPorts" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + ### Scaleway Required variables: From aefc730d5fd978e281006e9bc1805d88cffe485a Mon Sep 17 00:00:00 2001 From: Luvpreet Singh Date: Sat, 26 Jan 2019 03:06:44 +0530 Subject: [PATCH 30/55] fix(update-users): changed generate p12 password task (#1289) Changed task's module to generic python format for python2 and python3. --- roles/common/tasks/facts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 235e3ac9..6e79bfce 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -9,7 +9,7 @@ - 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())])' + 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 From 1e0bbdb5366b15aadc3813fee47c9f7f27ccfb81 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 28 Jan 2019 13:42:11 +0100 Subject: [PATCH 31/55] Travis-CI fixes --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6fa1d0fc..14036294 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN apk --no-cache add ${BUILD_PACKAGES} && \ python -m pip --no-cache-dir install virtualenv && \ python -m virtualenv env && \ source env/bin/activate && \ - python -m pip --no-cache-dir install -r requirements.txt && \ + python -m pip --no-cache-dir install -r requirements.txt --no-use-pep51 && \ apk del ${BUILD_PACKAGES} COPY . . RUN chmod 0755 /algo/algo-docker.sh From b42a08d9d5a9e51a6d123c3eae8f1b5921049860 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 28 Jan 2019 23:50:58 +0100 Subject: [PATCH 32/55] Allow windows users install VPN for all users in the system (#1310) --- docs/client-windows.md | 7 +++++++ roles/vpn/templates/client_windows.ps1.j2 | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/client-windows.md b/docs/client-windows.md index 53b62f22..323da8df 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -10,6 +10,13 @@ To install automatically, use the generated user Powershell script. ```powershell powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add ``` + +If you have more than one account on your Windows 10 machine (e.g. one with administrator privileges and one without) and would like to have the VPN connection available to all users, pass the parameter `-AllUsers` + +```powershell +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add -AllUsers +``` + 4. The command has help information available. To view its full help, run this from Powershell: ```powershell Get-Help -Name .\windows_USER.ps1 -Full | more diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4a846f35..e1021bbe 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -29,6 +29,9 @@ Note that this must be passed in as a SecureString, not a regular string. You can create a secure string with the `Read-Host -AsSecureString` cmdlet. See the examples for more information. +.PARAMETER AllUsers +Allow all users to use the VPN + .EXAMPLE client_USER.ps1 -Add @@ -63,6 +66,9 @@ Save the embedded CA cert and encrypted user PKCS12 file. [Parameter(ParameterSetName="Add")] [SecureString] $Pkcs12DecryptionPassword, + [Parameter(ParameterSetName="Add")] + [Switch] $AllUsers = $false, + [Parameter(Mandatory, ParameterSetName="Remove")] [Switch] $Remove, @@ -164,6 +170,7 @@ function Add-AlgoVPN { TunnelType = "IKEv2" AuthenticationMethod = "MachineCertificate" EncryptionLevel = "Required" + AllUserConnection = $AllUsers } Add-VpnConnection @addVpnParams From 95a67475ef44f76abd8667acf61f2b75e0dabd93 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 28 Jan 2019 23:51:28 +0100 Subject: [PATCH 33/55] encode wifi networks to base64 (#1303) --- roles/vpn/templates/mobileconfig.j2 | 4 ++-- server.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 8a0bb5f6..b48500c2 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -12,8 +12,8 @@ 1 OnDemandRules -{% if algo_ondemand_wifi_exclude != '_null' %} -{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|string).split(',') %} +{% if algo_ondemand_wifi_exclude|b64decode != '_null' %} +{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} Action Disconnect diff --git a/server.yml b/server.yml index b6e8340b..f643f4f8 100644 --- a/server.yml +++ b/server.yml @@ -49,7 +49,7 @@ 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_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude | b64encode }} algo_local_dns: {{ algo_local_dns }} algo_ssh_tunneling: {{ algo_ssh_tunneling }} algo_windows: {{ algo_windows }} From 8db83551d298ad624401846d8da292bc97452bdf Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 30 Jan 2019 07:23:11 +0100 Subject: [PATCH 34/55] add flags=(attach_disconnected) to dnscrypt-proxy apparmor profile (#1312) --- roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy index 7e900bc5..c2258688 100644 --- a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -1,6 +1,6 @@ #include -/usr/bin/dnscrypt-proxy { +/usr/bin/dnscrypt-proxy flags=(attach_disconnected) { #include #include #include From 763ef29e5b71d72762f3ed4b0a2e616f0cee1b81 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 4 Feb 2019 16:04:30 +0100 Subject: [PATCH 35/55] WiFi exclude list fix (#1318) --- input.yml | 8 ++++---- server.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/input.yml b/input.yml index f24ab2ba..b6e5e81c 100644 --- a/input.yml +++ b/input.yml @@ -116,10 +116,10 @@ {% if ondemand_wifi is defined %}{{ ondemand_wifi | bool }} {%- elif _ondemand_wifi.user_input is defined and _ondemand_wifi.user_input != "" %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} {%- else %}false{% endif %} - algo_ondemand_wifi_exclude: >- - {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude }} - {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input }} - {%- else %}_null{% endif %} + algo_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']) }} diff --git a/server.yml b/server.yml index f643f4f8..b6e8340b 100644 --- a/server.yml +++ b/server.yml @@ -49,7 +49,7 @@ 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 | b64encode }} + algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude }} algo_local_dns: {{ algo_local_dns }} algo_ssh_tunneling: {{ algo_ssh_tunneling }} algo_windows: {{ algo_windows }} From 4279c3552ed2303d33632f19127afc5a1ccf0b4c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 7 Feb 2019 15:09:09 +0100 Subject: [PATCH 36/55] Closes #1321 --- docs/deploy-from-ansible.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 361272d3..ccbb05e2 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -212,6 +212,7 @@ Possible options can be gathered via cli `aws lightsail get-regions` "Sid": "LightsailDeployment", "Effect": "Allow", "Action": [ + "lightsail:GetRegions", "lightsail:GetInstance", "lightsail:CreateInstances", "lightsail:OpenInstancePublicPorts" From 0c0bbc94bbe5c54f9794ba0b6275c2cb923d0cb6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 8 Feb 2019 13:34:01 +0100 Subject: [PATCH 37/55] Get started with Azure more easily (#1323) --- docs/cloud-azure.md | 102 ++++++++++++++-------------- roles/cloud-azure/tasks/prompts.yml | 48 ++----------- 2 files changed, 56 insertions(+), 94 deletions(-) diff --git a/docs/cloud-azure.md b/docs/cloud-azure.md index 261f4bcf..22239d6b 100644 --- a/docs/cloud-azure.md +++ b/docs/cloud-azure.md @@ -1,58 +1,60 @@ # Azure cloud setup -| Instruction | Screenshot(s) | -| ----------- | ---------- | -| 1. Go to https://portal.azure.com/ | | -| 2. Go to **Azure Active Directory** | [![step2-thumb]][step2-screen] | -| 3. Go to **App registrations** and click to **Add** | [![step3-thumb]][step3-screen] | -| 4. Fill out the forms and click **Create** | [![step4-thumb]][step4-screen] | -| 5. Click on the app name | [![step5-thumb]][step5-screen] | -| 6. Copy and save somewhere the **Application ID** and click on **Keys**. | [![step6-thumb]][step6-screen] | -| 7. Fill out the forms and click **Save**. Copy and save somewhere the **Secret ID** (the value) | [![step7-thumb]][step7-screen] | -| 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | -| 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | -| 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | -| 11. Select a role (Contributor will be sufficient)| [![step11-thumb]][step11-screen] | -| 12. Next, switch to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | +The easiest way to get started with the Azure CLI is by running it in an Azure Cloud Shell environment through your browser. -Now you can use Environment Variables: +Here you can find some information from [the official doc](https://docs.microsoft.com/en-us/cli/azure/get-started-with-azure-cli?view=azure-cli-latest). We put the essential commands together for simplest usage. -* AZURE_CLIENT_ID - from the 6th step -* AZURE_SECRET - from the 7th step -* AZURE_TENANT - from the 8th step -* AZURE_SUBSCRIPTION_ID - from the 9th step +## Install azure-cli -or create the credentials file ``~/.azure/credentials`: +- macOS ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-macos?view=azure-cli-latest)): + ```bash + $ brew update && brew install azure-cli + ``` -``` -[default] -client_id= -secret= -tenant= -subscription_id= -``` -or just pass those values to the Algo script +- Linux (deb-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest)): + ```bash + $ sudo apt-get update && sudo apt-get install \ + apt-transport-https \ + lsb-release \ + software-properties-common \ + dirmngr -y + $ AZ_REPO=$(lsb_release -cs) + $ echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \ + sudo tee /etc/apt/sources.list.d/azure-cli.list + $ sudo apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv \ + --keyserver packages.microsoft.com \ + --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF + $ sudo apt-get update + $ sudo apt-get install azure-cli + ``` -[step2-screen]: http://i.imgur.com/ENvSupE.png -[step3-screen]: http://i.imgur.com/sPLQaQe.jpg -[step4-screen]: http://i.imgur.com/di3xFCM.jpg -[step5-screen]: http://i.imgur.com/SipQyRA.jpg -[step6-screen]: http://i.imgur.com/RRTqV7C.jpg -[step7-screen]: http://i.imgur.com/ZnqJeVv.jpg -[step8-screen]: http://i.imgur.com/WAS8Ovl.png -[step9-screen]: http://i.imgur.com/IvTN7o1.jpg -[step10-screen]: http://i.imgur.com/j6dgo75.png -[step11-screen]: http://i.imgur.com/NUJ6k7i.jpg -[step12-screen]: http://i.imgur.com/VZv5qwb.jpg +- Linux (rpm-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-yum?view=azure-cli-latest)): + ```bash + $ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc + $ sudo sh -c 'echo -e "[azure-cli]\nname=Azure CLI\nbaseurl=https://packages.microsoft.com/yumrepos/azure-cli\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/azure-cli.repo' + $ sudo yum install azure-cli + ``` -[step2-thumb]: https://i.imgur.com/ENvSupEm.png -[step3-thumb]: https://i.imgur.com/sPLQaQem.jpg -[step4-thumb]: https://i.imgur.com/di3xFCMm.jpg -[step5-thumb]: https://i.imgur.com/SipQyRAm.jpg -[step6-thumb]: https://i.imgur.com/RRTqV7Cm.jpg -[step7-thumb]: https://i.imgur.com/ZnqJeVvm.jpg -[step8-thumb]: https://i.imgur.com/WAS8Ovlm.png -[step9-thumb]: https://i.imgur.com/IvTN7o1m.jpg -[step10-thumb]: https://i.imgur.com/j6dgo75m.png -[step11-thumb]: https://i.imgur.com/NUJ6k7im.jpg -[step12-thumb]: https://i.imgur.com/VZv5qwbm.jpg +- Windows ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest)): + For Windows the Azure CLI is installed via an MSI, which gives you access to the CLI through the Windows Command Prompt (CMD) or PowerShell. When installing for Windows Subsystem for Linux (WSL), packages are available for your Linux distribution. [Download the MSI installer](https://aka.ms/installazurecliwindows) + +If your OS is missing or to get more information see [the official doc](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) + + +## Sign in + +1. Run the `login` command: +```bash +az login +``` + + If the CLI can open your default browser, it will do so and load a sign-in page. + + Otherwise, you need to open a browser page and follow the instructions on the command line to enter an authorization code after navigating to https://aka.ms/devicelogin in your browser. + +2. Sign in with your account credentials in the browser. + +There are ways to sign in non-interactively, which are covered in detail in [Sign in with Azure CLI](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest). + + +**Now you are able to deploy an AlgoVPN instance without hassle** diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml index 28d42521..09717205 100644 --- a/roles/cloud-azure/tasks/prompts.yml +++ b/roles/cloud-azure/tasks/prompts.yml @@ -1,49 +1,9 @@ --- -- pause: - prompt: | - Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_secret - when: - - azure_secret is undefined - - lookup('env','AZURE_SECRET')|length <= 0 - -- pause: - prompt: | - Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_tenant - when: - - azure_tenant is undefined - - lookup('env','AZURE_TENANT')|length <= 0 - -- pause: - prompt: | - Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_client_id - when: - - azure_client_id is undefined - - lookup('env','AZURE_CLIENT_ID')|length <= 0 - -- pause: - prompt: | - Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_subscription_id - when: - - azure_subscription_id is undefined - - lookup('env','AZURE_SUBSCRIPTION_ID')|length <= 0 - - set_fact: - secret: "{{ azure_secret | default(_azure_secret.user_input|default(None)) | default(lookup('env','AZURE_SECRET'), true) }}" - tenant: "{{ azure_tenant | default(_azure_tenant.user_input|default(None)) | default(lookup('env','AZURE_TENANT'), true) }}" - client_id: "{{ azure_client_id | default(_azure_client_id.user_input|default(None)) | default(lookup('env','AZURE_CLIENT_ID'), true) }}" - subscription_id: "{{ azure_subscription_id | default(_azure_subscription_id.user_input|default(None)) | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" - block: - name: Set facts about the regions From 4be614fa35ec1a041b4feaa886eaf98650206e0f Mon Sep 17 00:00:00 2001 From: David Myers Date: Tue, 12 Feb 2019 05:19:38 -0500 Subject: [PATCH 38/55] Add note about new WireGuard for iOS default MTU (#1293) --- docs/troubleshooting.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index fa0472f2..493661cf 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -391,6 +391,10 @@ sudo ifconfig eth0 mtu 1440 ``` To make the change take affect after a reboot, on Ubuntu 18.04 and later edit the relevant file in the `/etc/netplan` directory (see `man netplan`). +#### Note for WireGuard iOS users + +As of WireGuard for iOS 0.0.20190107 the default MTU is 1280, a conservative value intended to allow mobile devices to continue to work as they switch between different networks which might have smaller than normal MTUs. In order to use this default MTU review the configuration in the WireGuard app and remove any value for MTU that might have been added automatically by Algo. + ### Clients appear stuck in a reconnection loop If you're using 'Connect on Demand' on iOS and your client device appears stuck in a reconnection loop after switching from WiFi to LTE or vice versa, you may want to try disabling DoS protection in strongSwan. From aac64a257cce17e02afa7d64544de585941704df Mon Sep 17 00:00:00 2001 From: David Myers Date: Sun, 17 Feb 2019 18:38:19 -0500 Subject: [PATCH 39/55] Document using WireGuard app on macOS (#1327) * Document using WireGuard app on macOS * Update README.md * Make WireGuard the default for Apple devices * clarify user list * fix tests * connect on demand --- README.md | 23 ++++++----------------- config.cfg | 15 +++++++-------- docs/client-apple-ipsec.md | 15 +++++++++++++++ docs/client-macos-wireguard.md | 19 +++++++++++-------- tests/update-users.sh | 2 +- 5 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 docs/client-apple-ipsec.md diff --git a/README.md b/README.md index 45b4f4ab..be4ce6be 100644 --- a/README.md +++ b/README.md @@ -89,27 +89,15 @@ Certificates and configuration files that users will need are placed in the `con ### Apple Devices -Apple devices can connect to an Algo VPN via IPsec using their built-in IPsec support or via WireGuard by installing WireGuard client software. +WireGuard is used to provide VPN services on Apple devices. Algo generates a WireGuard configuration file, `wireguard/.conf`, and a QR code, `wireguard/.png`, for each user defined in `config.cfg`. -#### Install WireGuard +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 iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the App Store. For each user you defined, Algo generated a WireGuard configuration file `wireguard/.conf` and a corresponding QR code image `wireguard/.png`. Either AirDrop the configuration file to the iOS device or use the WireGuard app to scan the QR code. To use "Connect On Demand" with WireGuard enable it by editing the 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. Enable "Connect on Demand" by editing the tunnel configuration in the WireGuard app. -Until the WireGuard app for macOS is ready, installing WireGuard on macOS is a little more complicated. See [Using MacOS as a Client with WireGuard](docs/client-macos-wireguard.md). +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). -#### Configure IPsec - -Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. - -#### Enable the VPN - -On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. - -#### Managing "Connect On Demand" - -If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". - -On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. +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). ### Android Devices @@ -208,6 +196,7 @@ After this process completes, the Algo VPN server will contain only the users li - 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) diff --git a/config.cfg b/config.cfg index b0c7756d..3f5bdcb9 100644 --- a/config.cfg +++ b/config.cfg @@ -1,15 +1,14 @@ --- -# Add up to 250 users here. -# For each user, configuration files will be generated for both an IPsec -# connection and a WireGuard connection. Multiple client devices can share an -# IPsec configuration but WireGuard clients must each use a unique -# WireGuard configuration. +# This is the list of user to generate. +# Every device must have a unique username. +# You can generate up to 250 users at one time. users: - - dan - - jack + - phone + - laptop + - desktop -# NOTE: If your usernames have leading 0's, like "000dan", you have to escape them +# NOTE: You must "escape" any usernames with leading 0's, like "000dan" ### Advanced users only below this line ### diff --git a/docs/client-apple-ipsec.md b/docs/client-apple-ipsec.md new file mode 100644 index 00000000..e740b231 --- /dev/null +++ b/docs/client-apple-ipsec.md @@ -0,0 +1,15 @@ +# Using the built-in IPSEC VPN on Apple Devices + +## Configure IPsec + +Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. + +## Enable the VPN + +On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. + +## Managing "Connect On Demand" + +If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". + +On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. \ No newline at end of file diff --git a/docs/client-macos-wireguard.md b/docs/client-macos-wireguard.md index 0d1db781..cce6ccc4 100644 --- a/docs/client-macos-wireguard.md +++ b/docs/client-macos-wireguard.md @@ -1,31 +1,34 @@ -# Using MacOS as a Client with WireGuard +# MacOS WireGuard Client Setup + +The WireGuard macOS app is unavailable for older operating systems. Please update your operating system if you can. If you are on a macOS High Sierra (10.13) or earlier, then you can still use WireGuard via their userspace drivers via the process detailed below. ## Install WireGuard -To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from MacOS +Install the wireguard-go userspace driver: ``` -# Install the wireguard-go userspace driver brew install wireguard-tools ``` ## Locate the Config File -The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your device. +Algo generates a WireGuard configuration file, `wireguard/.conf`, and a QR code, `wireguard/.png`, for each user defined in `config.cfg`. Find the configuration file and copy it to your device if you don't already have it. + +Note that each client you use to connect to Algo VPN must have a unique WireGuard config. ## Configure WireGuard -Finally, install the config file on your client as `/usr/local/etc/wireguard/wg0.conf` and start WireGuard: +You'll need to copy the appropriate WireGuard configuration file into a location where the userspace driver can find it. After it is in the right place, start the VPN, and verify connectivity. ``` -# Install the config file to the WireGuard configuration directory on your MacOS device +# Copy the config file to the WireGuard configuration directory on your macOS device mkdir /usr/local/etc/wireguard/ cp .conf /usr/local/etc/wireguard/wg0.conf -# Start the WireGuard VPN: +# Start the WireGuard VPN sudo wg-quick up wg0 -# Verify the connection to the Algo VPN: +# Verify the connection to the Algo VPN wg # See that your client is using the IP address of your Algo VPN: diff --git a/tests/update-users.sh b/tests/update-users.sh index ba40bb33..2387103f 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -11,7 +11,7 @@ else ansible-playbook users.yml -e "${USER_ARGS}" -t update-users fi -if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.crt | grep CRL +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.crt | grep CRL then echo "The CRL check passed" else From aa753419d490010886b1e9c604d7cb47567ebcee Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 20 Feb 2019 10:08:25 -0500 Subject: [PATCH 40/55] Clarify prompts (#1331) --- input.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/input.yml b/input.yml index b6e5e81c..34ae4f66 100644 --- a/input.yml +++ b/input.yml @@ -51,21 +51,21 @@ - pause: prompt: | - Do you want macOS/iOS clients to enable "VPN On Demand" when connected to cellular networks? + 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 clients to enable "VPN On Demand" when connected to Wi-Fi? + 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 trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN + 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: @@ -75,7 +75,7 @@ - pause: prompt: | - Do you want to install a DNS resolver on this VPN server, to block ads while surfing? + Do you want to install an ad blocking DNS resolver on this VPN server? [y/N] register: _local_dns when: local_dns is undefined From 3d15d971445f2f76f41988784a05d3b464d5455b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 22 Feb 2019 16:00:47 +0100 Subject: [PATCH 41/55] Closes #1059 --- docs/client-windows.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 323da8df..62d73cf2 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -29,7 +29,7 @@ Get-Help -Name .\windows_USER.ps1 -Full | more 3. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. ```powershell -Set-ExecutionPolicy Unrestricted -Scope CurrentUser +Set-ExecutionPolicy Unrestricted -Scope Process ``` 4. In the same window, run the necessary commands to install the certificates and create the VPN configuration. Note the lines at the top defining the VPN address, USER.p12 file location, and CA certificate location - change those lines to the IP address of your Algo server and the location you saved those two files. Also note that it will prompt for the "User p12 password", which is printed at the end of a successful Algo deployment. @@ -69,10 +69,4 @@ Set-VpnConnectionIPsecConfiguration @setVpnParams ``` -5. After you execute the user script, set the Execution Policy back before you close the PowerShell window. - -```powershell -Set-ExecutionPolicy Restricted -Scope CurrentUser -``` - Your VPN is now installed and ready to use. From ee362ca9ba88dffefd58df7f01f8f7220e3b819c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 25 Feb 2019 17:56:19 +0100 Subject: [PATCH 42/55] Disable wireguard PersistentKeepalive by default (#1338) --- config.cfg | 8 ++++++-- roles/wireguard/defaults/main.yml | 1 + roles/wireguard/templates/client.conf.j2 | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config.cfg b/config.cfg index 3f5bdcb9..15aa586e 100644 --- a/config.cfg +++ b/config.cfg @@ -22,6 +22,10 @@ vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true wireguard_port: 51820 +# If you're behind NAT or a firewall and you want to receive incoming connections long after network traffic has gone silent. +# This option will keep the "connection" open in the eyes of NAT. +# See: https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence +wireguard_PersistentKeepalive: 0 # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission @@ -36,9 +40,9 @@ reduce_mtu: 0 # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 -# Algo will use the following lists to block ads. You can add new block lists +# Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: -# /usr/local/sbin/adblock.sh +# /usr/local/sbin/adblock.sh # If you load very large blocklists, you may also have to modify resource limits: # /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf adblock_lists: diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 90da64f5..7961c6a9 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -1,3 +1,4 @@ --- +wireguard_PersistentKeepalive: 0 wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 2aa2b3de..b601abb5 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -9,4 +9,4 @@ DNS = {{ wireguard_dns_servers }} PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} -PersistentKeepalive = 25 +{{ 'PersistentKeepalive = ' + wireguard_PersistentKeepalive|string if wireguard_PersistentKeepalive > 0 else '' }} From 80f03c5d42b5906f1af81eae5c4a7a398941a4c7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 25 Feb 2019 17:58:09 +0100 Subject: [PATCH 43/55] Support for custom domain names in the endpoint (#1337) --- docs/deploy-from-ansible.md | 2 +- playbooks/cloud-post.yml | 2 +- roles/local/tasks/prompts.yml | 2 +- roles/vpn/defaults/main.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index ccbb05e2..9816f0bd 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -248,7 +248,7 @@ You need to source the rc file prior to run Algo. Download it from the OpenStack Required variables: - server - IP or hostname to access the server via SSH -- endpoint - Public IP address of your server +- endpoint - Public IP address or domain name of your server - ssh_user diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index 283ed60a..15611539 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -1,7 +1,7 @@ --- - name: Set subjectAltName as afact set_fact: - IP_subject_alt_name: "{% if algo_provider == 'local' %}{{ IP_subject_alt_name }}{% else %}{{ cloud_instance_ip }}{% endif %}" + IP_subject_alt_name: "{{ (IP_subject_alt_name if algo_provider == 'local' else cloud_instance_ip) | lower }}" - name: Add the server to an inventory group add_host: diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml index 1f5edc2e..a12b8807 100644 --- a/roles/local/tasks/prompts.yml +++ b/roles/local/tasks/prompts.yml @@ -31,7 +31,7 @@ - pause: prompt: | - Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) + Enter the public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate) [{{ cloud_instance_ip }}] register: _endpoint when: endpoint is undefined diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index a865dfb4..c9b81ce0 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -35,7 +35,7 @@ algo_local_dns: false ipv6_support: false dns_encryption: true domain: false -subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" +subjectAltName_IP: "{{ 'DNS:' if IP_subject_alt_name|regex_search('[a-z]') else 'IP:' }}{{ IP_subject_alt_name }}" subjectAltName_USER: "{% if '@' in item %}email:{{ item }}{% else %}DNS:{{ item }}{% endif %}" openssl_bin: openssl strongswan_enabled_plugins: From e06a087d57e4afb5aeaf1885c7961f76db3979d9 Mon Sep 17 00:00:00 2001 From: Demian Date: Tue, 26 Feb 2019 12:19:34 +0100 Subject: [PATCH 44/55] fix OS is undefined error (#1335) --- roles/common/tasks/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index a777eae6..fcb5af11 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -3,6 +3,8 @@ - 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' From 2e3c27f643c89586972fb39925829ea1bf907fa2 Mon Sep 17 00:00:00 2001 From: Tim H <6026716+tho@users.noreply.github.com> Date: Tue, 26 Feb 2019 11:40:29 -0500 Subject: [PATCH 45/55] Add catch-all VPN On Demand Rule (#739) If a user is not connected to a trusted Wi-Fi network or if the URLStringProbe fails none of the existing dictionaries match. According to the Apple Configuration Profile Reference[1] section "VPN Payload > On Demand Rules Dictionary Keys" a default behavior for unknown networks with no matching criteria should always be set as the last dictionary in the array. The current default behavior is to allow a connection to occur, but this behavior is not guaranteed. Tear down the VPN connection and do not reconnect on demand as long as the catch-all dictionary matches to guarantee the default behavior and more specifically allow users to access captive portals. [1]: https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html --- roles/vpn/templates/mobileconfig.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index b48500c2..686ed7e8 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -52,6 +52,10 @@ URLStringProbe http://captive.apple.com/hotspot-detect.html + + Action + Disconnect + {% else %} {% endif %} From e4ccc18ab0850bf96d3ef4e42ecbdd336f1ad357 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 6 Mar 2019 13:04:20 +0100 Subject: [PATCH 46/55] Update cloud-pre.yml --- playbooks/cloud-pre.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index 338e70dd..6a8071b0 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -13,6 +13,7 @@ 'wireguard_enabled "{{ wireguard_enabled }}"' \ 'dns_encryption "{{ dns_encryption }}"' \ > /dev/tty + tags: debug - name: Install the requirements local_action: From 9b6da9175bf64fafa7e23053b526ac3554f6f179 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 10 Mar 2019 18:16:34 +0100 Subject: [PATCH 47/55] Refactoring (#1334) ## Description Renames the vpn role to strongswan, and split up the variables to support 2 separate VPNs. Closes #1330 and closes #1162 Configures Ansible to use python3 on the server side. Closes #1024 Removes unneeded playbooks, reorganises a lot of variables Reorganises the `config` folder. Closes #1330
Here is how the config directory looks like now

``` configs/X.X.X.X/ |-- ipsec | |-- apple | | |-- desktop.mobileconfig | | |-- laptop.mobileconfig | | `-- phone.mobileconfig | |-- manual | | |-- cacert.pem | | |-- desktop.p12 | | |-- desktop.ssh.pem | | |-- ipsec_desktop.conf | | |-- ipsec_desktop.secrets | | |-- ipsec_laptop.conf | | |-- ipsec_laptop.secrets | | |-- ipsec_phone.conf | | |-- ipsec_phone.secrets | | |-- laptop.p12 | | |-- laptop.ssh.pem | | |-- phone.p12 | | `-- phone.ssh.pem | `-- windows | |-- desktop.ps1 | |-- laptop.ps1 | `-- phone.ps1 |-- ssh-tunnel | |-- desktop.pem | |-- desktop.pub | |-- laptop.pem | |-- laptop.pub | |-- phone.pem | |-- phone.pub | `-- ssh_config `-- wireguard |-- desktop.conf |-- desktop.png |-- laptop.conf |-- laptop.png |-- phone.conf `-- phone.png ``` ![finder](https://i.imgur.com/FtOmKO0.png)

## Motivation and Context This refactoring is focused to aim to the 1.0 release ## How Has This Been Tested? Deployed to several cloud providers with various options enabled and disabled ## Types of changes - [x] Refactoring ## Checklist: - [x] I have read the **CONTRIBUTING** document. - [x] My code follows the code style of this project. - [x] My change requires a change to the documentation. - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed. --- README.md | 8 +- config.cfg | 18 ++- input.yml | 71 +++++----- playbooks/cloud-post.yml | 2 +- playbooks/win_script_rebuild.yml | 67 --------- roles/client/tasks/main.yml | 6 +- roles/common/handlers/main.yml | 6 + roles/common/tasks/freebsd.yml | 13 +- roles/{vpn => common}/tasks/iptables.yml | 0 roles/common/tasks/ubuntu.yml | 35 ++--- roles/{vpn => common}/templates/rules.v4.j2 | 31 ++-- roles/{vpn => common}/templates/rules.v6.j2 | 24 ++-- roles/ssh_tunneling/defaults/main.yml | 2 + roles/ssh_tunneling/tasks/main.yml | 134 +++++++++++------- roles/ssh_tunneling/templates/known_hosts.j2 | 3 - roles/{vpn => strongswan}/defaults/main.yml | 32 ++--- roles/{vpn => strongswan}/handlers/main.yml | 6 - roles/{vpn => strongswan}/meta/main.yml | 0 .../tasks/client_configs.yml | 17 +-- roles/strongswan/tasks/distribute_keys.yml | 27 ++++ .../tasks/ipsec_configuration.yml | 8 +- roles/{vpn => strongswan}/tasks/main.yml | 6 - roles/{vpn => strongswan}/tasks/openssl.yml | 59 ++++---- roles/{vpn => strongswan}/tasks/ubuntu.yml | 3 - .../templates/100-CustomLimitations.conf.j2 | 0 .../templates/client_ipsec.conf.j2 | 0 .../templates/client_ipsec.secrets.j2 | 0 .../templates/client_windows.ps1.j2 | 0 .../templates/ipsec.conf.j2 | 2 +- .../templates/ipsec.secrets.j2 | 0 .../templates/mobileconfig.j2 | 0 .../templates/openssl.cnf.j2 | 0 .../templates/strongswan.conf.j2 | 0 roles/vpn/tasks/distribute_keys.yml | 27 ---- roles/wireguard/defaults/main.yml | 28 +++- roles/wireguard/tasks/keys.yml | 6 +- roles/wireguard/tasks/main.yml | 6 +- roles/wireguard/templates/client.conf.j2 | 4 +- roles/wireguard/templates/server.conf.j2 | 6 +- server.yml | 21 +-- tests/update-users.sh | 32 ++++- users.yml | 15 +- 42 files changed, 360 insertions(+), 365 deletions(-) delete mode 100644 playbooks/win_script_rebuild.yml rename roles/{vpn => common}/tasks/iptables.yml (100%) rename roles/{vpn => common}/templates/rules.v4.j2 (69%) rename roles/{vpn => common}/templates/rules.v6.j2 (77%) create mode 100644 roles/ssh_tunneling/defaults/main.yml delete mode 100644 roles/ssh_tunneling/templates/known_hosts.j2 rename roles/{vpn => strongswan}/defaults/main.yml (51%) rename roles/{vpn => strongswan}/handlers/main.yml (63%) rename roles/{vpn => strongswan}/meta/main.yml (100%) rename roles/{vpn => strongswan}/tasks/client_configs.yml (65%) create mode 100644 roles/strongswan/tasks/distribute_keys.yml rename roles/{vpn => strongswan}/tasks/ipsec_configuration.yml (87%) rename roles/{vpn => strongswan}/tasks/main.yml (84%) rename roles/{vpn => strongswan}/tasks/openssl.yml (79%) rename roles/{vpn => strongswan}/tasks/ubuntu.yml (95%) rename roles/{vpn => strongswan}/templates/100-CustomLimitations.conf.j2 (100%) rename roles/{vpn => strongswan}/templates/client_ipsec.conf.j2 (100%) rename roles/{vpn => strongswan}/templates/client_ipsec.secrets.j2 (100%) rename roles/{vpn => strongswan}/templates/client_windows.ps1.j2 (100%) rename roles/{vpn => strongswan}/templates/ipsec.conf.j2 (94%) rename roles/{vpn => strongswan}/templates/ipsec.secrets.j2 (100%) rename roles/{vpn => strongswan}/templates/mobileconfig.j2 (100%) rename roles/{vpn => strongswan}/templates/openssl.cnf.j2 (100%) rename roles/{vpn => strongswan}/templates/strongswan.conf.j2 (100%) delete mode 100644 roles/vpn/tasks/distribute_keys.yml diff --git a/README.md b/README.md index be4ce6be..2308c31c 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, #### Ubuntu Server 18.04 example 1. `sudo apt-get install strongswan libstrongswan-standard-plugins`: install strongSwan -2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` -3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//pki/private/.key` -4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//pki/cacert.pem` +2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//ipsec/manual/.crt` +3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//ipsec/manual/.key` +4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//ipsec/manual/cacert.pem` 5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. ` : ECDSA .key` 6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and ensure `leftcert` matches the `.crt` filename 7. `sudo ipsec restart`: pick up config changes @@ -160,7 +160,7 @@ If you turned on the optional SSH tunneling role, then local user accounts will Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. - `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` + `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs//ssh-tunnel/.pem` ## SSH into Algo Server diff --git a/config.cfg b/config.cfg index 15aa586e..bfcaac68 100644 --- a/config.cfg +++ b/config.cfg @@ -18,8 +18,14 @@ keys_clean_all: False # Clean up cloud python environments clean_environment: false -vpn_network: 10.19.48.0/24 -vpn_network_ipv6: 'fd9d:bc11:4020::/48' +# Deploy StrongSwan to enable IPsec support +ipsec_enabled: true + +# StrongSwan log level +# https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration +strongswan_log_level: 2 + +# Deploy WireGuard wireguard_enabled: true wireguard_port: 51820 # If you're behind NAT or a firewall and you want to receive incoming connections long after network traffic has gone silent. @@ -36,10 +42,6 @@ wireguard_PersistentKeepalive: 0 # See: https://github.com/trailofbits/algo/blob/master/docs/troubleshooting.md#various-websites-appear-to-be-offline-through-the-vpn reduce_mtu: 0 -# StrongSwan log level -# https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration -strongswan_log_level: 2 - # Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: # /usr/local/sbin/adblock.sh @@ -90,10 +92,6 @@ unattended_reboot: enabled: false time: 06:00 -pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" -VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" -CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" - # Block traffic between connected clients BetweenClients_DROP: true diff --git a/input.yml b/input.yml index 34ae4f66..5cc60170 100644 --- a/input.yml +++ b/input.yml @@ -48,30 +48,45 @@ 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 - - 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: | - 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: | - 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: | @@ -87,20 +102,6 @@ register: _ssh_tunneling when: ssh_tunneling is undefined - - pause: - prompt: | - Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) - [y/N] - register: _windows - when: windows is undefined - - - pause: - prompt: | - Do you want to retain the CA key? (required to add users in the future, but less secure) - [y/N] - register: _store_cakey - when: store_cakey is undefined - - name: Set facts based on the input set_fact: algo_server_name: >- diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index 15611539..0ada114d 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -9,7 +9,7 @@ groups: vpn-host ansible_connection: "{% if cloud_instance_ip == 'localhost' %}local{% else %}ssh{% endif %}" ansible_ssh_user: "{{ ansible_ssh_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" + ansible_python_interpreter: "/usr/bin/python3" algo_provider: "{{ algo_provider }}" algo_server_name: "{{ algo_server_name }}" algo_ondemand_cellular: "{{ algo_ondemand_cellular }}" diff --git a/playbooks/win_script_rebuild.yml b/playbooks/win_script_rebuild.yml deleted file mode 100644 index 12bdfe91..00000000 --- a/playbooks/win_script_rebuild.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- - -# This playbook is designed to help when modifying the Windows script template -# in roles/vpn/templates/client_windows.ps1.j2 -# It rebuilds the client_USER.ps1 scripts for each user defined in config.cfg, -# without redeploying users or opening an SSH connection to the Algo server at -# all. -# -# This playbook is _not_ part of a normal Algo deployment. -# It is only intended to speed up development of the client_USER.ps1 Windows -# Algo install scripts. -# -# REQUIREMENTS -# - Algo must have been deployed once -# - Windows users must have been enabled at deployment time -# - All users defined in config.cfg must not have changed -# - Only one Algo deployment exists in the configs/ directory -# - There must be exactly one subfolder in the configs/ directory: -# the folder named after the IP of the algo server - -- hosts: localhost - gather_facts: False - tags: always - vars_files: - - ../config.cfg - - tasks: - - - name: Get config subdir - shell: find ../configs/* -maxdepth 0 -type d | sed 's/.*\///' - register: config_subdir_result - - fail: - msg: - - "Found wrong number of config subdirs... stdout:" - - "{{ config_subdir_result.split('\n') }}" - when: config_subdir_result.stdout.split('\n') | length != 1 - - set_fact: - IP_subject_alt_name: "{{ config_subdir_result.stdout }}" - - debug: - var: IP_subject_alt_name - - - name: Register p12 PayloadContent - shell: cat private/{{ item }}.p12 | base64 - register: PayloadContent - args: - chdir: "../configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - - - name: Set facts for mobileconfigs - set_fact: - proxy_enabled: false - PayloadContentCA: "{{ lookup('file' , '../configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - - - name: Build the windows client powershell script - template: - src: ../roles/vpn/templates/client_windows.ps1.j2 - dest: ../configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 - mode: 0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - - - name: List windows client powershell scripts - debug: - msg: "configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1" - with_items: - - "{{ users }}" diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 60fafed2..e3b1634d 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -50,11 +50,11 @@ src: "{{ item.src }}" dest: "{{ item.dest }}" with_items: - - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ vpn_user }}.crt" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/certs/{{ vpn_user }}.crt" dest: "{{ configs_prefix }}/ipsec.d/certs/{{ vpn_user }}.crt" - - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/cacert.pem" dest: "{{ configs_prefix }}/ipsec.d/cacerts/{{ IP_subject_alt_name }}.pem" - - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ vpn_user }}.key" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/private/{{ vpn_user }}.key" dest: "{{ configs_prefix }}/ipsec.d/private/{{ vpn_user }}.key" notify: - restart strongswan diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 1415245e..06f5080d 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -19,3 +19,9 @@ ifconfig lo100 create && 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/freebsd.yml b/roles/common/tasks/freebsd.yml index 70ebe8fa..81362632 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,4 +1,16 @@ --- +- name: FreeBSD | Install prerequisites + package: + name: + - python3 + - sudo + vars: + ansible_python_interpreter: /usr/local/bin/python2.7 + +- name: Set python3 as the interpreter to use + set_fact: + ansible_python_interpreter: /usr/local/bin/python3 + - name: Gather facts setup: @@ -15,7 +27,6 @@ strongswan_additional_plugins: - kernel-pfroute - kernel-pfkey - ansible_python_interpreter: /usr/local/bin/python2.7 tools: - git - subversion diff --git a/roles/vpn/tasks/iptables.yml b/roles/common/tasks/iptables.yml similarity index 100% rename from roles/vpn/tasks/iptables.yml rename to roles/common/tasks/iptables.yml diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 37d469e8..a37a8c00 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -1,24 +1,4 @@ --- -- block: - - name: Ubuntu | Install prerequisites - apt: - name: "{{ item }}" - update_cache: true - with_items: - - python2.7 - - sudo - - - name: Ubuntu | Configure defaults - alternatives: - name: python - link: /usr/bin/python - path: /usr/bin/python2.7 - priority: 1 - tags: - - update-alternatives - vars: - ansible_python_interpreter: /usr/bin/python3 - - name: Gather facts setup: @@ -115,15 +95,20 @@ value: 1 - name: Install tools - package: name="{{ item }}" state=present + apt: + name: "{{ item }}" + state: present + update_cache: true with_items: - "{{ tools|default([]) }}" - name: Install headers apt: - name: "{{ item }}" + name: + - linux-headers-generic + - "linux-headers-{{ ansible_kernel }}" state: present when: install_headers - with_items: - - linux-headers-generic - - "linux-headers-{{ ansible_kernel }}" + +- include_tasks: iptables.yml + tags: iptables diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/common/templates/rules.v4.j2 similarity index 69% rename from roles/vpn/templates/rules.v4.j2 rename to roles/common/templates/rules.v4.j2 index 1b487e6d..d71f51fb 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/common/templates/rules.v4.j2 @@ -1,3 +1,6 @@ +{% set subnets = ([strongswan_network] if ipsec_enabled else []) + ([wireguard_network_ipv4] if wireguard_enabled else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled else []) + ([wireguard_port] if wireguard_enabled else []) %} + #### The mangle table # This table allows us to modify packet headers # Packets enter this table first @@ -10,8 +13,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu|int > 0 %} --A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} +{% if reduce_mtu|int > 0 and ipsec_enabled %} +-A FORWARD -s {{ strongswan_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} {% endif %} COMMIT @@ -27,7 +30,7 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE COMMIT @@ -54,12 +57,15 @@ COMMIT -A INPUT -p ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Accept IPSEC/WireGuard traffic to ports {{ subnets|join(',') }} +-A INPUT -p udp -m multiport --dports {{ ports|join(',') }} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT -# Allow any traffic from the VPN + +{% if ipsec_enabled %} +# Allow any traffic from the IPsec VPN -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT +{% endif %} # TODO: # The IP of the resolver should be bound to a DUMMY interface. @@ -70,10 +76,7 @@ COMMIT -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP %} -{% set BetweenClientsPolicy = "DROP" %} -{% endif %} --A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} +-A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }} # Forward any packet that's part of an established connection -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT @@ -83,12 +86,14 @@ COMMIT -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP +{% if ipsec_enabled %} # Forward any IPSEC traffic from the VPN network --A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT +-A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network }} -m policy --pol ipsec --dir in -j ACCEPT +{% endif %} -# Forward any traffic from the WireGuard VPN network {% if wireguard_enabled %} --A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network }} -m policy --pol none --dir in -j ACCEPT +# Forward any traffic from the WireGuard VPN network +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv4 }} -m policy --pol none --dir in -j ACCEPT {% endif %} COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 similarity index 77% rename from roles/vpn/templates/rules.v6.j2 rename to roles/common/templates/rules.v6.j2 index 6095e211..12bed2b4 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -1,3 +1,6 @@ +{% set subnets = ([strongswan_network_ipv6] if ipsec_enabled else []) + ([wireguard_network_ipv6] if wireguard_enabled else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled else []) + ([wireguard_port] if wireguard_enabled else []) %} + #### The mangle table # This table allows us to modify packet headers # Packets enter this table first @@ -10,8 +13,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu|int > 0 %} --A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} +{% if reduce_mtu|int > 0 and ipsec_enabled %} +-A FORWARD -s {{ strongswan_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} {% endif %} COMMIT @@ -26,7 +29,7 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE COMMIT @@ -60,8 +63,8 @@ COMMIT -A INPUT -m ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Accept IPSEC/WireGuard traffic to ports {{ subnets|join(',') }} +-A INPUT -p udp -m multiport --dports {{ ports|join(',') }} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT @@ -83,19 +86,18 @@ COMMIT -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP %} -{% set BetweenClientsPolicy = "DROP" %} -{% endif %} --A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} +-A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }} -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +{% if ipsec_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +{% endif %} {% if wireguard_enabled %} --A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT {% endif %} # Use the ICMPV6-CHECK chain, described above diff --git a/roles/ssh_tunneling/defaults/main.yml b/roles/ssh_tunneling/defaults/main.yml new file mode 100644 index 00000000..3ed9b592 --- /dev/null +++ b/roles/ssh_tunneling/defaults/main.yml @@ -0,0 +1,2 @@ +--- +ssh_tunnels_config_path: "configs/{{ IP_subject_alt_name }}/ssh-tunnel/" diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 259464b4..c52840f6 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -25,65 +25,93 @@ owner: root group: "{{ root_group|default('root') }}" - - 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 }}" - tags: update-users + - 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: The authorized keys file created - authorized_key: - user: "{{ item }}" - key: "{{ lookup('file', 'configs/' + IP_subject_alt_name + '/pki/public/' + item + '.pub') }}" - state: present - manage_dir: true - exclusive: true - with_items: "{{ users }}" - tags: update-users + - block: + - name: Clean up the ssh-tunnel directory + file: + dest: "{{ ssh_tunnels_config_path }}" + state: absent + when: keys_clean_all|bool == True - - name: Generate SSH fingerprints - shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null - register: ssh_fingerprints + - name: Ensure the config directories exist + file: + dest: "{{ ssh_tunnels_config_path }}" + state: directory + recurse: yes + mode: '0700' - - name: Fetch the known_hosts file - local_action: - module: template - src: known_hosts.j2 - dest: configs/{{ IP_subject_alt_name }}/known_hosts - become: no + - 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 - local_action: - module: template - src: ssh_config.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config - mode: 0600 - become: false - tags: update-users - with_items: "{{ users }}" + - 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: Get active users - getent: - database: group - key: algo - split: ':' - tags: update-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: 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(',') }}" + - 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 diff --git a/roles/ssh_tunneling/templates/known_hosts.j2 b/roles/ssh_tunneling/templates/known_hosts.j2 deleted file mode 100644 index 98d33c4d..00000000 --- a/roles/ssh_tunneling/templates/known_hosts.j2 +++ /dev/null @@ -1,3 +0,0 @@ -{% for item in ssh_fingerprints.stdout_lines %} -{{ item }} -{% endfor %} diff --git a/roles/vpn/defaults/main.yml b/roles/strongswan/defaults/main.yml similarity index 51% rename from roles/vpn/defaults/main.yml rename to roles/strongswan/defaults/main.yml index c9b81ce0..b8969332 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/strongswan/defaults/main.yml @@ -1,31 +1,11 @@ --- +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 -wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" -wireguard_interface: wg0 -wireguard_network_ipv4: - subnet: 10.19.49.0 - prefix: 24 - gateway: 10.19.49.1 - clients_range: 10.19.49 - clients_start: 2 -wireguard_network_ipv6: - subnet: 'fd9d:bc11:4021::' - prefix: 48 - gateway: 'fd9d:bc11:4021::1' - clients_range: 'fd9d:bc11:4021::' - clients_start: 2 -wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" -wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" -keys_clean_all: false -wireguard_dns_servers: >- - {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} - {{ local_service_ip }} - {% else %} - {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} - {% endif %} - algo_ondemand_cellular: false algo_ondemand_wifi: false algo_ondemand_wifi_exclude: '_null' @@ -65,3 +45,7 @@ ciphers: compat: ike: aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384! esp: aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384! + +pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" +VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" +CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" diff --git a/roles/vpn/handlers/main.yml b/roles/strongswan/handlers/main.yml similarity index 63% rename from roles/vpn/handlers/main.yml rename to roles/strongswan/handlers/main.yml index 8ce3163a..6e7968f4 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/strongswan/handlers/main.yml @@ -7,11 +7,5 @@ - name: restart apparmor service: name=apparmor state=restarted -- name: save iptables - shell: service netfilter-persistent save - -- name: restart iptables - service: name=netfilter-persistent state=restarted - - name: rereadcrls shell: ipsec rereadcrls; ipsec purgecrls diff --git a/roles/vpn/meta/main.yml b/roles/strongswan/meta/main.yml similarity index 100% rename from roles/vpn/meta/main.yml rename to roles/strongswan/meta/main.yml diff --git a/roles/vpn/tasks/client_configs.yml b/roles/strongswan/tasks/client_configs.yml similarity index 65% rename from roles/vpn/tasks/client_configs.yml rename to roles/strongswan/tasks/client_configs.yml index 827bef76..de4ff0f8 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/strongswan/tasks/client_configs.yml @@ -1,20 +1,19 @@ --- - - name: Register p12 PayloadContent shell: cat private/{{ item }}.p12 | base64 register: PayloadContent args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" with_items: "{{ users }}" - name: Set facts for mobileconfigs set_fact: - PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + PayloadContentCA: "{{ lookup('file' , '{{ ipsec_pki_path }}/cacert.pem')|b64encode }}" - name: Build the mobileconfigs template: src: mobileconfig.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + dest: "{{ ipsec_config_path }}/apple/{{ item.0 }}.mobileconfig" mode: 0600 with_together: - "{{ users }}" @@ -24,7 +23,7 @@ - name: Build the client ipsec config file template: src: client_ipsec.conf.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + dest: "{{ ipsec_config_path }}/manual/{{ item }}.conf" mode: 0600 with_items: - "{{ users }}" @@ -32,7 +31,7 @@ - name: Build the client ipsec secret file template: src: client_ipsec.secrets.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + dest: "{{ ipsec_config_path }}/manual/{{ item }}.secrets" mode: 0600 with_items: - "{{ users }}" @@ -40,7 +39,7 @@ - name: Build the windows client powershell script template: src: client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 + dest: "{{ ipsec_config_path }}/windows/{{ item.0 }}.ps1" mode: 0600 when: algo_windows with_together: @@ -49,8 +48,6 @@ - name: Restrict permissions for the local private directories file: - path: "{{ item }}" + path: "{{ ipsec_config_path }}" state: directory mode: 0700 - with_items: - - configs/{{ IP_subject_alt_name }} diff --git a/roles/strongswan/tasks/distribute_keys.yml b/roles/strongswan/tasks/distribute_keys.yml new file mode 100644 index 00000000..02df30bf --- /dev/null +++ b/roles/strongswan/tasks/distribute_keys.yml @@ -0,0 +1,27 @@ +--- + +- name: Copy the keys to the strongswan directory + copy: + src: "{{ ipsec_pki_path }}/{{ item.src }}" + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/{{ item.dest }}" + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + with_items: + - src: "cacert.pem" + dest: "cacerts/ca.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "certs/{{ IP_subject_alt_name }}.crt" + dest: "certs/{{ IP_subject_alt_name }}.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "private/{{ IP_subject_alt_name }}.key" + dest: "private/{{ IP_subject_alt_name }}.key" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + notify: + - restart strongswan diff --git a/roles/vpn/tasks/ipsec_configuration.yml b/roles/strongswan/tasks/ipsec_configuration.yml similarity index 87% rename from roles/vpn/tasks/ipsec_configuration.yml rename to roles/strongswan/tasks/ipsec_configuration.yml index cc7c21ec..ce5b3e5b 100644 --- a/roles/vpn/tasks/ipsec_configuration.yml +++ b/roles/strongswan/tasks/ipsec_configuration.yml @@ -3,23 +3,23 @@ - name: Setup the config files from our templates template: src: "{{ item.src }}" - dest: "{{ item.dest }}" + dest: "{{ config_prefix|default('/') }}etc/{{ item.dest }}" owner: "{{ item.owner }}" group: "{{ item.group }}" mode: "{{ item.mode }}" with_items: - src: strongswan.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/strongswan.conf" + dest: "strongswan.conf" owner: root group: "{{ root_group|default('root') }}" mode: "0644" - src: ipsec.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/ipsec.conf" + dest: "ipsec.conf" owner: root group: "{{ root_group|default('root') }}" mode: "0644" - src: ipsec.secrets.j2 - dest: "{{ config_prefix|default('/') }}etc/ipsec.secrets" + dest: "ipsec.secrets" owner: strongswan group: "{{ root_group|default('root') }}" mode: "0600" diff --git a/roles/vpn/tasks/main.yml b/roles/strongswan/tasks/main.yml similarity index 84% rename from roles/vpn/tasks/main.yml rename to roles/strongswan/tasks/main.yml index bfe929ca..6b9699e9 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/strongswan/tasks/main.yml @@ -1,11 +1,5 @@ --- - block: - - name: Include WireGuard role - include_role: - name: wireguard - tags: wireguard - when: wireguard_enabled and ansible_distribution == 'Ubuntu' - - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' diff --git a/roles/vpn/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml similarity index 79% rename from roles/vpn/tasks/openssl.yml rename to roles/strongswan/tasks/openssl.yml index 3a286be7..694bb83c 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -7,13 +7,13 @@ - name: Ensure the pki directory does not exist file: - dest: configs/{{ IP_subject_alt_name }}/pki + dest: "{{ ipsec_pki_path }}" state: absent when: keys_clean_all|bool == True - name: Ensure the pki directories exist file: - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + dest: "{{ ipsec_pki_path }}/{{ item }}" state: directory recurse: yes mode: '0700' @@ -26,9 +26,20 @@ - public - reqs + - name: Ensure the config directories exist + file: + dest: "{{ ipsec_config_path }}/{{ item }}" + state: directory + recurse: yes + mode: '0700' + with_items: + - apple + - windows + - manual + - name: Ensure the files exist file: - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + dest: "{{ ipsec_pki_path }}/{{ item }}" state: touch with_items: - ".rnd" @@ -40,7 +51,7 @@ - name: Generate the openssl server configs template: src: openssl.cnf.j2 - dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" + dest: "{{ ipsec_pki_path }}/openssl.cnf" - name: Build the CA pair shell: > @@ -55,20 +66,19 @@ -passout pass:"{{ CA_password }}" && touch {{ IP_subject_alt_name }}_ca_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: "{{ IP_subject_alt_name }}_ca_generated" executable: bash - name: Copy the CA certificate copy: - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" - mode: 0600 + src: "{{ ipsec_pki_path }}/cacert.pem" + dest: "{{ ipsec_config_path }}/manual/cacert.pem" - name: Generate the serial number shell: echo 01 > serial && touch serial_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: serial_generated - name: Build the server pair @@ -90,7 +100,7 @@ -subj "/CN={{ IP_subject_alt_name }}" && touch certs/{{ IP_subject_alt_name }}_crt_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: certs/{{ IP_subject_alt_name }}_crt_generated executable: bash @@ -113,23 +123,15 @@ -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: certs/{{ item }}_crt_generated executable: bash with_items: "{{ users }}" - - name: Create links for the private keys - file: - src: "pki/private/{{ item }}.key" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem" - state: link - force: true - with_items: "{{ users }}" - - name: Build openssh public keys openssl_publickey: - path: "configs/{{ IP_subject_alt_name }}/pki/public/{{ item }}.pub" - privatekey_path: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.key" + path: "{{ ipsec_pki_path }}/public/{{ item }}.pub" + privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" format: OpenSSH with_items: "{{ users }}" @@ -144,16 +146,15 @@ -out private/{{ item }}.p12 -passout pass:"{{ p12_export_password }}" args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" executable: bash with_items: "{{ users }}" register: p12 - name: Copy the p12 certificates copy: - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 + src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" + dest: "{{ ipsec_config_path }}/manual/{{ item }}.p12" with_items: - "{{ users }}" @@ -164,7 +165,7 @@ awk '{print $5}' | sed 's/\/CN=//g' args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path}}" register: valid_certs - name: Revoke non-existing users @@ -176,7 +177,7 @@ -out crl/{{ item }}.crt register: gencrl args: - chdir: configs/{{ IP_subject_alt_name }}/pki/ + chdir: "{{ ipsec_pki_path }}" creates: crl/{{ item }}.crt executable: bash when: item not in users @@ -192,7 +193,7 @@ - gencrl is defined - gencrl.changed args: - chdir: configs/{{ IP_subject_alt_name }}/pki/ + chdir: "{{ ipsec_pki_path }}" executable: bash delegate_to: localhost become: no @@ -201,7 +202,7 @@ - name: Copy the CRL to the vpn server copy: - src: configs/{{ IP_subject_alt_name }}/pki/crl/algo.root.pem + src: "{{ ipsec_pki_path }}/crl/algo.root.pem" dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem" when: - gencrl is defined diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/strongswan/tasks/ubuntu.yml similarity index 95% rename from roles/vpn/tasks/ubuntu.yml rename to roles/strongswan/tasks/ubuntu.yml index 6f585441..41b58352 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/strongswan/tasks/ubuntu.yml @@ -43,6 +43,3 @@ notify: - daemon-reload - restart strongswan - -- include_tasks: iptables.yml - tags: iptables diff --git a/roles/vpn/templates/100-CustomLimitations.conf.j2 b/roles/strongswan/templates/100-CustomLimitations.conf.j2 similarity index 100% rename from roles/vpn/templates/100-CustomLimitations.conf.j2 rename to roles/strongswan/templates/100-CustomLimitations.conf.j2 diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/strongswan/templates/client_ipsec.conf.j2 similarity index 100% rename from roles/vpn/templates/client_ipsec.conf.j2 rename to roles/strongswan/templates/client_ipsec.conf.j2 diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/strongswan/templates/client_ipsec.secrets.j2 similarity index 100% rename from roles/vpn/templates/client_ipsec.secrets.j2 rename to roles/strongswan/templates/client_ipsec.secrets.j2 diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/strongswan/templates/client_windows.ps1.j2 similarity index 100% rename from roles/vpn/templates/client_windows.ps1.j2 rename to roles/strongswan/templates/client_windows.ps1.j2 diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/strongswan/templates/ipsec.conf.j2 similarity index 94% rename from roles/vpn/templates/ipsec.conf.j2 rename to roles/strongswan/templates/ipsec.conf.j2 index 086e18af..68fa3464 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/strongswan/templates/ipsec.conf.j2 @@ -27,7 +27,7 @@ conn %default right=%any rightauth=pubkey - rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} + rightsourceip={{ strongswan_network }},{{ strongswan_network_ipv6 }} {% if algo_local_dns or dns_encryption %} rightdns={{ local_service_ip }} {% else %} diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/strongswan/templates/ipsec.secrets.j2 similarity index 100% rename from roles/vpn/templates/ipsec.secrets.j2 rename to roles/strongswan/templates/ipsec.secrets.j2 diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 similarity index 100% rename from roles/vpn/templates/mobileconfig.j2 rename to roles/strongswan/templates/mobileconfig.j2 diff --git a/roles/vpn/templates/openssl.cnf.j2 b/roles/strongswan/templates/openssl.cnf.j2 similarity index 100% rename from roles/vpn/templates/openssl.cnf.j2 rename to roles/strongswan/templates/openssl.cnf.j2 diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/strongswan/templates/strongswan.conf.j2 similarity index 100% rename from roles/vpn/templates/strongswan.conf.j2 rename to roles/strongswan/templates/strongswan.conf.j2 diff --git a/roles/vpn/tasks/distribute_keys.yml b/roles/vpn/tasks/distribute_keys.yml deleted file mode 100644 index d50ecfa4..00000000 --- a/roles/vpn/tasks/distribute_keys.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- - -- name: Copy the keys to the strongswan directory - copy: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - owner: "{{ item.owner }}" - group: "{{ item.group }}" - mode: "{{ item.mode }}" - with_items: - - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/cacerts/ca.crt" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ IP_subject_alt_name }}.crt" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ IP_subject_alt_name }}.key" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/private/{{ IP_subject_alt_name }}.key" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - notify: - - restart strongswan diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 7961c6a9..d0083366 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -1,4 +1,28 @@ --- wireguard_PersistentKeepalive: 0 -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_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 %} + {{ local_service_ip }} + {% else %} + {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + {% endif %} +wireguard_client_ip: "{{ _wireguard_network_ipv4['clients_range'] }}.{{ _wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ _wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ _wireguard_network_ipv6['clients_range'] }}{{ _wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ _wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_server_ip: "{{ _wireguard_network_ipv4['gateway'] }}/{{ _wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ _wireguard_network_ipv6['gateway'] }}/{{ _wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml index 33434081..e13a015a 100644 --- a/roles/wireguard/tasks/keys.yml +++ b/roles/wireguard/tasks/keys.yml @@ -20,7 +20,7 @@ - block: - name: Save private keys copy: - dest: "{{ wireguard_config_path }}/private/{{ item['item'] }}" + dest: "{{ wireguard_pki_path }}/private/{{ item['item'] }}" content: "{{ item['stdout'] }}" mode: "0600" no_log: true @@ -39,7 +39,7 @@ when: wg_genkey.changed - name: Generate public keys - shell: echo "{{ lookup('file', wireguard_config_path + '/private/' + item) }}" | wg pubkey + shell: echo "{{ lookup('file', wireguard_pki_path + '/private/' + item) }}" | wg pubkey register: wg_pubkey changed_when: false args: @@ -50,7 +50,7 @@ - name: Save public keys copy: - dest: "{{ wireguard_config_path }}/public/{{ item['item'] }}" + dest: "{{ wireguard_pki_path }}/public/{{ item['item'] }}" content: "{{ item['stdout'] }}" mode: "0600" no_log: true diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index fa184fdc..235eaa45 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Ensure the required directories exist file: - dest: "{{ wireguard_config_path }}/{{ item }}" + dest: "{{ wireguard_pki_path }}/{{ item }}" state: directory recurse: true with_items: @@ -28,7 +28,7 @@ - block: - name: WireGuard user list updated lineinfile: - dest: "{{ wireguard_config_path }}/index.txt" + dest: "{{ wireguard_pki_path }}/index.txt" create: true mode: "0600" insertafter: EOF @@ -37,7 +37,7 @@ with_items: "{{ users }}" - set_fact: - wireguard_users: "{{ (lookup('file', wireguard_config_path + 'index.txt')).split('\n') }}" + wireguard_users: "{{ (lookup('file', wireguard_pki_path + 'index.txt')).split('\n') }}" - name: WireGuard users config generated template: diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index b601abb5..4c2fc62a 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -1,12 +1,12 @@ [Interface] -PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} +PrivateKey = {{ lookup('file', wireguard_pki_path + '/private/' + item.1) }} Address = {{ wireguard_client_ip }} DNS = {{ wireguard_dns_servers }} {% if reduce_mtu|int > 0 %}MTU = {{ 1420 - reduce_mtu|int }} {% endif %} [Peer] -PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} +PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + IP_subject_alt_name) }} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} {{ 'PersistentKeepalive = ' + wireguard_PersistentKeepalive|string if wireguard_PersistentKeepalive > 0 else '' }} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index eb77f13a..247c7d2f 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -1,7 +1,7 @@ [Interface] Address = {{ wireguard_server_ip }} ListenPort = {{ wireguard_port }} -PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} +PrivateKey = {{ lookup('file', wireguard_pki_path + '/private/' + IP_subject_alt_name) }} SaveConfig = false {% for u in wireguard_users %} @@ -10,8 +10,8 @@ SaveConfig = false [Peer] # {{ u }} -PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} -AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index }}/128{% endif %} +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 %} {% endif %} {% endfor %} diff --git a/server.yml b/server.yml index b6e8340b..1ab38346 100644 --- a/server.yml +++ b/server.yml @@ -19,8 +19,9 @@ - role: wireguard when: wireguard_enabled tags: wireguard - - role: vpn - tags: vpn + - role: strongswan + when: ipsec_enabled + tags: ipsec - role: ssh_tunneling when: algo_ssh_tunneling tags: ssh_tunneling @@ -30,15 +31,17 @@ - name: Delete the CA key local_action: module: file - path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + path: "{{ ipsec_pki_path }}/private/cakey.pem" state: absent become: false - when: not algo_store_cakey + when: + - ipsec_enabled + - not algo_store_cakey - name: Dump the configuration local_action: module: copy - dest: "configs/{{ IP_subject_alt_name }}/config.yml" + dest: "configs/{{ IP_subject_alt_name }}/.config.yml" content: | server: {{ 'localhost' if inventory_hostname == 'localhost' else inventory_hostname }} server_user: {{ ansible_ssh_user }} @@ -55,6 +58,8 @@ 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 @@ -69,9 +74,9 @@ - debug: msg: - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - - " {% if algo_store_cakey %}{{ congrats.ca_key_pass }}{% endif %}" - - " {% if algo_provider != 'local' %}{{ congrats.ssh_access }}{% endif %}" + - " {{ 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 diff --git a/tests/update-users.sh b/tests/update-users.sh index 2387103f..c083994c 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -11,7 +11,11 @@ else ansible-playbook users.yml -e "${USER_ARGS}" -t update-users fi -if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.crt | grep CRL +# +# IPsec +# + +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/crl/phone.crt | grep CRL then echo "The CRL check passed" else @@ -19,10 +23,34 @@ if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.c exit 1 fi -if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/user1.crt | grep CN=user1 +if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/certs/user1.crt | grep CN=user1 then echo "The new user exists" else echo "The new user does not exist" exit 1 fi + +# +# WireGuard +# + +if sudo test -f configs/$LXC_IP/wireguard/user1.conf + then + echo "WireGuard: The new user exists" + else + echo "WireGuard: The new user does not exist" + exit 1 +fi + +# +# SSH tunneling +# + +if sudo test -f configs/$LXC_IP/ssh-tunnel/user1.ssh_config + then + echo "SSH Tunneling: The new user exists" + else + echo "SSH Tunneling: The new user does not exist" + exit 1 +fi diff --git a/users.yml b/users.yml index 64422638..3f742946 100644 --- a/users.yml +++ b/users.yml @@ -21,13 +21,15 @@ - name: Import host specific variables include_vars: - file: "configs/{{ algo_server }}/config.yml" + file: "configs/{{ algo_server }}/.config.yml" - pause: prompt: Enter the password for the private CA key echo: false register: _ca_password - when: ca_password is undefined + when: + - ca_password is undefined + - ipsec_enabled - name: Set facts based on the input set_fact: @@ -42,7 +44,7 @@ groups: vpn-host ansible_ssh_user: "{{ server_user|default('root') }}" ansible_connection: "{% if algo_server == 'localhost' %}local{% else %}ssh{% endif %}" - ansible_python_interpreter: "/usr/bin/python2.7" + ansible_python_interpreter: "/usr/bin/python3" CA_password: "{{ CA_password }}" rescue: - debug: var=fail_hint @@ -56,7 +58,7 @@ become: true vars_files: - config.cfg - - "configs/{{ inventory_hostname }}/config.yml" + - "configs/{{ inventory_hostname }}/.config.yml" pre_tasks: - block: @@ -74,8 +76,9 @@ - role: wireguard tags: [ 'vpn', 'wireguard' ] when: wireguard_enabled - - role: vpn - tags: vpn + - role: strongswan + when: ipsec_enabled + tags: ipsec - role: ssh_tunneling when: algo_ssh_tunneling From 8b5920067f2380ff7c75a5c6190df4585ef460c3 Mon Sep 17 00:00:00 2001 From: Les Aker Date: Mon, 11 Mar 2019 15:29:39 +0300 Subject: [PATCH 48/55] skip generation of SSH keypair when deploying locally (#1348) --- playbooks/cloud-pre.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index 6a8071b0..53de7fab 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -31,9 +31,11 @@ size: 2048 mode: "0600" type: RSA + when: algo_provider != "local" - name: Generate the SSH public key openssl_publickey: path: "{{ SSH_keys.public }}" privatekey_path: "{{ SSH_keys.private }}" format: OpenSSH + when: algo_provider != "local" From 9f190617b9e6feb6e4df38a98a9c0841efe6ea35 Mon Sep 17 00:00:00 2001 From: Ryan Kasper Date: Mon, 11 Mar 2019 20:08:09 -0600 Subject: [PATCH 49/55] Fix typo in doctl command (#1350) --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 493661cf..5bd0f84a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -215,7 +215,7 @@ The error is caused because Digital Ocean changed its API to treat the tag argum 1. Download [doctl](https://github.com/digitalocean/doctl) 2. Run `doctl auth init`; it will ask you for your token which you can get (or generate) on the API tab at DigitalOcean 3. Once you are authorized on DO, you can run `doctl compute tag list` to see the list of tags -4. Run `doctl compute tag delete enivronment:algo --force` to delete the environment:algo tag +4. Run `doctl compute tag delete environment:algo --force` to delete the environment:algo tag 5. Finally run `doctl compute tag list` to make sure that the tag has been deleted 6. Run algo as directed From 6d73c908a611a103498bbb2d4d8ec77847a606f2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 14 Mar 2019 18:11:57 +0100 Subject: [PATCH 50/55] Start dnscrypt-proxy after systemd-resolved (#1357) --- roles/dns_encryption/tasks/ubuntu.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 89515ddb..f9cd7ee0 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -47,10 +47,14 @@ owner: root group: root -- name: Ubuntu | Add capabilities to bind ports +- name: Ubuntu | Add custom requirements to successfully start the unit copy: - dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-capabilities.conf + dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-algo.conf content: | + [Unit] + After=systemd-resolved.service + Requires=systemd-resolved.service + [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE notify: From 13314dfaeada37409a6ad175d1c93d97f6c0fdb6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 15 Mar 2019 18:16:26 +0100 Subject: [PATCH 51/55] Modify naming in the cloud resources and client config files (#1353) * Modify naming in the cloud resources and client config files * Azure template: Eliminate unneeded variables --- roles/cloud-azure/files/deployment.json | 50 ++++++++----------- roles/cloud-azure/tasks/main.yml | 9 ++-- roles/cloud-ec2/files/stack.yml | 28 +++-------- roles/cloud-gce/tasks/main.yml | 6 +-- .../templates/client_windows.ps1.j2 | 2 +- roles/strongswan/templates/mobileconfig.j2 | 12 ++--- 6 files changed, 43 insertions(+), 64 deletions(-) diff --git a/roles/cloud-azure/files/deployment.json b/roles/cloud-azure/files/deployment.json index 646ea8a1..027e562b 100644 --- a/roles/cloud-azure/files/deployment.json +++ b/roles/cloud-azure/files/deployment.json @@ -2,15 +2,9 @@ "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", "contentVersion": "1.0.0.0", "parameters": { - "AlgoServerName": { - "type": "string" - }, "sshKeyData": { "type": "string" }, - "location": { - "type": "string" - }, "WireGuardPort": { "type": "int" }, @@ -22,15 +16,15 @@ } }, "variables": { - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('AlgoServerName'))]", - "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', parameters('AlgoServerName'))]" + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', resourceGroup().name)]", + "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', resourceGroup().name)]" }, "resources": [ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/networkSecurityGroups", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "securityRules": [ { @@ -95,8 +89,8 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/publicIPAddresses", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "publicIPAllocationMethod": "Static" } @@ -104,8 +98,8 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/virtualNetworks", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "addressSpace": { "addressPrefixes": [ @@ -114,7 +108,7 @@ }, "subnets": [ { - "name": "[parameters('AlgoServerName')]", + "name": "[resourceGroup().name]", "properties": { "addressPrefix": "10.10.0.0/24" } @@ -125,16 +119,16 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/networkInterfaces", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "dependsOn": [ - "[concat('Microsoft.Network/networkSecurityGroups/', parameters('AlgoServerName'))]", - "[concat('Microsoft.Network/publicIPAddresses/', parameters('AlgoServerName'))]", - "[concat('Microsoft.Network/virtualNetworks/', parameters('AlgoServerName'))]" + "[concat('Microsoft.Network/networkSecurityGroups/', resourceGroup().name)]", + "[concat('Microsoft.Network/publicIPAddresses/', resourceGroup().name)]", + "[concat('Microsoft.Network/virtualNetworks/', resourceGroup().name)]" ], "properties": { "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', resourceGroup().name)]" }, "ipConfigurations": [ { @@ -142,7 +136,7 @@ "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { - "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/publicIPAddresses', resourceGroup().name)]" }, "subnet": { "id": "[variables('subnet1Ref')]" @@ -155,17 +149,17 @@ { "apiVersion": "2016-04-30-preview", "type": "Microsoft.Compute/virtualMachines", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "dependsOn": [ - "[concat('Microsoft.Network/networkInterfaces/', parameters('AlgoServerName'))]" + "[concat('Microsoft.Network/networkInterfaces/', resourceGroup().name)]" ], "properties": { "hardwareProfile": { "vmSize": "[parameters('vmSize')]" }, "osProfile": { - "computerName": "[parameters('AlgoServerName')]", + "computerName": "[resourceGroup().name]", "adminUsername": "ubuntu", "linuxConfiguration": { "disablePasswordAuthentication": true, @@ -193,7 +187,7 @@ "networkProfile": { "networkInterfaces": [ { - "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/networkInterfaces', resourceGroup().name)]" } ] } @@ -203,7 +197,7 @@ "outputs": { "publicIPAddresses": { "type": "string", - "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',parameters('AlgoServerName')),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',resourceGroup().name),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", } } } diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 38adc741..113352ce 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -16,20 +16,17 @@ - name: Create AlgoVPN Server azure_rm_deployment: state: present - deployment_name: "AlgoVPN-{{ algo_server_name }}" + 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: "AlgoVPN-{{ algo_server_name }}" + resource_group_name: "{{ algo_server_name }}" + location: "{{ algo_region }}" parameters: - AlgoServerName: - value: "{{ algo_server_name }}" sshKeyData: value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - location: - value: "{{ algo_region }}" WireGuardPort: value: "{{ wireguard_port }}" vmSize: diff --git a/roles/cloud-ec2/files/stack.yml b/roles/cloud-ec2/files/stack.yml index 3660613b..829a2cb3 100644 --- a/roles/cloud-ec2/files/stack.yml +++ b/roles/cloud-ec2/files/stack.yml @@ -21,9 +21,7 @@ Resources: InstanceTenancy: default Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName VPCIPv6: Type: AWS::EC2::VPCCidrBlock @@ -35,22 +33,18 @@ Resources: Type: AWS::EC2::InternetGateway Properties: Tags: - - Key: Environment - Value: Algo - Key: Name - Value: Algo + Value: !Ref AWS::StackName Subnet: Type: AWS::EC2::Subnet Properties: CidrBlock: 172.16.254.0/23 MapPublicIpOnLaunch: false - Tags: - - Key: Environment - Value: Algo - - Key: Name - Value: Algo VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Ref AWS::StackName VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment @@ -63,10 +57,8 @@ Resources: Properties: VpcId: !Ref VPC Tags: - - Key: Environment - Value: Algo - Key: Name - Value: Algo + Value: !Ref AWS::StackName Route: Type: AWS::EC2::Route @@ -140,9 +132,7 @@ Resources: CidrIp: 0.0.0.0/0 Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName EC2Instance: Type: AWS::EC2::Instance @@ -181,9 +171,7 @@ Resources: cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName ElasticIP: Type: AWS::EC2::EIP diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index e04b3d80..baa5f469 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -8,8 +8,8 @@ - name: Network configured gce_net: - name: "algo-net-{{ algo_server_name }}" - fwname: "algo-net-{{ algo_server_name }}-fw" + name: "{{ algo_server_name }}" + fwname: "{{ algo_server_name }}-fw" allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" state: "present" mode: auto @@ -45,7 +45,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ algo_server_name }}" + network: "{{ algo_server_name }}" tags: - "environment-algo" register: google_vm diff --git a/roles/strongswan/templates/client_windows.ps1.j2 b/roles/strongswan/templates/client_windows.ps1.j2 index e1021bbe..da53383f 100644 --- a/roles/strongswan/templates/client_windows.ps1.j2 +++ b/roles/strongswan/templates/client_windows.ps1.j2 @@ -85,7 +85,7 @@ Save the embedded CA cert and encrypted user PKCS12 file. $ErrorActionPreference = "Stop" $VpnServerAddress = "{{ IP_subject_alt_name }}" -$VpnName = "Algo VPN {{ IP_subject_alt_name }} IKEv2" +$VpnName = "AlgoVPN {{ algo_server_name }} IKEv2" $VpnUser = "{{ item.0 }}" $CaCertificateBase64 = "{{ PayloadContentCA }}" $UserPkcs12Base64 = "{{ item.1.stdout }}" diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index 686ed7e8..e9d66701 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -116,7 +116,7 @@ PayloadDescription Configures VPN settings PayloadDisplayName - VPN + {{ algo_server_name }} PayloadIdentifier com.apple.vpn.managed.{{ VPN_PayloadIdentifier }} PayloadType @@ -133,7 +133,7 @@ 0
UserDefinedName - Algo VPN {{ IP_subject_alt_name }} IKEv2 + AlgoVPN {{ algo_server_name }} IKEv2 VPNType IKEv2 @@ -149,7 +149,7 @@ PayloadDescription Adds a PKCS#12-formatted certificate PayloadDisplayName - {{ item.0 }}.p12 + {{ algo_server_name }} PayloadIdentifier com.apple.security.pkcs12.{{ pkcs12_PayloadCertificateUUID }} PayloadType @@ -169,7 +169,7 @@ PayloadDescription Adds a CA root certificate PayloadDisplayName - {{ IP_subject_alt_name }} + {{ algo_server_name }} PayloadIdentifier com.apple.security.root.{{ CA_PayloadIdentifier }} PayloadType @@ -181,11 +181,11 @@ PayloadDisplayName - {{ IP_subject_alt_name }} IKEv2 + AlgoVPN {{ algo_server_name }} IKEv2 PayloadIdentifier donut.local.{{ 500000 | random | to_uuid | upper }} PayloadOrganization - Algo VPN + AlgoVPN PayloadRemovalDisallowed PayloadType From ba11ca623c68859ae02b1f01f618b85fba375aa5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 17 Mar 2019 11:19:24 -0400 Subject: [PATCH 52/55] AGPLv3 change (#1351) --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 2 + 2 files changed, 659 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 7365a69d..bae94e18 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -The MIT License (MIT) + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2016 Trail of Bits + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md index 2308c31c..c4016c85 100644 --- a/README.md +++ b/README.md @@ -246,3 +246,5 @@ All donations support continued development. Thanks! * We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). * 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 From 82def687eeb3bd3e090283422ec5e34c844c7a8d Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 19 Mar 2019 08:57:05 +0100 Subject: [PATCH 53/55] Update CHANGELOG.md --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd579d..843d8ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.1 (Unreleased) + +### Added + +### Fixed + +## 1.0 (Mar 19, 2019) + +### Added +- Tagged releases and changelog [\#724](https://github.com/trailofbits/algo/issues/724) +- Add support for custom domain names [\#759](https://github.com/trailofbits/algo/issues/759) + +### Fixed +- Set the name shown to the user \(client\) to be the server name specified in the install script [\#491](https://github.com/trailofbits/algo/issues/491) +- AGPLv3 change [\#1351](https://github.com/trailofbits/algo/pull/1351) +- Migrate to python3 [\#1024](https://github.com/trailofbits/algo/issues/1024) +- Reorganize the project around ipsec + wireguard [\#1330](https://github.com/trailofbits/algo/issues/1330) +- Configuration folder reorganization [\#1330](https://github.com/trailofbits/algo/issues/1330) +- Remove WireGuard KeepAlive and include as an option in config [\#1251](https://github.com/trailofbits/algo/issues/1251) +- Dnscrypt-proxy no longer works after reboot [\#1356](https://github.com/trailofbits/algo/issues/1356) + ## 20 Oct 2018 ### Added - AWS Lightsail From fea061213153cc2472f77cb5f3a3df91ea7763cf Mon Sep 17 00:00:00 2001 From: Fabian Foerg <3429782+faf0@users.noreply.github.com> Date: Tue, 19 Mar 2019 09:49:18 -0700 Subject: [PATCH 54/55] Simplify Apple Profile Configuration Template (#1033) * Simplify Apple Profile Configuration Template * enable lstrip_blocks * remove ldashes --- roles/strongswan/templates/mobileconfig.j2 | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index e9d66701..6cf0ea13 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -1,3 +1,4 @@ +#jinja2:lstrip_blocks: True @@ -12,8 +13,8 @@ 1 OnDemandRules -{% if algo_ondemand_wifi_exclude|b64decode != '_null' %} -{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} + {% if algo_ondemand_wifi_exclude|b64decode != '_null' %} + {% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} Action Disconnect @@ -21,20 +22,19 @@ WiFi SSIDMatch -{% for network_name in WIFI_EXCLUDE_LIST %} + {% for network_name in WIFI_EXCLUDE_LIST %} {{ network_name|e }} -{% endfor %} + {% endfor %} -{% else %} -{% endif %} + {% endif %} Action -{% if algo_ondemand_wifi %} + {% if algo_ondemand_wifi %} Connect {% else %} Disconnect -{% endif %} + {% endif %} InterfaceTypeMatch WiFi URLStringProbe @@ -42,11 +42,11 @@ Action -{% if algo_ondemand_cellular %} + {% if algo_ondemand_cellular %} Connect {% else %} Disconnect -{% endif %} + {% endif %} InterfaceTypeMatch Cellular URLStringProbe @@ -57,7 +57,6 @@ Disconnect -{% else %} {% endif %} AuthenticationMethod Certificate From 374919cee7f17b9338e59104252ad082add079bf Mon Sep 17 00:00:00 2001 From: adamluk Date: Mon, 25 Mar 2019 07:55:38 +0000 Subject: [PATCH 55/55] Update 10-algo-lo100.network.j2 (#1369) --- roles/common/templates/10-algo-lo100.network.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/templates/10-algo-lo100.network.j2 b/roles/common/templates/10-algo-lo100.network.j2 index 257396c6..87280511 100644 --- a/roles/common/templates/10-algo-lo100.network.j2 +++ b/roles/common/templates/10-algo-lo100.network.j2 @@ -2,6 +2,6 @@ Name=lo [Network] -Label=lo:100 +Description=lo:100 Address={{ local_service_ip }}/32 Address=FCAA::1/64