diff --git a/roles/dns/handlers/main.yml b/roles/dns/handlers/main.yml index fe677147..29a2c2f2 100644 --- a/roles/dns/handlers/main.yml +++ b/roles/dns/handlers/main.yml @@ -1,5 +1,5 @@ --- -- name: daemon reload +- name: daemon-reload systemd: daemon_reload: true diff --git a/roles/dns/tasks/ubuntu.yml b/roles/dns/tasks/ubuntu.yml index 3733fd63..f54f643b 100644 --- a/roles/dns/tasks/ubuntu.yml +++ b/roles/dns/tasks/ubuntu.yml @@ -62,3 +62,38 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE notify: - restart dnscrypt-proxy + +- name: Ubuntu | Apply systemd security hardening for dnscrypt-proxy + copy: + dest: /etc/systemd/system/dnscrypt-proxy.service.d/90-security-hardening.conf + content: | + # Algo VPN systemd security hardening for dnscrypt-proxy + # Additional hardening on top of comprehensive AppArmor + [Service] + # Privilege restrictions + NoNewPrivileges=yes + + # Filesystem isolation (complements AppArmor) + ProtectSystem=strict + ProtectHome=yes + PrivateTmp=yes + PrivateDevices=yes + ProtectKernelTunables=yes + ProtectControlGroups=yes + + # Network restrictions + RestrictAddressFamilies=AF_INET AF_INET6 + + # Allow access to dnscrypt-proxy cache (AppArmor also controls this) + ReadWritePaths=/var/cache/dnscrypt-proxy + + # System call filtering (complements AppArmor restrictions) + SystemCallFilter=@system-service @network-io + SystemCallFilter=~@debug @mount @swap @reboot @raw-io + SystemCallErrorNumber=EPERM + owner: root + group: root + mode: 0644 + notify: + - daemon-reload + - restart dnscrypt-proxy diff --git a/roles/strongswan/defaults/main.yml b/roles/strongswan/defaults/main.yml index 2483b3af..ad1b97af 100644 --- a/roles/strongswan/defaults/main.yml +++ b/roles/strongswan/defaults/main.yml @@ -11,7 +11,12 @@ algo_ondemand_wifi_exclude: _null algo_dns_adblocking: false ipv6_support: false dns_encryption: true +# Random UUID for CA name constraints - prevents certificate reuse across different Algo deployments +# This unique identifier ensures each CA can only issue certificates for its specific server instance openssl_constraint_random_id: "{{ IP_subject_alt_name | to_uuid }}.algo" +# Subject Alternative Name (SAN) configuration - CRITICAL for client compatibility +# Modern clients (especially macOS/iOS) REQUIRE SAN extension in server certificates +# 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 }} @@ -21,12 +26,16 @@ 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:0:0:0:0:0:0:0/0:0:0:0:0:0:0:0 {%- endif -%} diff --git a/roles/strongswan/tasks/client_configs.yml b/roles/strongswan/tasks/client_configs.yml index 08fc24cf..814f25e2 100644 --- a/roles/strongswan/tasks/client_configs.yml +++ b/roles/strongswan/tasks/client_configs.yml @@ -33,6 +33,7 @@ with_items: - "{{ users }}" + - name: Build the client ipsec secret file template: src: client_ipsec.secrets.j2 diff --git a/roles/strongswan/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml index 76d6fbcb..e4d5272f 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -13,15 +13,18 @@ dest: "{{ ipsec_pki_path }}/{{ item }}" state: directory recurse: true + mode: "0700" with_items: - certs - private + - public - name: Ensure the config directories exist file: dest: "{{ ipsec_config_path }}/{{ item }}" state: directory recurse: true + mode: "0700" with_items: - apple - manual @@ -34,7 +37,10 @@ curve: secp384r1 mode: "0600" - - name: Create certificate signing request (CSR) for CA certificate + # CRITICAL: Create CA certificate with proper security constraints + # Name constraints provide defense-in-depth security by restricting the scope of certificates + # this CA can issue, preventing misuse if the CA key is compromised + - name: Create certificate signing request (CSR) for CA certificate with security constraints community.crypto.openssl_csr_pipe: privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" privatekey_passphrase: "{{ CA_password }}" @@ -42,10 +48,35 @@ use_common_name_for_san: true basic_constraints: - 'CA:TRUE' + - 'pathlen:0' basic_constraints_critical: true key_usage: - keyCertSign + - cRLSign key_usage_critical: true + # Restrict CA to only sign VPN-related certificates + extended_key_usage: + - serverAuth + - clientAuth + - '1.3.6.1.5.5.7.3.17' # IPsec End Entity + extended_key_usage_critical: true + # Name constraints to restrict certificate scope + name_constraints_permitted: + - "{{ subjectAltName_type }}:{{ IP_subject_alt_name }}{{ '/255.255.255.255' if subjectAltName_type == 'IP' else '' }}" + - "DNS:{{ openssl_constraint_random_id }}" + - "email:{{ openssl_constraint_random_id }}" + name_constraints_excluded: + - "DNS:.com" + - "DNS:.org" + - "DNS:.net" + - "DNS:.gov" + - "DNS:.edu" + - "DNS:.mil" + - "DNS:.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" + name_constraints_critical: true register: ca_csr - name: Create self-signed CA certificate from CSR @@ -55,8 +86,14 @@ privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" privatekey_passphrase: "{{ CA_password }}" provider: selfsigned + mode: "0644" - - name: Create private keys + - name: Copy the CA certificate + copy: + src: "{{ ipsec_pki_path }}/cacert.pem" + dest: "{{ ipsec_config_path }}/manual/cacert.pem" + + - name: Create private keys for users and server community.crypto.openssl_privatekey: path: "{{ ipsec_pki_path }}/private/{{ item }}.key" type: ECC @@ -67,17 +104,59 @@ - "{{ IP_subject_alt_name }}" register: client_key_jobs - - name: Create CSRs + # Create CSRs with proper Subject Alternative Names + # CRITICAL: Server certificates need SAN extension for modern clients, + # especially macOS/iOS which perform strict certificate validation for IKEv2. + # Without SAN containing the server IP, clients will reject the certificate. + - name: Create CSRs for server certificate with SAN + community.crypto.openssl_csr_pipe: + privatekey_path: "{{ ipsec_pki_path }}/private/{{ IP_subject_alt_name }}.key" + subject_alt_name: "{{ subjectAltName.split(',') }}" + common_name: "{{ IP_subject_alt_name }}" + key_usage: + - digitalSignature + - keyEncipherment + key_usage_critical: false + # Server authentication for IKEv2 VPN connections + extended_key_usage: + - serverAuth + - '1.3.6.1.5.5.7.3.17' # IPsec End Entity + extended_key_usage_critical: false + register: server_csr + + - name: Create CSRs for client certificates community.crypto.openssl_csr_pipe: privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" - subject_alt_name: "{{ subjectAltName | split(',') if item == IP_subject_alt_name else [subjectAltName_USER] }}" + subject_alt_name: + - "email:{{ item }}@{{ openssl_constraint_random_id }}" common_name: "{{ item }}" - with_items: - - "{{ users }}" - - "{{ IP_subject_alt_name }}" + key_usage: + - digitalSignature + - keyEncipherment + key_usage_critical: false + # Client certificates should not have serverAuth + extended_key_usage: + - clientAuth + - '1.3.6.1.5.5.7.3.17' # IPsec End Entity + extended_key_usage_critical: false + with_items: "{{ users }}" register: client_csr_jobs - - name: Sign clients certificates with our CA + # Sign server certificate with proper extensions + - name: Sign server certificate with CA + community.crypto.x509_certificate: + csr_content: "{{ server_csr.csr }}" + path: "{{ ipsec_pki_path }}/certs/{{ IP_subject_alt_name }}.crt" + provider: ownca + ownca_path: "{{ ipsec_pki_path }}/cacert.pem" + ownca_privatekey_path: "{{ ipsec_pki_path }}/private/cakey.pem" + ownca_privatekey_passphrase: "{{ CA_password }}" + ownca_not_after: +3650d + ownca_not_before: "-1d" + mode: "0644" + + # Sign client certificates with CA + - name: Sign client certificates with CA community.crypto.x509_certificate: csr_content: "{{ item.csr }}" path: "{{ ipsec_pki_path }}/certs/{{ item.item }}.crt" @@ -87,6 +166,7 @@ ownca_privatekey_passphrase: "{{ CA_password }}" ownca_not_after: +3650d ownca_not_before: "-1d" + mode: "0644" with_items: "{{ client_csr_jobs.results }}" register: client_sign_results @@ -101,6 +181,19 @@ encryption_level: "compatibility2022" with_items: "{{ users }}" + - name: Generate p12 files with CA certificate included + community.crypto.openssl_pkcs12: + path: "{{ ipsec_pki_path }}/private/{{ item }}_ca.p12" + friendly_name: "{{ item }}" + privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" + certificate_path: "{{ ipsec_pki_path }}/certs/{{ item }}.crt" + other_certificates: + - "{{ ipsec_pki_path }}/cacert.pem" + passphrase: "{{ p12_export_password }}" + mode: "0600" + encryption_level: "compatibility2022" + with_items: "{{ users }}" + - name: Copy the p12 certificates copy: src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" @@ -108,6 +201,13 @@ with_items: - "{{ users }}" + - name: Build openssh public keys + community.crypto.openssl_publickey: + path: "{{ ipsec_pki_path }}/public/{{ item }}.pub" + privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" + format: OpenSSH + with_items: "{{ users }}" + - name: Add all users to the file ansible.builtin.lineinfile: path: "{{ ipsec_pki_path }}/all-users" @@ -142,6 +242,7 @@ issuer: CN: "{{ IP_subject_alt_name }}" revoked_certificates: "{{ revoked_certificates }}" + mode: "0644" delegate_to: localhost become: false vars: @@ -152,4 +253,4 @@ src: "{{ ipsec_pki_path }}/crl.pem" dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem" notify: - - rereadcrls + - rereadcrls \ No newline at end of file diff --git a/roles/strongswan/templates/100-CustomLimitations.conf.j2 b/roles/strongswan/templates/100-CustomLimitations.conf.j2 index d7430af1..16446710 100644 --- a/roles/strongswan/templates/100-CustomLimitations.conf.j2 +++ b/roles/strongswan/templates/100-CustomLimitations.conf.j2 @@ -1,2 +1,24 @@ +# Algo VPN systemd security hardening for StrongSwan +# Enhanced hardening on top of existing AppArmor [Service] -MemoryLimit=16777216 +# Privilege restrictions +NoNewPrivileges=yes + +# Filesystem isolation (complements AppArmor) +ProtectHome=yes +PrivateTmp=yes +ProtectKernelTunables=yes +ProtectControlGroups=yes + +# Network restrictions - include IPsec kernel communication requirements +RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_PACKET + +# Allow access to IPsec configuration, state, and kernel interfaces +ReadWritePaths=/etc/ipsec.d /var/lib/strongswan +ReadOnlyPaths=/proc/net/pfkey + +# System call filtering (complements AppArmor restrictions) +# Allow crypto operations, remove cpu-emulation restriction for crypto algorithms +SystemCallFilter=@system-service @network-io +SystemCallFilter=~@debug @mount @swap @reboot +SystemCallErrorNumber=EPERM diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index 8405f8ef..bf6f6709 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -73,10 +73,13 @@ DeadPeerDetectionRate Medium + DisableMOBIKE 0 + DisableRedirect 1 + EnableCertificateRevocationCheck 0 EnablePFS @@ -96,19 +99,24 @@ {{ item.0 }}@{{ openssl_constraint_random_id }} PayloadCertificateUUID {{ pkcs12_PayloadCertificateUUID }} + CertificateType ECDSA384 ServerCertificateIssuerCommonName {{ IP_subject_alt_name }} + ServerCertificateCommonName + {{ IP_subject_alt_name }} RemoteAddress {{ IP_subject_alt_name }} RemoteIdentifier {{ IP_subject_alt_name }} + UseConfigurationAttributeInternalIPSubnet 0 IPv4 + OverridePrimary 1 diff --git a/roles/wireguard/handlers/main.yml b/roles/wireguard/handlers/main.yml index d13ee31c..d8a58836 100644 --- a/roles/wireguard/handlers/main.yml +++ b/roles/wireguard/handlers/main.yml @@ -1,4 +1,8 @@ --- +- name: daemon-reload + systemd: + daemon_reload: true + - name: restart wireguard service: name: "{{ service_name }}" diff --git a/roles/wireguard/tasks/ubuntu.yml b/roles/wireguard/tasks/ubuntu.yml index 412aa538..63d61d41 100644 --- a/roles/wireguard/tasks/ubuntu.yml +++ b/roles/wireguard/tasks/ubuntu.yml @@ -10,3 +10,45 @@ set_fact: service_name: wg-quick@{{ wireguard_interface }} tags: always + +- name: Ubuntu | Ensure that the WireGuard service directory exists + file: + path: /etc/systemd/system/wg-quick@{{ wireguard_interface }}.service.d/ + state: directory + mode: 0755 + owner: root + group: root + +- name: Ubuntu | Apply systemd security hardening for WireGuard + copy: + dest: /etc/systemd/system/wg-quick@{{ wireguard_interface }}.service.d/90-security-hardening.conf + content: | + # Algo VPN systemd security hardening for WireGuard + [Service] + # Privilege restrictions + NoNewPrivileges=yes + + # Filesystem isolation + ProtectSystem=strict + ProtectHome=yes + PrivateTmp=yes + ProtectKernelTunables=yes + ProtectControlGroups=yes + + # Network restrictions - WireGuard needs NETLINK for interface management + RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK + + # Allow access to WireGuard configuration + ReadWritePaths=/etc/wireguard + ReadOnlyPaths=/etc/resolv.conf + + # System call filtering - allow network and system service calls + SystemCallFilter=@system-service @network-io + SystemCallFilter=~@debug @mount @swap @reboot @raw-io + SystemCallErrorNumber=EPERM + owner: root + group: root + mode: 0644 + notify: + - daemon-reload + - restart wireguard