Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various improvements required to connect to a managed remote host #65

Merged
merged 26 commits into from Aug 18, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74a53f2
enable use of shared_key_content variable (as mentioned in README.md)
badnetmask Aug 9, 2022
ba4a427
allow custom definition of leftid/rightid when tunnel is psk
badnetmask Aug 10, 2022
f73b191
add no_log to opportunistic task, otherwise shared_key_content can le…
badnetmask Aug 10, 2022
7975805
no need to add the current host to the list of hosts when only one pt…
badnetmask Aug 10, 2022
ba8bc88
update README with what has been implemented (so far)
badnetmask Aug 10, 2022
83367e7
implements the ike, esp and type options
badnetmask Aug 10, 2022
14f2b19
remove unnecessary note in README
badnetmask Aug 10, 2022
c90c5b0
add extra note about PSK in README
badnetmask Aug 10, 2022
fc84094
amend task title to reflect new functionality
richm Aug 16, 2022
2421be3
have to check for key exists before dereference
richm Aug 16, 2022
65fdc58
add test for shared_key_content
richm Aug 16, 2022
790f5df
fix yamllint issue
richm Aug 16, 2022
342ddda
more yamllint fixes
richm Aug 16, 2022
b042876
rename ike->ike_enc_alg and esp->esp_enc_alg
badnetmask Aug 16, 2022
5ffaf49
implement ikelifetime and salifetime
badnetmask Aug 16, 2022
7e1837c
Revert "rename ike->ike_enc_alg and esp->esp_enc_alg"
badnetmask Aug 17, 2022
b8aea97
use ascii double quotes
richm Aug 17, 2022
6bc1b39
add test for new parameters; update docs
richm Aug 17, 2022
7c61677
Update README.md
badnetmask Aug 18, 2022
4432c1e
Merge pull request #1 from richm/various_improvements-richm
badnetmask Aug 18, 2022
76ac157
add retransmit_timeout option
badnetmask Aug 18, 2022
1703eab
added test for retransmit_timeout
richm Aug 18, 2022
bdbe947
add support for dpddelay, dpdtimeout, dpdaction, leftupdown; refactor…
richm Aug 18, 2022
aa60be7
Merge pull request #2 from richm/various_improvements-rich2
badnetmask Aug 18, 2022
ea0e1c1
address issue with leftupdown: null
richm Aug 18, 2022
9e27c03
Merge pull request #3 from richm/various_improvements-rich2
badnetmask Aug 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 32 additions & 25 deletions README.md
Expand Up @@ -20,6 +20,8 @@ all:

The role will set up a vpn tunnel between each pair of hosts in the list of `vpn_connections`, using the default parameters, including generating keys as needed. This role assumes that the names of the hosts under `hosts` are the same as the names of the hosts used in the Ansible inventory, and that you can use those names to configure the tunnels (i.e. they are real FQDNs that resolve correctly).

The exception to the above is when you define a `hostname` variable under any given host, containing an FQDN, in which case the role will assume this is a managed host and won't attempt to make any changes to it (more details in [hosts](#hosts))

## Requirements

