Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 46 additions & 78 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,50 +32,45 @@
- vpn_ensure_openssl | d(true)
- not "/usr/bin/openssl" is exists

- name: Enforce default auth method as needed
- name: Ensure __vpn_connections_fixed is defined and empty
set_fact:
vpn_connections: |
{% for tunnel in vpn_connections %}
{% set _ = tunnel.__setitem__(
"auth_method", tunnel.auth_method | d(vpn_auth_method)
) %}
{% endfor %}
{{ vpn_connections }}
__vpn_connections_fixed: []

- name: Add missing fields to tunnel items
set_fact:
__vpn_connections_fixed: "{{ __vpn_connections_fixed |
union([item | combine(fixed_item) | combine(_host_item)]) | list }}"
loop: "{{ vpn_connections }}"
vars:
fixed_item:
auth_method: "{{ item.auth_method | d(vpn_auth_method) }}"
opportunistic: "{{ item.opportunistic | d(vpn_opportunistic) }}"
_hosts: "{{ item.hosts if item.hosts | d({}) | length > 1
else (item.hosts | combine(_host)) if item.hosts | d({}) | length == 1
else {} }}"
_host_item: "{{ {'hosts': _hosts} if _hosts | length > 0 else {} }}"
_host: "{{ {inventory_hostname: ''} }}"
no_log: true

# any tunnel definition that is not an opportunistic tunnel should have
# hosts defined and not be empty
- name: Make sure that the hosts list is not empty
vars:
# noqa jinja[spacing]
failure: >-
{% for tunnel in vpn_connections %}
{%- if not tunnel.opportunistic | d(vpn_opportunistic) -%}
{%- if not 'hosts' in tunnel or not tunnel.hosts -%}
True
{%- endif -%}
{%- endif -%}
{% endfor %}
count_not_opps: "{{ __vpn_connections_fixed | selectattr('opportunistic') |
list | length }}"
count_has_hosts: "{{ __vpn_connections_fixed | rejectattr('opportunistic') |
selectattr('hosts', 'defined') | selectattr('hosts') | list | length }}"
fail:
msg: list of hosts is empty for one or more tunnels
when: '"True" in failure'

- name: Make sure there is at least one pair of hosts in each connection
set_fact:
vpn_connections: |
{% set new_vpn_connections = [] %}
{% for tunnel in vpn_connections %}
{% if not tunnel.opportunistic | d(vpn_opportunistic) %}
{% if tunnel.hosts | length == 1 %}
{% set _ = tunnel.hosts.update({inventory_hostname: null}) %}
{% endif %}
{% endif %}
{% set _ = new_vpn_connections.append(tunnel) %}
{% endfor %}
{{ new_vpn_connections }}
when:
- __vpn_connections_fixed | length > 0
- count_not_opps == count_has_hosts

- name: Ensure cert_names are populated when auth_method is cert
vars:
# noqa jinja[spacing]
failure: >-
{% for tunnel in vpn_connections %}
{% for tunnel in __vpn_connections_fixed %}
{% if tunnel.auth_method == 'cert' %}
{% if tunnel.opportunistic | d(vpn_opportunistic) %}
{% for host in ansible_play_hosts %}
Expand All @@ -93,42 +88,24 @@
{% endfor %}
{% endif %}
{% endif %}
{% else %}
False
{% endfor %}
fail:
msg: cert_name is missing or empty for one or more hosts in a tunnel
when: '"True" in failure'

