Security Hardening and Certificate Authority Constraints (#14811)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dan Guido 2025-08-04 20:22:41 -07:00 committed by GitHub
parent 9e0de205fb
commit 0aaca43019
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 154 additions and 5 deletions

View file

@ -1,5 +1,5 @@
---
- name: daemon reload
- name: daemon-reload
systemd:
daemon_reload: true

View file

@ -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

View file

@ -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 -%}

View file

@ -33,6 +33,7 @@
with_items:
- "{{ users }}"
- name: Build the client ipsec secret file
template:
src: client_ipsec.secrets.j2

View file

@ -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 }}" &&

View file

@ -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

View file

@ -73,10 +73,13 @@
</dict>
<key>DeadPeerDetectionRate</key>
<string>Medium</string>
<!-- MOBIKE allows VPN to survive network changes (WiFi to cellular) -->
<key>DisableMOBIKE</key>
<integer>0</integer>
<!-- Disable IKEv2 redirects for security -->
<key>DisableRedirect</key>
<integer>1</integer>
<!-- Disable CRL checking for performance and reliability -->
<key>EnableCertificateRevocationCheck</key>
<integer>0</integer>
<key>EnablePFS</key>
@ -96,19 +99,24 @@
<string>{{ item.0 }}@{{ openssl_constraint_random_id }}</string>
<key>PayloadCertificateUUID</key>
<string>{{ pkcs12_PayloadCertificateUUID }}</string>
<!-- Use ECDSA P-384 certificates for strong security -->
<key>CertificateType</key>
<string>ECDSA384</string>
<key>ServerCertificateIssuerCommonName</key>
<string>{{ IP_subject_alt_name }}</string>
<key>ServerCertificateCommonName</key>
<string>{{ IP_subject_alt_name }}</string>
<key>RemoteAddress</key>
<string>{{ IP_subject_alt_name }}</string>
<key>RemoteIdentifier</key>
<string>{{ IP_subject_alt_name }}</string>
<!-- Use server-provided internal IP assignment -->
<key>UseConfigurationAttributeInternalIPSubnet</key>
<integer>0</integer>
</dict>
<key>IPv4</key>
<dict>
<!-- Override primary network interface for full VPN routing -->
<key>OverridePrimary</key>
<integer>1</integer>
</dict>

View file

@ -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.

View file

@ -1,4 +1,8 @@
---
- name: daemon-reload
systemd:
daemon_reload: true
- name: restart wireguard
service:
name: "{{ service_name }}"

View file

@ -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