The Ansible controller requires the python `ipaddress` package on EL7 systems,
Expand All @@ -37,7 +39,7 @@ These global variables should be applied to the configuration for every tunnel (
| vpn\_regen\_keys | Whether pre-shared keys should be regenerated for sets of hosts with existing keys. | bool | no | false |
| vpn\_opportunistic | Whether an opportunistic mesh configuration should be used. | bool | no | false |
| vpn\_default\_policy | The default policy group to add target machines to under a mesh configuration. | str | no | `private-or-clear` |
| [vpn\_ensure\_openssl](#vpn_ensure_openssl) | Ensure the `openssl` package is installed on the controller. | bool | no | true |
| [vpn\_ensure\_openssl](#vpn_ensure_openssl) | Ensure the `openssl` package is installed on the controller. | bool | no | true |
| [vpn\_connections](#vpn_connections) | List of VPN connections to make. | list | yes | - |

### vpn_auth_method
Expand All @@ -51,13 +53,13 @@ Acceptable values:

The role uses `openssl` to generate PSKs. It requires this to be installed on the controller node.
The default value is `true`. If you have pre-generated your PSKs, or you are not using PSKs, then
set `vpn_ensure_openssl: false`.
set `vpn_ensure_openssl: false`. You can also define the PSKs using the `shared_key_content` variable in a host in any given tunnel.

### vpn_connections

`vpn_connections` is a list of connections. Each connection is either:

* A list of hosts specified by `hosts`. In this host-to-host use case, the role creates tunnels between each pair of hosts. At least one tunnel must be defined in this list.
* A list of hosts specified by `hosts`. In this host-to-host use case, the role creates tunnels between each pair of hosts. At least one tunnel must be defined in this list. If a single tunnel is required, you only need to specify the remote side.

* A mesh configuration consisting of one or more subnets and profiles. In this mesh use case, the role deploys an opportunistic mesh configuration using the `policy`/`cidr` pairs that you define in the `policies`.

Expand Down Expand Up @@ -121,8 +123,16 @@ For each host key in this dictionary, the following host-specific parameters can
| Parameter | Description | Type | Required | Default | Libreswan Equivalent |
|-----------------------------------|-----------------------------------------------------------------------------------------------|:-----------:|:--------:|-------------------------|:----------------------------:|
| [hostname](#hostname) | Host name or IP address to use for setting up a VPN connection. | str | no | - | left/right |
| [cert_name](#cert_name) | Certificate nickname of this host's certificate in the NSS database. | str | no | - | leftcert/rightcert |
| [cert_name](#cert_name) | Certificate nickname of this host's certificate in the NSS database. (Only used when `auth_method` is `cert`) | str | no | - | leftcert/rightcert |
| subnets | A list of the subnets that should be available via the VPN connection. | list | no | - | leftsubnets/rightsubnets |
| shared_key_content | A pre-defined PSK. If not defined, the role will generate one using `openssl`. **IMPORTANT:** it is strongly suggested that you don't set a string in your inventory, but instead read this from a Vault. Also, the PSK will be visible while running in verbose or debug mode. | str | no | - | PSK from ipsec.secrets file |
| leftid | How the left participant (local) should be identified for authentication. | str | no | the local host FQDN (not the controller) | leftid |
| rightid | How the right participant (remote) should be identified for authentication. | str | no | the remote host FQDN | rightid |
| ike_enc_alg | IKE encryption/authentication algorithm to be used for the connection (phase 1 aka ISAKMP SA). | str | no | - | ike |
| esp_enc_alg | Specifies the algorithms that will be offered/accepted for a Child SA negotiation. | str | no | - | esp |
| ikelifetime | How long the keying channel of a connection (buzzphrase: “IKE SA” or “Parent SA”) should last before being renegotiated. | str | no | - | ikelifetime |
richm marked this conversation as resolved.
Show resolved Hide resolved
| salifetime | How long a particular instance of a connection (a set of encryption/authentication keys for user packets) should last, from successful negotiation to expiry | str | no | - | salifetime |
richm marked this conversation as resolved.
Show resolved Hide resolved
| type | The type of the connection. | str | no | tunnel | type |

#### hostname

Expand Down Expand Up @@ -258,6 +268,24 @@ This playbook sets up host-to-host tunnels between each pair of hosts in the lis
cert_name: bastion3cert
```

### Host-to-managed-host (remote is an appliance, or not managed via Ansible)

This playbook sets up a host-to-host tunnel between the current host in the inventory, and a remote host not managed by Ansible (like an appliance) which requires proper identification. In this example `this_host` should be manually set with the same name as `inventory_hostname`.

```yaml
vars:
vpn_connections:
- auth_method: psk
auto: start
hosts:
this_host:
leftid: idofthecliet
nfsserver:
hostname: nfsserver.example.com
shared_key_content: "secure psk from vault"
rightid: idoftheserver
```

### Opportunistic Mesh VPN configuration

This playbook sets up an opportunistic mesh VPN configuration on each host in the list of `hosts`, using certificates for authentication. In this example, the controller machine shares the same CIDR as both of the target machines (`192.168.110.0/24`) and has IP address `192.168.110.7`. Therefore the controller machine will fall under a `private` policy which will automatically be created for the CIDR `192.168.110.0/24`. To prevent an SSH connection loss during the play, a `clear` policy for the controller machine has been added to the list of `policies`. Note that there is also an item in the `policies` list where the `cidr` is equal to `default`. This is because this playbook is overriding the default policy rule to make it `private` instead of `private-or-clear`.
Expand Down Expand Up @@ -294,30 +322,9 @@ The following global variables will be added. Additionally, `pubkey` will be add
| vpn\_enc\_alg | VPN encryption algorithm to use. See [Algorithms section](#algorithms) for acceptable values. | str | no | - |
| vpn\_auth\_alg | VPN authentication algorithm to use. | str | no | SHA-2 |
| vpn\_wait | If tasks should wait for the VPN tunnel to be started up. | bool | no | false |
| [vpn\_lifetime](#vpn_lifetime) | How long a VPN instance should last before being renegotiated. Acceptable values are an integer optionally followed by **s** (a time in seconds) or a decimal number followed by **m**, **h**, or **d** (a time in minutes, hours, or days respectively). | int | no | - |
| vpn\_public\_key\_src | Path to file on the controller host containing public key used by default. | str | no | - |
| vpn\_public\_key\_content | Contains the public key used by default for public key authentication without certificates. | str | no | - |

Two dictionaries (`ike` and `ipsec`) will be added to the `vpn_connections` dictionary:

| Parameter | Description | Type | Required | Default | Libreswan Equivalent |
|-------------------------------------------|---------------------------------------------------------------------------------------|:-----------:|:--------:|-------------------------|-------------------------|
| ike | Defines information specific to setting up the IKE protocol. | dict | no | - | - |
| ike.enc_alg | Which encryption algorithm to use for IKE. | str | no | vpn\_enc\_alg | ike |
| ike.enc_keysize | Size of encryption key to use for IKE. | str | no | - | ike |
| ike.auth_alg | Which authentication algorithm to use for IKE. | str | no | vpn\_auth\_alg | ike |
| ike.dh_group | Which Diffie-Hellman group to use for IKE. | str | no | - | ike |
| ike.lifetime | How long keying channel of an IKE connection should last before being renegotiated. | str | no | vpn\_lifetime | ikelifetime |
| ipsec | Defines information specific to setting up IPSec protocol. | dict | no | - | - |
| ipsec.enc_alg | Which encryption algorithm to use for IPSec. | str | no | vpn\_enc\_alg | esp |
| ipsec.enc_keysize | Size of the encryption key used for IPSec. | str | no | - | esp |
| ipsec.auth_alg | Which authentication algorithm to use for IPSec. | str | no | vpn\_auth\_alg | esp |
| ipsec.dh_group | Which Diffie-Hellman group to use for IPSec. | str | no | - | esp |
| ipsec.lifetime | How long keying channel of an IPSec connection should last before being renegotiated. | str | no | vpn\_lifetime | salifetime |
| ipsec.mode | The type of the connection. User can specify `tunnel` or `transport`, however Libreswan defaults this value to `tunnel` if not specified. If the hosts are behind NAT, the user should specify `transport`. | str | no | `tunnel` | type |
| [shared_key_src](#shared_key_src) | **Not recommended.** Path to file on the controller host containing a PSK. | str | no | - | From ipsec.secrets file |
| [shared_key_content](#shared_key_content) | **Not recommended.** The actual PSK in a vault secret or base64 encoded string. | str | no | - | From ipsec.secrets file |

The following variables will be added under the [`hosts`](#hosts) dictionary:

| Parameter | Description | Type | Required | Default | Libreswan Equivalent |
Expand Down
25 changes: 22 additions & 3 deletions tasks/main.yml
Expand Up @@ -46,6 +46,20 @@
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 }}

- name: Ensure cert_names are populated when auth_method is cert
vars:
failure: >-
Expand All @@ -72,7 +86,7 @@
msg: cert_name is missing or empty for one or more hosts in a tunnel
when: '"True" in failure'

- name: generate psks
- name: Generate PSKs or use provided shared_key_content
no_log: true
set_fact:
__vpn_psks: |
Expand All @@ -89,8 +103,12 @@
{% if not host2 in __vpn_psks[__vpn_idx] %}
{% set _ = __vpn_psks[__vpn_idx].__setitem__(host2, {}) %}
{% endif %}
{% set psk = lookup('lines', 'openssl rand -base64 48') %}
{% set val = {'pre_shared_key':psk} %}
{% if 'shared_key_content' in tunnel.hosts[host2] %}
{% set val = {'pre_shared_key':tunnel.hosts[host2]['shared_key_content']} %}
{% else %}
{% set psk = lookup('lines', 'openssl rand -base64 48') %}
{% set val = {'pre_shared_key':psk} %}
{%- endif -%}
richm marked this conversation as resolved.
Show resolved Hide resolved
{% set _ = __vpn_psks[__vpn_idx][host1].__setitem__(host2, val) %}
{% set _ = __vpn_psks[__vpn_idx][host2].__setitem__(host1, val) %}
{% endfor %}
Expand Down Expand Up @@ -161,3 +179,4 @@
loop: "{{ vpn_connections }}"
loop_control:
loop_var: conn
no_log: true
22 changes: 20 additions & 2 deletions templates/libreswan-host-to-host.conf.j2
Expand Up @@ -3,12 +3,15 @@
{% for tunnel in vpn_connections %}
{% 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) %}
{% for host in tunnel.hosts %}
{% if host == inventory_hostname or host == ansible_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) %}
conn {{ tunnel.name ~ '-' if 'name' in tunnel and tunnel.name else '' }}{{ host }}-to-{{ otherhost }}
left={{ host }}
leftid={{ host | vpn_ipaddr | ternary('','@') }}{{ host }}
leftid={{ host | vpn_ipaddr | ternary('','@') }}{{ leftid }}
{% if tunnel.hosts[host] is mapping and 'subnets' in tunnel.hosts[host] %}
leftsubnets={
{%- for subnet in tunnel.hosts[host].subnets -%}
Expand All @@ -21,7 +24,7 @@ conn {{ tunnel.name ~ '-' if 'name' in tunnel and tunnel.name else '' }}{{ host
{% endfor %}
right={{ otherhost }}
{% if tunnel.auth_method == 'psk' %}
rightid={{ otherhost | vpn_ipaddr | ternary('','@') }}{{ otherhost }}
rightid={{ otherhost | vpn_ipaddr | ternary('','@') }}{{ rightid }}
{% endif %}
{% if tunnel.hosts[item] is mapping and 'subnets' in tunnel.hosts[item] %}
rightsubnets={
Expand All @@ -32,6 +35,21 @@ conn {{ tunnel.name ~ '-' if 'name' in tunnel and tunnel.name else '' }}{{ host
}
{% endif %}
ikev2={{ __vpn_ikev2 }}
{% if 'ike_enc_alg' in tunnel %}
ike={{ tunnel.ike_enc_alg }}
{% endif %}
{% if 'esp_enc_alg' in tunnel %}
esp={{ tunnel.esp_enc_alg }}
{% endif %}
{% if 'ikelifetime' in tunnel %}
ikelifetime={{ tunnel.ikelifetime }}
{% endif %}
{% if 'salifetime' in tunnel %}
salifetime={{ tunnel.salifetime }}
{% endif %}
{% if 'type' in tunnel %}
type={{ tunnel.type }}
{% endif %}
{% if 'auto' in tunnel %}
auto={{ tunnel.auto }}
{% endif %}
Expand Down
5 changes: 4 additions & 1 deletion templates/libreswan-host-to-host.secrets.j2
Expand Up @@ -7,9 +7,12 @@
{% if host == inventory_hostname or host == ansible_host %}
{% for otherhost, otherval in __vpn_psks[__vpn_idx][host].items() %}
{% if otherhost == item.item %}
{% 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)) %}
{{ host | vpn_ipaddr | ternary('','@') }}{{ host }} {{ otherhost | vpn_ipaddr | ternary('','@') }}{{ otherhost }} : PSK "{{ otherval['pre_shared_key'] }}"
{% set rightid = tunnel.hosts[item.item].rightid | d(otherhost) %}
{{ host | vpn_ipaddr | ternary('','@') }}{{ leftid }} {{ otherhost | vpn_ipaddr | ternary('','@') }}{{ rightid }} : PSK "{{ otherval['pre_shared_key'] }}"
{% endif %}
{% endfor %}
{% endif %}
Expand Down
8 changes: 8 additions & 0 deletions tests/tests_host_to_host_psk.yml
Expand Up @@ -8,6 +8,7 @@

vars:
__vpn_num_hosts: 2
__vpn_shared_key_content: this is the shared key

tasks:

Expand All @@ -22,8 +23,15 @@
vpn_connections: |
{% for tunnel in vpn_connections %}
{% set _ = tunnel.__setitem__("auto", "start") %}
{% if __vpn_shared_key_content | d(false) %}
{% set _ = tunnel["hosts"].__setitem__(__host, __dict) %}
{% endif %}
{% endfor %}
{{ vpn_connections }}
vars:
__host: host01.local
__dict:
shared_key_content: "{{ __vpn_shared_key_content }}"

- name: Use vpn role
include_role:
Expand Down