- name: Generate PSKs or use provided shared_key_content
no_log: true
- name: Reset __vpn_psks
set_fact:
# noqa jinja[spacing]
__vpn_psks: |
{% set __vpn_psks = {} %}
{% for tunnel in vpn_connections %}
{% if not tunnel.opportunistic | d(vpn_opportunistic) %}
{% set __vpn_idx = loop.index0 %}
{% if tunnel.auth_method == 'psk' %}
{% set _ = __vpn_psks.__setitem__(__vpn_idx, {}) %}
{% for host1, host2 in tunnel.hosts.keys() | combinations(2) %}
{% if not host1 in __vpn_psks[__vpn_idx] %}
{% set _ = __vpn_psks[__vpn_idx].__setitem__(host1, {}) %}
{% endif %}
{% if not host2 in __vpn_psks[__vpn_idx] %}
{% set _ = __vpn_psks[__vpn_idx].__setitem__(host2, {}) %}
{% endif %}
{% if 'shared_key_content' in tunnel %}
{% set val = {'pre_shared_key':tunnel['shared_key_content']} %}
{% else %}
{% set psk = lookup('lines', 'openssl rand -base64 48') %}
{% set val = {'pre_shared_key':psk} %}
{%- endif -%}
{% set _ = __vpn_psks[__vpn_idx][host1].__setitem__(host2, val) %}
{% set _ = __vpn_psks[__vpn_idx][host2].__setitem__(host1, val) %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{{ __vpn_psks }}
__vpn_psks: []

- name: Get PSKs for each tunnel
include_tasks: vpn_get_psks_for_tunnel.yml
loop: "{{ __vpn_connections_fixed }}"
loop_control:
loop_var: tunnel
index_var: tunnel_idx
no_log: true

# The run_once host might not be the first one in hostvars - we do not have
# a good way to know which host in hostvars was the run_once, and it might
Expand All @@ -144,20 +121,10 @@

- name: Build host-to-host tunnels
vars:
tunnels: |
{% set unique_tunnels = [] %}
{% for tunnel in vpn_connections %}
{% if not tunnel.opportunistic | d(vpn_opportunistic) %}
{% if inventory_hostname in tunnel.hosts %}
{% for node in tunnel.hosts %}
{% if node != inventory_hostname %}
{% set _ = unique_tunnels.append(node) %}
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}
{{ unique_tunnels | unique }}
tunnels: "{{ __vpn_connections_fixed | rejectattr('opportunistic') |
selectattr('hosts', 'contains', inventory_hostname) | map(attribute='hosts') |
map('dict2items') | flatten | map(attribute='key') |
flatten | reject('match', '^' ~ inventory_hostname ~ '$') | unique | list }}"
block:
- name: Create ipsec.conf files
template:
Expand All @@ -174,6 +141,7 @@
loop: "{{ tunnels }}"

- name: Create ipsec.secrets files
no_log: true
template:
src: "{{ vpn_provider }}-host-to-host.secrets.j2"
dest: "/etc/ipsec.d/{{ inventory_hostname }}-to-{{ item.item }}.secrets"
Expand All @@ -186,7 +154,7 @@
- name: Build opportunistic configuration
include_tasks: tasks/mesh_conf.yml
when: conn.opportunistic | d(vpn_opportunistic)
loop: "{{ vpn_connections }}"
loop: "{{ __vpn_connections_fixed }}"
loop_control:
loop_var: conn
no_log: true
117 changes: 58 additions & 59 deletions tasks/mesh_conf.yml
Original file line number Diff line number Diff line change
@@ -1,66 +1,65 @@
---
# yamllint disable rule:line-length
- name: Set current IP fact for each host
set_fact:
current_ip: "{{ ansible_default_ipv4.address | d(ansible_default_ipv6.address) }}"
- name: Set mesh configuration
vars:
conn_policies: "{{ conn.policies | selectattr('cidr', 'match', '^default$') | map(attribute='policy') | join(',') }}"
pol_default: "{{ vpn_default_policy if conn_policies | length == 0 else conn_policies }}"
block:
- name: Set current IP fact for each host
set_fact:
__vpn_current_ip: "{{ ansible_default_ipv4.address | d(ansible_default_ipv6.address) }}"

- name: Set IP with prefix register
shell: |-
set -euo pipefail
ip addr show | grep {{ current_ip }} | awk '{print $2}'
register:
ip_with_prefix_register
changed_when: false
- name: Set IP with prefix register
shell: |-
set -euo pipefail
ip addr show | grep {{ __vpn_current_ip }} | awk '{print $2}'
register: __vpn_ip_with_prefix_register
changed_when: false

- name: Set net CIDR fact
set_fact:
current_subnet: "{{ ip_with_prefix_register.stdout | vpn_ipaddr('subnet') }}"
- name: Set net CIDR fact
set_fact:
__vpn_current_subnet: "{{ __vpn_ip_with_prefix_register.stdout | vpn_ipaddr('subnet') }}"

- name: Set policies fact
set_fact:
policies: "{{ conn.policies | rejectattr('cidr', 'match', '^default$') | list }}"
- name: Set policies fact
set_fact:
__vpn_policies: "{{ conn.policies | rejectattr('cidr', 'match', '^default$') | list }}"

