From 2abbf22196d6c28c2cc81a4e5c8154c0a5947799 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 31 Jan 2020 11:24:29 +0100 Subject: [PATCH] Alternative Ingress IP (#1605) * Separate ingress IP draft * task name fix * placeholder --- .github/workflows/main.yml | 1 - config.cfg | 7 + docs/cloud-alternative-ingress-ip.md | 22 ++ docs/images/cloud-alternative-ingress-ip.png | Bin 0 -> 34294 bytes library/digital_ocean_floating_ip.py | 288 ++++++++++++++++++ playbooks/cloud-post.yml | 1 + roles/cloud-digitalocean/tasks/main.yml | 13 + roles/common/defaults/main.yml | 7 + roles/common/handlers/main.yml | 3 + roles/common/tasks/aip/digitalocean.yml | 13 + roles/common/tasks/aip/main.yml | 10 + roles/common/tasks/aip/placeholder.yml | 0 roles/common/tasks/ubuntu.yml | 14 + .../templates/99-algo-ipv6-egress.yaml.j2 | 6 + roles/common/templates/rules.v4.j2 | 2 +- roles/common/templates/rules.v6.j2 | 2 +- server.yml | 1 + 17 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 docs/cloud-alternative-ingress-ip.md create mode 100644 docs/images/cloud-alternative-ingress-ip.png create mode 100644 library/digital_ocean_floating_ip.py create mode 100644 roles/common/tasks/aip/digitalocean.yml create mode 100644 roles/common/tasks/aip/main.yml create mode 100644 roles/common/tasks/aip/placeholder.yml create mode 100644 roles/common/templates/99-algo-ipv6-egress.yaml.j2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e63964..662f7c8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,7 +93,6 @@ jobs: set -x sudo -E bash -x ./tests/wireguard-client.sh sudo env "PATH=$PATH" ./tests/ipsec-client.sh - sudo ./tests/ssh-tunnel.sh local-deploy: runs-on: ubuntu-16.04 diff --git a/config.cfg b/config.cfg index 4a73c03..7d3027e 100644 --- a/config.cfg +++ b/config.cfg @@ -26,6 +26,13 @@ ipsec_enabled: true wireguard_enabled: true wireguard_port: 51820 +# This feature allows you to configure the Algo server to send outbound traffic +# through a different external IP address than the one you are establishing the VPN connection with. +# More info https://trailofbits.github.io/algo/cloud-alternative-ingress-ip.html +# Available for the following cloud providers: +# - DigitalOcean +alternative_ingress_ip: false + # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission # Unit) than the normal value of 1500 and if you don't reduce the MTU of your diff --git a/docs/cloud-alternative-ingress-ip.md b/docs/cloud-alternative-ingress-ip.md new file mode 100644 index 0000000..5c123e9 --- /dev/null +++ b/docs/cloud-alternative-ingress-ip.md @@ -0,0 +1,22 @@ +# Alternative Ingress IP + +This feature allows you to configure the Algo server to send outbound traffic through a different external IP address than the one you are establishing the VPN connection with. + +![cloud-alternative-ingress-ip](/docs/images/cloud-alternative-ingress-ip.png) + +Additional info might be found in [this issue](https://github.com/trailofbits/algo/issues/1047) + + + + +#### Caveats + +##### Extra charges + +- DigitalOcean: Floating IPs are free when assigned to a Droplet, but after manually deleting a Droplet you need to also delete the Floating IP or you'll get charged for it. + +##### IPv6 + +Some cloud providers provision a VM with an `/128` address block size. This is the only IPv6 address provided and for outbound and incoming traffic. + +If the provided address block size is bigger, e.g., `/64`, Algo takes a separate address than the one is assigned to the server to send outbound IPv6 traffic. diff --git a/docs/images/cloud-alternative-ingress-ip.png b/docs/images/cloud-alternative-ingress-ip.png new file mode 100644 index 0000000000000000000000000000000000000000..82de4fbf666d3b7e9825097434ed0a3def9f145a GIT binary patch literal 34294 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 j9U>R-3;$nO;F3Q)E~OEp5sDrP{3k9fB~ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: digital_ocean_floating_ip +short_description: Manage DigitalOcean Floating IPs +description: + - Create/delete/assign a floating IP. +version_added: "2.4" +author: "Patrick Marques (@pmarques)" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent'] + ip: + description: + - Public IP address of the Floating IP. Used to remove an IP + region: + description: + - The region that the Floating IP is reserved to. + droplet_id: + description: + - The Droplet that the Floating IP has been assigned to. + oauth_token: + description: + - DigitalOcean OAuth token. + required: true +notes: + - Version 2 of DigitalOcean API is used. +requirements: + - "python >= 2.6" +''' + + +EXAMPLES = ''' +- name: "Create a Floating IP in region lon1" + digital_ocean_floating_ip: + state: present + region: lon1 + +- name: "Create a Floating IP assigned to Droplet ID 123456" + digital_ocean_floating_ip: + state: present + droplet_id: 123456 + +- name: "Delete a Floating IP with ip 1.2.3.4" + digital_ocean_floating_ip: + state: absent + ip: "1.2.3.4" + +''' + + +RETURN = ''' +# Digital Ocean API info https://developers.digitalocean.com/documentation/v2/#floating-ips +data: + description: a DigitalOcean Floating IP resource + returned: success and no resource constraint + type: dict + sample: { + "action": { + "id": 68212728, + "status": "in-progress", + "type": "assign_ip", + "started_at": "2015-10-15T17:45:44Z", + "completed_at": null, + "resource_id": 758603823, + "resource_type": "floating_ip", + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + "512mb", + "1gb", + "2gb", + "4gb", + "8gb", + "16gb", + "32gb", + "48gb", + "64gb" + ], + "features": [ + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": true + }, + "region_slug": "nyc3" + } + } +''' + +import json +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.digital_ocean import DigitalOceanHelper + +class Response(object): + + def __init__(self, resp, info): + self.body = None + if resp: + self.body = resp.read() + self.info = info + + @property + def json(self): + if not self.body: + if "body" in self.info: + return json.loads(self.info["body"]) + return None + try: + return json.loads(self.body) + except ValueError: + return None + + @property + def status_code(self): + return self.info["status"] + +def wait_action(module, rest, ip, action_id, timeout=10): + end_time = time.time() + 10 + while time.time() < end_time: + response = rest.get('floating_ips/{0}/actions/{1}'.format(ip, action_id)) + status_code = response.status_code + status = response.json['action']['status'] + # TODO: check status_code == 200? + if status == 'completed': + return True + elif status == 'errored': + module.fail_json(msg='Floating ip action error [ip: {0}: action: {1}]'.format( + ip, action_id), data=json) + + module.fail_json(msg='Floating ip action timeout [ip: {0}: action: {1}]'.format( + ip, action_id), data=json) + + +def core(module): + api_token = module.params['oauth_token'] + state = module.params['state'] + ip = module.params['ip'] + droplet_id = module.params['droplet_id'] + + rest = DigitalOceanHelper(module) + + if state in ('present'): + if droplet_id is not None and module.params['ip'] is not None: + # Lets try to associate the ip to the specified droplet + associate_floating_ips(module, rest) + else: + create_floating_ips(module, rest) + + elif state in ('absent'): + response = rest.delete("floating_ips/{0}".format(ip)) + status_code = response.status_code + json_data = response.json + if status_code == 204: + module.exit_json(changed=True) + elif status_code == 404: + module.exit_json(changed=False) + else: + module.exit_json(changed=False, data=json_data) + + +def get_floating_ip_details(module, rest): + ip = module.params['ip'] + + response = rest.get("floating_ips/{0}".format(ip)) + status_code = response.status_code + json_data = response.json + if status_code == 200: + return json_data['floating_ip'] + else: + module.fail_json(msg="Error assigning floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def assign_floating_id_to_droplet(module, rest): + ip = module.params['ip'] + + payload = { + "type": "assign", + "droplet_id": module.params['droplet_id'], + } + + response = rest.post("floating_ips/{0}/actions".format(ip), data=payload) + status_code = response.status_code + json_data = response.json + if status_code == 201: + wait_action(module, rest, ip, json_data['action']['id']) + + module.exit_json(changed=True, data=json_data) + else: + module.fail_json(msg="Error creating floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def associate_floating_ips(module, rest): + floating_ip = get_floating_ip_details(module, rest) + droplet = floating_ip['droplet'] + + # TODO: If already assigned to a droplet verify if is one of the specified as valid + if droplet is not None and str(droplet['id']) in [module.params['droplet_id']]: + module.exit_json(changed=False) + else: + assign_floating_id_to_droplet(module, rest) + + +def create_floating_ips(module, rest): + payload = { + } + floating_ip_data = None + + if module.params['region'] is not None: + payload["region"] = module.params['region'] + + if module.params['droplet_id'] is not None: + payload["droplet_id"] = module.params['droplet_id'] + + floating_ips = rest.get_paginated_data(base_url='floating_ips?', data_key_name='floating_ips') + + for floating_ip in floating_ips: + if floating_ip['droplet'] and floating_ip['droplet']['id'] == module.params['droplet_id']: + floating_ip_data = {'floating_ip': floating_ip} + + if floating_ip_data: + module.exit_json(changed=False, data=floating_ip_data) + else: + response = rest.post("floating_ips", data=payload) + status_code = response.status_code + json_data = response.json + + if status_code == 202: + module.exit_json(changed=True, data=json_data) + else: + module.fail_json(msg="Error creating floating ip [{0}: {1}]".format( + status_code, json_data["message"]), region=module.params['region']) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + state=dict(choices=['present', 'absent'], default='present'), + ip=dict(aliases=['id'], required=False), + region=dict(required=False), + droplet_id=dict(required=False, type='int'), + oauth_token=dict( + no_log=True, + # Support environment variable for DigitalOcean OAuth Token + fallback=(env_fallback, ['DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN']), + required=True, + ), + validate_certs=dict(type='bool', default=True), + timeout=dict(type='int', default=30), + ), + required_if=[ + ('state', 'delete', ['ip']) + ], + mutually_exclusive=[ + ['region', 'droplet_id'] + ], + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index 1495473..3ae2387 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -20,6 +20,7 @@ algo_ssh_tunneling: "{{ algo_ssh_tunneling }}" algo_store_pki: "{{ algo_store_pki }}" IP_subject_alt_name: "{{ IP_subject_alt_name }}" + alternative_ingress_ip: "{{ alternative_ingress_ip | default(omit) }}" cloudinit: "{{ cloudinit|default(false) }}" - name: Additional variables for the server diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index b41becd..2013a22 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -26,6 +26,19 @@ - Environment:Algo register: digital_ocean_droplet +- block: + - name: "Create a Floating IP" + digital_ocean_floating_ip: + state: present + oauth_token: "{{ algo_do_token }}" + droplet_id: "{{ digital_ocean_droplet.data.droplet.id }}" + register: digital_ocean_floating_ip + + - name: Set the static ip as a fact + set_fact: + cloud_alternative_ingress_ip: "{{ digital_ocean_floating_ip.data.floating_ip.ip }}" + when: alternative_ingress_ip + - set_fact: cloud_instance_ip: "{{ digital_ocean_droplet.data.ip_address }}" ansible_ssh_user: algo diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index f358d3e..4a2c6de 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -1,2 +1,9 @@ --- install_headers: true +aip_supported_providers: + - digitalocean +snat_aipv4: false +ipv6_default: "{{ ansible_default_ipv6.address + '/' + ansible_default_ipv6.prefix }}" +ipv6_subnet_size: "{{ ipv6_default | ipaddr('size') }}" +ipv6_egress_ip: >- + {{ (ipv6_default | next_nth_usable(15 | random(seed=algo_server_name + ansible_fqdn))) + '/124' if ipv6_subnet_size|int > 1 else ipv6_default }} diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index ebbe91a..6bcae5c 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -22,3 +22,6 @@ - name: restart iptables service: name=netfilter-persistent state=restarted + +- name: netplan apply + command: netplan apply diff --git a/roles/common/tasks/aip/digitalocean.yml b/roles/common/tasks/aip/digitalocean.yml new file mode 100644 index 0000000..cd5032f --- /dev/null +++ b/roles/common/tasks/aip/digitalocean.yml @@ -0,0 +1,13 @@ +--- +- name: Get the anchor IP + uri: + url: http://169.254.169.254/metadata/v1/interfaces/public/0/anchor_ipv4/address + return_content: true + register: anchor_ipv4 + until: anchor_ipv4 is succeeded + retries: 30 + delay: 10 + +- name: Set SNAT IP as a fact + set_fact: + snat_aipv4: "{{ anchor_ipv4.content }}" diff --git a/roles/common/tasks/aip/main.yml b/roles/common/tasks/aip/main.yml new file mode 100644 index 0000000..6055fd3 --- /dev/null +++ b/roles/common/tasks/aip/main.yml @@ -0,0 +1,10 @@ +--- +- name: Include alternative ingress ip configuration + include_tasks: + file: "{{ algo_provider if algo_provider in aip_supported_providers else 'placeholder' }}.yml" + when: algo_provider in aip_supported_providers + +- name: Verify SNAT IPv4 found + assert: + that: snat_aipv4 | ipv4 + msg: The SNAT IPv4 address not found. Cannot proceed with the alternative ingress ip. diff --git a/roles/common/tasks/aip/placeholder.yml b/roles/common/tasks/aip/placeholder.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 97c8616..6355bbf 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -78,6 +78,16 @@ - name: Gather additional facts import_tasks: facts.yml +- name: IPv6 egress alias configured + template: + src: 99-algo-ipv6-egress.yaml.j2 + dest: /etc/netplan/99-algo-ipv6-egress.yaml + when: + - ipv6_support + - ipv6_subnet_size|int > 1 + notify: + - netplan apply + - name: Set OS specific facts set_fact: tools: @@ -112,5 +122,9 @@ state: present when: install_headers +- name: Configure the alternative ingress ip + include_tasks: aip/main.yml + when: alternative_ingress_ip + - include_tasks: iptables.yml tags: iptables diff --git a/roles/common/templates/99-algo-ipv6-egress.yaml.j2 b/roles/common/templates/99-algo-ipv6-egress.yaml.j2 new file mode 100644 index 0000000..c0aa9a2 --- /dev/null +++ b/roles/common/templates/99-algo-ipv6-egress.yaml.j2 @@ -0,0 +1,6 @@ +network: + version: 2 + ethernets: + {{ ansible_default_ipv6.interface }}: + addresses: + - {{ ipv6_egress_ip }} diff --git a/roles/common/templates/rules.v4.j2 b/roles/common/templates/rules.v4.j2 index 0f5bfba..764008a 100644 --- a/roles/common/templates/rules.v4.j2 +++ b/roles/common/templates/rules.v4.j2 @@ -35,7 +35,7 @@ COMMIT -A PREROUTING --in-interface {{ ansible_default_ipv4['interface'] }} -p udp --dport {{ wireguard_port_avoid }} -j REDIRECT --to-port {{ wireguard_port_actual }} {% endif %} # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out {{ '-j SNAT --to ' + snat_aipv4 if snat_aipv4 else '-j MASQUERADE' }} COMMIT diff --git a/roles/common/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 index 47226b7..96642a7 100644 --- a/roles/common/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -34,7 +34,7 @@ COMMIT -A PREROUTING --in-interface {{ ansible_default_ipv6['interface'] }} -p udp --dport {{ wireguard_port_avoid }} -j REDIRECT --to-port {{ wireguard_port_actual }} {% endif %} # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j SNAT --to {{ ipv6_egress_ip | ipaddr('address') }} COMMIT diff --git a/server.yml b/server.yml index 782d713..fb472f0 100644 --- a/server.yml +++ b/server.yml @@ -35,6 +35,7 @@ IdentityFile {{ SSH_keys.private }} KeepAlive yes ServerAliveInterval 30 + when: inventory_hostname != 'localhost' become: false delegate_to: localhost