From e30ffc097075b5d061c9aa2069cb8fbbe4c6db84 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 8 Feb 2026 10:38:39 -0500 Subject: [PATCH 1/3] fix: add explicit bool filters for Ansible 12 jinja2_native compatibility Ansible 12 enables jinja2_native by default, which means string values like "true"/"false" are no longer automatically coerced to booleans in when: conditions and Jinja2 if statements. Add | bool filters to all boolean variable references in tasks, templates, and handlers. Also reformats long single-line Jinja2 conditionals into multi-line for readability, fixes GCE default() calls for native mode, adds help command to the algo script, and updates test fixtures to register the bool filter. Co-Authored-By: Claude Opus 4.6 --- algo | 24 +++++++++++ input.yml | 40 ++++++++++++++----- roles/cloud-azure/tasks/main.yml | 5 ++- roles/cloud-cloudstack/tasks/main.yml | 5 ++- roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/cloud-digitalocean/tasks/prompts.yml | 5 ++- roles/cloud-ec2/tasks/prompts.yml | 5 ++- roles/cloud-gce/tasks/prompts.yml | 4 +- roles/cloud-hetzner/tasks/prompts.yml | 5 ++- roles/cloud-linode/tasks/prompts.yml | 5 ++- roles/cloud-vultr/tasks/prompts.yml | 5 ++- roles/common/defaults/main.yml | 5 ++- roles/common/tasks/iptables.yml | 2 +- roles/common/tasks/ubuntu.yml | 5 ++- roles/common/templates/rules.v4.j2 | 22 +++++----- roles/common/templates/rules.v6.j2 | 20 +++++----- roles/dns/handlers/main.yml | 4 +- roles/dns/tasks/main.yml | 6 +-- .../templates/dnscrypt-proxy/filters.toml.j2 | 2 +- .../templates/dnscrypt-proxy/global.toml.j2 | 6 +-- roles/strongswan/defaults/main.yml | 6 ++- roles/strongswan/tasks/main.yml | 2 +- roles/strongswan/tasks/openssl.yml | 4 +- roles/strongswan/templates/ipsec.conf.j2 | 6 +-- roles/wireguard/defaults/main.yml | 12 ++++-- roles/wireguard/tasks/main.yml | 2 +- roles/wireguard/templates/server.conf.j2 | 2 +- server.yml | 18 ++++----- tests/unit/test_iptables_rules.py | 10 +++++ tests/unit/test_wireguard_key_generation.py | 2 +- users.yml | 18 ++++++--- 31 files changed, 177 insertions(+), 82 deletions(-) diff --git a/algo b/algo index 1ebb09507..a69287a65 100755 --- a/algo +++ b/algo @@ -160,6 +160,30 @@ fi # Run the appropriate playbook case "$1" in + help|-h|--help) + echo "Usage: ./algo [COMMAND] [ANSIBLE_OPTIONS]" + echo "" + echo "Set up a personal VPN in the cloud." + echo "" + echo "Commands:" + echo " (default) Deploy a new VPN server" + echo " update-users Add or remove users on an existing server" + echo "" + echo "Configuration:" + echo " Edit config.cfg to set users, DNS, and VPN options before deploying." + echo "" + echo "Non-interactive deployment:" + echo " ./algo -e 'provider=digitalocean server_name=algo region=nyc3 do_token=TOKEN'" + echo "" + echo "Common Ansible options (passed through):" + echo " -e KEY=VALUE Set variable (bypass interactive prompts)" + echo " -v, -vvv Increase output verbosity" + echo " --skip-tags TAG Skip specific components" + echo " -t, --tags TAG Run only specific components" + echo "" + echo "Docs: https://trailofbits.github.io/algo/" + exit 0 + ;; update-users) uv run ansible-playbook users.yml "${@:2}" -t update-users ;; *) diff --git a/input.yml b/input.yml index 8dbf3daac..7b65110c9 100644 --- a/input.yml +++ b/input.yml @@ -109,22 +109,44 @@ - name: Set facts based on the input set_fact: algo_server_name: >- - {% if server_name is defined %}{% set _server = server_name %}{%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input | length > 0 -%} + {%- if server_name is defined -%}{% set _server = server_name %} + {%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input | length > 0 -%} {%- set _server = _algo_server_name.user_input -%} - {%- else %}{% set _server = defaults['server_name'] %}{% endif -%} + {%- else -%}{% set _server = defaults['server_name'] %} + {%- endif -%} {{ _server | regex_replace('(?!\.)(\W|_)', '-') }} algo_ondemand_cellular: >- - {% if ondemand_cellular is defined %}{{ ondemand_cellular | bool }}{%- elif _ondemand_cellular.user_input is defined %}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }}{%- else %}{{ false }}{% endif %} + {%- if ondemand_cellular is defined -%}{{ ondemand_cellular | bool }} + {%- elif _ondemand_cellular.user_input is defined -%}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }} + {%- else -%}{{ false }} + {%- endif -%} algo_ondemand_wifi: >- - {% if ondemand_wifi is defined %}{{ ondemand_wifi | bool }}{%- elif _ondemand_wifi.user_input is defined %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }}{%- else %}{{ false }}{% endif %} + {%- if ondemand_wifi is defined -%}{{ ondemand_wifi | bool }} + {%- elif _ondemand_wifi.user_input is defined -%}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} + {%- else -%}{{ false }} + {%- endif -%} algo_ondemand_wifi_exclude: >- - {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude | b64encode }}{%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input | length > 0 -%} - {{ _ondemand_wifi_exclude.user_input | b64encode }}{%- else %}{{ '_null' | b64encode }}{% endif %} + {%- if ondemand_wifi_exclude is defined -%}{{ ondemand_wifi_exclude | b64encode }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input | length > 0 -%} + {{ _ondemand_wifi_exclude.user_input | b64encode }} + {%- else -%}{{ '_null' | b64encode }} + {%- endif -%} algo_dns_adblocking: >- - {% if dns_adblocking is defined %}{{ dns_adblocking | bool }}{%- elif _dns_adblocking.user_input is defined %}{{ booleans_map[_dns_adblocking.user_input] | default(defaults['dns_adblocking']) }}{%- else %}{{ false }}{% endif %} + {%- if dns_adblocking is defined -%}{{ dns_adblocking | bool }} + {%- elif _dns_adblocking.user_input is defined -%}{{ booleans_map[_dns_adblocking.user_input] | default(defaults['dns_adblocking']) }} + {%- else -%}{{ false }} + {%- endif -%} algo_ssh_tunneling: >- - {% if ssh_tunneling is defined %}{{ ssh_tunneling | bool }}{%- elif _ssh_tunneling.user_input is defined %}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }}{%- else %}{{ false }}{% endif %} + {%- if ssh_tunneling is defined -%}{{ ssh_tunneling | bool }} + {%- elif _ssh_tunneling.user_input is defined -%}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }} + {%- else -%}{{ false }} + {%- endif -%} algo_store_pki: >- - {% if ipsec_enabled %}{%- if store_pki is defined %}{{ store_pki | bool }}{%- elif _store_pki.user_input is defined %}{{ booleans_map[_store_pki.user_input] | default(defaults['store_pki']) }}{%- else %}{{ false }}{% endif %}{% endif %} + {%- if ipsec_enabled -%} + {%- if store_pki is defined -%}{{ store_pki | bool }} + {%- elif _store_pki.user_input is defined -%}{{ booleans_map[_store_pki.user_input] | default(defaults['store_pki']) }} + {%- else -%}{{ false }} + {%- endif -%} + {%- endif -%} rescue: - include_tasks: playbooks/rescue.yml diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index e95d48a11..dd1cc2f41 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -4,7 +4,10 @@ - set_fact: algo_region: >- - {% if region is defined %}{{ region }}{%- elif _algo_region.user_input %}{{ azure_regions[_algo_region.user_input | int - 1]['name'] }}{%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ azure_regions[_algo_region.user_input | int - 1]['name'] }} + {%- else -%}{{ azure_regions[default_region | int - 1]['name'] }} + {%- endif -%} - name: Create AlgoVPN Server azure.azcollection.azure_rm_deployment: diff --git a/roles/cloud-cloudstack/tasks/main.yml b/roles/cloud-cloudstack/tasks/main.yml index 08c0561e4..c50a2ab00 100644 --- a/roles/cloud-cloudstack/tasks/main.yml +++ b/roles/cloud-cloudstack/tasks/main.yml @@ -5,7 +5,10 @@ - block: - set_fact: algo_region: >- - {%- if region is defined -%}{{ region }}{%- elif _algo_region.user_input is defined and _algo_region.user_input | length > 0 -%}{{ cs_zones[_algo_region.user_input | int - 1]['name'] }}{%- else -%}{{ cs_zones[default_zone | int - 1]['name'] }}{%- endif -%} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input | length > 0 -%}{{ cs_zones[_algo_region.user_input | int - 1]['name'] }} + {%- else -%}{{ cs_zones[default_zone | int - 1]['name'] }} + {%- endif -%} - name: Security group created cs_securitygroup: diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 845deeb0a..8ac74f609 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -41,7 +41,7 @@ - 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 + when: alternative_ingress_ip | bool - set_fact: cloud_instance_ip: "{{ (droplet.networks.v4 | selectattr('type', '==', 'public')).0.ip_address }}" diff --git a/roles/cloud-digitalocean/tasks/prompts.yml b/roles/cloud-digitalocean/tasks/prompts.yml index 4a0dc25d9..a7415aff7 100644 --- a/roles/cloud-digitalocean/tasks/prompts.yml +++ b/roles/cloud-digitalocean/tasks/prompts.yml @@ -102,4 +102,7 @@ - name: Set additional facts set_fact: algo_do_region: >- - {% if region is defined %}{{ region }}{%- elif _algo_region.user_input %}{{ do_regions[_algo_region.user_input | int - 1]['slug'] }}{%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ do_regions[_algo_region.user_input | int - 1]['slug'] }} + {%- else -%}{{ do_regions[default_region | int - 1]['slug'] }} + {%- endif -%} diff --git a/roles/cloud-ec2/tasks/prompts.yml b/roles/cloud-ec2/tasks/prompts.yml index 393aa63b9..87299ee73 100644 --- a/roles/cloud-ec2/tasks/prompts.yml +++ b/roles/cloud-ec2/tasks/prompts.yml @@ -106,7 +106,10 @@ - name: Set algo_region and stack_name facts set_fact: algo_region: >- - {%- if region is defined -%}{{ region }}{%- elif _algo_region.user_input -%}{{ aws_regions[_algo_region.user_input | int - 1]['region_name'] }}{%- else -%}{{ aws_regions[default_region | int - 1]['region_name'] }}{%- endif -%} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ aws_regions[_algo_region.user_input | int - 1]['region_name'] }} + {%- else -%}{{ aws_regions[default_region | int - 1]['region_name'] }} + {%- endif -%} stack_name: "{{ algo_server_name | replace('.', '-') }}" - block: diff --git a/roles/cloud-gce/tasks/prompts.yml b/roles/cloud-gce/tasks/prompts.yml index 924516a2e..ccd9ea997 100644 --- a/roles/cloud-gce/tasks/prompts.yml +++ b/roles/cloud-gce/tasks/prompts.yml @@ -21,8 +21,8 @@ no_log: true - set_fact: - service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env', 'GCE_EMAIL')) }}" - project_id: "{{ credentials_file_lookup.project_id | default(lookup('env', 'GCE_PROJECT')) }}" + service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env', 'GCE_EMAIL'), true) }}" + project_id: "{{ credentials_file_lookup.project_id | default(lookup('env', 'GCE_PROJECT'), true) }}" no_log: true - block: diff --git a/roles/cloud-hetzner/tasks/prompts.yml b/roles/cloud-hetzner/tasks/prompts.yml index 156cc5b0f..b82e68714 100644 --- a/roles/cloud-hetzner/tasks/prompts.yml +++ b/roles/cloud-hetzner/tasks/prompts.yml @@ -45,4 +45,7 @@ - name: Set additional facts set_fact: algo_hcloud_region: >- - {%- if region is defined -%}{{ region }}{%- elif _algo_region.user_input -%}{{ hcloud_regions[_algo_region.user_input | int - 1]['location'] }}{%- else -%}{{ hcloud_regions[default_region | int - 1]['location'] }}{%- endif -%} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ hcloud_regions[_algo_region.user_input | int - 1]['location'] }} + {%- else -%}{{ hcloud_regions[default_region | int - 1]['location'] }} + {%- endif -%} diff --git a/roles/cloud-linode/tasks/prompts.yml b/roles/cloud-linode/tasks/prompts.yml index bb0070c67..a26420e3b 100644 --- a/roles/cloud-linode/tasks/prompts.yml +++ b/roles/cloud-linode/tasks/prompts.yml @@ -47,5 +47,8 @@ - name: Set additional facts set_fact: algo_linode_region: >- - {%- if region is defined -%}{{ region }}{%- elif _algo_region.user_input -%}{{ linode_regions[_algo_region.user_input | int - 1]['id'] }}{%- else -%}{{ linode_regions[default_region | int - 1]['id'] }}{%- endif -%} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ linode_regions[_algo_region.user_input | int - 1]['id'] }} + {%- else -%}{{ linode_regions[default_region | int - 1]['id'] }} + {%- endif -%} public_key: "{{ lookup('file', SSH_keys.public) }}" diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index c7f809198..fb8c0b4a3 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -55,4 +55,7 @@ - name: Set the desired region as a fact set_fact: algo_vultr_region: >- - {%- if region is defined -%}{{ region }}{%- elif _algo_region.user_input -%}{{ vultr_regions[_algo_region.user_input | int - 1]['id'] }}{%- else -%}{{ vultr_regions[default_region | int - 1]['id'] }}{%- endif -%} + {%- if region is defined -%}{{ region }} + {%- elif _algo_region.user_input -%}{{ vultr_regions[_algo_region.user_input | int - 1]['id'] }} + {%- else -%}{{ vultr_regions[default_region | int - 1]['id'] }} + {%- endif -%} diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index 26156dff3..1b3af775e 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -6,4 +6,7 @@ snat_aipv4: false ipv6_default: "{{ ansible_default_ipv6.address + '/' + ansible_default_ipv6.prefix }}" ipv6_subnet_size: "{{ ipv6_default | ansible.utils.ipaddr('size') }}" ipv6_egress_ip: >- - {{ (ipv6_default | ansible.utils.next_nth_usable(15 | random(seed=algo_server_name + ansible_fqdn))) + '/124' if ipv6_subnet_size | int > 1 else ipv6_default }} + {{ (ipv6_default | ansible.utils.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/tasks/iptables.yml b/roles/common/tasks/iptables.yml index dc921aa70..a725e819b 100644 --- a/roles/common/tasks/iptables.yml +++ b/roles/common/tasks/iptables.yml @@ -18,7 +18,7 @@ owner: root group: root mode: '0640' - when: ipv6_support + when: ipv6_support | bool loop: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index a73f472f1..e03d9d261 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -131,7 +131,8 @@ - openssl - gnupg2 - cron - sysctl: "{{ [{'item': 'net.ipv4.ip_forward', 'value': 1}, {'item': 'net.ipv4.conf.all.forwarding', 'value': 1}, {'item': 'net.ipv4.conf.all.route_localnet', 'value': 1}] + ([{'item': 'net.ipv6.conf.all.forwarding', 'value': 1}] if ipv6_support else []) }}" + # yamllint disable-line rule:line-length + sysctl: "{{ [{'item': 'net.ipv4.ip_forward', 'value': 1}, {'item': 'net.ipv4.conf.all.forwarding', 'value': 1}, {'item': 'net.ipv4.conf.all.route_localnet', 'value': 1}] + ([{'item': 'net.ipv6.conf.all.forwarding', 'value': 1}] if ipv6_support | bool else []) }}" - name: Install packages (batch optimization) include_tasks: packages.yml @@ -158,7 +159,7 @@ - name: Configure the alternative ingress ip include_tasks: aip/main.yml - when: alternative_ingress_ip + when: alternative_ingress_ip | bool - name: Ubuntu 22.04+ | Use iptables-legacy for compatibility block: diff --git a/roles/common/templates/rules.v4.j2 b/roles/common/templates/rules.v4.j2 index 9ed8a502d..bbe0619e7 100644 --- a/roles/common/templates/rules.v4.j2 +++ b/roles/common/templates/rules.v4.j2 @@ -1,5 +1,5 @@ -{% 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 []) + ([wireguard_port_actual] if wireguard_enabled and wireguard_port | int == wireguard_port_avoid | int else []) %} +{% set subnets = ([strongswan_network] if ipsec_enabled | bool else []) + ([wireguard_network_ipv4] if wireguard_enabled | bool else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled | bool else []) + ([wireguard_port] if wireguard_enabled | bool else []) + ([wireguard_port_actual] if wireguard_enabled | bool and wireguard_port | int == wireguard_port_avoid | int else []) %} #### The mangle table # This table allows us to modify packet headers @@ -13,7 +13,7 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu | int > 0 and ipsec_enabled %} +{% if reduce_mtu | int > 0 and ipsec_enabled | bool %} -A FORWARD -s {{ strongswan_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu | int }} {% endif %} @@ -29,20 +29,20 @@ COMMIT :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if wireguard_enabled and wireguard_port | int == wireguard_port_avoid | int %} +{% if wireguard_enabled | bool and wireguard_port | int == wireguard_port_avoid | int %} # Handle the special case of allowing access to WireGuard over an already used # port like 53 -A PREROUTING -s {{ subnets | join(',') }} -p udp --dport {{ wireguard_port_avoid }} -j RETURN -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 -{% if ipsec_enabled %} +{% if ipsec_enabled | bool %} # For IPsec traffic - NAT the decrypted packets from the VPN subnet --A POSTROUTING -s {{ strongswan_network }} -o {{ ansible_default_ipv4['interface'] }} {{ '-j SNAT --to ' + snat_aipv4 if snat_aipv4 else '-j MASQUERADE' }} +-A POSTROUTING -s {{ strongswan_network }} -o {{ ansible_default_ipv4['interface'] }} {{ '-j SNAT --to ' + snat_aipv4 if snat_aipv4 | bool else '-j MASQUERADE' }} {% endif %} -{% if wireguard_enabled %} +{% if wireguard_enabled | bool %} # For WireGuard traffic - NAT packets from the VPN subnet --A POSTROUTING -s {{ wireguard_network_ipv4 }} -o {{ ansible_default_ipv4['interface'] }} {{ '-j SNAT --to ' + snat_aipv4 if snat_aipv4 else '-j MASQUERADE' }} +-A POSTROUTING -s {{ wireguard_network_ipv4 }} -o {{ ansible_default_ipv4['interface'] }} {{ '-j SNAT --to ' + snat_aipv4 if snat_aipv4 | bool else '-j MASQUERADE' }} {% endif %} @@ -75,7 +75,7 @@ COMMIT # Allow new traffic to port {{ ansible_ssh_port }} (SSH) -A INPUT -p tcp --dport {{ ansible_ssh_port }} -m conntrack --ctstate NEW -j ACCEPT -{% if ipsec_enabled %} +{% if ipsec_enabled | bool %} # Allow any traffic from the IPsec VPN -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT {% endif %} @@ -106,12 +106,12 @@ COMMIT -A FORWARD -p udp -m multiport --ports 137,138 -j {{ "DROP" if block_netbios else "ACCEPT" }} -A FORWARD -p tcp -m multiport --ports 137,139 -j {{ "DROP" if block_netbios else "ACCEPT" }} -{% if ipsec_enabled %} +{% if ipsec_enabled | bool %} # Forward any IPSEC traffic from the VPN network -A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network }} -m policy --pol ipsec --dir in -j ACCEPT {% endif %} -{% if wireguard_enabled %} +{% if wireguard_enabled | bool %} # Forward any traffic from the WireGuard VPN network -A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv4 }} -j ACCEPT {% endif %} diff --git a/roles/common/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 index e060b5138..32120be56 100644 --- a/roles/common/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -1,5 +1,5 @@ -{% 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 []) + ([wireguard_port_actual] if wireguard_enabled and wireguard_port | int == wireguard_port_avoid | int else []) %} +{% set subnets = ([strongswan_network_ipv6] if ipsec_enabled | bool else []) + ([wireguard_network_ipv6] if wireguard_enabled | bool else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled | bool else []) + ([wireguard_port] if wireguard_enabled | bool else []) + ([wireguard_port_actual] if wireguard_enabled | bool and wireguard_port | int == wireguard_port_avoid | int else []) %} #### The mangle table # This table allows us to modify packet headers @@ -13,7 +13,7 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu | int > 0 and ipsec_enabled %} +{% if reduce_mtu | int > 0 and ipsec_enabled | bool %} -A FORWARD -s {{ strongswan_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu | int }} {% endif %} @@ -28,20 +28,20 @@ COMMIT :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if wireguard_enabled and wireguard_port | int == wireguard_port_avoid | int %} +{% if wireguard_enabled | bool and wireguard_port | int == wireguard_port_avoid | int %} # Handle the special case of allowing access to WireGuard over an already used # port like 53 -A PREROUTING -s {{ subnets | join(',') }} -p udp --dport {{ wireguard_port_avoid }} -j RETURN -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 -{% if ipsec_enabled %} +{% if ipsec_enabled | bool %} # For IPsec traffic - NAT the decrypted packets from the VPN subnet --A POSTROUTING -s {{ strongswan_network_ipv6 }} -o {{ ansible_default_ipv6['interface'] }} {{ '-j SNAT --to ' + ipv6_egress_ip | ansible.utils.ipaddr('address') if alternative_ingress_ip else '-j MASQUERADE' }} +-A POSTROUTING -s {{ strongswan_network_ipv6 }} -o {{ ansible_default_ipv6['interface'] }} {{ '-j SNAT --to ' + ipv6_egress_ip | ansible.utils.ipaddr('address') if alternative_ingress_ip | bool else '-j MASQUERADE' }} {% endif %} -{% if wireguard_enabled %} +{% if wireguard_enabled | bool %} # For WireGuard traffic - NAT packets from the VPN subnet --A POSTROUTING -s {{ wireguard_network_ipv6 }} -o {{ ansible_default_ipv6['interface'] }} {{ '-j SNAT --to ' + ipv6_egress_ip | ansible.utils.ipaddr('address') if alternative_ingress_ip else '-j MASQUERADE' }} +-A POSTROUTING -s {{ wireguard_network_ipv6 }} -o {{ ansible_default_ipv6['interface'] }} {{ '-j SNAT --to ' + ipv6_egress_ip | ansible.utils.ipaddr('address') if alternative_ingress_ip | bool else '-j MASQUERADE' }} {% endif %} COMMIT @@ -109,10 +109,10 @@ COMMIT -A FORWARD -p tcp -m multiport --ports 137,139 -j {{ "DROP" if block_netbios else "ACCEPT" }} -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -{% if ipsec_enabled %} +{% if ipsec_enabled | bool %} -A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT {% endif %} -{% if wireguard_enabled %} +{% if wireguard_enabled | bool %} -A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv6 }} -j ACCEPT {% endif %} diff --git a/roles/dns/handlers/main.yml b/roles/dns/handlers/main.yml index 963a06626..5dfec28dc 100644 --- a/roles/dns/handlers/main.yml +++ b/roles/dns/handlers/main.yml @@ -8,11 +8,11 @@ name: dnscrypt-proxy.socket state: restarted daemon_reload: true - when: uses_systemd_socket + when: uses_systemd_socket | bool - name: restart dnscrypt-proxy systemd: name: dnscrypt-proxy state: restarted daemon_reload: true - when: uses_systemd_socket + when: uses_systemd_socket | bool diff --git a/roles/dns/tasks/main.yml b/roles/dns/tasks/main.yml index 157f3e384..c3aae3bae 100644 --- a/roles/dns/tasks/main.yml +++ b/roles/dns/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Include tasks for Debian/Ubuntu include_tasks: ubuntu.yml - when: is_debian_based + when: is_debian_based | bool - name: dnscrypt-proxy ip-blacklist configured template: @@ -21,7 +21,7 @@ - name: Include DNS adblocking tasks import_tasks: dns_adblocking.yml - when: algo_dns_adblocking + when: algo_dns_adblocking | bool - meta: flush_handlers @@ -31,7 +31,7 @@ enabled: true state: started daemon_reload: true - when: uses_systemd_socket + when: uses_systemd_socket | bool - name: dnscrypt-proxy enabled and started service: diff --git a/roles/dns/templates/dnscrypt-proxy/filters.toml.j2 b/roles/dns/templates/dnscrypt-proxy/filters.toml.j2 index 9fb39eb8c..e08513646 100644 --- a/roles/dns/templates/dnscrypt-proxy/filters.toml.j2 +++ b/roles/dns/templates/dnscrypt-proxy/filters.toml.j2 @@ -33,7 +33,7 @@ block_ipv6 = false ###################################################### [blacklist] - {{ "blacklist_file = 'blacklist.txt'" if algo_dns_adblocking else "" }} + {{ "blacklist_file = 'blacklist.txt'" if algo_dns_adblocking | bool else "" }} diff --git a/roles/dns/templates/dnscrypt-proxy/global.toml.j2 b/roles/dns/templates/dnscrypt-proxy/global.toml.j2 index 74b5a39a8..5026dd7a8 100644 --- a/roles/dns/templates/dnscrypt-proxy/global.toml.j2 +++ b/roles/dns/templates/dnscrypt-proxy/global.toml.j2 @@ -6,20 +6,20 @@ {# Allow either list to be empty. Output nothing if both are empty. #} {% set servers = [] %} {% if dnscrypt_servers.ipv4 %}{% set servers = dnscrypt_servers.ipv4 %}{% endif %} -{% if ipv6_support and dnscrypt_servers.ipv6 %}{% set servers = servers + dnscrypt_servers.ipv6 %}{% endif %} +{% if ipv6_support | bool and dnscrypt_servers.ipv6 %}{% set servers = servers + dnscrypt_servers.ipv6 %}{% endif %} {% if servers %}server_names = ['{{ servers | join("', '") }}']{% endif %} ## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. ## Note: When using systemd socket activation, choose an empty set (i.e. [] ). -{% if uses_systemd_socket %} +{% if uses_systemd_socket | bool %} # Using systemd socket activation on Debian/Ubuntu listen_addresses = [] {% else %} # Direct binding on non-systemd systems listen_addresses = [ - '{{ local_service_ip }}:53'{% if ipv6_support %}, + '{{ local_service_ip }}:53'{% if ipv6_support | bool %}, '[{{ local_service_ipv6 }}]:53'{% endif %} ] {% endif %} diff --git a/roles/strongswan/defaults/main.yml b/roles/strongswan/defaults/main.yml index ffd614e57..959028a2b 100644 --- a/roles/strongswan/defaults/main.yml +++ b/roles/strongswan/defaults/main.yml @@ -19,10 +19,12 @@ openssl_constraint_random_id: "{{ IP_subject_alt_name | to_uuid }}.algo" # Without SAN, IKEv2 connections will fail with certificate validation errors subjectAltName_type: "{{ 'DNS' if IP_subject_alt_name | regex_search('[a-z]') else 'IP' }}" subjectAltName: >- - {{ subjectAltName_type }}:{{ IP_subject_alt_name }}{%- if ipv6_support -%},IP:{{ ansible_default_ipv6['address'] }}{%- endif -%} + {{ subjectAltName_type }}:{{ IP_subject_alt_name }}{%- if ipv6_support | bool -%},IP:{{ ansible_default_ipv6['address'] }}{%- endif -%} subjectAltName_USER: email:{{ item }}@{{ openssl_constraint_random_id }} +# yamllint disable rule:line-length nameConstraints: >- - critical,permitted;{{ subjectAltName_type }}:{{ IP_subject_alt_name }}{{- '/255.255.255.255' if subjectAltName_type == 'IP' else '' -}}{%- if subjectAltName_type == 'IP' -%},permitted;DNS:{{ openssl_constraint_random_id }},excluded;DNS:.com,excluded;DNS:.org,excluded;DNS:.net,excluded;DNS:.gov,excluded;DNS:.edu,excluded;DNS:.mil,excluded;DNS:.int,excluded;IP:10.0.0.0/255.0.0.0,excluded;IP:172.16.0.0/255.240.0.0,excluded;IP:192.168.0.0/255.255.0.0{%- else -%},excluded;IP:0.0.0.0/0.0.0.0{%- endif -%},permitted;email:{{ openssl_constraint_random_id }},excluded;email:.com,excluded;email:.org,excluded;email:.net,excluded;email:.gov,excluded;email:.edu,excluded;email:.mil,excluded;email:.int{%- if ipv6_support -%},permitted;IP:{{ ansible_default_ipv6['address'] }}/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff,excluded;IP:fc00:0:0:0:0:0:0:0/fe00:0:0:0:0:0:0:0,excluded;IP:fe80:0:0:0:0:0:0:0/ffc0:0:0:0:0:0:0:0,excluded;IP:2001:db8:0:0:0:0:0:0/ffff:fff8:0:0:0:0:0:0{%- else -%},excluded;IP:::/0{%- endif -%} + critical,permitted;{{ subjectAltName_type }}:{{ IP_subject_alt_name }}{{- '/255.255.255.255' if subjectAltName_type == 'IP' else '' -}}{%- if subjectAltName_type == 'IP' -%},permitted;DNS:{{ openssl_constraint_random_id }},excluded;DNS:.com,excluded;DNS:.org,excluded;DNS:.net,excluded;DNS:.gov,excluded;DNS:.edu,excluded;DNS:.mil,excluded;DNS:.int,excluded;IP:10.0.0.0/255.0.0.0,excluded;IP:172.16.0.0/255.240.0.0,excluded;IP:192.168.0.0/255.255.0.0{%- else -%},excluded;IP:0.0.0.0/0.0.0.0{%- endif -%},permitted;email:{{ openssl_constraint_random_id }},excluded;email:.com,excluded;email:.org,excluded;email:.net,excluded;email:.gov,excluded;email:.edu,excluded;email:.mil,excluded;email:.int{%- if ipv6_support | bool -%},permitted;IP:{{ ansible_default_ipv6['address'] }}/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff,excluded;IP:fc00:0:0:0:0:0:0:0/fe00:0:0:0:0:0:0:0,excluded;IP:fe80:0:0:0:0:0:0:0/ffc0:0:0:0:0:0:0:0,excluded;IP:2001:db8:0:0:0:0:0:0/ffff:fff8:0:0:0:0:0:0{%- else -%},excluded;IP:::/0{%- endif -%} +# yamllint enable rule:line-length openssl_bin: openssl strongswan_enabled_plugins: - aes diff --git a/roles/strongswan/tasks/main.yml b/roles/strongswan/tasks/main.yml index 6a88926cf..baab22a12 100644 --- a/roles/strongswan/tasks/main.yml +++ b/roles/strongswan/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Include tasks for Debian/Ubuntu include_tasks: ubuntu.yml - when: is_debian_based + when: is_debian_based | bool - name: Ensure that the strongswan user exists user: diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index 9d4950764..5553238eb 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -68,7 +68,7 @@ 'DNS:' + openssl_constraint_random_id, 'email:' + openssl_constraint_random_id ] + ( - ['IP:' + ansible_default_ipv6['address'] + '/128'] if ipv6_support else [] + ['IP:' + ansible_default_ipv6['address'] + '/128'] if ipv6_support | bool else [] ) }} # Block public domains/networks to prevent certificate abuse for impersonation attacks # Public TLD exclusion, Email domain exclusion, RFC 1918: prevents lateral movement @@ -79,7 +79,7 @@ 'email:.com', 'email:.org', 'email:.net', 'email:.gov', 'email:.edu', 'email:.mil', 'email:.int', 'IP:10.0.0.0/255.0.0.0', 'IP:172.16.0.0/255.240.0.0', 'IP:192.168.0.0/255.255.0.0' ] + ( - ['IP:fc00::/7', 'IP:fe80::/10', 'IP:2001:db8::/32'] if ipv6_support else ['IP:::/0'] + ['IP:fc00::/7', 'IP:fe80::/10', 'IP:2001:db8::/32'] if ipv6_support | bool else ['IP:::/0'] ) }} name_constraints_critical: true register: ca_csr diff --git a/roles/strongswan/templates/ipsec.conf.j2 b/roles/strongswan/templates/ipsec.conf.j2 index 6f6fff9eb..b561dd792 100644 --- a/roles/strongswan/templates/ipsec.conf.j2 +++ b/roles/strongswan/templates/ipsec.conf.j2 @@ -25,10 +25,10 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ strongswan_network }},{{ strongswan_network_ipv6 }} -{% if algo_dns_adblocking or dns_encryption %} - rightdns={{ local_service_ip }}{{ ',' + local_service_ipv6 if ipv6_support else '' }} +{% if algo_dns_adblocking | bool or dns_encryption | bool %} + rightdns={{ local_service_ip }}{{ ',' + local_service_ipv6 if ipv6_support | bool else '' }} {% else %} - rightdns={% 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 %} + rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support | bool %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} {% endif %} conn ikev2-pubkey diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 31d62c124..b258508c2 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -7,10 +7,16 @@ wireguard_port_avoid: 53 wireguard_port_actual: 51820 keys_clean_all: false wireguard_dns_servers: >- - {% if algo_dns_adblocking | default(false) | bool or dns_encryption | default(false) | bool %}{{ local_service_ip }}{{ ', ' + local_service_ipv6 if ipv6_support else '' }}{% 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 %} + {%- if algo_dns_adblocking | default(false) | bool or dns_encryption | default(false) | bool -%} + {{ local_service_ip }}{{ ', ' + local_service_ipv6 if ipv6_support | bool else '' }} + {%- else -%} + {%- for host in dns_servers.ipv4 -%}{{ host }}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- if ipv6_support | bool -%},{%- for host in dns_servers.ipv6 -%}{{ host }}{% if not loop.last %},{% endif %}{%- endfor -%} + {%- endif -%} + {%- endif -%} wireguard_client_ip: >- {{ wireguard_network_ipv4 | ansible.utils.ipmath(index | int + 2) }} - {{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int + 2) if ipv6_support else '' }} + {{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int + 2) if ipv6_support | bool else '' }} wireguard_server_ip: >- {{ wireguard_network_ipv4 | ansible.utils.ipaddr('1') }} - {{ ',' + wireguard_network_ipv6 | ansible.utils.ipaddr('1') if ipv6_support else '' }} + {{ ',' + wireguard_network_ipv6 | ansible.utils.ipaddr('1') if ipv6_support | bool else '' }} diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 2f505c77a..6a81351c5 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -13,7 +13,7 @@ - name: Include tasks for Debian/Ubuntu include_tasks: ubuntu.yml - when: is_debian_based + when: is_debian_based | bool tags: always diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index 6d0aa7189..d3f6be9a4 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -12,6 +12,6 @@ SaveConfig = false # {{ u }} PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + u) }} PresharedKey = {{ lookup('file', wireguard_pki_path + '/preshared/' + u) }} -AllowedIPs = {{ wireguard_network_ipv4 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv4('address') }}/32{{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv6('address') + '/128' if ipv6_support else '' }} +AllowedIPs = {{ wireguard_network_ipv4 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv4('address') }}/32{{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv6('address') + '/128' if ipv6_support | bool else '' }} {% endif %} {% endfor %} diff --git a/server.yml b/server.yml index 9da8a617d..ba7d1a4ae 100644 --- a/server.yml +++ b/server.yml @@ -15,7 +15,7 @@ sleep: 10 # Check every 10 seconds (less aggressive) state: present become: false - when: cloudinit + when: cloudinit | bool - block: - name: Ensure the config directory exists @@ -60,28 +60,28 @@ async: 300 poll: 0 register: dns_job - when: algo_dns_adblocking or dns_encryption + when: algo_dns_adblocking | bool or dns_encryption | bool tags: dns - import_role: {name: wireguard} async: 300 poll: 0 register: wireguard_job - when: wireguard_enabled + when: wireguard_enabled | bool tags: wireguard - import_role: {name: strongswan} async: 300 poll: 0 register: strongswan_job - when: ipsec_enabled + when: ipsec_enabled | bool tags: ipsec - import_role: {name: ssh_tunneling} async: 300 poll: 0 register: ssh_tunneling_job - when: algo_ssh_tunneling + when: algo_ssh_tunneling | bool tags: ssh_tunneling # --- Build job list and wait for completion --- @@ -119,19 +119,19 @@ tags: [dns, wireguard, ipsec, ssh_tunneling] block: - import_role: {name: dns} - when: algo_dns_adblocking or dns_encryption + when: algo_dns_adblocking | bool or dns_encryption | bool tags: dns - import_role: {name: wireguard} - when: wireguard_enabled + when: wireguard_enabled | bool tags: wireguard - import_role: {name: strongswan} - when: ipsec_enabled + when: ipsec_enabled | bool tags: ipsec - import_role: {name: ssh_tunneling} - when: algo_ssh_tunneling + when: algo_ssh_tunneling | bool tags: ssh_tunneling - import_role: diff --git a/tests/unit/test_iptables_rules.py b/tests/unit/test_iptables_rules.py index 1d459db98..e1002025c 100644 --- a/tests/unit/test_iptables_rules.py +++ b/tests/unit/test_iptables_rules.py @@ -12,10 +12,20 @@ from jinja2 import Environment, FileSystemLoader +def _ansible_bool(value): + """Simulate the Ansible bool filter for test purposes.""" + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() not in ("false", "no", "0", "") + return bool(value) + + def load_template(template_name): """Load a Jinja2 template from the roles/common/templates directory.""" template_dir = Path(__file__).parent.parent.parent / "roles" / "common" / "templates" env = Environment(loader=FileSystemLoader(str(template_dir))) + env.filters["bool"] = _ansible_bool return env.get_template(template_name) diff --git a/tests/unit/test_wireguard_key_generation.py b/tests/unit/test_wireguard_key_generation.py index 1b7019cb2..2ba8f4a54 100644 --- a/tests/unit/test_wireguard_key_generation.py +++ b/tests/unit/test_wireguard_key_generation.py @@ -69,7 +69,7 @@ def generate_test_private_key(): def test_x25519_pubkey_from_raw_file(): """Test our x25519_pubkey module with raw private key file""" - raw_key_path, b64_key = generate_test_private_key() + raw_key_path, _b64_key = generate_test_private_key() try: # Import here so we can mock the module_utils if needed diff --git a/users.yml b/users.yml index 1522d8753..6633ad877 100644 --- a/users.yml +++ b/users.yml @@ -45,7 +45,10 @@ - name: Set facts based on the input set_fact: algo_server: >- - {% if server is defined %}{{ server }}{%- elif _server.user_input %}{{ server_list[_server.user_input | int - 1].server }}{%- else %}omit{% endif %} + {%- if server is defined -%}{{ server }} + {%- elif _server.user_input -%}{{ server_list[_server.user_input | int - 1].server }} + {%- else -%}omit + {%- endif -%} - name: Import host specific variables include_vars: @@ -112,7 +115,7 @@ - algo_server != 'localhost' - ssh_check is failed - - when: ipsec_enabled + - when: ipsec_enabled | bool block: - name: CA password prompt pause: @@ -124,7 +127,10 @@ - name: Set facts based on the input set_fact: CA_password: >- - {%- if ca_password is defined -%}{{ ca_password }}{%- elif _ca_password.user_input -%}{{ _ca_password.user_input }}{%- else -%}omit{%- endif -%} + {%- if ca_password is defined -%}{{ ca_password }} + {%- elif _ca_password.user_input -%}{{ _ca_password.user_input }} + {%- else -%}omit + {%- endif -%} - name: Local pre-tasks import_tasks: playbooks/cloud-pre.yml @@ -156,16 +162,16 @@ - import_role: name: wireguard - when: wireguard_enabled + when: wireguard_enabled | bool - import_role: name: strongswan - when: ipsec_enabled + when: ipsec_enabled | bool tags: ipsec - import_role: name: ssh_tunneling - when: algo_ssh_tunneling + when: algo_ssh_tunneling | bool - debug: msg: From 1743bc2dd22ded96b3122a8964cb8d740749c7ed Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 8 Feb 2026 10:46:20 -0500 Subject: [PATCH 2/3] ci: add j2lint for Jinja2 template linting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add j2lint (aristanetworks/j2lint) to catch syntax errors, spacing issues, and operator formatting in Jinja2 templates. Integrated into pre-commit hooks, lint.yml CI, and smart-tests.yml. Rules S3/S5/S6/S7/V1 are ignored — they enforce conventions incompatible with Ansible's config-file-embedded templates. Also fixes int+1 → int + 1 operator spacing in server.conf.j2. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/lint.yml | 22 ++++++++++ .github/workflows/smart-tests.yml | 6 +++ .pre-commit-config.yaml | 7 ++++ pyproject.toml | 1 + roles/wireguard/templates/server.conf.j2 | 2 +- uv.lock | 51 +++++++++++++++++++++++- 6 files changed, 87 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 438b9391e..72761c479 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -51,6 +51,28 @@ jobs: - name: Run yamllint run: uv run --with yamllint yamllint -c .yamllint . + jinja2-lint: + name: Jinja2 template linting + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.1 + with: + persist-credentials: false + + - name: Setup uv environment + uses: ./.github/actions/setup-uv + + - name: Run j2lint + run: | + # Lint Jinja2 templates for syntax and style issues + # Ignored rules (incompatible with Ansible config-file templates): + # S3: indentation (dictated by output format, not Jinja style) + # S5: tabs (some config formats require them) + # S6: whitespace-control delimiters ({%- -%} are standard Ansible) + # S7: single-statement-per-line (inline Jinja in config output) + # V1: lowercase variables (existing names like IP_subject_alt_name) + uv run --with j2lint j2lint roles/ --ignore S3 S5 S6 S7 V1 + python-lint: name: Python linting runs-on: ubuntu-22.04 diff --git a/.github/workflows/smart-tests.yml b/.github/workflows/smart-tests.yml index 603adc6b9..4779047ad 100644 --- a/.github/workflows/smart-tests.yml +++ b/.github/workflows/smart-tests.yml @@ -59,6 +59,7 @@ jobs: - '**/*.yml' - '**/*.yaml' - '**/*.sh' + - '**/*.j2' - '.ansible-lint' - '.yamllint' - 'pyproject.toml' @@ -254,6 +255,11 @@ jobs: # Run Ansible linter uv run --with ansible-lint ansible-lint + # Check Jinja2 templates + if git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | grep -q '\.j2$'; then + uv run --with j2lint j2lint roles/ --ignore S3 S5 S6 S7 V1 + fi + # Check shell scripts if any changed if git diff --name-only "${BASE_SHA}" "${HEAD_SHA}" | grep -q '\.sh$'; then find . -name "*.sh" -type f -not -path "./.git/*" -exec shellcheck {} + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd22a20cc..b70d9f16d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,13 @@ repos: types: [python] pass_filenames: false + - id: j2lint + name: Jinja2 template lint + entry: bash -c 'uv run j2lint roles/ --ignore S3 S5 S6 S7 V1' + language: system + files: '\.j2$' + pass_filenames: false + - id: ansible-lint name: Ansible-lint entry: bash -c 'uv run ansible-lint --force-color || echo "Ansible-lint had issues - check output"' diff --git a/pyproject.toml b/pyproject.toml index 914abb6b9..bc61b0d24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ dev-dependencies = [ "ruff>=0.8.0", # Python linter and formatter "yamllint>=1.35.0", # YAML linter "ansible-lint>=24.0.0", # Ansible linter + "j2lint>=1.2.0", # Jinja2 template linter ] [tool.pytest.ini_options] diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index d3f6be9a4..d07716860 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -12,6 +12,6 @@ SaveConfig = false # {{ u }} PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + u) }} PresharedKey = {{ lookup('file', wireguard_pki_path + '/preshared/' + u) }} -AllowedIPs = {{ wireguard_network_ipv4 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv4('address') }}/32{{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int+1) | ansible.utils.ipv6('address') + '/128' if ipv6_support | bool else '' }} +AllowedIPs = {{ wireguard_network_ipv4 | ansible.utils.ipmath(index | int + 1) | ansible.utils.ipv4('address') }}/32{{ ',' + wireguard_network_ipv6 | ansible.utils.ipmath(index | int + 1) | ansible.utils.ipv6('address') + '/128' if ipv6_support | bool else '' }} {% endif %} {% endfor %} diff --git a/uv.lock b/uv.lock index db361ae08..08a5e13f4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.12'", @@ -64,6 +64,7 @@ openstack = [ [package.dev-dependencies] dev = [ { name = "ansible-lint" }, + { name = "j2lint" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, @@ -95,6 +96,7 @@ provides-extras = ["aws", "azure", "gcp", "hetzner", "linode", "openstack", "clo [package.metadata.requires-dev] dev = [ { name = "ansible-lint", specifier = ">=24.0.0" }, + { name = "j2lint", specifier = ">=1.2.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-xdist", specifier = ">=3.0.0" }, { name = "ruff", specifier = ">=0.8.0" }, @@ -687,6 +689,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "j2lint" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/b8/c3a194c6ec14d51104ac9070564b42c0335ba131d7d27e3e137d227c2a1d/j2lint-1.2.0.tar.gz", hash = "sha256:3bb9d2a3e3eb1647fe6c469bfbd2c2a9a8597378cda8590ed9eb7df7f3334639", size = 32451, upload-time = "2025-04-04T07:51:17.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7c/f24df7f5fca454036ed4d87847c81390793d120c3c89629cef8d79ecc3fc/j2lint-1.2.0-py3-none-any.whl", hash = "sha256:cda7fab7af742f25dcb4925168de785721dcf66c7add2063d8e3643241e19e4e", size = 34880, upload-time = "2025-04-04T07:51:15.536Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -787,6 +802,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/84/c914f5eeeefdeccfc9b4d1acab9e68f27f86f97478a655c59ef02a4ddfa4/linode_api4-5.39.0-py3-none-any.whl", hash = "sha256:57c5f125959c25f981ddbcbf6796cd1eb8d868947f0685b8131b93566bfb96af", size = 150177, upload-time = "2026-01-16T18:44:36.321Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.2" @@ -835,6 +862,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "msal" version = "1.33.0" @@ -1243,6 +1279,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" }, ] +[[package]] +name = "rich" +version = "14.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, +] + [[package]] name = "rpds-py" version = "0.27.1" From 2350e48a7ac9b2cbe6ec7ec94722652e0397026f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 8 Feb 2026 11:19:16 -0500 Subject: [PATCH 3/3] fix: resolve all ansible-lint warnings and enforce zero-tolerance policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 18 jinja[spacing] errors across 12 files by moving Jinja2 block delimiters to prevent YAML >- folding from introducing trailing spaces. Fix 27 key-order[task] warnings across 17 files by reordering task keys to canonical order (name → when → tags → environment → become → block). Promote key-order[task] and yaml[line-length] from warn_list to hard errors by removing warn_list entirely from .ansible-lint. Add zero-tolerance warning policy to CLAUDE.md explaining why warnings are unacceptable in a security tool and documenting resolution order. Co-Authored-By: Claude Opus 4.6 --- .ansible-lint | 4 -- CLAUDE.md | 16 ++++++ input.yml | 60 +++++++++++----------- roles/cloud-azure/tasks/main.yml | 8 +-- roles/cloud-azure/tasks/prompts.yml | 4 +- roles/cloud-cloudstack/tasks/main.yml | 20 ++++---- roles/cloud-digitalocean/tasks/main.yml | 4 +- roles/cloud-digitalocean/tasks/prompts.yml | 8 +-- roles/cloud-ec2/tasks/prompts.yml | 24 ++++----- roles/cloud-gce/tasks/main.yml | 4 +- roles/cloud-gce/tasks/prompts.yml | 4 +- roles/cloud-hetzner/tasks/prompts.yml | 8 +-- roles/cloud-lightsail/tasks/prompts.yml | 4 +- roles/cloud-linode/tasks/prompts.yml | 8 +-- roles/cloud-scaleway/tasks/main.yml | 6 +-- roles/cloud-vultr/tasks/main.yml | 7 ++- roles/cloud-vultr/tasks/prompts.yml | 8 +-- roles/common/defaults/main.yml | 4 +- roles/common/tasks/ubuntu.yml | 6 +-- roles/dns/tasks/ubuntu.yml | 10 ++-- roles/local/tasks/prompts.yml | 4 +- roles/privacy/tasks/main.yml | 3 +- roles/ssh_tunneling/tasks/main.yml | 10 ++-- roles/strongswan/tasks/openssl.yml | 12 ++--- roles/strongswan/tasks/ubuntu.yml | 6 +-- roles/wireguard/defaults/main.yml | 4 +- roles/wireguard/tasks/main.yml | 10 ++-- server.yml | 12 ++--- users.yml | 22 ++++---- 29 files changed, 155 insertions(+), 145 deletions(-) diff --git a/.ansible-lint b/.ansible-lint index c74792eee..4ebc635a8 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -22,10 +22,6 @@ skip_list: - 'no-handler' # Handler usage - some legitimate non-handler use cases - 'name[missing]' # All tasks should be named - 113 issues to fix (temporary) -warn_list: - - yaml[line-length] # Line length - informational only - - key-order[task] # Task key ordering - many existing violations, fix gradually - # Enable additional rules enable_list: - no-log-password diff --git a/CLAUDE.md b/CLAUDE.md index db16d3567..201c0ce7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,22 @@ Common lint issues to fix before submitting: - Jinja2 spacing errors (`{{foo}}` should be `{{ foo }}`) - Missing `mode:` on file/directory tasks +### Zero-Tolerance Warning Policy + +**No warnings are tolerated in CI.** Every linter finding must be either fixed or explicitly allowlisted in the tool's config file (`.ansible-lint`, `pyproject.toml`, etc.). + +Why this matters for Algo: +- **Security tool** - VPN misconfigurations silently break privacy guarantees. A "cosmetic" warning today hides a real bug tomorrow. +- **Ansible complexity** - YAML+Jinja2 linting catches real runtime failures (wrong key order breaks `when` evaluation, spacing errors cause template failures). Warnings in Ansible are not style nits. +- **CI signal integrity** - If 30 warnings scroll by on every run, the 31st one (a real regression) goes unnoticed. Zero warnings means every new finding gets human attention. + +Resolution order of preference: +1. **Fix it** - Preferred. Most findings have straightforward fixes. +2. **Allowlist in config** - If the rule is wrong for this project, add to `skip_list` with a comment explaining why. +3. **Inline suppress** - Last resort. Use `# noqa: rule-name` with a comment justifying the exception. + +Never use `warn_list` in `.ansible-lint` — it exists as a migration tool, not a permanent home. Rules either pass or are explicitly skipped. + ### Design Requirements When adding or modifying features, verify these before requesting review: diff --git a/input.yml b/input.yml index 7b65110c9..97f3189f0 100644 --- a/input.yml +++ b/input.yml @@ -109,44 +109,44 @@ - name: Set facts based on the input set_fact: algo_server_name: >- - {%- if server_name is defined -%}{% set _server = server_name %} - {%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input | length > 0 -%} - {%- set _server = _algo_server_name.user_input -%} - {%- else -%}{% set _server = defaults['server_name'] %} - {%- endif -%} + {%- if server_name is defined -%}{% set _server = server_name %}{%- + elif _algo_server_name.user_input is defined and _algo_server_name.user_input | length > 0 -%}{%- + set _server = _algo_server_name.user_input -%}{%- + else -%}{% set _server = defaults['server_name'] %}{%- + endif -%} {{ _server | regex_replace('(?!\.)(\W|_)', '-') }} algo_ondemand_cellular: >- - {%- if ondemand_cellular is defined -%}{{ ondemand_cellular | bool }} - {%- elif _ondemand_cellular.user_input is defined -%}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }} - {%- else -%}{{ false }} - {%- endif -%} + {%- if ondemand_cellular is defined -%}{{ ondemand_cellular | bool }}{%- + elif _ondemand_cellular.user_input is defined -%}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }}{%- + else -%}{{ false }}{%- + endif -%} algo_ondemand_wifi: >- - {%- if ondemand_wifi is defined -%}{{ ondemand_wifi | bool }} - {%- elif _ondemand_wifi.user_input is defined -%}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} - {%- else -%}{{ false }} - {%- endif -%} + {%- if ondemand_wifi is defined -%}{{ ondemand_wifi | bool }}{%- + elif _ondemand_wifi.user_input is defined -%}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }}{%- + else -%}{{ false }}{%- + endif -%} algo_ondemand_wifi_exclude: >- - {%- if ondemand_wifi_exclude is defined -%}{{ ondemand_wifi_exclude | b64encode }} - {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input | length > 0 -%} - {{ _ondemand_wifi_exclude.user_input | b64encode }} - {%- else -%}{{ '_null' | b64encode }} - {%- endif -%} + {%- if ondemand_wifi_exclude is defined -%}{{ ondemand_wifi_exclude | b64encode }}{%- + elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input | length > 0 -%} + {{ _ondemand_wifi_exclude.user_input | b64encode }}{%- + else -%}{{ '_null' | b64encode }}{%- + endif -%} algo_dns_adblocking: >- - {%- if dns_adblocking is defined -%}{{ dns_adblocking | bool }} - {%- elif _dns_adblocking.user_input is defined -%}{{ booleans_map[_dns_adblocking.user_input] | default(defaults['dns_adblocking']) }} - {%- else -%}{{ false }} - {%- endif -%} + {%- if dns_adblocking is defined -%}{{ dns_adblocking | bool }}{%- + elif _dns_adblocking.user_input is defined -%}{{ booleans_map[_dns_adblocking.user_input] | default(defaults['dns_adblocking']) }}{%- + else -%}{{ false }}{%- + endif -%} algo_ssh_tunneling: >- - {%- if ssh_tunneling is defined -%}{{ ssh_tunneling | bool }} - {%- elif _ssh_tunneling.user_input is defined -%}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }} - {%- else -%}{{ false }} - {%- endif -%} + {%- if ssh_tunneling is defined -%}{{ ssh_tunneling | bool }}{%- + elif _ssh_tunneling.user_input is defined -%}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }}{%- + else -%}{{ false }}{%- + endif -%} algo_store_pki: >- {%- if ipsec_enabled -%} - {%- if store_pki is defined -%}{{ store_pki | bool }} - {%- elif _store_pki.user_input is defined -%}{{ booleans_map[_store_pki.user_input] | default(defaults['store_pki']) }} - {%- else -%}{{ false }} - {%- endif -%} + {%- if store_pki is defined -%}{{ store_pki | bool }}{%- + elif _store_pki.user_input is defined -%}{{ booleans_map[_store_pki.user_input] | default(defaults['store_pki']) }}{%- + else -%}{{ false }}{%- + endif -%} {%- endif -%} rescue: - include_tasks: playbooks/rescue.yml diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index dd1cc2f41..af038d115 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -4,10 +4,10 @@ - set_fact: algo_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ azure_regions[_algo_region.user_input | int - 1]['name'] }} - {%- else -%}{{ azure_regions[default_region | int - 1]['name'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ azure_regions[_algo_region.user_input | int - 1]['name'] }}{%- + else -%}{{ azure_regions[default_region | int - 1]['name'] }}{%- + endif -%} - name: Create AlgoVPN Server azure.azcollection.azure_rm_deployment: diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml index ae04d60cd..a40f33c96 100644 --- a/roles/cloud-azure/tasks/prompts.yml +++ b/roles/cloud-azure/tasks/prompts.yml @@ -6,7 +6,8 @@ subscription_id: "{{ azure_subscription_id | default(lookup('env', 'AZURE_SUBSCRIPTION_ID'), true) }}" no_log: true -- block: +- when: region is undefined + block: - name: Set the default region set_fact: default_region: >- @@ -22,4 +23,3 @@ Enter the number of your desired region [{{ default_region }}] register: _algo_region - when: region is undefined diff --git a/roles/cloud-cloudstack/tasks/main.yml b/roles/cloud-cloudstack/tasks/main.yml index c50a2ab00..6ef3ced93 100644 --- a/roles/cloud-cloudstack/tasks/main.yml +++ b/roles/cloud-cloudstack/tasks/main.yml @@ -2,13 +2,18 @@ - name: Include prompts import_tasks: prompts.yml -- block: +- environment: + CLOUDSTACK_KEY: "{{ algo_cs_key }}" + CLOUDSTACK_SECRET: "{{ algo_cs_token }}" + CLOUDSTACK_ENDPOINT: "{{ algo_cs_url }}" + no_log: true + block: - set_fact: algo_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input is defined and _algo_region.user_input | length > 0 -%}{{ cs_zones[_algo_region.user_input | int - 1]['name'] }} - {%- else -%}{{ cs_zones[default_zone | int - 1]['name'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input is defined and _algo_region.user_input | length > 0 -%}{{ cs_zones[_algo_region.user_input | int - 1]['name'] }}{%- + else -%}{{ cs_zones[default_zone | int - 1]['name'] }}{%- + endif -%} - name: Security group created cs_securitygroup: @@ -51,8 +56,3 @@ ansible_ssh_user: algo ansible_ssh_port: "{{ ssh_port }}" cloudinit: true - environment: - CLOUDSTACK_KEY: "{{ algo_cs_key }}" - CLOUDSTACK_SECRET: "{{ algo_cs_token }}" - CLOUDSTACK_ENDPOINT: "{{ algo_cs_url }}" - no_log: true diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 8ac74f609..96037676a 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -30,7 +30,8 @@ - set_fact: droplet: "{{ digital_ocean_droplet.data.droplet | default(digital_ocean_droplet.data) }}" -- block: +- when: alternative_ingress_ip | bool + block: - name: Create a Floating IP community.digitalocean.digital_ocean_floating_ip: state: present @@ -41,7 +42,6 @@ - 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 | bool - set_fact: cloud_instance_ip: "{{ (droplet.networks.v4 | selectattr('type', '==', 'public')).0.ip_address }}" diff --git a/roles/cloud-digitalocean/tasks/prompts.yml b/roles/cloud-digitalocean/tasks/prompts.yml index a7415aff7..28a365836 100644 --- a/roles/cloud-digitalocean/tasks/prompts.yml +++ b/roles/cloud-digitalocean/tasks/prompts.yml @@ -102,7 +102,7 @@ - name: Set additional facts set_fact: algo_do_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ do_regions[_algo_region.user_input | int - 1]['slug'] }} - {%- else -%}{{ do_regions[default_region | int - 1]['slug'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ do_regions[_algo_region.user_input | int - 1]['slug'] }}{%- + else -%}{{ do_regions[default_region | int - 1]['slug'] }}{%- + endif -%} diff --git a/roles/cloud-ec2/tasks/prompts.yml b/roles/cloud-ec2/tasks/prompts.yml index 87299ee73..4bf188775 100644 --- a/roles/cloud-ec2/tasks/prompts.yml +++ b/roles/cloud-ec2/tasks/prompts.yml @@ -6,7 +6,10 @@ aws_profile: "{{ lookup('env', 'AWS_PROFILE') | default('default', true) }}" # Try to read credentials from file if not already provided -- block: +- when: + - aws_access_key is undefined + - lookup('env', 'AWS_ACCESS_KEY_ID')|length <= 0 + block: - name: Check if AWS credentials file exists stat: path: "{{ aws_credentials_path }}" @@ -20,9 +23,6 @@ _file_session_token: "{{ lookup('ini', 'aws_session_token', section=aws_profile, file=aws_credentials_path, errors='ignore') | default('', true) }}" when: aws_creds_file.stat.exists no_log: true - when: - - aws_access_key is undefined - - lookup('env', 'AWS_ACCESS_KEY_ID')|length <= 0 # Prompt for credentials if still not available - pause: @@ -69,7 +69,8 @@ | default('') }} no_log: true -- block: +- when: region is undefined + block: - name: Get regions aws_region_info: aws_access_key: "{{ access_key }}" @@ -101,18 +102,18 @@ Enter the number of your desired region [{{ default_region }}] register: _algo_region - when: region is undefined - name: Set algo_region and stack_name facts set_fact: algo_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ aws_regions[_algo_region.user_input | int - 1]['region_name'] }} - {%- else -%}{{ aws_regions[default_region | int - 1]['region_name'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ aws_regions[_algo_region.user_input | int - 1]['region_name'] }}{%- + else -%}{{ aws_regions[default_region | int - 1]['region_name'] }}{%- + endif -%} stack_name: "{{ algo_server_name | replace('.', '-') }}" -- block: +- when: cloud_providers.ec2.use_existing_eip + block: - name: Get existing available Elastic IPs ec2_eip_info: aws_access_key: "{{ access_key }}" @@ -137,4 +138,3 @@ - set_fact: existing_eip: "{{ available_eip_addresses[_use_existing_eip.user_input | int - 1]['allocation_id'] }}" - when: cloud_providers.ec2.use_existing_eip diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 9bf9b4373..247dcaea4 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -32,7 +32,8 @@ - "{{ ssh_port }}" - ip_protocol: icmp -- block: +- when: cloud_providers.gce.external_static_ip + block: - name: External IP allocated gcp_compute_address: auth_kind: serviceaccount @@ -45,7 +46,6 @@ - name: Set External IP as a fact set_fact: external_ip: "{{ gcp_compute_address.address }}" - when: cloud_providers.gce.external_static_ip - name: Instance created gcp_compute_instance: diff --git a/roles/cloud-gce/tasks/prompts.yml b/roles/cloud-gce/tasks/prompts.yml index ccd9ea997..69ad317da 100644 --- a/roles/cloud-gce/tasks/prompts.yml +++ b/roles/cloud-gce/tasks/prompts.yml @@ -25,7 +25,8 @@ project_id: "{{ credentials_file_lookup.project_id | default(lookup('env', 'GCE_PROJECT'), true) }}" no_log: true -- block: +- when: region is undefined + block: - name: Get regions gcp_compute_location_info: auth_kind: serviceaccount @@ -57,7 +58,6 @@ Enter the number of your desired region [{{ default_region }}] register: _gce_region - when: region is undefined - name: Set region as a fact set_fact: diff --git a/roles/cloud-hetzner/tasks/prompts.yml b/roles/cloud-hetzner/tasks/prompts.yml index b82e68714..d3a26a25e 100644 --- a/roles/cloud-hetzner/tasks/prompts.yml +++ b/roles/cloud-hetzner/tasks/prompts.yml @@ -45,7 +45,7 @@ - name: Set additional facts set_fact: algo_hcloud_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ hcloud_regions[_algo_region.user_input | int - 1]['location'] }} - {%- else -%}{{ hcloud_regions[default_region | int - 1]['location'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ hcloud_regions[_algo_region.user_input | int - 1]['location'] }}{%- + else -%}{{ hcloud_regions[default_region | int - 1]['location'] }}{%- + endif -%} diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml index 6d4d792db..d64f5ae25 100644 --- a/roles/cloud-lightsail/tasks/prompts.yml +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -25,7 +25,8 @@ secret_key: "{{ aws_secret_key | default(_aws_secret_key.user_input | default(None)) | default(lookup('env', 'AWS_SECRET_ACCESS_KEY'), true) }}" no_log: true -- block: +- when: region is undefined + block: - name: Get regions lightsail_region_facts: aws_access_key: "{{ access_key }}" @@ -56,7 +57,6 @@ Enter the number of your desired region [{{ default_region }}] register: _algo_region - when: region is undefined - set_fact: stack_name: "{{ algo_server_name | replace('.', '-') }}" diff --git a/roles/cloud-linode/tasks/prompts.yml b/roles/cloud-linode/tasks/prompts.yml index a26420e3b..b910b00b0 100644 --- a/roles/cloud-linode/tasks/prompts.yml +++ b/roles/cloud-linode/tasks/prompts.yml @@ -47,8 +47,8 @@ - name: Set additional facts set_fact: algo_linode_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ linode_regions[_algo_region.user_input | int - 1]['id'] }} - {%- else -%}{{ linode_regions[default_region | int - 1]['id'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ linode_regions[_algo_region.user_input | int - 1]['id'] }}{%- + else -%}{{ linode_regions[default_region | int - 1]['id'] }}{%- + endif -%} public_key: "{{ lookup('file', SSH_keys.public) }}" diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 646bd48d6..7af764e33 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -2,7 +2,9 @@ - name: Include prompts import_tasks: prompts.yml -- block: +- environment: + SCW_TOKEN: "{{ algo_scaleway_token }}" + block: - name: Get Ubuntu 22.04 image ID from Scaleway Marketplace API uri: url: "https://api-marketplace.scaleway.com/images?arch={{ cloud_providers.scaleway.arch }}&include_eol=false" @@ -65,8 +67,6 @@ until: algo_instance.msg.public_ip retries: 3 delay: 3 - environment: - SCW_TOKEN: "{{ algo_scaleway_token }}" - set_fact: cloud_instance_ip: "{{ algo_instance.msg.public_ip.address }}" diff --git a/roles/cloud-vultr/tasks/main.yml b/roles/cloud-vultr/tasks/main.yml index a64e69399..d0ae58edb 100644 --- a/roles/cloud-vultr/tasks/main.yml +++ b/roles/cloud-vultr/tasks/main.yml @@ -2,7 +2,9 @@ - name: Include prompts import_tasks: prompts.yml -- block: +- environment: + VULTR_API_KEY: "{{ lookup('ini', 'key', section='default', file=algo_vultr_config) }}" + block: - name: Set cloud-init script as fact set_fact: algo_cloud_init_script: "{{ lookup('template', 'files/cloud-init/base.yml') }}" @@ -56,6 +58,3 @@ ansible_ssh_user: algo ansible_ssh_port: "{{ ssh_port }}" cloudinit: true - - environment: - VULTR_API_KEY: "{{ lookup('ini', 'key', section='default', file=algo_vultr_config) }}" diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index fb8c0b4a3..6ada5223d 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -55,7 +55,7 @@ - name: Set the desired region as a fact set_fact: algo_vultr_region: >- - {%- if region is defined -%}{{ region }} - {%- elif _algo_region.user_input -%}{{ vultr_regions[_algo_region.user_input | int - 1]['id'] }} - {%- else -%}{{ vultr_regions[default_region | int - 1]['id'] }} - {%- endif -%} + {%- if region is defined -%}{{ region }}{%- + elif _algo_region.user_input -%}{{ vultr_regions[_algo_region.user_input | int - 1]['id'] }}{%- + else -%}{{ vultr_regions[default_region | int - 1]['id'] }}{%- + endif -%} diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index 1b3af775e..85bc7653c 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -6,7 +6,7 @@ snat_aipv4: false ipv6_default: "{{ ansible_default_ipv6.address + '/' + ansible_default_ipv6.prefix }}" ipv6_subnet_size: "{{ ipv6_default | ansible.utils.ipaddr('size') }}" ipv6_egress_ip: >- - {{ (ipv6_default | ansible.utils.next_nth_usable( - 15 | random(seed=algo_server_name + ansible_fqdn))) + {{ (ipv6_default | ansible.utils.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/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index e03d9d261..aef50b9f9 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -2,6 +2,7 @@ - name: Gather facts setup: - name: Cloud only tasks + when: algo_provider != "local" block: - name: Install software updates apt: @@ -61,7 +62,6 @@ (reboot_required.stdout == 'optional' and not performance_skip_optional_reboots|default(false)) ) become: false - when: algo_provider != "local" - name: Include unattended upgrades configuration import_tasks: unattended-upgrades.yml @@ -162,6 +162,8 @@ when: alternative_ingress_ip | bool - name: Ubuntu 22.04+ | Use iptables-legacy for compatibility + when: is_ubuntu_22_plus + tags: iptables block: - name: Install iptables packages apt: @@ -178,8 +180,6 @@ loop: - iptables - ip6tables - when: is_ubuntu_22_plus - tags: iptables - include_tasks: iptables.yml tags: iptables diff --git a/roles/dns/tasks/ubuntu.yml b/roles/dns/tasks/ubuntu.yml index c13084ecb..581bb4b6a 100644 --- a/roles/dns/tasks/ubuntu.yml +++ b/roles/dns/tasks/ubuntu.yml @@ -1,5 +1,6 @@ --- -- block: +- when: ansible_facts['distribution_version'] is version('20.04', '<') + block: - name: Add the repository apt_repository: state: present @@ -17,7 +18,6 @@ owner: root group: root mode: '0644' - when: ansible_facts['distribution_version'] is version('20.04', '<') - name: Install dnscrypt-proxy (individual) apt: @@ -26,7 +26,9 @@ update_cache: true when: not performance_parallel_packages | default(true) -- block: +- when: apparmor_enabled|default(false)|bool + tags: apparmor + block: - name: Ubuntu | Configure AppArmor policy for dnscrypt-proxy copy: src: apparmor.profile.dnscrypt-proxy @@ -39,8 +41,6 @@ - name: Ubuntu | Enforce the dnscrypt-proxy AppArmor policy command: aa-enforce usr.bin.dnscrypt-proxy changed_when: false - tags: apparmor - when: apparmor_enabled|default(false)|bool - name: Ubuntu | Ensure that the dnscrypt-proxy service directory exist file: diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml index 5fae265ef..5f85abb9c 100644 --- a/roles/local/tasks/prompts.yml +++ b/roles/local/tasks/prompts.yml @@ -51,7 +51,8 @@ cloud_instance_ip: >- {%- if server is defined -%}{{ server }}{%- elif _algo_server.user_input -%}{{ _algo_server.user_input }}{%- else -%}localhost{%- endif -%} -- block: +- when: cloud_instance_ip != "localhost" + block: - pause: prompt: | What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) @@ -63,7 +64,6 @@ set_fact: ansible_ssh_user: >- {%- if ssh_user is defined -%}{{ ssh_user }}{%- elif _algo_ssh_user.user_input -%}{{ _algo_ssh_user.user_input }}{%- else -%}root{%- endif -%} - when: cloud_instance_ip != "localhost" - pause: prompt: | diff --git a/roles/privacy/tasks/main.yml b/roles/privacy/tasks/main.yml index aad22ac88..6112e6b4a 100644 --- a/roles/privacy/tasks/main.yml +++ b/roles/privacy/tasks/main.yml @@ -8,6 +8,7 @@ msg: "Privacy enhancements are {{ 'enabled' if privacy_enhancements_enabled else 'disabled' }}" - name: Privacy enhancements block + when: privacy_enhancements_enabled | bool block: - name: Include log rotation tasks include_tasks: log_rotation.yml @@ -32,5 +33,3 @@ - name: Display privacy enhancements completion debug: msg: "Privacy enhancements have been successfully applied" - - when: privacy_enhancements_enabled | bool diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 75f1f263b..9d7a42020 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -27,7 +27,8 @@ owner: root group: "{{ root_group | default('root') }}" -- block: +- tags: update-users + block: - name: Ensure that the SSH users exist user: name: "{{ item }}" @@ -41,7 +42,9 @@ append: true loop: "{{ users }}" - - block: + - become: false + delegate_to: localhost + block: - name: Clean up the ssh-tunnel directory file: dest: "{{ ssh_tunnels_config_path }}" @@ -89,8 +92,6 @@ dest: "{{ ssh_tunnels_config_path }}/{{ item }}.ssh_config" mode: '0700' loop: "{{ users }}" - delegate_to: localhost - become: false - name: The authorized keys file created authorized_key: @@ -115,4 +116,3 @@ force: true when: item not in users loop: "{{ getent_group['algo'][2].split(',') }}" - tags: update-users diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index 5553238eb..f2ddd2a3f 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -1,5 +1,10 @@ --- -- block: +- become: false + delegate_to: localhost + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + certificate_validity_days: 3650 # 10 years - configurable certificate lifespan + block: - debug: var=subjectAltName - name: Ensure the pki directory does not exist @@ -267,11 +272,6 @@ file: path: "{{ ipsec_pki_path }}/crl.pem" mode: "0644" - delegate_to: localhost - become: false - vars: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - certificate_validity_days: 3650 # 10 years - configurable certificate lifespan - name: Copy the CRL to the vpn server copy: diff --git a/roles/strongswan/tasks/ubuntu.yml b/roles/strongswan/tasks/ubuntu.yml index 4015d5cd2..8e4ddcb18 100644 --- a/roles/strongswan/tasks/ubuntu.yml +++ b/roles/strongswan/tasks/ubuntu.yml @@ -17,7 +17,9 @@ install_recommends: true when: not performance_parallel_packages | default(true) -- block: +- when: apparmor_enabled|default(false)|bool + tags: apparmor + block: # https://bugs.launchpad.net/ubuntu/+source/strongswan/+bug/1826238 - name: Ubuntu | Charon profile for apparmor configured copy: @@ -35,8 +37,6 @@ - /usr/lib/ipsec/charon - /usr/lib/ipsec/lookip - /usr/lib/ipsec/stroke - tags: apparmor - when: apparmor_enabled|default(false)|bool - name: Ubuntu | Enable services service: name={{ item }} enabled=yes diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index b258508c2..d49d432dc 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -8,8 +8,8 @@ wireguard_port_actual: 51820 keys_clean_all: false wireguard_dns_servers: >- {%- if algo_dns_adblocking | default(false) | bool or dns_encryption | default(false) | bool -%} - {{ local_service_ip }}{{ ', ' + local_service_ipv6 if ipv6_support | bool else '' }} - {%- else -%} + {{ local_service_ip }}{{ ', ' + local_service_ipv6 if ipv6_support | bool else '' }}{%- + else -%} {%- for host in dns_servers.ipv4 -%}{{ host }}{% if not loop.last %},{% endif %}{%- endfor -%} {%- if ipv6_support | bool -%},{%- for host in dns_servers.ipv6 -%}{{ host }}{% if not loop.last %},{% endif %}{%- endfor -%} {%- endif -%} diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 6a81351c5..15670e244 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -23,8 +23,11 @@ become: false tags: update-users -- block: - - block: +- tags: update-users + block: + - become: false + delegate_to: localhost + block: - name: WireGuard user list updated lineinfile: dest: "{{ wireguard_pki_path }}/index.txt" @@ -72,8 +75,6 @@ chdir: "{{ wireguard_config_path }}" executable: bash no_log: true - become: false - delegate_to: localhost - name: WireGuard configured template: @@ -81,7 +82,6 @@ dest: "{{ config_prefix | default('/') }}etc/wireguard/{{ wireguard_interface }}.conf" mode: "0600" notify: restart wireguard - tags: update-users - name: WireGuard enabled and started service: diff --git a/server.yml b/server.yml index ba7d1a4ae..e02c5930c 100644 --- a/server.yml +++ b/server.yml @@ -17,7 +17,10 @@ become: false when: cloudinit | bool - - block: + - when: inventory_hostname != 'localhost' + become: false + delegate_to: localhost + block: - name: Ensure the config directory exists file: dest: configs/{{ IP_subject_alt_name }} @@ -37,9 +40,6 @@ IdentityFile {{ SSH_keys.private | realpath }} KeepAlive yes ServerAliveInterval 30 - when: inventory_hostname != 'localhost' - become: false - delegate_to: localhost - import_role: name: common @@ -139,7 +139,8 @@ when: privacy_enhancements_enabled | default(true) tags: privacy - - block: + - tags: always + block: - name: Dump the configuration copy: dest: configs/{{ IP_subject_alt_name }}/.config.yml @@ -195,6 +196,5 @@ - " {{ congrats.p12_pass if algo_ssh_tunneling or ipsec_enabled else '' }}" - " {{ congrats.ca_key_pass if algo_store_pki and ipsec_enabled else '' }}" - " {{ congrats.ssh_access if algo_provider != 'local' else '' }}" - tags: always rescue: - include_tasks: playbooks/rescue.yml diff --git a/users.yml b/users.yml index 6633ad877..a3f95d12f 100644 --- a/users.yml +++ b/users.yml @@ -7,7 +7,8 @@ - config.cfg tasks: - - block: + - when: server is undefined + block: - name: Get list of installed config files find: paths: configs/ @@ -39,16 +40,15 @@ {{ loop.index }}. {{ r.server }} ({{ r.IP_subject_alt_name }}) {% endfor %} register: _server - when: server is undefined - block: - name: Set facts based on the input set_fact: algo_server: >- - {%- if server is defined -%}{{ server }} - {%- elif _server.user_input -%}{{ server_list[_server.user_input | int - 1].server }} - {%- else -%}omit - {%- endif -%} + {%- if server is defined -%}{{ server }}{%- + elif _server.user_input -%}{{ server_list[_server.user_input | int - 1].server }}{%- + else -%}omit{%- + endif -%} - name: Import host specific variables include_vars: @@ -64,6 +64,7 @@ when: users | default([]) | length == 0 - name: Local deployment permission validation + when: algo_server == 'localhost' or algo_provider | default('') == 'local' block: - name: Get config directory owner stat: @@ -87,7 +88,6 @@ PREVENT: Always run update-users the same way as initial deployment (both with sudo, or both without sudo). when: config_dir_stat.stat.pw_name != ansible_user_id - when: algo_server == 'localhost' or algo_provider | default('') == 'local' - name: Test SSH connectivity to server wait_for: @@ -127,10 +127,10 @@ - name: Set facts based on the input set_fact: CA_password: >- - {%- if ca_password is defined -%}{{ ca_password }} - {%- elif _ca_password.user_input -%}{{ _ca_password.user_input }} - {%- else -%}omit - {%- endif -%} + {%- if ca_password is defined -%}{{ ca_password }}{%- + elif _ca_password.user_input -%}{{ _ca_password.user_input }}{%- + else -%}omit{%- + endif -%} - name: Local pre-tasks import_tasks: playbooks/cloud-pre.yml