- name: Apply the default policy as needed
delegate_to: localhost
run_once: true
vars:
# noqa jinja[spacing]
pol_default: >-
{% set pol = conn.policies | selectattr('cidr', 'match', '^default$') | map(attribute='policy') | join(',') %}
{%- if pol | length == 0 -%}{{ vpn_default_policy }}{%- else -%}{{ pol }}{%- endif -%}
set_fact:
policies: |
{% for node in ansible_play_hosts %}
{% set node_in_pol = {'flag': false} %}
{% for policy in policies %}
{% if hostvars[node].current_ip | vpn_ipaddr(policy.cidr) | vpn_ipaddr('bool') %}
{% if node_in_pol.update({'flag': true}) %}{% endif %}
{% endif %}
{% endfor %}
{% if not node_in_pol.flag | bool %}
{% set new_pol = {} %}
{% set _ = new_pol.__setitem__('policy', pol_default) %}
{% set _ = new_pol.__setitem__('cidr', hostvars[node].current_subnet) %}
{% set _ = policies.append(new_pol) %}
{% endif %}
{% endfor %}
{{ policies | unique }}
- name: Apply the default policy as needed
set_fact:
__new_vpn_policies: "{{ __new_vpn_policies | d([]) + new_policy_or_empty | list }}"
delegate_to: localhost
run_once: true
loop: "{{ the_product }}"
vars:
the_product: "{{ policy_cidrs | product(node_ips_subnets) | list }}"
in_subnet: "{{ item.1.0 | vpn_ipaddr(item.0) | vpn_ipaddr('bool') }}"
new_policy:
policy: "{{ pol_default }}"
cidr: "{{ item.1.1 }}"
new_policy_or_empty: "{{ [new_policy] if not in_subnet else [] }}"
policy_cidrs: "{{ __vpn_policies | map(attribute='cidr') | list }}"
node_ips: "{{ ansible_play_hosts | map('extract', hostvars, '__vpn_current_ip') | list }}"
node_subnets: "{{ ansible_play_hosts | map('extract', hostvars, '__vpn_current_subnet') | list }}"
node_ips_subnets: "{{ node_ips | zip(node_subnets) | list }}"

- name: Reset policies to include the added ones above
set_fact:
__vpn_policies: "{{ __vpn_policies + __new_vpn_policies | unique | list }}"

- name: Write tunnel policies for each network
template:
src: 'policy.j2'
dest: "/etc/ipsec.d/policies/{{ item }}"
mode: '0644'
loop: "{{ policies | map(attribute='policy') | unique | list }}"
notify:
- __vpn_handler_enable_start_vpn
- __vpn_handler_init_mesh_conns
- name: Write tunnel policies for each network
template:
src: 'policy.j2'
dest: "/etc/ipsec.d/policies/{{ item }}"
mode: '0644'
loop: "{{ __vpn_policies | map(attribute='policy') | unique | list }}"
notify:
- __vpn_handler_enable_start_vpn
- __vpn_handler_init_mesh_conns

- name: Deploy opportunistic configuration to each node
template:
src: "{{ vpn_provider }}-mesh.conf.j2"
dest: "/etc/ipsec.d/mesh.conf"
mode: '0644'
notify:
- __vpn_handler_enable_start_vpn
- __vpn_handler_init_mesh_conns
- name: Deploy opportunistic configuration to each node
template:
src: "{{ vpn_provider }}-mesh.conf.j2"
dest: "/etc/ipsec.d/mesh.conf"
mode: '0644'
notify:
- __vpn_handler_enable_start_vpn
- __vpn_handler_init_mesh_conns
21 changes: 21 additions & 0 deletions tasks/vpn_get_psks_for_tunnel.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
- name: Generate PSKs when not opportunistic and using psk
when:
- not tunnel.opportunistic
- tunnel.auth_method == "psk"
block:
- name: Reset __vpn_host_pairs
set_fact:
__vpn_host_pairs: []

- name: Generate host list for PSKs
set_fact:
__vpn_host_pairs: "{{ __vpn_host_pairs | d([]) + [{'host_pairs': item, 'pre_shared_key': pre_shared_key}] | list }}"
loop: "{{ tunnel.hosts | d({}) | dict2items | flatten | map(attribute='key') | combinations(2) | list }}"
vars:
pre_shared_key: "{{ tunnel.shared_key_content | d(lookup('lines', 'openssl rand -base64 48')) }}"
no_log: true

