--- - name: Add WireGuard Client hosts: production become: yes gather_facts: yes vars: wireguard_interface: "wg0" wireguard_config_path: "/etc/wireguard" wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf" wireguard_client_configs_path: "/etc/wireguard/clients" wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients" wireguard_dns_servers: [] pre_tasks: - name: Set WireGuard network set_fact: wireguard_network: "{{ wireguard_network | default('10.8.0.0/24') }}" - name: Set WireGuard other variables with defaults set_fact: wireguard_port: "{{ wireguard_port | default(51820) }}" client_ip: "{{ client_ip | default('') }}" # IMPORTANT: Default to VPN network only (not 0.0.0.0/0) # This ensures SSH access via normal IP remains available allowed_ips: "{{ allowed_ips | default(wireguard_network) }}" tasks: - name: Validate client name fail: msg: "client_name is required. Usage: ansible-playbook ... -e 'client_name=myclient'" when: client_name is not defined or client_name == "" - name: Get server external IP address uri: url: https://api.ipify.org return_content: yes register: server_external_ip changed_when: false failed_when: false - name: Set server external IP set_fact: server_external_ip_content: "{{ ansible_host | default(server_external_ip.content | default('')) }}" - name: Check if WireGuard config exists stat: path: "{{ wireguard_config_file }}" register: wireguard_config_exists - name: Fail if WireGuard not configured fail: msg: "WireGuard server not configured. Please run setup-wireguard.yml first." when: not wireguard_config_exists.stat.exists - name: Read WireGuard server config slurp: src: "{{ wireguard_config_file }}" register: wireguard_server_config_read - name: Extract server IP from config set_fact: server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}" - name: Extract WireGuard server IP octets set_fact: wireguard_server_ip_octets: "{{ server_vpn_ip.split('.') }}" when: client_ip == "" - name: Gather existing client addresses set_fact: existing_client_ips: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('AllowedIPs = ([0-9A-Za-z.]+)/32', '\\1')) }}" when: client_ip == "" - name: Calculate client IP if not provided vars: existing_last_octets: "{{ (existing_client_ips | default([])) | map('regex_replace', '^(?:\\d+\\.\\d+\\.\\d+\\.)', '') | select('match', '^[0-9]+$') | map('int') | list }}" server_last_octet: "{{ wireguard_server_ip_octets[3] | int }}" next_octet_candidate: "{{ (existing_last_octets + [server_last_octet]) | map('int') | list | max + 1 if (existing_last_octets + [server_last_octet]) else server_last_octet + 1 }}" set_fact: client_ip: "{{ [ wireguard_server_ip_octets[0], wireguard_server_ip_octets[1], wireguard_server_ip_octets[2], next_octet_candidate ] | join('.') }}" when: client_ip == "" - name: Generate client private key command: "wg genkey" register: client_private_key changed_when: true no_log: yes - name: Generate client public key command: "wg pubkey" args: stdin: "{{ client_private_key.stdout }}" register: client_public_key changed_when: false no_log: yes - name: Add client to WireGuard server config blockinfile: path: "{{ wireguard_config_file }}" block: | # Client: {{ client_name }} [Peer] PublicKey = {{ client_public_key.stdout }} AllowedIPs = {{ client_ip }}/32 marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}" register: wireguard_client_block - name: Ensure client configs directory exists file: path: "{{ wireguard_client_configs_path }}" state: directory mode: '0700' owner: root group: root - name: Ensure local client configs directory exists file: path: "{{ wireguard_local_client_configs_dir }}" state: directory mode: '0700' delegate_to: localhost become: no run_once: true - name: Get server public key shell: "cat {{ wireguard_config_path }}/{{ wireguard_interface }}_private.key | wg pubkey" register: server_public_key_cmd changed_when: false no_log: yes failed_when: false - name: Create client configuration file template: src: "{{ playbook_dir }}/../templates/wireguard-client.conf.j2" dest: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf" mode: '0600' owner: root group: root - name: Download client configuration to control machine fetch: src: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf" dest: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf" flat: yes mode: '0600' - name: Ensure local client configuration has strict permissions file: path: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf" mode: '0600' delegate_to: localhost become: no - name: Read WireGuard server config to find server IP slurp: src: "{{ wireguard_config_file }}" register: wireguard_server_config_read - name: Restart WireGuard service systemd: name: "wg-quick@{{ wireguard_interface }}" state: restarted when: wireguard_client_block.changed - name: Display client configuration debug: msg: | ======================================== WireGuard Client Added: {{ client_name }} ======================================== Client Configuration File: {{ wireguard_client_configs_path }}/{{ client_name }}.conf Local Copy: {{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf Client IP: {{ client_ip }} Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }} To use this configuration: 1. Copy the config file to your client machine 2. Install WireGuard client 3. Run: sudo wg-quick up {{ client_name }} Or scan the QR code (if qrencode installed): qrencode -t ansiutf8 < {{ wireguard_client_configs_path }}/{{ client_name }}.conf ======================================== - name: Generate QR code for client config command: "qrencode -t ansiutf8 -r {{ wireguard_client_configs_path }}/{{ client_name }}.conf" register: qr_code changed_when: false failed_when: false - name: Display QR code debug: msg: "{{ qr_code.stdout }}" when: qr_code.rc == 0