diff options
Diffstat (limited to 'roles/common')
-rw-r--r-- | roles/common/files/etc/logcheck/ignore.d.server/strongswan-local | 19 | ||||
-rw-r--r-- | roles/common/files/etc/strongswan.d/charon.conf | 303 | ||||
-rw-r--r-- | roles/common/files/etc/strongswan.d/charon/socket-default.conf | 20 | ||||
-rwxr-xr-x | roles/common/files/usr/local/sbin/update-firewall.sh | 102 | ||||
-rw-r--r-- | roles/common/handlers/main.yml | 6 | ||||
-rw-r--r-- | roles/common/tasks/ipsec.yml | 96 | ||||
-rw-r--r-- | roles/common/tasks/logging.yml | 1 | ||||
-rw-r--r-- | roles/common/tasks/main.yml | 5 | ||||
-rw-r--r-- | roles/common/templates/etc/fail2ban/jail.local.j2 | 2 | ||||
-rw-r--r-- | roles/common/templates/etc/ipsec.conf.j2 | 43 | ||||
-rw-r--r-- | roles/common/templates/etc/ipsec.secrets.j2 | 5 | ||||
-rw-r--r-- | roles/common/templates/etc/iptables/services.j2 | 7 | ||||
-rwxr-xr-x | roles/common/templates/etc/network/if-up.d/ipsec.j2 | 47 |
13 files changed, 585 insertions, 71 deletions
diff --git a/roles/common/files/etc/logcheck/ignore.d.server/strongswan-local b/roles/common/files/etc/logcheck/ignore.d.server/strongswan-local new file mode 100644 index 0000000..13218b0 --- /dev/null +++ b/roles/common/files/etc/logcheck/ignore.d.server/strongswan-local @@ -0,0 +1,19 @@ +# Ansible Managed +# Do NOT edit this file directly! +# +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[JOB\] spawning [[:digit:]]+ worker threads$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[KNL\] creating acquire job for policy [[:xdigit:].:]{3,39}/[[:digit:]]+(\[\w+(/[[:alnum:]-]+)?\])? === [[:xdigit:].:]{3,39}/[[:digit:]]+(\[\w+(/[[:alnum:]-]+)?\])? with reqid \{[[:digit:]]+\}$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[KNL\] creating rekey job for ESP CHILD_SA with SPI [[:xdigit:]]{8} and reqid \{[[:digit:]]+\}$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[KNL\] creating delete job for ESP CHILD_SA with SPI [[:xdigit:]]{8} and reqid \{[[:digit:]]+\}$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[KNL\] querying SAD entry with SPI [[:xdigit:]]{8} failed: No such process \([[:digit:]]+\)$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] initiating IKE_SA [[:alnum:]._-]+\[[[:digit:]]+\] to [[:xdigit:].:]{3,39}$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] [[:xdigit:].:]{3,39} is initiating an IKE_SA$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] establishing CHILD_SA [[:alnum:]._-]+(\{[[:digit:]]+\})?$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] IKE_SA [[:alnum:]._-]+\[[[:digit:]]+\] established between [[:xdigit:].:]{3,39}\[[^]\"]+\]\.\.\.[[:xdigit:].:]{3,39}\[[^]]+\]$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] CHILD_SA [[:alnum:]._-]+\{[[:digit:]]+\} established with SPIs [[:xdigit:]]{8}_i [[:xdigit:]]{8}_o and TS [[:xdigit:].:]{3,39}/[[:digit:]]+ === [[:xdigit:].:]{3,39}/[[:digit:]]+$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] closing CHILD_SA [[:alnum:]._-]+\{[[:digit:]]+\} with SPIs [[:xdigit:]]{8}_i \([[:digit:]]+ bytes\) [[:xdigit:]]{8}_o \([[:digit:]]+ bytes\) and TS [[:xdigit:].:]{3,39}/[[:digit:]]+ === [[:xdigit:].:]{3,39}/[[:digit:]]+$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] reauthenticating IKE_SA [[:alnum:]._-]+\[[[:digit:]]+\]$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[JOB\] deleting IKE_SA after [[:digit:]]+ seconds of CHILD_SA inactivity$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] deleting IKE_SA [[:alnum:]._-]+\[[[:digit:]]+\] between [[:xdigit:].:]{3,39}\[[^]\"]+\]\.\.\.[[:xdigit:].:]{3,39}\[[^]]+\]$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ (charon|ipsec\[[[:digit:]]+\]): [[:digit:]]+\[IKE\] IKE_SA deleted$ +^\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ vpn: [+-] .* [[:xdigit:].:]{3,39}/[[:digit:]]+ == [[:xdigit:].:]{3,39} -- [[:xdigit:].:]{3,39} == [[:xdigit:].:]{3,39}/[[:digit:]]+$ diff --git a/roles/common/files/etc/strongswan.d/charon.conf b/roles/common/files/etc/strongswan.d/charon.conf new file mode 100644 index 0000000..17e917a --- /dev/null +++ b/roles/common/files/etc/strongswan.d/charon.conf @@ -0,0 +1,303 @@ +# Options for the charon IKE daemon. +charon { + + # Accept unencrypted ID and HASH payloads in IKEv1 Main Mode. + # accept_unencrypted_mainmode_messages = no + + # Maximum number of half-open IKE_SAs for a single peer IP. + # block_threshold = 5 + + # Whether relations in validated certificate chains should be cached in + # memory. + # cert_cache = yes + + # Send Cisco Unity vendor ID payload (IKEv1 only). + # cisco_unity = no + + # Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed. + # close_ike_on_child_failure = no + + # Number of half-open IKE_SAs that activate the cookie mechanism. + # cookie_threshold = 10 + + # Use ANSI X9.42 DH exponent size or optimum size matched to cryptographic + # strength. + # dh_exponent_ansi_x9_42 = yes + + # DNS server assigned to peer via configuration payload (CP). + # dns1 = + + # DNS server assigned to peer via configuration payload (CP). + # dns2 = + + # Enable Denial of Service protection using cookies and aggressiveness + # checks. + # dos_protection = yes + + # Compliance with the errata for RFC 4753. + # ecp_x_coordinate_only = yes + + # Free objects during authentication (might conflict with plugins). + # flush_auth_cfg = no + + # Maximum size (complete IP datagram size in bytes) of a sent IKE fragment + # when using proprietary IKEv1 or standardized IKEv2 fragmentation (0 for + # address family specific default values). If specified this limit is + # used for both IPv4 and IPv6. + # fragment_size = 0 + + # Name of the group the daemon changes to after startup. + # group = + + # Timeout in seconds for connecting IKE_SAs (also see IKE_SA_INIT DROPPING). + # half_open_timeout = 30 + + # Enable hash and URL support. + # hash_and_url = no + + # Allow IKEv1 Aggressive Mode with pre-shared keys as responder. + # i_dont_care_about_security_and_use_aggressive_mode_psk = no + + # A space-separated list of routing tables to be excluded from route + # lookups. + # ignore_routing_tables = + + # Maximum number of IKE_SAs that can be established at the same time before + # new connection attempts are blocked. + # ikesa_limit = 0 + + # Number of exclusively locked segments in the hash table. + # ikesa_table_segments = 1 + + # Size of the IKE_SA hash table. + # ikesa_table_size = 1 + + # Whether to close IKE_SA if the only CHILD_SA closed due to inactivity. + inactivity_close_ike = yes + + # Limit new connections based on the current number of half open IKE_SAs, + # see IKE_SA_INIT DROPPING in strongswan.conf(5). + # init_limit_half_open = 0 + + # Limit new connections based on the number of queued jobs. + # init_limit_job_load = 0 + + # Causes charon daemon to ignore IKE initiation requests. + # initiator_only = no + + # Install routes into a separate routing table for established IPsec + # tunnels. + install_routes = no + + # Install virtual IP addresses. + install_virtual_ip = no + + # The name of the interface on which virtual IP addresses should be + # installed. + # install_virtual_ip_on = + + # Check daemon, libstrongswan and plugin integrity at startup. + # integrity_test = no + + # A comma-separated list of network interfaces that should be ignored, if + # interfaces_use is specified this option has no effect. + # interfaces_ignore = + + # A comma-separated list of network interfaces that should be used by + # charon. All other interfaces are ignored. + # interfaces_use = + + # NAT keep alive interval. + # keep_alive = 20s + + # Plugins to load in the IKE daemon charon. + # load = + + # Determine plugins to load via each plugin's load option. + # load_modular = no + + # Maximum packet size accepted by charon. + # max_packet = 10000 + + # Enable multiple authentication exchanges (RFC 4739). + # multiple_authentication = yes + + # WINS servers assigned to peer via configuration payload (CP). + # nbns1 = + + # WINS servers assigned to peer via configuration payload (CP). + # nbns2 = + + # UDP port used locally. If set to 0 a random port will be allocated. + # port = 500 + + # UDP port used locally in case of NAT-T. If set to 0 a random port will be + # allocated. Has to be different from charon.port, otherwise a random port + # will be allocated. + # port_nat_t = 4500 + + # By default public IPv6 addresses are preferred over temporary ones (RFC + # 4941), to make connections more stable. Enable this option to reverse + # this. + # prefer_temporary_addrs = no + + # Process RTM_NEWROUTE and RTM_DELROUTE events. + # process_route = yes + + # Delay in ms for receiving packets, to simulate larger RTT. + # receive_delay = 0 + + # Delay request messages. + # receive_delay_request = yes + + # Delay response messages. + # receive_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # receive_delay_type = 0 + + # Size of the AH/ESP replay window, in packets. + # replay_window = 32 + + # Base to use for calculating exponential back off, see IKEv2 RETRANSMISSION + # in strongswan.conf(5). + # retransmit_base = 1.8 + + # Timeout in seconds before sending first retransmit. + # retransmit_timeout = 4.0 + + # Number of times to retransmit a packet before giving up. + # retransmit_tries = 5 + + # Interval to use when retrying to initiate an IKE_SA (e.g. if DNS + # resolution failed), 0 to disable retries. + # retry_initiate_interval = 0 + + # Initiate CHILD_SA within existing IKE_SAs. + # reuse_ikesa = yes + + # Numerical routing table to install routes to. + # routing_table = + + # Priority of the routing table. + # routing_table_prio = + + # Delay in ms for sending packets, to simulate larger RTT. + # send_delay = 0 + + # Delay request messages. + # send_delay_request = yes + + # Delay response messages. + # send_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # send_delay_type = 0 + + # Send strongSwan vendor ID payload + # send_vendor_id = no + + # Number of worker threads in charon. + # threads = 16 + + # Name of the user the daemon changes to after startup. + # user = + + crypto_test { + + # Benchmark crypto algorithms and order them by efficiency. + # bench = no + + # Buffer size used for crypto benchmark. + # bench_size = 1024 + + # Number of iterations to test each algorithm. + # bench_time = 50 + + # Test crypto algorithms during registration (requires test vectors + # provided by the test-vectors plugin). + # on_add = no + + # Test crypto algorithms on each crypto primitive instantiation. + # on_create = no + + # Strictly require at least one test vector to enable an algorithm. + # required = no + + # Whether to test RNG with TRUE quality; requires a lot of entropy. + # rng_true = no + + } + + host_resolver { + + # Maximum number of concurrent resolver threads (they are terminated if + # unused). + # max_threads = 3 + + # Minimum number of resolver threads to keep around. + # min_threads = 0 + + } + + leak_detective { + + # Includes source file names and line numbers in leak detective output. + # detailed = yes + + # Threshold in bytes for leaks to be reported (0 to report all). + # usage_threshold = 10240 + + # Threshold in number of allocations for leaks to be reported (0 to + # report all). + # usage_threshold_count = 0 + + } + + processor { + + # Section to configure the number of reserved threads per priority class + # see JOB PRIORITY MANAGEMENT in strongswan.conf(5). + priority_threads { + + } + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is started. + start-scripts { + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is terminated. + stop-scripts { + + } + + tls { + + # List of TLS encryption ciphers. + # cipher = + + # List of TLS key exchange methods. + # key_exchange = + + # List of TLS MAC algorithms. + # mac = + + # List of TLS cipher suites. + # suites = + + } + + x509 { + + # Discard certificates with unsupported or unknown critical extensions. + # enforce_critical = yes + + } + +} + diff --git a/roles/common/files/etc/strongswan.d/charon/socket-default.conf b/roles/common/files/etc/strongswan.d/charon/socket-default.conf new file mode 100644 index 0000000..1bc20f1 --- /dev/null +++ b/roles/common/files/etc/strongswan.d/charon/socket-default.conf @@ -0,0 +1,20 @@ +socket-default { + + # Firewall mark to set on outbound packets. + # fwmark = + + # Whether to load the plugin. Can also be an integer to increase the + # priority of this plugin. + load = yes + + # Set source address on outbound packets, if possible. + # set_source = yes + + # Listen on IPv4, if possible. + # use_ipv4 = yes + + # Listen on IPv6, if possible. + use_ipv6 = no + +} + diff --git a/roles/common/files/usr/local/sbin/update-firewall.sh b/roles/common/files/usr/local/sbin/update-firewall.sh index f25f507..065bae2 100755 --- a/roles/common/files/usr/local/sbin/update-firewall.sh +++ b/roles/common/files/usr/local/sbin/update-firewall.sh @@ -40,8 +40,12 @@ check=0 verbose=0 addrfam= -secmark=0xA99 # must match that in /etc/network/if-up.d/ipsec -secproto=esp # must match /etc/ipsec.conf; ESP is the default (vs AH/IPComp) +secproto=esp # must match /etc/ipsec.conf; ESP is the default (vs AH/IPComp) +if [ -x /usr/sbin/ipsec ] && /usr/sbin/ipsec status >/dev/null; then + ipsec=y +else + ipsec=n +fi fail2ban_re='^(\[[0-9]+:[0-9]+\]\s+)?-A fail2ban-\S' IPSec_re=" -m policy --dir (in|out) --pol ipsec --reqid [0-9]+ --proto $secproto -j ACCEPT$" @@ -152,21 +156,9 @@ run() { tables[$f]=filter # The default interface associated with this address. - local if=$( /bin/ip -$f route show to default scope global \ + local if=$( /bin/ip -$f -o route show to default scope global \ | sed -nr '/^default via \S+ dev (\S+).*/ {s//\1/p;q}' ) - # The virtual interface reserved for IPSec. - local ifsec=$( /bin/ip -o -$f link show \ - | sed -nr "/^[0-9]+:\s+(sec[0-9]+)@$if:\s.*/ {s//\1/p;q}" ) - - # The (host-scoped) IP reserved for IPSec. - local ipsec= - if [ "$ifsec" -a $f = 4 ]; then - tables[$f]='mangle nat filter' - ipsec=$( /bin/ip -$f address show dev "$ifsec" scope host \ - | sed -nr '/^\s+inet\s(\S+).*/ {s//\1/p;q}' ) - fi - # Store the old (current) ruleset local old=$(mktemp --tmpdir current-rules.v$f.XXXXXX) \ new=$(mktemp --tmpdir new-rules.v$f.XXXXXX) @@ -203,8 +195,9 @@ run() { grep -E -- "$fail2ban_re" "$old" || true fi >> "$new" - if [ "$ipsec" ]; then - # (Host-to-host) IPSec tunnels come first. TODO: test IPSec with IPv6. + if [ "$f" = 4 -a "$ipsec" = y ]; then + # Our IPSec tunnels are IPv4 only. + # (Host-to-host) IPSec tunnels come first. grep -E -- "$IPSec_re" "$old" >> "$new" || true # Allow any IPsec $secproto protocol packets to be sent and received. @@ -219,15 +212,24 @@ run() { # http://baldric.net/loose-iptables-firewall-for-servers/ local ip - if [ "$f" = 4 ]; then + if [ "$f" = 4 -a "$ipsec" = y ]; then # Private-use networks (RFC 1918) and link local (RFC 3927) - local MyNetwork=$( /bin/ip -4 address show dev $if scope global \ - | sed -nr 's/^\s+inet\s(\S+).*/\1/p') + local MyIPSec="$( /bin/ip -4 -o route show table 220 dev $if | sed 's/\s.*//' )" + local MyNetwork="$( /bin/ip -4 -o address show dev $if scope global \ + | sed -nr "s/^[0-9]+:\s+$if\s+inet\s(\S+).*/\1/p" \ + | while read ip; do + for ips in $MyIPSec; do + [ "$ips" = "$(/usr/bin/netmask -nc "$ip" "$ips" | sed 's/^ *//')" ] || echo "$ip" + done + done + )" [ "$MyNetwork" ] && \ for ip in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16; do # Don't lock us out if we are behind a NAT ;-) - [ "$ip" = "$(/usr/bin/netmask -nc $ip $MyNetwork | sed 's/ //g')" ] \ - || iptables -A INPUT -i $if -s "$ip" -j DROP + for myip in $MyNetwork; do + [ "$ip" = "$(/usr/bin/netmask -nc "$ip" "$myip" | sed 's/^ *//')" ] \ + || iptables -A INPUT -i $if -s "$ip" -j DROP + done done # Other martian packets: "This" network, multicast, broadcast (RFCs @@ -259,14 +261,14 @@ run() { local localhost=$(inet46 $f '127.0.0.1/8' '::1/128') iptables -A INPUT -i lo -s "$localhost" -d "$localhost" -j ACCEPT iptables -A OUTPUT -o lo -s "$localhost" -d "$localhost" -j ACCEPT - - if [ "$ipsec" ]; then - # ACCEPT any, *IPSec* traffic destinating to the non-routable - # $ipsec. Also ACCEPT all traffic originating from $ipsec, as - # it is MASQUERADE'd. - iptables -A INPUT -d "$ipsec" -i $if -m policy --dir in \ - --pol ipsec --proto $secproto -j ACCEPT - iptables -A OUTPUT -m mark --mark "$secmark" -o $if -j ACCEPT + if [ "$f" = 4 -a "$ipsec" = y ]; then + # Allow local access to our virtual IP + /bin/ip -4 -o route show table 220 dev $if \ + | sed -nr 's/.*\ssrc\s+([[:digit:].]{7,15})(\s.*)?/\1/p' \ + | while read ipsec; do + iptables -A INPUT -i lo -s "$ipsec" -d "$ipsec" -j ACCEPT + iptables -A OUTPUT -o lo -s "$ipsec" -d "$ipsec" -j ACCEPT + done fi # Prepare fail2ban. We make fail2ban insert its rules in a @@ -330,46 +332,6 @@ run() { ######################################################################## commit - if [ "$ipsec" ]; then - # DNAT the IPSec paquets to $ipsec after decapsulation, and SNAT - # them before encapsulation. We need to do the NAT'ing before - # packets enter the IPSec stack because they are signed - # afterwards, and NAT'ing would mess up the signature. - ipt-chains mangle PREROUTING:ACCEPT INPUT:ACCEPT \ - FORWARD:DROP \ - OUTPUT:ACCEPT POSTROUTING:ACCEPT - - # Packets which destination is $ipsec *must* be associated with - # an IPSec policy. - iptables -A INPUT -d "$ipsec" -i $if -m policy --dir in \ - --pol ipsec --proto $secproto -j ACCEPT - iptables -A INPUT -d "$ipsec" -i $if -j DROP - - # Packets originating from our (non-routable) $ipsec are marked; - # if there is no xfrm lookup (i.e., no matching IPSec - # association), the packet will retain its mark and be null - # routed later on. Otherwise, the packet is re-queued unmarked. - iptables -A OUTPUT -o $if -j MARK --set-mark 0x0 - iptables -A OUTPUT -s "$ipsec" -o $if -m policy --dir out \ - --pol none -j MARK --set-mark $secmark - commit - - ipt-chains nat PREROUTING:ACCEPT INPUT:ACCEPT \ - OUTPUT:ACCEPT POSTROUTING:ACCEPT - - # DNAT all marked packets after decapsulation. - iptables -A PREROUTING \! -d "$ipsec" -i $if -m policy --dir in \ - --pol ipsec --proto $secproto -j DNAT --to "${ipsec%/*}" - - # Packets originating from our IPSec are SNAT'ed (MASQUERADE). - # (And null-routed later on unless there is an xfrm - # association.) - iptables -A POSTROUTING -m mark --mark $secmark -o $if -j MASQUERADE - commit - fi - - ######################################################################## - local rv1=0 rv2=0 persistent=/etc/iptables/rules.v$f local oldz=$(mktemp --tmpdir current-rules.v$f.XXXXXX) diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index ebb40a0..6ca53be 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -17,9 +17,15 @@ - name: Update rkhunter's data file command: /usr/bin/rkhunter --propupd +- name: Update firewall + command: /usr/local/sbin/update-firewall.sh -c + - name: Restart fail2ban service: name=fail2ban state=restarted +- name: Restart IPSec + service: name=ipsec state=restarted + - name: Reload networking # /etc/init.d/networking doesn't answer the status command; but since # it should be "up" whenever ansible has access to the machine, we use diff --git a/roles/common/tasks/ipsec.yml b/roles/common/tasks/ipsec.yml new file mode 100644 index 0000000..b82c281 --- /dev/null +++ b/roles/common/tasks/ipsec.yml @@ -0,0 +1,96 @@ +- name: Install strongSwan + apt: pkg={{ item }} + with_items: + - strongswan-charon + # for the GCM and openssl plugins + - libstrongswan-standard-plugins + notify: + - Update firewall + - Restart IPSec + +- name: Auto-create a dedicated virtual subnet for IPSec + template: src=etc/network/if-up.d/ipsec.j2 + dest=/etc/network/if-up.d/ipsec + owner=root group=root + mode=0755 + notify: + - Reload networking + +- name: Auto-deactivate the dedicated virtual subnet for IPSec + file: src=../if-up.d/ipsec + dest=/etc/network/if-down.d/ipsec + owner=root group=root state=link force=yes + +- meta: flush_handlers + + +- name: Configure IPSec + template: src=etc/ipsec.conf.j2 + dest=/etc/ipsec.conf + owner=root group=root + mode=0644 + register: r1 + notify: + - Restart IPSec + +- name: Configure IPSec's secrets + template: src=etc/ipsec.secrets.j2 + dest=/etc/ipsec.secrets + owner=root group=root + mode=0600 + register: r2 + notify: + - Restart IPSec + +- name: Configure Charon + copy: src=etc/strongswan.d/{{ item }} + dest=/etc/strongswan.d/{{ item }} + owner=root group=root + mode=0644 + with_items: + - charon.conf + - charon/socket-default.conf + register: r3 + notify: + - Restart IPSec + +- name: Generate a private key and a X.509 certificate for IPSec + command: genkeypair.sh x509 + --pubkey=/etc/ipsec.d/certs/{{ inventory_hostname_short }}.pem + --privkey=/etc/ipsec.d/private/{{ inventory_hostname_short }}.key + --ou=IPSec --cn={{ inventory_hostname_short }} + -t rsa -b 4096 -h sha512 + register: r4 + changed_when: r4.rc == 0 + failed_when: r4.rc > 1 + notify: + - Restart IPSec + tags: + - genkey + +- name: Fetch IPSec X.509 certificate + # Ensure we don't fetch private data + become: False + fetch_cmd: cmd="openssl x509" + stdin=/etc/ipsec.d/certs/{{ inventory_hostname_short }}.pem + dest=certs/ipsec/{{ inventory_hostname_short }}.pem + tags: + - genkey + +# Don't copy our pubkey due to a possible race condition. Only the +# remote machine has authority regarding its key. +- name: Copy IPSec X.509 certificates (except ours) + copy: src=certs/ipsec/{{ hostvars[item].inventory_hostname_short }}.pem + dest=/etc/ipsec.d/certs/{{ hostvars[item].inventory_hostname_short }}.pem + owner=root group=root + mode=0644 + with_items: "{{ groups.all | difference([inventory_hostname]) }}" + register: r5 + tags: + - genkey + notify: + - Restart IPSec + +- name: Start IPSec + service: name=ipsec state=started + when: not (r1.changed or r2.changed or r3.changed or r4.changed or r5.changed) diff --git a/roles/common/tasks/logging.yml b/roles/common/tasks/logging.yml index 3b86294..b27fc41 100644 --- a/roles/common/tasks/logging.yml +++ b/roles/common/tasks/logging.yml @@ -47,6 +47,7 @@ - ignore.d.server/common-local - ignore.d.server/dovecot-local - ignore.d.server/postfix-local + - ignore.d.server/strongswan-local # logcheck-sudo already exists, but changing the filename for our # local modifications would defeat the ruleset - violations.ignore.d/logcheck-sudo diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 1226d37..88d44f3 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -47,6 +47,11 @@ command: gendhparam.sh /etc/ssl/dhparams.pem 2048 creates=/etc/ssl/dhparams.pem tags: genkey +- include: ipsec.yml + tags: + - strongswan + - ipsec + when: "groups.all | length > 1" - include: logging.yml tags: logging - include: ntp.yml diff --git a/roles/common/templates/etc/fail2ban/jail.local.j2 b/roles/common/templates/etc/fail2ban/jail.local.j2 index f1c9833..eb6a7fb 100644 --- a/roles/common/templates/etc/fail2ban/jail.local.j2 +++ b/roles/common/templates/etc/fail2ban/jail.local.j2 @@ -14,7 +14,7 @@ chain = fail2ban action = %(action_)s # Don't ban ourselves. -ignoreip = 127.0.0.0/8 {{ groups.all | sort | join(' ') }} +ignoreip = 127.0.0.0/8 {{ ipsec_subnet }} # # JAILS diff --git a/roles/common/templates/etc/ipsec.conf.j2 b/roles/common/templates/etc/ipsec.conf.j2 new file mode 100644 index 0000000..4d6aa68 --- /dev/null +++ b/roles/common/templates/etc/ipsec.conf.j2 @@ -0,0 +1,43 @@ +# {{ ansible_managed }} +# Do NOT edit this file directly! + +config setup + charondebug = "dmn 0, lib 0, cfg 0, ike 0, enc 0, net 0" + +conn %default + keyexchange = ikev2 + keyingtries = %forever + ike = aes128gcm16-prfsha256-ecp256,aes256gcm16-prfsha384-ecp384! + esp = aes128gcm16-ecp256,aes256gcm16-ecp384! +{% if 'NATed' not in group_names %} + mobike = no +{% endif %} +{% if 'DynDNS' in group_names %} + leftallowany = yes +{% endif %} + leftauth = pubkey + left = %defaultroute + leftsubnet = {{ ipsec[inventory_hostname_short] | ipv4 }}/32 + leftcert = {{ inventory_hostname_short }}.pem + leftfirewall = yes + lefthostaccess = yes + rightauth = pubkey + auto = route + dpdaction = hold + inactivity = 30m + modeconfig = push + +{% for host in groups.all | difference([inventory_hostname]) | sort %} + +conn {{ hostvars[host].inventory_hostname_short }} + right = {{ hostvars[host].inventory_hostname }} +{% if 'DynDNS' in hostvars[host].group_names %} + rightallowany = yes +{% endif %} + rightcert = {{ hostvars[host].inventory_hostname_short }}.pem + rightsubnet = {{ ipsec[ hostvars[host].inventory_hostname_short ] | ipv4 }}/32 +{% if 'NATed' not in group_names and 'NATed' in hostvars[host].group_names %} + mobike = yes +{% endif %} + +{%- endfor %} diff --git a/roles/common/templates/etc/ipsec.secrets.j2 b/roles/common/templates/etc/ipsec.secrets.j2 new file mode 100644 index 0000000..6ed670d --- /dev/null +++ b/roles/common/templates/etc/ipsec.secrets.j2 @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +# Do NOT edit this file directly! + +# Our VPN uses RSA only. +: RSA {{ inventory_hostname_short }}.key diff --git a/roles/common/templates/etc/iptables/services.j2 b/roles/common/templates/etc/iptables/services.j2 index 303fa06..6bd2533 100644 --- a/roles/common/templates/etc/iptables/services.j2 +++ b/roles/common/templates/etc/iptables/services.j2 @@ -4,6 +4,13 @@ # direction protocol destination port source port # (in|out|inout)[46]? (tcp|udp|..) (port|port:port|port,port) (port|port:port|port,port) +{% if groups.all | length > 1 %} +inout4 udp 500 500 # ISAKMP +{% if groups.NATed | length > 0 %} +inout4 udp 4500 4500 # IPSec NAT Traversal +{% endif %} +{% endif %} + out tcp 80,443 # HTTP/HTTPS out tcp 9418 # GIT out udp 53 # DNS diff --git a/roles/common/templates/etc/network/if-up.d/ipsec.j2 b/roles/common/templates/etc/network/if-up.d/ipsec.j2 new file mode 100755 index 0000000..7dd41d4 --- /dev/null +++ b/roles/common/templates/etc/network/if-up.d/ipsec.j2 @@ -0,0 +1,47 @@ +#!/bin/sh + +# A post-up/down hook to automatically create/delete a virtual subnet +# for IPSec (inet4 only). +# Copyright © 2016 Guilhem Moulin <guilhem@fripost.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -ue +PATH=/usr/sbin:/usr/bin:/sbin:/bin + +# Ignore the loopback interface and non inet4 families. +[ "$IFACE" != lo -a "$ADDRFAM" = inet ] || exit 0 + +# Only the device with the default, globally-scoped route, is of +# interest here. +ip="$( ip -4 -o route show to default scope global \ + | sed -nr '/^default via (\S+) dev (\S+).*/ {s//\2 \1/p;q}' )" +[ "${ip% *}" = "$IFACE" ] || exit 0 +ip="${ip##* }" + +vip="{{ ipsec[inventory_hostname_short] }}" +vsubnet="{{ ipsec_subnet }}" + +case "$MODE" in + start) ip address add "$vip/32" dev "$IFACE" scope global || true + # Nullroute the subnet used for IPSec to avoid data leaks + # in the absence of xfrm lookup (i.e., when there is no + # matching IPSec Security Association). + ip route replace prohibit "$vsubnet" proto static || true + ip route replace table 220 to "$vsubnet" via "$ip" dev "$IFACE" proto static src "$vip" || true + ;; + stop) ip route del table 220 to "$vsubnet" via "$ip" dev "$IFACE" proto static src "$vip" || true + ip route del prohibit "$vsubnet" proto static || true + ip address del "$vip/32" dev "$IFACE" scope global || true +esac |