- name: Generate PSKs or use provided shared_key_content
set_fact:
__vpn_psks: "{{ __vpn_psks | d({}) | combine({tunnel_idx: __vpn_host_pairs}) }}"
no_log: true
2 changes: 1 addition & 1 deletion templates/libreswan-host-to-host.conf.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#jinja2: lstrip_blocks: True
{{ ansible_managed | comment }}
{{ "system_role:vpn" | comment(prefix="", postfix="") }}
{% for tunnel in vpn_connections %}
{% for tunnel in __vpn_connections_fixed %}
{% if item in tunnel.hosts %}
{% set otherhost = tunnel.hosts[item].hostname | d((hostvars[item] | d({})).ansible_host | d(item)) %}
{% set rightid = tunnel.hosts[item].rightid | d(otherhost) %}
Expand Down
12 changes: 7 additions & 5 deletions templates/libreswan-host-to-host.secrets.j2
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
#jinja2: lstrip_blocks: True
{{ ansible_managed | comment }}
{{ "system_role:vpn" | comment(prefix="", postfix="") }}
{% for tunnel in vpn_connections %}
{% for tunnel in __vpn_connections_fixed %}
{% set __vpn_idx = loop.index0 %}
{% if tunnel.auth_method == 'psk' %}
{% for host, val in tunnel.hosts.items() %}
{% if host == inventory_hostname or host == ansible_host %}
{% for otherhost, otherval in __vpn_psks[__vpn_idx][host].items() %}
{% if otherhost == item.item %}
{% for host_pairs_key in __vpn_psks[__vpn_idx] %}
{% set host_pairs = host_pairs_key['host_pairs'] %}
{% set pre_shared_key = host_pairs_key['pre_shared_key'] %}
{% if (host_pairs[0] == item.item and host_pairs[1] == host) or (host_pairs[1] == item.item and host_pairs[0] == host) %}
{% set thishost = host %}
{% set host = tunnel.hosts[host].hostname | d((hostvars[host] | d({})).ansible_host | d(host)) %}
{% set leftid = tunnel.hosts[thishost].leftid | d(host) %}
{% set otherhost = tunnel.hosts[otherhost].hostname | d((hostvars[otherhost] | d({})).ansible_host | d(otherhost)) %}
{% set otherhost = tunnel.hosts[item.item].hostname | d((hostvars[item.item] | d({})).ansible_host | d(item.item)) %}
{% set rightid = tunnel.hosts[item.item].rightid | d(otherhost) %}
{{ host | vpn_ipaddr | ternary('','@') }}{{ leftid }} {{ otherhost | vpn_ipaddr | ternary('','@') }}{{ rightid }} : PSK "{{ otherval['pre_shared_key'] }}"
{{ host | vpn_ipaddr | ternary('','@') }}{{ leftid }} {{ otherhost | vpn_ipaddr | ternary('','@') }}{{ rightid }} : PSK "{{ pre_shared_key }}"
{% endif %}
{% endfor %}
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion templates/policy.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{ ansible_managed | comment }}
{{ "system_role:vpn" | comment(prefix="", postfix="") }}
{% for policy in policies %}
{% for policy in __vpn_policies %}
{% if policy.policy == item %}
{{ policy.cidr }}
{% endif %}
Expand Down
24 changes: 8 additions & 16 deletions tests/tasks/add_hosts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,15 @@
name: "{{ 'host%02x.local' | format(item) }}"
groups: testing
cert_name: "{{ __vpn_dynamic_hosts_sample_cert }}"
current_ip: "{{ __vpn_dynamic_hosts_sample_ip }}"
current_subnet: "{{ __vpn_dynamic_hosts_sample_cidr }}"
__vpn_current_ip: "{{ __vpn_dynamic_hosts_sample_ip }}"
__vpn_current_subnet: "{{ __vpn_dynamic_hosts_sample_cidr }}"
loop: "{{ range(1, __vpn_num_hosts + 1) | list }}"

- name: Create mock vpn_connections
set_fact:
vpn_connections: |
{% set vpn_connections = [] %}
{% set myhosts = {} %}
{% for host in (ansible_play_batch + groups['testing'] + [inventory_hostname]) | unique %}
{% if '/' in host %}
{% set _ = myhosts.__setitem__(__vpn_main_hostname, "") %}
{% else %}
{% set _ = myhosts.__setitem__(host, "") %}
{% endif %}
{% endfor %}
{% set empty_host = {} %}
{% set _ = empty_host.__setitem__('hosts', myhosts) %}
{% set _ = vpn_connections.append(empty_host) %}
{{ vpn_connections }}
vpn_connections:
- hosts: "{{ myhosts }}"
vars:
myhosts: "{{ dict(hostlist | product(['']) | list) }}"
hostlist: "{{ (ansible_play_batch + groups['testing'] + [inventory_hostname]) | unique |
map('regex_replace', '^.*/.*', __vpn_main_hostname) | list }}"
Loading
Loading