mirror of
https://github.com/trailofbits/algo.git
synced 2026-08-17 21:25:50 +02:00
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:
co-authored by
Claude Opus 4.6
parent
0056bc725c
commit
984831bcab
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {} +
|
||||
|
||||
@@ -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"'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ;;
|
||||
*)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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('.', '-') }}"
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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) }}"
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '' }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user