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