fix: add explicit bool filters for Ansible 12 jinja2_native compatibility (#14963)

* 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 <noreply@anthropic.com>

* ci: add j2lint for Jinja2 template linting

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 <noreply@anthropic.com>

* fix: resolve all ansible-lint warnings and enforce zero-tolerance policy

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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dan Guido
2026-02-08 11:21:56 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 0056bc725c
commit 984831bcab
48 changed files with 348 additions and 158 deletions
-4
View File
@@ -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
+22
View File
@@ -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
+6
View File
@@ -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 {} +
+7
View File
@@ -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"'
+16
View File
@@ -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:
+24
View File
@@ -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 ;;
*)
+32 -10
View File
@@ -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 -%}
{%- 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 %}{% 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
+1
View File
@@ -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]
+4 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+10 -7
View File
@@ -2,10 +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:
@@ -48,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
+2 -2
View File
@@ -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
- set_fact:
cloud_instance_ip: "{{ (droplet.networks.v4 | selectattr('type', '==', 'public')).0.ip_address }}"
+4 -1
View File
@@ -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 -%}
+12 -9
View File
@@ -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,15 +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 }}"
@@ -134,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
+2 -2
View File
@@ -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:
+4 -4
View File
@@ -21,11 +21,12 @@
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:
- 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:
+4 -1
View File
@@ -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 -%}
+2 -2
View File
@@ -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('.', '-') }}"
+4 -1
View File
@@ -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) }}"
+3 -3
View File
@@ -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 }}"
+3 -4
View File
@@ -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) }}"
+4 -1
View File
@@ -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 -%}
+4 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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:
+6 -5
View File
@@ -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
@@ -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,9 +159,11 @@
- 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
when: is_ubuntu_22_plus
tags: iptables
block:
- name: Install iptables packages
apt:
@@ -177,8 +180,6 @@
loop:
- iptables
- ip6tables
when: is_ubuntu_22_plus
tags: iptables
- include_tasks: iptables.yml
tags: iptables
+11 -11
View File
@@ -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 %}
+10 -10
View File
@@ -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 %}
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+5 -5
View File
@@ -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:
@@ -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 "" }}
@@ -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 %}
+2 -2
View File
@@ -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: |
+1 -2
View File
@@ -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
+5 -5
View File
@@ -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
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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:
+8 -8
View File
@@ -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
@@ -68,7 +73,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 +84,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
@@ -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:
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+9 -3
View File
@@ -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 '' }}
+6 -6
View File
@@ -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
@@ -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:
+1 -1
View File
@@ -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 %}
+15 -15
View File
@@ -15,9 +15,12 @@
sleep: 10 # Check every 10 seconds (less aggressive)
state: present
become: false
when: cloudinit
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
@@ -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:
@@ -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
+10
View File
@@ -12,10 +12,20 @@ import pytest
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)
+1 -1
View File
@@ -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
+15 -9
View File
@@ -7,7 +7,8 @@
- config.cfg
tasks:
- block:
- when: server is undefined
block:
- name: Get list of installed config files
find:
paths: configs/
@@ -39,13 +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:
@@ -61,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:
@@ -84,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:
@@ -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:
Generated
+50 -1
View File
@@ -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"