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