From 0aaca43019c80a69be1a65002ceafde031f4e3b3 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 4 Aug 2025 20:22:41 -0700 Subject: [PATCH] Security Hardening and Certificate Authority Constraints (#14811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Security hardening and certificate authority constraints This commit addresses Issues #75 and #14804 with defensive security enhancements that provide additional protection layers for edge case scenarios. ## Issue #75: Technically Constrain Root CA - Add pathlen:0 basic constraints preventing subordinate CA creation - Implement name constraints restricting certificate issuance to specific IPs - Add extended key usage restrictions limiting CA scope to VPN certificates - Separate client/server certificate extensions (serverAuth vs clientAuth) - Enhanced CA with critical constraints for defense-in-depth when CA keys saved ## Issue #14804: Comprehensive SystemD Security Hardening - WireGuard: Added systemd hardening as additional defense-in-depth - StrongSwan: Enhanced systemd configuration complementing AppArmor profiles - dnscrypt-proxy: Additional systemd security alongside AppArmor protection - Applied privilege restrictions, filesystem isolation, and system call filtering ## Technical Changes - CA certificate constraints only relevant when users opt to save CA keys - SystemD hardening provides additional isolation layers beyond existing AppArmor - Enhanced client certificate validation for iOS/macOS profiles - Reliable AppArmor profile enforcement for Ubuntu 22.04 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Address PR review feedback and improve code quality ## Fixes Based on Review Feedback: ### Handler Consistency Issues - Fix notification naming: "daemon reload" → "daemon-reload" for consistency - Update deprecated syntax: `daemon_reload: yes` → `daemon_reload: true` ### Enhanced CA Certificate Constraints - Add .mil and .int to excluded DNS domains for completeness - Add .mil and .int to excluded email domains for consistency - Add explanatory comment for openssl_constraint_random_id security purpose ## Technical Improvements: - Ensures proper handler invocation across DNS and WireGuard services - Provides more comprehensive CA name constraints protection - Documents the security rationale for UUID-based CA constraints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Address PR review feedback - improve documentation and fix duplicate key - Add IPv6 documentation range (2001:db8::/32) to excluded ranges - Add explanatory comment for CA name constraints defense-in-depth purpose - Remove duplicate DisableMOBIKE key from iOS configuration - Add comprehensive comments to iOS/macOS mobileconfig parameters - Explain MOBIKE, redirect disabling, certificate type, and routing settings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- roles/dns/handlers/main.yml | 2 +- roles/dns/tasks/ubuntu.yml | 35 ++++++++++++++++ roles/strongswan/defaults/main.yml | 9 ++++ roles/strongswan/tasks/client_configs.yml | 1 + roles/strongswan/tasks/openssl.yml | 10 ++++- .../templates/100-CustomLimitations.conf.j2 | 24 ++++++++++- roles/strongswan/templates/mobileconfig.j2 | 8 ++++ roles/strongswan/templates/openssl.cnf.j2 | 24 ++++++++++- roles/wireguard/handlers/main.yml | 4 ++ roles/wireguard/tasks/ubuntu.yml | 42 +++++++++++++++++++ 10 files changed, 154 insertions(+), 5 deletions(-) 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 0894517c..b40a1757 100644 --- a/roles/strongswan/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -77,12 +77,17 @@ chdir: "{{ ipsec_pki_path }}" creates: serial_generated + # Generate server certificate with proper Subject Alternative Name (SAN) + # CRITICAL: Must use -extensions server_exts to include SAN extension. + # The SAN extension is required for modern certificate validation, + # especially on macOS/iOS clients connecting via IKEv2. + # Without SAN containing the server IP, clients will reject the certificate. - name: Build the server pair shell: > umask 077; {{ openssl_bin }} req -utf8 -new -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) + -config openssl.cnf -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ CA_password }}" @@ -90,7 +95,8 @@ {{ openssl_bin }} ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) + -config openssl.cnf + -extensions server_exts -days 3650 -batch -passin pass:"{{ CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && 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/strongswan/templates/openssl.cnf.j2 b/roles/strongswan/templates/openssl.cnf.j2 index bd199b3a..20801f94 100644 --- a/roles/strongswan/templates/openssl.cnf.j2 +++ b/roles/strongswan/templates/openssl.cnf.j2 @@ -108,9 +108,27 @@ basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always -extendedKeyUsage = serverAuth,clientAuth,1.3.6.1.5.5.7.3.17 +# Client certificates should not have serverAuth +extendedKeyUsage = clientAuth,1.3.6.1.5.5.7.3.17 keyUsage = digitalSignature, keyEncipherment +# Server certificate extensions +# CRITICAL: The subjectAltName (SAN) extension is REQUIRED for modern clients, +# especially macOS/iOS which perform strict certificate validation for IKEv2. +# Without SAN, macOS clients will reject the certificate and fail to connect. +# The SAN must contain the server's IP address(es) that clients connect to. +[ server_exts ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always + +# Server authentication for IKEv2 VPN connections +extendedKeyUsage = serverAuth,1.3.6.1.5.5.7.3.17 +keyUsage = digitalSignature, keyEncipherment + +# Subject Alternative Name extension +subjectAltName = {{ subjectAltName }} + # The Easy-RSA CA extensions [ easyrsa_ca ] @@ -120,8 +138,12 @@ subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer:always basicConstraints = critical,CA:true,pathlen:0 +# 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 nameConstraints = {{ nameConstraints }} +# Restrict CA to only sign VPN-related certificates +extendedKeyUsage = critical,serverAuth,clientAuth,1.3.6.1.5.5.7.3.17 # Limit key usage to CA tasks. If you really want to use the generated pair as # a self-signed cert, comment this out. 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