From 3fafa03aeb3640a86d9cd8c639d085df6a8d085d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 20 May 2016 01:19:27 +0200 Subject: Set up IPSec tunnels between each pair of hosts. We use a dedicated, non-routable, IPv4 subnet for IPSec. Furthermore the subnet is nullrouted in the absence of xfrm lookup (i.e., when there is no matching IPSec Security Association) to avoid data leaks. Each host is associated with an IP in that subnet (thus only reachble within that subnet, either by the host itself or by its IPSec peers). The peers authenticate each other using RSA public key authentication. Kernel traps are used to ensure that connections are only established when traffic is detected between the peers; after 30m of inactivity (this value needs to be less than the rekeying period) the connection is brought down and a kernel trap is installed. --- .../etc/logcheck/ignore.d.server/strongswan-local | 19 ++ roles/common/files/etc/strongswan.d/charon.conf | 303 +++++++++++++++++++++ .../etc/strongswan.d/charon/socket-default.conf | 20 ++ .../common/files/usr/local/sbin/update-firewall.sh | 102 +++---- roles/common/handlers/main.yml | 6 + roles/common/tasks/ipsec.yml | 96 +++++++ roles/common/tasks/logging.yml | 1 + roles/common/tasks/main.yml | 5 + roles/common/templates/etc/fail2ban/jail.local.j2 | 2 +- roles/common/templates/etc/ipsec.conf.j2 | 43 +++ roles/common/templates/etc/ipsec.secrets.j2 | 5 + roles/common/templates/etc/iptables/services.j2 | 7 + .../common/templates/etc/network/if-up.d/ipsec.j2 | 47 ++++ 13 files changed, 585 insertions(+), 71 deletions(-) create mode 100644 roles/common/files/etc/logcheck/ignore.d.server/strongswan-local create mode 100644 roles/common/files/etc/strongswan.d/charon.conf create mode 100644 roles/common/files/etc/strongswan.d/charon/socket-default.conf create mode 100644 roles/common/tasks/ipsec.yml create mode 100644 roles/common/templates/etc/ipsec.conf.j2 create mode 100644 roles/common/templates/etc/ipsec.secrets.j2 create mode 100755 roles/common/templates/etc/network/if-up.d/ipsec.j2 (limited to 'roles') 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 +# +# 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 . + +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 -- cgit v1.2.3